#!/usr/bin/env python3 import argparse import os import pwd import re import signal import sys import time import typing from binascii import hexlify, unhexlify from pathlib import Path from threading import Thread import dbus import dbus.mainloop.glib import dbus.service import yaml from usb import core as usb_core dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) from gi.repository import GLib import logging import logging.handlers from validitysensor import init from validitysensor.tls import tls from validitysensor.usb import usb from validitysensor.sid import sid_from_string from validitysensor.db import subtype_to_string, db from validitysensor.sensor import sensor, RebootException GLib.threads_init() INTERFACE_NAME = 'io.github.uunicorn.Fprint.Device' loop = GLib.MainLoop() class NoEnrolledPrints(dbus.DBusException): _dbus_error_name = 'net.reactivated.Fprint.Error.NoEnrolledPrints' def __init__(self): super().__init__('No enrolled prints found') class Device(dbus.service.Object): def __init__(self, bus_name: dbus.Bus, config: typing.Dict[str, typing.Any]): dbus.service.Object.__init__(self, bus_name, '/io/github/uunicorn/Fprint/Device') self.config = config def user2identity(self, user: str, uid: int): """Compute UID to SID mapping""" if user in self.config['user_to_sid']: sidstr = self.config['user_to_sid'][user] else: sidstr = 'S-1-5-21-111111111-1111111111-1111111111-%d' % uid return sid_from_string(sidstr) def _get_user_sid(self, user: str): """Resolve user name to UID and SID""" pw = pwd.getpwnam(user) uid = pw.pw_uid return db.lookup_user(self.user2identity(user, uid)) @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature="s", out_signature="as") def ListEnrolledFingers(self, user): try: logging.debug('In ListEnrolledFingers %s' % user) usr = self._get_user_sid(user) if usr is None: return [] rc = [subtype_to_string(f['subtype']) for f in usr.fingers] logging.debug(repr(rc)) return rc except Exception as e: raise e @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='s', out_signature='') def DeleteEnrolledFingers(self, user): logging.debug('In DeleteEnrolledFingers %s' % user) usr = self._get_user_sid(user) if usr is None: return db.del_record(usr.dbid) @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='ss', out_signature='') def VerifyStart(self, user, finger): logging.debug('In VerifyStart for %s, %s' % (user, finger)) usr = self._get_user_sid(user) if usr is None: raise NoEnrolledPrints() self.VerifyFingerSelected('any') def update_cb(e): self.VerifyStatus('verify-retry-scan', False) def run(): try: # TODO: pass down the user db record id and implement a proper Sensor.verify() method usrid, subtype, hsh = sensor.identify(update_cb) if usr.dbid == usrid: self.VerifyStatus('verify-match', True) else: self.VerifyStatus('verify-no-match', True) except usb_core.USBError as e: logging.exception(e) self.VerifyStatus('verify-no-match', True) loop.quit() except Exception as e: logging.exception(e) self.VerifyStatus('verify-no-match', True) thread = Thread(target=run) thread.daemon = True thread.start() @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='', out_signature='') def Cancel(self): sensor.cancel() @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='ss', out_signature='') def EnrollStart(self, user, finger_name): logging.debug('In EnrollStart %s for %s' % (finger_name, user)) usr = self._get_user_sid(user) def update_cb(rsp, e): if e is not None: self.EnrollStatus('enroll-retry-scan', False) else: self.EnrollStatus('enroll-stage-passed', False) def run(): try: sensor.enroll(usr, 0xf5, update_cb) # TODO parse the finger name self.EnrollStatus('enroll-completed', True) except usb_core.USBError as e: logging.exception(e) self.EnrollStatus('enroll-failed', True) loop.quit() except Exception as e: logging.exception(e) self.EnrollStatus('enroll-failed', True) thread = Thread(target=run) thread.daemon = True thread.start() @dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb') def VerifyStatus(self, result, done): logging.debug('VerifyStatus') @dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='s') def VerifyFingerSelected(self, finger): logging.debug('VerifyFingerSelected') @dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb') def EnrollStatus(self, result, done): logging.debug('EnrollStatus') @dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='s', out_signature='s') def RunCmd(self, cmd): logging.debug('RunCmd') return hexlify(tls.app(unhexlify(cmd))).decode() backoff_file = '/usr/share/python-validity/backoff' # I don't know how to tell systemd to backoff in case of multiple instance of the same template service, help! def backoff(): if os.path.isfile(backoff_file): with open(backoff_file, 'r') as f: lines = list(map(float, f.readlines())) else: lines = [] while len(lines) > 0 and lines[0] + 60 < time.time(): lines = lines[1:] lines += [time.time()] with open(backoff_file, 'w') as f: for l in lines: f.write('%f\n' % l) if len(lines) > 10: raise Exception('Refusing to start more than 10 times per minute') def main(): parser = argparse.ArgumentParser('Open fprintd DBus service') parser.add_argument('--debug', help='Enable tracing', action='store_true') parser.add_argument('--devpath', help='USB device path: usb--
') parser.add_argument('--configpath', default='/etc/python-validity', type=Path, help='Path to config files') args = parser.parse_args() if args.debug: level = logging.DEBUG usb.trace_enabled = True tls.trace_enabled = True else: level = logging.INFO handler = logging.handlers.SysLogHandler(address='/dev/log') logging.basicConfig(level=level, handlers=[handler]) # Load and perform basic validation of config file. try: with (args.configpath / 'dbus-service.yaml').open(mode='rt') as configfd: config = yaml.load(configfd) except FileNotFoundError: # No configuration file. Create default config = {'user_to_sid': {}} if 'user_to_sid' not in config: raise Exception("No user to SID mapping in config file!") # Be kind to user, and allow empty mapping that got turned into a null by mistake. if config['user_to_sid'] is None: config['user_to_sid'] = {} if not all(isinstance(e, str) for e in config['user_to_sid'].keys()): raise Exception("Keys in user to SID map must be strings") if not all(isinstance(e, str) for e in config['user_to_sid'].values()): raise Exception("Values in user to SID map must be strings") backoff() try: if args.devpath is None: init.open() else: z = re.match(r'^usb-(\d+)-(\d+)$', args.devpath) if not z: parser.error('Option --devpath should look like this: usb--
') init.open_devpath(*map(int, z.groups())) except RebootException: logging.debug('Initialization ended up in rebooting the sensor. Normal exit.') sys.exit(0) bus = dbus.SystemBus() svc = Device(bus, config) def watch_cb(name): if name == '': logging.debug('Manager is offline') else: logging.debug('Manager is back online, registering') mgr = bus.get_object(name, '/net/reactivated/Fprint/Manager') mgr = dbus.Interface(mgr, 'net.reactivated.Fprint.Manager') mgr.RegisterDevice(svc) watcher = bus.watch_name_owner('net.reactivated.Fprint', watch_cb) # Kick off the open-fprintd if it was not started yet bus.get_object('net.reactivated.Fprint', '/net/reactivated/Fprint/Manager') def die(x): logging.info('Caught signal %d. Stopping...' % x) loop.quit() GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, die, signal.SIGINT) GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGTERM, die, signal.SIGINT) try: loop.run() except Exception as e: logging.exception(e) raise e if __name__ == '__main__': main()