Support config files for SID mapping.
This allows interoperability with Windows.
This commit is contained in:
parent
9898b5d80a
commit
d2944ba762
10 changed files with 122 additions and 52 deletions
18
README.md
18
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
1
debian/control
vendored
|
|
@ -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
1
debian/install
vendored
|
|
@ -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
2
debian/rules
vendored
|
|
@ -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
8
etc/dbus-service.yaml
Normal 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"
|
||||
3
setup.py
3
setup.py
|
|
@ -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']),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
Loading…
Reference in a new issue