From 1f8b0a7863dced1d1684b96e5571a843e3c1e1f7 Mon Sep 17 00:00:00 2001 From: Daniel Theodoro Date: Thu, 23 Jul 2020 22:27:50 +0200 Subject: [PATCH] Add a new module to manage Yubikey devices (#785) * 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 --- .travis.yml | 2 +- README.rst | 4 +- i3pystatus/yubikey.py | 133 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 i3pystatus/yubikey.py diff --git a/.travis.yml b/.travis.yml index 55ac0b7e..4eb2d165 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python sudo: false python: - - "3.4" + - "3.6" install: - "pip install -r dev-requirements.txt" script: "./ci-build.sh" diff --git a/README.rst b/README.rst index 207988a8..0f14ff0b 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ 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. @@ -15,7 +15,7 @@ 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. diff --git a/i3pystatus/yubikey.py b/i3pystatus/yubikey.py new file mode 100644 index 00000000..556b098c --- /dev/null +++ b/i3pystatus/yubikey.py @@ -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 + """ + + 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\d+).*$", + re.IGNORECASE + ) + + status_regex = re.compile( + r".*device enabled.*(?P\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}", + }