Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Table of Contents
* [list devices failed ](#list-devices-failed)
* [Errors on startup](#errors-on-startup)
* [Fingerprint not working after waking up from suspend](#fingerprint-not-working-after-waking-up-from-suspend)
* [Synaptics 06cb:00a8](#synaptics-06cb00a8)
* [Enabling fingerprint for system authentication](#enabling-fingerprint-for-system-authentication)
* [The actual change from pam-auth-update](#the-actual-change-from-pam-auth-update)
* [Windows interoperability](#windows-interoperability)
Expand Down Expand Up @@ -85,6 +86,44 @@ $ sudo systemctl enable open-fprintd-resume open-fprintd-suspend

For even more error procedures, check [this Arch comment thread](https://aur.archlinux.org/packages/python-validity/#comment-755904) or [this python-validity bug comment thread](https://github.com/uunicorn/python-validity/issues/3).

## Synaptics 06cb:00a8

The "Synaptics WBDI Fingerprint Reader - USB 073" (06cb:00a8, sensor type 0x2449) is
found in laptops built on the Clevo N15xCU/ZU, among them the PC Specialist N150CU,
System76 Darter Pro, TUXEDO InfinityBook v5 and Wortmann Terra Mobile 1550. It has no
flash of its own, so python-validity keeps its pairing, its calibration and the enrolled
prints in `/var/lib/python-validity/`, and matches prints on the host. See
[issue #179](https://github.com/uunicorn/python-validity/issues/179) for how it was worked out.

1. Install numpy and OpenCV, which the host-side matcher needs:
```
$ sudo apt install python3-numpy python3-opencv # Debian, Ubuntu
$ sudo dnf install python3-numpy python3-opencv # Fedora
```
2. The sensor needs three firmware modules from the Windows driver's
`synaWudfBioUsb.dll`, version 5.5.2707.1073. Take the DLL from your laptop's
Windows fingerprint driver package (on an installed Windows, search
`C:\Windows\System32\DriverStore\FileRepository` for it) and extract the modules:
```
$ sudo validity-sensors-firmware --driver-dll /path/to/synaWudfBioUsb.dll
```
3. Restart the service and enroll:
```
$ sudo systemctl restart python3-validity
$ fprintd-enroll
```
The first operation pairs and calibrates the sensor, which takes a few seconds. Keep
the sensor clear of fingers until `fprintd-enroll` asks for one. Enrolling takes 18
touches.

The sensor reboots at the end of every operation, so the service opens it for each
enroll or verify rather than holding it open. Pairing uses the DMI product serial, so the
service must run as root, as packaged.

open-fprintd 0.7 and earlier show 5 enroll stages instead of 18, don't read the
finger-present and finger-needed properties, and can't delete a single finger. Enrolling,
verifying and deleting all of a user's fingers all work.

## Enabling fingerprint for system authentication

if it doesn't come automatically, you might need to make changes to files in `/etc/pam.d` to enable fingerprint login (depending on your distro).
Expand Down
16 changes: 15 additions & 1 deletion bin/validity-sensors-firmware
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import urllib.request

from usb import core as usb_core

from validitysensor import dll_tables
from validitysensor.init_data_dir import PYTHON_VALIDITY_DATA_DIR
from validitysensor.firmware_tables import FIRMWARE_NAMES, FIRMWARE_URIS
from validitysensor.usb import SupportedDevices
from validitysensor.usb import FLASHLESS_DEVICES, SupportedDevices



Expand Down Expand Up @@ -76,6 +77,7 @@ def download_and_extract_fw(dev_type, fwdir, fwuri=None):
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--driver-uri')
parser.add_argument('--driver-dll', help='synaWudfBioUsb.dll to extract the 06cb:00a8 modules from')

args = parser.parse_args()

Expand All @@ -91,6 +93,18 @@ if __name__ == "__main__":
if not dev_type:
raise Exception('No supported validity device found')

if dev_type in FLASHLESS_DEVICES:
# No public download of a driver package for this sensor is known yet, so the
# modules come from a synaWudfBioUsb.dll the user has (from the laptop's Windows
# driver package).
if not args.driver_dll:
parser.error('{} needs --driver-dll /path/to/synaWudfBioUsb.dll'.format(dev_type.name))
with open(args.driver_dll, 'rb') as f:
modules = dll_tables.extract(f.read())
dll_tables.save(modules)
print('Saved {} firmware modules to {}'.format(len(modules), dll_tables.MODULES_PATH))
sys.exit(0)

try:
subprocess.check_call(['innoextract', '--version'], stdout=subprocess.DEVNULL)
except Exception as e:
Expand Down
196 changes: 181 additions & 15 deletions dbus_service/dbus-service
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env python3

import argparse
import contextlib
import functools
import logging
import logging.handlers
import os
Expand All @@ -27,7 +29,7 @@ from validitysensor.init_data_dir import PYTHON_VALIDITY_DATA_DIR, init_data_dir
from validitysensor.sensor import sensor, RebootException
from validitysensor.sid import sid_from_string
from validitysensor.tls import tls
from validitysensor.usb import usb
from validitysensor.usb import usb, supported_devices, FLASHLESS_DEVICES
from validitysensor.fingerprint_constants import finger_ids

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
Expand All @@ -36,8 +38,6 @@ INTERFACE_NAME = 'io.github.uunicorn.Fprint.Device'

loop = GLib.MainLoop()

init_data_dir()

class NoEnrolledPrints(dbus.DBusException):
_dbus_error_name = 'net.reactivated.Fprint.Error.NoEnrolledPrints'

Expand Down Expand Up @@ -193,6 +193,154 @@ class Device(dbus.service.Object):
return hexlify(tls.app(unhexlify(cmd))).decode()


class FprintError(dbus.DBusException):
def __init__(self, name, message):
super().__init__(message, name='net.reactivated.Fprint.Error.' + name)


@contextlib.contextmanager
def fprint_errors():
"""hostmatch.backend exceptions -> the fprintd D-Bus errors clients know"""
from validitysensor.hostmatch.backend import Busy, InvalidFinger, NoEnrolledPrints
from validitysensor.hostmatch.store import StoreError

try:
yield
except Busy as e:
raise FprintError('AlreadyInUse', str(e)) from None
except NoEnrolledPrints as e:
raise FprintError('NoEnrolledPrints', str(e)) from None
except InvalidFinger as e:
raise FprintError('InvalidFingername', str(e)) from None
except StoreError as e:
raise FprintError('Internal', str(e)) from None


class HostDevice(dbus.service.Object):
"""The device for a flashless sensor, whose prints are stored and matched on the host.

Each operation opens its own session with the sensor and closes it when it ends (the
sensor reboots at the end of every session), so nothing is opened at startup.

Beyond what Device offers: DeleteEnrolledFinger, and org.freedesktop.DBus.Properties
with num-enroll-stages, name, scan-type, finger-needed and finger-present, which an
open-fprintd that asks for them passes on to fprintd clients. No RunCmd: raw sensor
commands over D-Bus aren't something to expose.
"""

def __init__(self, bus_name, make_backend):
dbus.service.Object.__init__(self, bus_name, '/io/github/uunicorn/Fprint/Device')
self.backend = make_backend(self.emit)

def emit(self, signal_name, *args):
"""Thread-safe: queue a signal onto the main loop."""
if signal_name == 'PropertiesChanged':
changed, = args
args = (INTERFACE_NAME, dbus.Dictionary(changed, signature='sv'),
dbus.Array([], signature='s'))

def fire():
getattr(self, signal_name)(*args)
return False

GLib.idle_add(fire)

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='s', out_signature='as')
def ListEnrolledFingers(self, user):
with fprint_errors():
return self.backend.list_enrolled_fingers(str(user))

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='s', out_signature='')
def DeleteEnrolledFingers(self, user):
with fprint_errors():
self.backend.delete_enrolled_fingers(str(user))

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='ss', out_signature='')
def DeleteEnrolledFinger(self, user, finger):
with fprint_errors():
self.backend.delete_enrolled_finger(str(user), str(finger))

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='ss', out_signature='')
def EnrollStart(self, user, finger):
logging.debug('In EnrollStart %s for %s' % (finger, user))
with fprint_errors():
self.backend.enroll_start(str(user), str(finger))

@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))
with fprint_errors():
self.backend.verify_start(str(user), str(finger))

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='', out_signature='')
def Cancel(self):
self.backend.cancel()

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='', out_signature='')
def Suspend(self):
# A session doesn't survive a suspend
logging.debug('In Suspend')
self.backend.cancel()

@dbus.service.method(dbus_interface=INTERFACE_NAME, in_signature='', out_signature='')
def Resume(self):
# Nothing to reopen: every operation opens its own session
logging.debug('In Resume')

@dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb')
def VerifyStatus(self, result, done):
logging.debug('VerifyStatus %s %s' % (result, done))

@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 %s %s' % (result, done))

@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v')
def Get(self, interface, prop):
props = self.GetAll(interface)
if prop not in props:
raise dbus.DBusException('No property %s' % prop,
name='org.freedesktop.DBus.Error.UnknownProperty')
return props[prop]

@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}')
def GetAll(self, interface):
if interface != INTERFACE_NAME:
raise dbus.DBusException('No interface %s' % interface,
name='org.freedesktop.DBus.Error.UnknownInterface')
return dbus.Dictionary(self.backend.properties, signature='sv')

