Skip to content

Commit

Permalink
Add a new module to manage Yubikey devices (#785)
Browse files Browse the repository at this point in the history
* Add a new module to manage a Yubikey device

* Set python 3.6+ as a requeriment

* remove the enable_shell parameter used by run_through_shell in the yubikey module
  • Loading branch information
dth0 authored Jul 23, 2020
1 parent d6b9c95 commit 1f8b0a7
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: python
sudo: false
python:
- "3.4"
- "3.6"
install:
- "pip install -r dev-requirements.txt"
script: "./ci-build.sh"
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ i3pystatus
i3pystatus is a large collection of status modules compatible with i3bar from the i3 window manager.

:License: MIT
:Python: 3.4+
:Python: 3.6+
:Governance: Patches that don't break the build (Travis or docs) are generally just merged. This is a "do-it-yourself" project, so to speak.
:Releases: No further releases are planned. Install it from Git.

Installation
------------

**Supported Python versions**
i3pystatus requires Python 3.4 or newer and is not compatible with
i3pystatus requires Python 3.6 or newer and is not compatible with
Python 2.x. Some modules require additional dependencies
documented in the docs.

Expand Down
133 changes: 133 additions & 0 deletions i3pystatus/yubikey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import re
import os
import time

from i3pystatus import IntervalModule
from i3pystatus.core.command import run_through_shell


class Yubikey(IntervalModule):
"""
This module allows you to lock and unlock your Yubikey in order to avoid
the OTP to be triggered accidentally.
@author Daniel Theodoro <daniel.theodoro AT gmail.com>
"""

interval = 1
format = "Yubikey: 🔒"
unlocked_format = "Yubikey: 🔓"
timeout = 5
color = "#00FF00"
unlock_color = "#FF0000"

settings = (
("format", "Format string"),
("unlocked_format", "Format string when the key is unlocked"),
("timeout", "How long the Yubikey will be unlocked (default: 5)"),
("color", "Standard color"),
("unlock_color", "Set the color used when the Yubikey is unlocked"),
)

on_leftclick = ["set_lock", True]

find_regex = re.compile(
r".*yubikey.*id=(?P<yubid>\d+).*$",
re.IGNORECASE
)

status_regex = re.compile(
r".*device enabled.*(?P<status>\d)$",
re.IGNORECASE
)

lock_file = f"/var/tmp/Yubikey-{os.geteuid()}.lock"

def __init__(self):
super().__init__()

@property
def _device_id(self):
command = run_through_shell("xinput list")

rval = ""

if command.rc == 0:
for line in command.out.splitlines():
match = self.find_regex.match(line)
if match:
rval = match.groupdict().get("yubid", "")
break

return rval

def device_status(self):

rval = "notfound"

if not self._device_id:
return rval

result = run_through_shell(f"xinput list-props {self._device_id}")
if result.rc == 0:
match = self.status_regex.match(result.out.splitlines()[1])
if match and "status" in match.groupdict():
status = int(match.groupdict()["status"])
if status:
rval = "unlocked"
else:
rval = "locked"

return rval

def _check_lock(self):
try:
st = os.stat(self.lock_file)

if int(time.time() - st.st_ctime) > self.timeout:
self.set_lock()

except IOError:
self.set_lock()

def set_lock(self, unlock=False):

if unlock:
command = "enable"
else:
command = "disable"

run_through_shell(f"xinput {command} {self._device_id}")
open(self.lock_file, mode="w").close()

def _clear_lock(self):
try:
os.unlink(self.lock_file)
except FileNotFoundError:
pass

def run(self):
status = self.device_status()

if status == "notfound":
self._clear_lock()
self.output = {
"full_text": "",
}
else:
if status == "unlocked":
self.output = {
"full_text": self.unlocked_format,
"color": self.unlock_color
}
self._check_lock()

elif status == "locked":
self.output = {
"full_text": self.format,
"color": self.color
}
else:
self.output = {
"full_text": f"Error: {status}",
}

0 comments on commit 1f8b0a7

Please sign in to comment.