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 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 ## Playground
This package contains a set of scripts you can use to do a low-level debugging of the sensor protocol. 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 #!/usr/bin/env python3
import signal
import argparse import argparse
import re
import os import os
import pwd import pwd
import re
import signal
import sys import sys
import time import time
import typing
from binascii import hexlify, unhexlify
from pathlib import Path
from threading import Thread
import dbus import dbus
import dbus.mainloop.glib import dbus.mainloop.glib
import dbus.service import dbus.service
import yaml
from usb import core as usb_core from usb import core as usb_core
from binascii import hexlify, unhexlify
from threading import Thread
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
from gi.repository import GLib from gi.repository import GLib
import logging import logging
@ -23,42 +28,53 @@ from validitysensor.tls import tls
from validitysensor.usb import usb from validitysensor.usb import usb
from validitysensor.sid import sid_from_string from validitysensor.sid import sid_from_string
from validitysensor.db import subtype_to_string, db from validitysensor.db import subtype_to_string, db
from validitysensor.sensor import sensor, reboot, RebootException from validitysensor.sensor import sensor, RebootException
GLib.threads_init() GLib.threads_init()
INTERFACE_NAME='io.github.uunicorn.Fprint.Device' INTERFACE_NAME = 'io.github.uunicorn.Fprint.Device'
loop = GLib.MainLoop() loop = GLib.MainLoop()
class NoEnrolledPrints(dbus.DBusException): class NoEnrolledPrints(dbus.DBusException):
_dbus_error_name = 'net.reactivated.Fprint.Error.NoEnrolledPrints' _dbus_error_name = 'net.reactivated.Fprint.Error.NoEnrolledPrints'
def __init__(self): def __init__(self):
super().__init__('No enrolled prints found') 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): 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') 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, @dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature="s", in_signature="s",
out_signature="as") out_signature="as")
def ListEnrolledFingers(self, usr): def ListEnrolledFingers(self, user):
try: try:
logging.debug('In ListEnrolledFingers %s' % usr) logging.debug('In ListEnrolledFingers %s' % user)
pw=pwd.getpwnam(usr) usr = self._get_user_sid(user)
uid=pw.pw_uid
usr=db.lookup_user(uid2identity(uid))
if usr == None: if usr is None:
return [] return []
rc = [subtype_to_string(f['subtype']) for f in usr.fingers] rc = [subtype_to_string(f['subtype']) for f in usr.fingers]
logging.debug(repr(rc)) logging.debug(repr(rc))
return rc return rc
@ -66,14 +82,13 @@ class Device(dbus.service.Object):
raise e raise e
@dbus.service.method(dbus_interface=INTERFACE_NAME, @dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='s', in_signature='s',
out_signature='') out_signature='')
def DeleteEnrolledFingers(self, user): def DeleteEnrolledFingers(self, user):
logging.debug('In DeleteEnrolledFingers %s' % user) logging.debug('In DeleteEnrolledFingers %s' % user)
pw=pwd.getpwnam(user) usr = self._get_user_sid(user)
usr=db.lookup_user(uid2identity(pw.pw_uid))
if usr == None: if usr is None:
return return
db.del_record(usr.dbid) db.del_record(usr.dbid)
@ -84,10 +99,9 @@ class Device(dbus.service.Object):
def VerifyStart(self, user, finger): def VerifyStart(self, user, finger):
logging.debug('In VerifyStart for %s, %s' % (user, finger)) logging.debug('In VerifyStart for %s, %s' % (user, finger))
pw = pwd.getpwnam(user) usr = self._get_user_sid(user)
usr = db.lookup_user(uid2identity(pw.pw_uid))
if usr == None: if usr is None:
raise NoEnrolledPrints() raise NoEnrolledPrints()
self.VerifyFingerSelected('any') self.VerifyFingerSelected('any')
@ -114,7 +128,6 @@ class Device(dbus.service.Object):
thread = Thread(target=run) thread = Thread(target=run)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@dbus.service.method(dbus_interface=INTERFACE_NAME, @dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='', in_signature='',
@ -127,8 +140,9 @@ class Device(dbus.service.Object):
out_signature='') out_signature='')
def EnrollStart(self, user, finger_name): def EnrollStart(self, user, finger_name):
logging.debug('In EnrollStart %s for %s' % (finger_name, user)) 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): def update_cb(rsp, e):
if e is not None: if e is not None:
self.EnrollStatus('enroll-retry-scan', False) self.EnrollStatus('enroll-retry-scan', False)
@ -137,7 +151,7 @@ class Device(dbus.service.Object):
def run(): def run():
try: 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) self.EnrollStatus('enroll-completed', True)
except usb_core.USBError as e: except usb_core.USBError as e:
logging.exception(e) logging.exception(e)
@ -150,7 +164,6 @@ class Device(dbus.service.Object):
thread = Thread(target=run) thread = Thread(target=run)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb') @dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb')
def VerifyStatus(self, result, done): def VerifyStatus(self, result, done):
@ -171,7 +184,9 @@ class Device(dbus.service.Object):
logging.debug('RunCmd') logging.debug('RunCmd')
return hexlify(tls.app(unhexlify(cmd))).decode() 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! # I don't know how to tell systemd to backoff in case of multiple instance of the same template service, help!
def backoff(): def backoff():
@ -179,10 +194,10 @@ def backoff():
with open(backoff_file, 'r') as f: with open(backoff_file, 'r') as f:
lines = list(map(float, f.readlines())) lines = list(map(float, f.readlines()))
else: else:
lines=[] lines = []
while len(lines) > 0 and lines[0] + 60 < time.time(): while len(lines) > 0 and lines[0] + 60 < time.time():
lines=lines[1:] lines = lines[1:]
lines += [time.time()] lines += [time.time()]
@ -193,10 +208,12 @@ def backoff():
if len(lines) > 10: if len(lines) > 10:
raise Exception('Refusing to start more than 10 times per minute') raise Exception('Refusing to start more than 10 times per minute')
if __name__ == '__main__':
def main():
parser = argparse.ArgumentParser('Open fprintd DBus service') parser = argparse.ArgumentParser('Open fprintd DBus service')
parser.add_argument('--debug', help='Enable tracing', action='store_true') parser.add_argument('--debug', help='Enable tracing', action='store_true')
parser.add_argument('--devpath', help='USB device path: usb-<busnum>-<address>') 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() args = parser.parse_args()
if args.debug: if args.debug:
@ -206,16 +223,34 @@ if __name__ == '__main__':
else: else:
level = logging.INFO level = logging.INFO
handler = logging.handlers.SysLogHandler(address = '/dev/log') handler = logging.handlers.SysLogHandler(address='/dev/log')
logging.basicConfig(level=level, handlers=[handler]) 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() backoff()
try: try:
if args.devpath is None: if args.devpath is None:
init.open() init.open()
else: else:
z = re.match('^usb-(\d+)-(\d+)$', args.devpath) z = re.match(r'^usb-(\d+)-(\d+)$', args.devpath)
if not z: if not z:
parser.error('Option --devpath should look like this: usb-<busnum>-<address>') parser.error('Option --devpath should look like this: usb-<busnum>-<address>')
@ -226,9 +261,8 @@ if __name__ == '__main__':
bus = dbus.SystemBus() bus = dbus.SystemBus()
svc = Device(bus) svc = Device(bus, config)
watcher = None
def watch_cb(name): def watch_cb(name):
if name == '': if name == '':
logging.debug('Manager is offline') logging.debug('Manager is offline')
@ -237,13 +271,14 @@ if __name__ == '__main__':
mgr = bus.get_object(name, '/net/reactivated/Fprint/Manager') mgr = bus.get_object(name, '/net/reactivated/Fprint/Manager')
mgr = dbus.Interface(mgr, 'net.reactivated.Fprint.Manager') mgr = dbus.Interface(mgr, 'net.reactivated.Fprint.Manager')
mgr.RegisterDevice(svc) mgr.RegisterDevice(svc)
watcher = bus.watch_name_owner('net.reactivated.Fprint', watch_cb) watcher = bus.watch_name_owner('net.reactivated.Fprint', watch_cb)
# Kick off the open-fprintd if it was not started yet # Kick off the open-fprintd if it was not started yet
bus.get_object('net.reactivated.Fprint', '/net/reactivated/Fprint/Manager') bus.get_object('net.reactivated.Fprint', '/net/reactivated/Fprint/Manager')
def die(x): def die(x):
logging.info('Cought signal %d. Stopping...' % x) logging.info('Caught signal %d. Stopping...' % x)
loop.quit() loop.quit()
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, die, signal.SIGINT) GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, die, signal.SIGINT)
@ -254,3 +289,6 @@ if __name__ == '__main__':
logging.exception(e) logging.exception(e)
raise e raise e
if __name__ == '__main__':
main()