@dbus.service.signal(dbus.PROPERTIES_IFACE, signature='sa{sv}as')
def PropertiesChanged(self, interface, changed, invalidated):
pass


def host_backend(usb_id, emit):
from validitysensor.hostmatch.backend import Backend, DeviceSession
from validitysensor.hostmatch.matcher import Matcher
from validitysensor.hostmatch.store import TemplateStore

return Backend(TemplateStore(), Matcher(), emit,
open_session=functools.partial(DeviceSession, *usb_id))


def find_flashless(devpath):
"""(idVendor, idProduct) of the sensor we'd open, if it's a flashless one."""
if devpath is None:
dev = usb_core.find(custom_match=lambda d: (d.idVendor, d.idProduct) in supported_devices)
else:
busnum, address = devpath
dev = usb_core.find(custom_match=lambda d: d.bus == busnum and d.address == address)
if dev is None:
return None
usb_id = (dev.idVendor, dev.idProduct)
return usb_id if supported_devices.get(usb_id) in FLASHLESS_DEVICES else None


backoff_file = PYTHON_VALIDITY_DATA_DIR + 'backoff'


Expand Down Expand Up @@ -255,28 +403,40 @@ def main():
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")

init_data_dir()
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-<busnum>-<address>')
devpath = None
if args.devpath is not None:
z = re.match(r'^usb-(\d+)-(\d+)$', args.devpath)
if not z:
parser.error('Option --devpath should look like this: usb-<busnum>-<address>')
devpath = tuple(map(int, z.groups()))

