You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
7.4 KiB
203 lines
7.4 KiB
#!/usr/bin/env python3 |
|
|
|
#TODO: |
|
# - resolve exploit where key is plugged in and out quickly, resulting in an unlocked state |
|
# - Add async scheduler that runs update_lock_state() every 1 minute or so |
|
|
|
import os |
|
import sys |
|
import subprocess |
|
import argparse |
|
from datetime import datetime |
|
from configparser import ConfigParser |
|
from time import sleep |
|
|
|
import pyudev |
|
import yubico |
|
import logzero |
|
from logzero import logger |
|
from usb.core import USBError |
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__)) |
|
home_dir = os.path.expanduser("~") |
|
|
|
config = ConfigParser() |
|
config.read([f"{script_dir}/config.ini", f"{home_dir}/.yubilock"]) |
|
|
|
screensaver = config.get("HOSTCONFIG", "screensaver", fallback="xscreensaver") |
|
|
|
yubikey_serials = config["USERCONFIG"]["yubikey_serial"].split(',') |
|
# Convert stringlist to intlist |
|
for i, serial in enumerate(yubikey_serials): |
|
yubikey_serials[i] = int(serial) |
|
|
|
def setup_logger(logfile): |
|
logzero.loglevel(10 if args.verbose else 100) |
|
loglevel = config.getint("HOSTCONFIG", "loglevel", fallback=10) |
|
logzero.logfile(logfile, loglevel=loglevel, maxBytes=40960, backupCount=4) |
|
logger.info('script started') |
|
|
|
def execute(command: str, shell_on: bool = False, background: bool = False): |
|
if background : |
|
subprocess.run(command, shell=shell_on, |
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False) |
|
return |
|
process = subprocess.Popen(command, shell=shell_on, stdout=subprocess.PIPE, |
|
stderr=subprocess.PIPE) #Carefull with variables: shellcode injection |
|
out, err = process.communicate() |
|
if out != b'' : |
|
logger.info(out.decode('utf-8').rstrip()) |
|
if err != b'' : |
|
logger.error(err.decode('utf-8').rstrip()) |
|
return '' if out == b'' else out.decode('utf-8') |
|
|
|
def screensaver_running(): |
|
if screensaver == 'xscreensaver': |
|
graphic_program_instances = execute(f"{script_dir}/kill_screensaver_graphic_program.sh -d | grep graphic_processes | wc -l", shell_on=True) |
|
if int(graphic_program_instances) > 0: return True |
|
if execute(f"DISPLAY=:0 xscreensaver-command -time | grep 'screen locked' >/dev/null 2>&1", shell_on=True): return True |
|
elif screensaver == 'gnome-screensaver': |
|
screensaver_state = execute("gnome-screensaver-command -q | tr -d '\n'", shell_on=True) |
|
if screensaver_state == 'The screensaver is active': return True |
|
if screensaver_state == 'The screensaver is inactive': return False |
|
return False |
|
|
|
def lock_screen(): |
|
if args.dummy : |
|
return |
|
if config.getboolean('USERCONFIG', 'remove_sudo_timestamp_when_locking', fallback=True): |
|
execute('sudo -K', shell_on=True) |
|
|
|
if screensaver == 'xscreensaver': |
|
execute('DISPLAY=:0 xscreensaver-command -lock', shell_on=True) |
|
elif screensaver == 'gnome-screensaver': |
|
execute('gnome-screensaver-command -l', shell_on=True) |
|
return |
|
|
|
def unlock_screen(): |
|
if args.dummy : |
|
return |
|
|
|
if screensaver == 'xscreensaver': |
|
logger.info('xscreen process to be killed:') |
|
xscreensaver_pid = execute(r'ps -A | grep -oPm 1 "\d{2,}(?=\s.+xscreensaver)" || echo null', |
|
shell_on=True) |
|
|
|
if xscreensaver_pid != 'null': |
|
execute('kill %s' % xscreensaver_pid, shell_on=True) |
|
execute(f"{script_dir}/kill_screensaver_graphic_program.sh", shell_on=True) |
|
|
|
# restart xscreensaver process |
|
execute('DISPLAY=:0 xscreensaver -no-splash&', shell_on=True, background = True) |
|
elif screensaver == 'gnome-screensaver': |
|
execute(f"{script_dir}/loginctl-unlock.sh", shell_on=True, background = True) |
|
return |
|
|
|
def get_yubikey_serials() -> int: |
|
serials = [] |
|
try: |
|
skip = 0 |
|
while skip < 255: |
|
yubikey = yubico.find_yubikey(debug = False, skip = skip) |
|
serials.append(yubikey.serial()) |
|
logger.info('yubikey connected, serial= %s', yubikey.serial()) |
|
skip += 1 |
|
except yubico.yubikey.YubiKeyError: |
|
pass |
|
except USBError as error: |
|
logger.info('get_yubikey_serials() threw: %s', str(error)) |
|
|
|
if not serials: |
|
logger.info('no yubikey connected') |
|
|
|
return serials |
|
|
|
def update_lock_state(): |
|
if any(serial in yubikey_serials for serial in get_yubikey_serials()): |
|
if screensaver_running(): |
|
logger.debug('screen will be unlocked') |
|
unlock_screen() |
|
else: |
|
if not screensaver_running(): |
|
logger.debug('screen will be locked') |
|
lock_screen() |
|
|
|
def device_added(device, last_yubikey_time): |
|
logger.debug('device connected: %s', device.sys_name) |
|
if (datetime.now() - last_yubikey_time).total_seconds() > 0.3 : |
|
# This is not nice, yubikey call can cause key to reconnect. |
|
# This cooldown prevents endless reconnect loops |
|
|
|
update_lock_state() |
|
return datetime.now() |
|
return last_yubikey_time |
|
|
|
def device_removed(device: pyudev.Device, last_yubikey_time): |
|
logger.debug('device removed: %s', device.sys_name) |
|
if (datetime.now() - last_yubikey_time).total_seconds() > 0.3 : |
|
# This is not nice, yubikey call can cause key to reconnect. |
|
# This cooldown prevents endless reconnect loops |
|
|
|
update_lock_state() |
|
return datetime.now() |
|
return last_yubikey_time |
|
|
|
def get_args(): |
|
parser = argparse.ArgumentParser(description="Deamon that monitors for a specific YubiKey \ |
|
serial number. When the YubiKey is connected, any running xscreensaver instance will be \ |
|
unlocked. When it is removed, xscreensaver will lock again.") |
|
parser.add_argument('-d', '--dummy', action='store_true', \ |
|
help='don\'t actually lock and unlock the display') |
|
parser.add_argument('-v', '--verbose', action='store_true', help='increase output verbosity') |
|
return parser.parse_args() |
|
|
|
|
|
def daemon(monitor): |
|
|
|
last_yubikey_time = datetime.fromtimestamp(0) |
|
lastdevice = 'hidraw-2' |
|
|
|
try: |
|
for device in iter(monitor.poll, None): |
|
if device.sys_name[:6] == 'hidraw': |
|
if abs(int(device.sys_name[6:]) - int(lastdevice[6:])) == 1 : |
|
continue |
|
lastdevice = device.sys_name |
|
if device.action == 'add': |
|
last_yubikey_time = device_added(device, last_yubikey_time) |
|
elif device.action == 'remove': |
|
last_yubikey_time = device_removed(device, last_yubikey_time) |
|
else: |
|
logger.info('device ' + device.action + ': ' + device.sys_name) |
|
except KeyboardInterrupt: |
|
logger.info('script was terminiated by KeyboardInterrupt') |
|
|
|
def get_hid_event_monitor(): |
|
context = pyudev.Context() |
|
monitor = pyudev.Monitor.from_netlink(context) |
|
monitor.filter_by('hidraw') #human interface device connected/disconnected |
|
return monitor |
|
|
|
if __name__ == "__main__": |
|
args = get_args() |
|
|
|
setup_logger(config.get("HOSTCONFIG", "logfile", |
|
fallback="log.log")) |
|
|
|
|
|
if screensaver == 'xscreensaver': |
|
# start xscreensaver process |
|
execute('DISPLAY=:0 xscreensaver -no-splash&', shell_on=True, background = True) |
|
|
|
# Lock the machine if no key is inserted when the script is started |
|
update_lock_state() |
|
|
|
daemon(get_hid_event_monitor()) |
|
|
|
if screensaver == 'xscreensaver': |
|
# restart xscreensaver process before leaving |
|
execute('DISPLAY=:0 xscreensaver -no-splash&', shell_on=True, background = True) |
|
|
|
sleep(0.2) |
|
|
|
sys.exit(0)
|
|
|