Support config files for SID mapping.

This allows interoperability with Windows.
This commit is contained in:
Arvid Norlander 2020-08-03 19:34:07 +02:00
parent 9898b5d80a
commit d2944ba762
No known key found for this signature in database
GPG key ID: E824A8E5D8D29AA0
10 changed files with 122 additions and 52 deletions

View file

@ -57,6 +57,24 @@ auth [success=2 default=ignore] pam_fprintd.so max_tries=1 timeout=10 # debug
auth [success=1 default=ignore] pam_unix.so nullok_secure try_first_pass
```
## Windows interoperability
Note: This section is likely only relevant if you will be dual booting.
To be able to use the same set of fingerprints for Windows and Linux, you first
need to extract the Windows user IDs (known as SIDs). To do this, start Windows,
start `cmd.exe` and run `wmic useraccount get name,sid`. This will provide a
list of all users and the corresponding SIDs.
You can then create a mapping from the Linux user names (as written in the
first `:`-separated field of `/etc/passwd`). This mapping is defined in
`/etc/python-validity/dbus-service.yaml`. For example:
```yaml
user_to_sid:
"myusername": "S-1-5-21-1234567890-1234567890-1234567890-1001"
"someotheruser": "S-1-5-21-1234567890-1234567890-1234567890-1003"
```
## Playground
This package contains a set of scripts you can use to do a low-level debugging of the sensor protocol.

View file

@ -1,18 +1,23 @@
#!/usr/bin/env python3
import signal
import argparse
import re
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
from binascii import hexlify, unhexlify
from threading import Thread
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
from gi.repository import GLib
import logging
@ -23,42 +28,53 @@ 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, reboot, RebootException
from validitysensor.sensor import sensor, RebootException
GLib.threads_init()
INTERFACE_NAME='io.github.uunicorn.Fprint.Device'
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')
def uid2identity(uid):
sidstr='S-1-5-21-111111111-1111111111-1111111111-%d' % uid
return sid_from_string(sidstr)
class Device(dbus.service.Object):
def __init__(self, bus_name):
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, usr):
in_signature="s",
out_signature="as")
def ListEnrolledFingers(self, user):
try:
logging.debug('In ListEnrolledFingers %s' % usr)
logging.debug('In ListEnrolledFingers %s' % user)
pw=pwd.getpwnam(usr)
uid=pw.pw_uid
usr=db.lookup_user(uid2identity(uid))
usr = self._get_user_sid(user)
if usr == None:
if usr is None:
return []
rc = [subtype_to_string(f['subtype']) for f in usr.fingers]
logging.debug(repr(rc))
return rc
@ -66,14 +82,13 @@ class Device(dbus.service.Object):
raise e
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='s',
out_signature='')
in_signature='s',
out_signature='')
def DeleteEnrolledFingers(self, user):
logging.debug('In DeleteEnrolledFingers %s' % user)
pw=pwd.getpwnam(user)
usr=db.lookup_user(uid2identity(pw.pw_uid))
usr = self._get_user_sid(user)
if usr == None:
if usr is None:
return
db.del_record(usr.dbid)
@ -84,10 +99,9 @@ class Device(dbus.service.Object):
def VerifyStart(self, user, finger):
logging.debug('In VerifyStart for %s, %s' % (user, finger))
pw = pwd.getpwnam(user)
usr = db.lookup_user(uid2identity(pw.pw_uid))
usr = self._get_user_sid(user)
if usr == None:
if usr is None:
raise NoEnrolledPrints()
self.VerifyFingerSelected('any')
@ -114,7 +128,6 @@ class Device(dbus.service.Object):
thread = Thread(target=run)
thread.daemon = True
thread.start()
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='',
@ -127,8 +140,9 @@ class Device(dbus.service.Object):
out_signature='')
def EnrollStart(self, user, finger_name):
logging.debug('In EnrollStart %s for %s' % (finger_name, user))
pw=pwd.getpwnam(user)
uid=pw.pw_uid
usr = self._get_user_sid(user)
def update_cb(rsp, e):
if e is not None:
self.EnrollStatus('enroll-retry-scan', False)
@ -137,7 +151,7 @@ class Device(dbus.service.Object):
def run():
try:
sensor.enroll(uid2identity(uid), 0xf5, update_cb) # TODO parse the finger name
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)
@ -150,7 +164,6 @@ class Device(dbus.service.Object):
thread = Thread(target=run)
thread.daemon = True
thread.start()
@dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb')
def VerifyStatus(self, result, done):
@ -171,7 +184,9 @@ class Device(dbus.service.Object):
logging.debug('RunCmd')
return hexlify(tls.app(unhexlify(cmd))).decode()
backoff_file='/usr/share/python-validity/backoff'
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():
@ -179,10 +194,10 @@ def backoff():
with open(backoff_file, 'r') as f:
lines = list(map(float, f.readlines()))
else:
lines=[]
lines = []
while len(lines) > 0 and lines[0] + 60 < time.time():
lines=lines[1:]
lines = lines[1:]
lines += [time.time()]
@ -193,10 +208,12 @@ def backoff():
if len(lines) > 10:
raise Exception('Refusing to start more than 10 times per minute')
if __name__ == '__main__':
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-<busnum>-<address>')
parser.add_argument('--configpath', default='/etc/python-validity', type=Path, help='Path to config files')
args = parser.parse_args()
if args.debug:
@ -206,16 +223,34 @@ if __name__ == '__main__':
else:
level = logging.INFO
handler = logging.handlers.SysLogHandler(address = '/dev/log')
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('^usb-(\d+)-(\d+)$', args.devpath)
z = re.match(r'^usb-(\d+)-(\d+)$', args.devpath)
if not z:
parser.error('Option --devpath should look like this: usb-<busnum>-<address>')
@ -226,9 +261,8 @@ if __name__ == '__main__':
bus = dbus.SystemBus()
svc = Device(bus)
svc = Device(bus, config)
watcher = None
def watch_cb(name):
if name == '':
logging.debug('Manager is offline')
@ -237,13 +271,14 @@ if __name__ == '__main__':
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('Cought signal %d. Stopping...' % x)
logging.info('Caught signal %d. Stopping...' % x)
loop.quit()
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, die, signal.SIGINT)
@ -254,3 +289,6 @@ if __name__ == '__main__':
logging.exception(e)
raise e
if __name__ == '__main__':
main()

1
debian/control vendored
View file

@ -15,6 +15,7 @@ Depends: ${python3:Depends},
${misc:Depends},
python3-dbus,
python3-usb,
python3-yaml,
dbus,
open-fprintd (>= 0.2~),
innoextract (>= 1.6~)

1
debian/install vendored
View file

@ -4,3 +4,4 @@ usr/lib/python*/*-packages/validitysensor/*.py
usr/lib/python-validity
usr/share/dbus-1
usr/share/python-validity
etc/python-validity

2
debian/rules vendored
View file

@ -12,4 +12,4 @@ override_dh_installsystemd:
override_dh_auto_install:
python3 ./setup.py install --root=$(CURDIR)/debian/tmp --prefix=/usr --install-layout=deb
install -Dm 644 etc/dbus-service.yaml $(CURDIR)/debian/tmp/etc/python-validity/dbus-service.yaml

8
etc/dbus-service.yaml Normal file
View file

@ -0,0 +1,8 @@
# Mapping for users
# Add mappings from a user to a SID to use the same fingerprints as enrolled by
# Windows. By default, mappings will be generated from the numerical UID if not
# found here. This is fine if you are not dual booting and want to share
# mappings with Windows.
user_to_sid:
# Example:
#"username": "S-1-5-21-1234567890-1234567890-1234567890-1234"

View file

@ -12,7 +12,8 @@ setup(name='python-validity',
],
install_requires=[
'cryptography >= 2.1.4',
'pyusb >= 1.0.0'
'pyusb >= 1.0.0',
'pyyaml >= 3.12'
],
data_files=[
('share/dbus-1/system.d/', ['dbus_service/io.github.uunicorn.Fprint.conf']),

View file

@ -1,3 +1,5 @@
import typing
from .tls import tls
from .util import assert_status
from binascii import hexlify
@ -23,7 +25,7 @@ class User():
def __repr__(self):
return '<User: dbid=%04x identity=%s fingers=%s>' % (self.dbid, repr(self.identity), repr(self.fingers))
def subtype_to_string(s):
def subtype_to_string(s: int):
if s < 0xf5 or s > 0xfe:
return 'Unknown'
@ -66,7 +68,7 @@ def parse_identity(b):
raise Exception('Don''t know how to handle identity type %d' % t)
def parse_user(rsp):
def parse_user(rsp: bytes):
assert_status(rsp[:2])
rsp=rsp[2:]
@ -88,7 +90,7 @@ def parse_user(rsp):
return user
def identity_to_bytes(identity):
def identity_to_bytes(identity: str):
if isinstance(identity, SidIdentity):
b=identity.to_bytes()
b = pack('<LL', 3, len(b)) + b
@ -152,7 +154,7 @@ class Db():
def get_user(self, dbid):
return parse_user(tls.cmd(pack('<BHHH', 0x4a, dbid, 0, 0)))
def lookup_user(self, identity):
def lookup_user(self, identity: str) -> typing.Optional[User]:
stg = self.get_user_storage(name='StgWindsor')
data = identity_to_bytes(identity)

View file

@ -1,4 +1,4 @@
import typing
from enum import Enum
from hashlib import sha256
import os.path
@ -795,7 +795,7 @@ class Sensor():
return rc
def match_finger(self):
def match_finger(self) -> typing.Tuple[int, int, bytes]:
try:
stg_id=0 # match against any storage
usr_id=0 # match against any user

View file

@ -1,8 +1,9 @@
from struct import unpack, pack
import typing
class SidIdentity():
def __init__(self, revision, auth, subauth):
def __init__(self, revision: int, auth: int, subauth: typing.Sequence[int]):
self.revision = revision
self.auth = auth
self.subauth = subauth
@ -17,7 +18,7 @@ class SidIdentity():
def __repr__(self):
return 'S-%d-%d-%s' % (self.revision, self.auth, '-'.join(map(str, self.subauth)))
def sid_from_bytes(b):
def sid_from_bytes(b: bytes):
revision = b[0]
subcnt = b[1]
auth = 0
@ -30,7 +31,7 @@ def sid_from_bytes(b):
return SidIdentity(revision, auth, subauth)
def sid_from_string(s):
def sid_from_string(s: str):
parts = s.split('-')
if parts[0] != 'S':