init.open_devpath(*map(int, z.groups()))
except RebootException:
logging.debug('Initialization ended up in rebooting the sensor. Normal exit.')
sys.exit(0)
flashless = find_flashless(devpath)
if flashless is None:
try:
if devpath is None:
init.open()
else:
init.open_devpath(*devpath)
except RebootException:
logging.debug('Initialization ended up in rebooting the sensor. Normal exit.')
sys.exit(0)

bus = dbus.SystemBus()

svc = Device(bus, config)
if flashless is None:
svc = Device(bus, config)
else:
svc = HostDevice(bus, functools.partial(host_backend, flashless))

def watch_cb(name):
if name == '':
logging.debug('Manager is offline')
if isinstance(svc, HostDevice):
# Nobody is left to stop a running operation
svc.backend.cancel()
else:
logging.debug('Manager is back online, registering')
mgr = bus.get_object(name, '/net/reactivated/Fprint/Manager')
Expand All @@ -299,6 +459,12 @@ def main():
except Exception as e:
logging.exception(e)
raise e
finally:
if isinstance(svc, HostDevice):
# Let a running operation end its session, so the next start doesn't find the
# sensor mid-TLS
svc.backend.cancel()
svc.backend.join(timeout=15)


if __name__ == '__main__':
Expand Down
2 changes: 2 additions & 0 deletions dbus_service/io.github.uunicorn.Fprint.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
<policy user="root">
<!-- Allow open-fprintd service call our methods -->
<allow send_interface="io.github.uunicorn.Fprint.Device"/>
<!-- and read the device properties (num-enroll-stages etc.) -->
<allow send_interface="org.freedesktop.DBus.Properties"/>
</policy>
</busconfig>
2 changes: 2 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Depends: ${python3:Depends},
dbus,
open-fprintd (>= 0.6~),
innoextract (>= 1.6~)
Recommends: python3-numpy,
python3-opencv
Description: Validity Fingerprint Sensor DBus Driver
This package adds support to some Validity sensors.
.
Expand Down
1 change: 1 addition & 0 deletions debian/install
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
usr/bin
usr/lib/python*/*-packages/*.egg-info
usr/lib/python*/*-packages/validitysensor/*.py
usr/lib/python*/*-packages/validitysensor/hostmatch/*.py
usr/lib/python-validity
usr/share/dbus-1
usr/share/python-validity
Expand Down
3 changes: 3 additions & 0 deletions debian/python3-validity.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ After=open-fprintd.service
Type=simple
ExecStart=/usr/lib/python-validity/dbus-service --debug
Restart=no
# Host-side state for flashless sensors (06cb:00a8): pairing, calibration, prints.
StateDirectory=python-validity
StateDirectoryMode=0700

[Install]
WantedBy=multi-user.target
8 changes: 8 additions & 0 deletions debian/python3-validity.udev
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ ENV{DEVTYPE}!="usb_device", GOTO="python_validity_end"
ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0090", GOTO="python_validity_match"
ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0097", GOTO="python_validity_match"
ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="009a", GOTO="python_validity_match"
ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="00a8", GOTO="python_validity_flashless"

GOTO="python_validity_end"

LABEL="python_validity_flashless"

# No stop on remove: this sensor reboots and re-enumerates at the end of every
# session, and the service opens it per operation and copes with it being absent.
ACTION=="add|change", RUN+="/bin/systemctl --no-block start python3-validity.service"
GOTO="python_validity_end"

LABEL="python_validity_match"

#TAG+="validity"
Expand Down
Loading