-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
3 changed files
with
136 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}", | ||
} |