diff --git a/README.md b/README.md index 938e431..e986f92 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # python-validity -Validity fingerprint sensor library. -Originally designed to capture some of my findings for 138a:0097, but if you manage to get it working for some other Validity sensor - pull requests are welcome. +Validity fingerprint sensor driver. ## Setting up -On a Debian-based system, to re-install from sources (useful for testing): +On Ubuntu system: ``` -./setup.py install --force --install-layout deb --prefix=/usr --root=/ +$ sudo apt remove fprintd +$ sudo add-apt-repository ppa:uunicorn/open-fprintd +$ sudo aptget update +$ sudo apt install open-fprintd fprintd-clients python-validity +...wait a bit... +$ fprintd-enroll ``` ## Initialization diff --git a/bin/validity-sensors-firmware b/bin/validity-sensors-firmware new file mode 100755 index 0000000..998017f --- /dev/null +++ b/bin/validity-sensors-firmware @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# 2020 - Marco Trevisan +# +# Initializer for ThinkPad's validity sensors 138a:0090 and 138a:0097 and 06cb:009a +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import os +import subprocess +import sys +import tempfile +import urllib.request +import shutil + +from enum import Enum, auto +from time import sleep +from usb import core as usb_core + +python_validity_data='/usr/share/python-validity/' + +# FIXME: supported usb ids are duplicated in usb.py +class VFS(Enum): + DEV_90 = (0x138a, 0x0090) + DEV_97 = (0x138a, 0x0097) + DEV_9a = (0x06cb, 0x009a) + +DEFAULT_URIS = { + VFS.DEV_90: { + 'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe', + 'referral': 'https://support.lenovo.com/us/en/downloads/DS120491', + }, + VFS.DEV_97: { + 'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', + 'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe' + }, + VFS.DEV_9a: { + 'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', + 'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe' + } +} + +# FIXME: filenames are duplicated in upload_fwext.py +DEFAULT_FW_NAMES = { + VFS.DEV_90: '6_07f_Lenovo.xpfwext', + VFS.DEV_97: '6_07f_lenovo_mis_qm.xpfwext', + VFS.DEV_9a: '6_07f_lenovo_mis_qm.xpfwext' +} + + +def download_and_extract_fw(dev_type, fwdir, fwuri=None): + fwuri = fwuri if fwuri else DEFAULT_URIS[dev_type]['driver'] + fwarchive = os.path.join(fwdir, 'fwinstaller.exe') + fwname = DEFAULT_FW_NAMES[dev_type] + + print('Downloading {} to extract {}'.format(fwuri, fwname)) + + req = urllib.request.Request(fwuri) + req.add_header('Referer', DEFAULT_URIS[dev_type].get('referral', '')) + req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)') + + with urllib.request.urlopen(req) as response: + with open(fwarchive, 'wb') as out_file: + out_file.write(response.read()) + + subprocess.check_call(['innoextract', + '--output-dir', fwdir, + '--include', fwname, + '--collisions', 'overwrite', + fwarchive + ]) + + fwpath = subprocess.check_output([ + 'find', fwdir, '-name', fwname]).decode('utf-8').strip() + print('Found firmware at {}'.format(fwpath)) + + if not fwpath: + raise Exception('No {} found in the archive'.format(fwname)) + + return fwpath + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--driver-uri') + + args = parser.parse_args() + + if os.geteuid() != 0: + raise Exception('This script needs to be executed as root') + + dev_type = None + for d in VFS: + dev = usb_core.find(idVendor=d.value[0], idProduct=d.value[1]) + if dev: + dev_type = d + + if not dev_type: + raise Exception('No supported validity device found') + + try: + subprocess.check_call(['innoextract', '--version'], + stdout=subprocess.DEVNULL) + except Exception as e: + print('Impossible to run innoextract: {}'.format(e)) + sys.exit(1) + + with tempfile.TemporaryDirectory() as fwdir: + fwpath = download_and_extract_fw(dev_type, fwdir, fwuri=args.driver_uri) + shutil.copy(fwpath, python_validity_data) diff --git a/bin/validity-sensors-initializer b/bin/validity-sensors-initializer deleted file mode 100755 index c588318..0000000 --- a/bin/validity-sensors-initializer +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# 2020 - Marco Trevisan -# -# Initializer for ThinkPad's validity sensors 138a:0090 and 138a:0097 and 06cb:009a -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import argparse -import os -import subprocess -import sys -import tempfile -import urllib.request -import shutil - - -from enum import Enum, auto -from time import sleep -from usb import core as usb_core - -from validitysensor.flash import read_tls_flash -from validitysensor.init_db import init_db -from validitysensor.init_flash import init_flash -from validitysensor.sensor import sensor as vfs_sensor -from validitysensor.sensor import factory_reset -from validitysensor.tls import tls as vfs_tls -from validitysensor.upload_fwext import upload_fwext -from validitysensor.usb import usb as vfs_usb - -class VFS(Enum): - DEV_90 = (0x138a, 0x0090) - DEV_97 = (0x138a, 0x0097) - DEV_9a = (0x06cb, 0x009a) - -DEFAULT_URIS = { - VFS.DEV_90: { - 'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe', - 'referral': 'https://support.lenovo.com/us/en/downloads/DS120491', - }, - VFS.DEV_97: { - 'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', - 'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe' - }, - VFS.DEV_9a: { - 'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', - 'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe' - } -} - -DEFAULT_FW_NAMES = { - VFS.DEV_90: '6_07f_Lenovo.xpfwext', - VFS.DEV_97: '6_07f_lenovo_mis_qm.xpfwext', - VFS.DEV_9a: '6_07f_lenovo_mis_qm.xpfwext' -} - - -class VFSInitializer(): - def __init__(self, args, usb_dev, dev_type): - self.args = args - self.usb_dev = usb_dev - self.dev_type = dev_type - self.dev_str = repr(usb_dev) - - print('Found device {}'.format(self.dev_str)) - - if self.args.host_product or self.args.host_serial: - vfs_tls.set_hwkey(product_name=self.args.host_product, - serial_number=self.args.host_serial) - - def open_device(self, init=False): - print('Opening device',hex(self.dev_type.value[1])) - vfs_usb.open(vendor=self.dev_type.value[0], product=self.dev_type.value[1]) - - if init: - vfs_usb.send_init() - - # try to init TLS session from the flash - vfs_tls.parseTlsFlash(read_tls_flash()) - vfs_tls.open() - - def restart(self): - vfs_tls.reset() - self.open_device(init=True) - - def download_and_extract_fw(self, fwdir, fwuri=None): - fwuri = fwuri if fwuri else DEFAULT_URIS[self.dev_type]['driver'] - fwarchive = os.path.join(fwdir, 'fwinstaller.exe') - fwname = DEFAULT_FW_NAMES[self.dev_type] - - print('Downloading {} to extract {}'.format(fwuri, fwname)) - - req = urllib.request.Request(fwuri) - req.add_header('Referer', DEFAULT_URIS[self.dev_type].get('referral', '')) - req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)') - - with urllib.request.urlopen(req) as response: - with open(fwarchive, 'wb') as out_file: - out_file.write(response.read()) - - subprocess.check_call(['innoextract', - '--output-dir', fwdir, - '--include', fwname, - '--collisions', 'overwrite', - fwarchive - ]) - - fwpath = subprocess.check_output([ - 'find', fwdir, '-name', fwname]).decode('utf-8').strip() - print('Found firmware at {}'.format(fwpath)) - - if not fwpath: - raise Exception('No {} found in the archive'.format(fwname)) - - return fwpath - - def sleep(self, sec=3): - print('Sleeping...') - sleep(sec) - - def try_factory_reset(self): - self.open_device() - try: - print('Factory reset...') - factory_reset() - except Exception as e: - print('Factory reset failed with {}, this should not happen, but ' \ - 'we can ignore it, if pairing works...'.format(e)) - - def pair(self): - print('Pairing the sensor with device') - - max_retries = 5 - err = None - for i in range(0, max_retries): - try: - self.open_device() - - print('Initializing flash...') - init_flash() - break - except Exception as e: - if err is None: - err = e - self.sleep() - print('Try {} failed with error: {}'.format(i+1, e)) - finally: - max_retries -= 1 - - if max_retries == 0: - print('Device didn\'t show up after reset, retry...') - raise(err) - - self.sleep() - self.restart() - - print('Uploading firmware...') - upload_fwext() - - self.sleep() - self.restart() - - print('Calibrating...') - vfs_sensor.open() - vfs_sensor.calibrate() - - print('Init database...') - init_db() - - vfs_tls.reset() - - print('That\'s it, pairing with {} finished'.format(self.dev_str)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--driver-uri') - parser.add_argument('--firmware-path', type=argparse.FileType('r')) - parser.add_argument('--calibration-data', type=argparse.FileType('r')) - parser.add_argument('--host-product') - parser.add_argument('--host-serial') - parser.add_argument('--simulate-virtualbox', action='store_true') - parser.add_argument('--download-fw-only', action='store_true') - - args = parser.parse_args() - - if args.simulate_virtualbox and (args.host_product or args.host_serial): - parser.error("--simulate-virtualbox is incompatible with host params.") - - if os.geteuid() != 0: - raise Exception('This script needs to be executed as root') - - usb_dev = None - for d in VFS: - dev = usb_core.find(idVendor=d.value[0], idProduct=d.value[1]) - if dev: - dev_type = d - usb_dev = dev - - if not usb_dev: - raise Exception('No supported validity device found') - - try: - subprocess.check_call(['innoextract', '--version'], - stdout=subprocess.DEVNULL) - except Exception as e: - print('Impossible to run innoextract: {}'.format(e)) - sys.exit(1) - - vfs_initializer = VFSInitializer(args, usb_dev, dev_type) - - with tempfile.TemporaryDirectory() as fwdir: - if args.firmware_path: - fwpath = args.firmware_path.name - else: - fwpath = vfs_initializer.download_and_extract_fw(fwdir, - fwuri=args.driver_uri) - - shutil.copy(fwpath, '/usr/share/python-validity/') - - if not args.download_fw_only: - input('The device will be now reset to factory and associated to the ' \ - 'current laptop.\nPress Enter to continue (or Ctrl+C to cancel)...') - - vfs_initializer.try_factory_reset() - vfs_initializer.sleep() - vfs_initializer.pair() diff --git a/dbus_service/dbus-service b/dbus_service/dbus-service index 62ca9df..3c22625 100755 --- a/dbus_service/dbus-service +++ b/dbus_service/dbus-service @@ -2,6 +2,7 @@ import argparse import re +import sys import dbus import dbus.mainloop.glib import dbus.service @@ -15,7 +16,7 @@ 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 +from validitysensor.sensor import sensor, reboot, RebootException import pwd GLib.threads_init() @@ -166,14 +167,18 @@ if __name__ == '__main__': usb.trace_enabled = True tls.trace_enabled = True - if args.devpath is None: - init.open() - else: - z = re.match('^usb-(\d+)-(\d+)$', args.devpath) - if not z: - parser.error('Option --devpath should look like this: usb--
') + try: + if args.devpath is None: + init.open() + else: + z = re.match('^usb-(\d+)-(\d+)$', args.devpath) + if not z: + parser.error('Option --devpath should look like this: usb--
') - init.open_devpath(*map(int, z.groups())) + init.open_devpath(*map(int, z.groups())) + except RebootException: + print('Initialization ended up in rebooting the sensor. Normal exit.') + sys.exit(0) bus = dbus.SystemBus() diff --git a/debian/python3-validity.postinst b/debian/python3-validity.postinst index 0a27942..ca62874 100644 --- a/debian/python3-validity.postinst +++ b/debian/python3-validity.postinst @@ -4,6 +4,7 @@ set -e #DEBHELPER# if [ "$1" = "configure" ]; then + validity-sensors-firmware || true systemctl daemon-reload || true udevadm control --reload-rules || true udevadm trigger || true diff --git a/debian/python3-validity@.service b/debian/python3-validity@.service index 03bf2fd..d69c127 100644 --- a/debian/python3-validity@.service +++ b/debian/python3-validity@.service @@ -5,5 +5,6 @@ Description=python-validity driver dbus service for %I [Service] Type=simple ExecStart=/usr/lib/python-validity/dbus-service --devpath %i +StandardOutput=syslog Restart=no diff --git a/scripts/factory-reset.py b/scripts/factory-reset.py index 8589616..a997d50 100644 --- a/scripts/factory-reset.py +++ b/scripts/factory-reset.py @@ -1,6 +1,9 @@ from validitysensor.usb import usb -from validitysensor.sensor import factory_reset +from validitysensor.sensor import factory_reset, RebootException -usb.open() -factory_reset() +try: + usb.open() + factory_reset() +except RebootException: + pass diff --git a/scripts/pair.py b/scripts/pair.py deleted file mode 100644 index a16d694..0000000 --- a/scripts/pair.py +++ /dev/null @@ -1,44 +0,0 @@ - -from time import sleep - -from validitysensor.usb import usb -from validitysensor.tls import tls -from validitysensor.flash import read_tls_flash -from validitysensor.init_flash import init_flash -from validitysensor.upload_fwext import upload_fwext -from validitysensor.init_db import init_db -from validitysensor.sensor import sensor, reboot - -#usb.trace_enabled=True -#tls.trace_enabled=True - -def restart(): - print('Sleeping...') - sleep(3) - tls.reset() - usb.open() - usb.send_init() - tls.parseTlsFlash(read_tls_flash()) - tls.open() - -try: - usb.open() - print('Initializing flash...') - init_flash() - - restart() - print('Uploading firmware...') - upload_fwext() - - restart() - print('Calibrating...') - sensor.open(False) - sensor.calibrate() - - print('Init database...') - init_db() - - print('That\'s it, pairing\'s finished') -finally: - sleep(1) - reboot() diff --git a/setup.py b/setup.py index 9309715..64bab60 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup(name='python-validity', packages=['validitysensor'], scripts=[ 'bin/validity-led-dance', - 'bin/validity-sensors-initializer', + 'bin/validity-sensors-firmware', ], install_requires=[ 'cryptography >= 2.1.4', @@ -21,7 +21,6 @@ setup(name='python-validity', 'scripts/dbus-cmd.py', 'scripts/lsdbus.py', 'scripts/factory-reset.py', - 'scripts/pair.py', 'scripts/prototype.py' ]), ] diff --git a/validitysensor/flash.py b/validitysensor/flash.py index 984efd8..ce4dad8 100644 --- a/validitysensor/flash.py +++ b/validitysensor/flash.py @@ -121,10 +121,10 @@ def write_flash_all(partition, ptr, buf): write_flash(partition, ptr, chunk) ptr += len(chunk) -def read_flash_all(partition, start, end): +def read_flash_all(partition, start, size): bs = 0x1000 - blocks = [read_flash(partition, addr, bs) for addr in range(start, end, bs)] - return b''.join(blocks) + blocks = [read_flash(partition, addr, bs) for addr in range(start, start+size, bs)] + return b''.join(blocks)[:size] def write_fw_signature(partition, signature): rsp=tls.cmd(pack(' 0: - raise Exception('Flash is already partitioned') - cmd = unhex('4f 0000 0000') cmd += with_hdr(0, serialize_flash_params(info.ic)) cmd += with_hdr(1, b''.join([serialize_partition(p) for p in layout]) + get_partition_signature()) @@ -108,6 +103,14 @@ def partition_flash(layout, client_public): # ^ TODO - figure out what the rest of rsp means def init_flash(): + info = get_flash_info() + + if len(info.partitions) > 0: + print('Flash has %d partitions.' % len(info.partitions)) + return + else: + print('Flash was not initialized yet. Formatting...') + assert_status(usb.cmd(reset_blob)) skey = ec.generate_private_key(ec.SECP256R1(), crypto_backend) @@ -115,7 +118,7 @@ def init_flash(): client_private = snums.private_value client_public = snums.public_numbers - partition_flash(flash_layout_hardcoded, client_public) + partition_flash(info, flash_layout_hardcoded, client_public) rsp=usb.cmd(unhex('01')) assert_status(rsp) @@ -150,5 +153,6 @@ def init_flash(): # Persist certs and keys on cert partition. write_flash(1, 0, tls.makeTlsFlash()) - # Reboot + # Reboot. + # The device will disconnect and our service will be started by udev as soon as it is connected again. reboot() diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index 354b4a7..fa5a4e7 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -5,7 +5,7 @@ import os.path from .tls import tls from .usb import usb from .db import db, subtype_to_string -from .flash import write_enable, flush_changes, read_flash, erase_flash, write_flash_all +from .flash import write_enable, flush_changes, read_flash, erase_flash, write_flash_all, read_flash_all from time import sleep from struct import pack, unpack from .table_types import SensorTypeInfo, SensorCaptureProg @@ -55,9 +55,12 @@ def write_hw_reg32(addr, val): rsp=tls.cmd(pack('