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
This commit is contained in:
Daniel Theodoro 2020-07-23 22:27:50 +02:00 committed by GitHub
parent d6b9c95a4c
commit 1f8b0a7863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 3 deletions

View File

@ -1,7 +1,7 @@
language: python
sudo: false
python:
- "3.4"
- "3.6"
install:
- "pip install -r dev-requirements.txt"
script: "./ci-build.sh"

View File

@ -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.

133
i3pystatus/yubikey.py Normal file
View File

@ -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}",
}