1
debian/control vendored
View file

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

1
debian/install vendored
View file

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

2
debian/rules vendored
View file

@ -12,4 +12,4 @@ override_dh_installsystemd:
override_dh_auto_install: override_dh_auto_install:
python3 ./setup.py install --root=$(CURDIR)/debian/tmp --prefix=/usr --install-layout=deb 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=[ install_requires=[
'cryptography >= 2.1.4', 'cryptography >= 2.1.4',
'pyusb >= 1.0.0' 'pyusb >= 1.0.0',
'pyyaml >= 3.12'
], ],
data_files=[ data_files=[
('share/dbus-1/system.d/', ['dbus_service/io.github.uunicorn.Fprint.conf']), ('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 .tls import tls
from .util import assert_status from .util import assert_status
from binascii import hexlify from binascii import hexlify
@ -23,7 +25,7 @@ class User():
def __repr__(self): def __repr__(self):
return '<User: dbid=%04x identity=%s fingers=%s>' % (self.dbid, repr(self.identity), repr(self.fingers)) 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: if s < 0xf5 or s > 0xfe:
return 'Unknown' return 'Unknown'
@ -66,7 +68,7 @@ def parse_identity(b):
raise Exception('Don''t know how to handle identity type %d' % t) 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]) assert_status(rsp[:2])
rsp=rsp[2:] rsp=rsp[2:]
@ -88,7 +90,7 @@ def parse_user(rsp):
return user return user
def identity_to_bytes(identity): def identity_to_bytes(identity: str):
if isinstance(identity, SidIdentity): if isinstance(identity, SidIdentity):
b=identity.to_bytes() b=identity.to_bytes()
b = pack('<LL', 3, len(b)) + b b = pack('<LL', 3, len(b)) + b
@ -152,7 +154,7 @@ class Db():
def get_user(self, dbid): def get_user(self, dbid):
return parse_user(tls.cmd(pack('<BHHH', 0x4a, dbid, 0, 0))) 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') stg = self.get_user_storage(name='StgWindsor')
data = identity_to_bytes(identity) data = identity_to_bytes(identity)

View file

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

View file

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