diff --git a/README.md b/README.md index fd6419a..3e8a6e9 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,56 @@ 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. ## Setting up -Use Wine to extract [official Lenovo device driver](https://support.lenovo.com/es/es/downloads/DS120491) (also [part of the SCCM package](https://support.lenovo.com/ec/th/downloads/DS112113)). The only reason you need to do this is to find `6_07f_lenovo_mis.xpfwext` and copy it to this project location. - -You can just install that .exe and your file will be in `$WINEPREFIX/drive_c/DRIVERS/WIN/FPR/WBF_Drivers/6_07f_Lenovo.xpfwext` in case you use the device driver, or in -`$WINEPREFIX/drive_c/DRIVERS/SCCM/tp_t460s_*/*/Security/MT7FP22W/6_07f_Lenovo.xpfwext` if you used the SCCM package. - -Otherwise just use [innounp](https://sourceforge.net/projects/innounp/files/latest/download) and extract the exe file (`n1cgn08w.exe` in the device driver case or `tp_t460s_*.exe` in the SSCM one) with a command such as: - - WINEPREFIX=$XDG_RUNTIME_DIR/tmpwine wine ./innounp.exe -x -e n1cgn08w.exe 6_07f_Lenovo.xpfwext - -This file contains firmware and without it the pairing wont work. You will not need wine after this. To install Python dependencies run ``` $ pip3 install -r requirements.txt ``` -## Factory reset +## Initialization + +### Automatic factory reset, pairing and firmware flashing + +This repo includes `validity-sensors-initializer.py`, a simple tool that +helps initializing Validity fingerprint readers under linux, loading their +binary firmware and initializing them. + +This tool currently only supports these sensors: +- 138a:0090 Validity Sensors, Inc. VFS7500 Touch Fingerprint Sensor +- 138a:0097 Validity Sensors, Inc. +Which are present in various ThinkPad and HP laptops. + +These devices communicate with the laptop via an encrypted protocol and they +need to be paired with the host computer in order to work and compute the +TLS keys. +Such initialization is normally done by the Windows driver, however thanks to +the amazing efforts of Viktor Dragomiretskyy (uunicorn), and previously of +Nikita Mikhailov, we have reverse-engineerd the pairing process, and so it's +possible to do it under Linux with only native tools as well. + +The procedure is quite simple: +- Device is factory-reset and its flash repartitioned +- A TLS key is negotiated, generated via host hw ID and serial +- Windows driver is downloaded from Lenovo to extract the device firmware +- The device firmware is uploaded to the device +- The device is calibrated + +Once the chip is paired with the computer via this tool, it's possible to use +it in libfprint using the driver at +- https://github.com/3v1n0/libfprint/ + +--- + +### Getting the firmware + +It's possible to just extract [official Lenovo device driver for vfs0097](https://support.lenovo.com/us/en/downloads/DS121407) or [driver for vfs0090](https://support.lenovo.com/us/en/downloads/DS120491) (also [part of the SCCM package](https://support.lenovo.com/ec/th/downloads/DS112113) using [innoextract](https://constexpr.org/innoextract/) (available for all the distros), or `wine`. + +The only reason you need to do this is to find `6_07f_lenovo_mis.xpfwext` (for vfs0097) or `6_07f_Lenovo.xpfwext` (for vfs0090) and copy it to this project location. + + innoextract n1mgf03w.exe -e -I 6_07f_lenovo_mis.xpfwext # vfs0097 + innoextract n1cgn08w.exe -e -I 6_07f_Lenovo.xpfwext # vfs0090 + +### Factory reset If your device was previously paired with another OS or computer, you need to do a factory reset. This will erase all fingers from the internal database and make the device ready for pairing. ``` @@ -27,7 +60,7 @@ $ python3 factory-reset.py $ ``` -## Pairing +### Pairing After performing a factory reset you need to pair your device with a host computer. This must be done only once, before you can enroll/identify/verify fingers. ``` diff --git a/dbus-service.py b/dbus-service.py index a86fdce..054bd3c 100644 --- a/dbus-service.py +++ b/dbus-service.py @@ -6,8 +6,8 @@ from pydbus.generic import signal import pkg_resources from time import sleep from prototype import * -from proto97.db import subtype_to_string -from proto97.sensor import cancel_capture +from proto9x.db import subtype_to_string +from proto9x.sensor import cancel_capture import pwd print("Starting up") diff --git a/factory-reset.py b/factory-reset.py index b3e5c2d..9ff94c9 100644 --- a/factory-reset.py +++ b/factory-reset.py @@ -1,6 +1,6 @@ -from proto97.usb import usb -from proto97.sensor import factory_reset +from proto9x.usb import usb +from proto9x.sensor import factory_reset usb.open() factory_reset() diff --git a/led-dance.py b/led-dance.py new file mode 100644 index 0000000..cf0e21f --- /dev/null +++ b/led-dance.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import os + +from binascii import unhexlify +from enum import Enum +from proto9x.flash import read_flash +from proto9x.sensor import * +from proto9x.tls import tls as vfs_tls +from proto9x.usb import usb as vfs_usb +from time import sleep +from usb import core as usb_core + +class VFS(Enum): + DEV_90 = 0x0090 + DEV_97 = 0x0097 + + +if __name__ == "__main__": + 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=0x138a, idProduct=d.value) + if dev: + usb_dev = dev + + if not usb_dev: + raise Exception('No supported validity device found') + + vfs_usb.open(product=usb_dev.idProduct) + vfs_usb.send_init() + + try: + with open('/sys/class/dmi/id/product_name', 'r') as node: + product_name = node.read().strip() + with open('/sys/class/dmi/id/product_serial', 'r') as node: + product_serial = node.read().strip() + except: + product_name = 'VirtualBox' + product_serial = '0' + + vfs_tls.set_hwkey(product_name=product_name, serial_number=product_serial) + + # try to init TLS session from the flash + vfs_tls.parseTlsFlash(read_flash(1, 0, 0x1000)) + vfs_tls.open() + + for i in range(0, 10): + glow_start_scan() + sleep(0.05) + glow_end_enroll() + sleep(0.05) + + led_script = unhexlify( + '39ff100000ff03000001ff002000000000ffff0000ffff0000ff03000001ff00' \ + '200000000000000000ffff0000ff03000001ff002000000000ffff0000000000' \ + '0000000000000000000000000000000000000000000000000000000000000000' \ + '0000000000000000000000000000000000000000000000000000000000') + + assert_status(tls.app(led_script)) diff --git a/pair.py b/pair.py index b0749a4..22b2735 100644 --- a/pair.py +++ b/pair.py @@ -1,13 +1,13 @@ from time import sleep -from proto97.usb import usb -from proto97.tls import tls -from proto97.flash import read_flash -from proto97.init_flash import init_flash -from proto97.upload_fwext import upload_fwext -from proto97.calibrate import calibrate -from proto97.init_db import init_db +from proto9x.usb import usb +from proto9x.tls import tls +from proto9x.flash import read_flash +from proto9x.init_flash import init_flash +from proto9x.upload_fwext import upload_fwext +from proto9x.calibrate import calibrate +from proto9x.init_db import init_db #usb.trace_enabled=True #tls.trace_enabled=True diff --git a/proto97/usb.py b/proto97/usb.py deleted file mode 100644 index 0d837cb..0000000 --- a/proto97/usb.py +++ /dev/null @@ -1,119 +0,0 @@ - -import usb.core as ucore -from binascii import * -from .util import assert_status, unhex -from struct import unpack -from usb.util import claim_interface, release_interface -from queue import Queue -from threading import Thread -from usb.core import USBError - -init_hardcoded=unhex(''' -06020000015cb560afa595d0dcf4fca09ebb69301e2b9e24a5dfb6f2602833427d3bd65222c418ff15b54587ab2854c4fe9aea74fa55567 -2036fa3ec73c6912c58b1b49cbdcc7664165482cd70d5bb95d9d469626fdc2e012d74157c5792260968a2a4572b1ccccc26ec2e2ed0ddda -1b0931d3d0566460915d43e0a654a0582706be9113ea88f0c15ba058507efb0dadecb4a4cc64444dddf050b6e8d4ebb34ebba33e8651c75 -e5fb28f85c83197df1de460d9e1cb82208753ceff0ef60a823dba75d05548f5b3a5a0e2972232f7403bd6869da90e5371a0ab8ad23972f1 -597630f5ff7c8b8272800563477288b5591bbb0341d3975efc1778225767fa35480ff7f8dd633e4034ac32e4af58b86ebc63552cb35b12b -285255deaf3a32bf46cdc5ad3bc1c9ed1bcc112c72143f9aec568e2cacfa89ba0c7bb65590d8b93e6871a33c6c6983c0acd04e737ff55ee -e024ca6b9a48332c1a69a5a3fdd24b964cf7e7c55229bb0b48a6e339eb2c42d07ec850a4ee780660ad6c77ffa302a63bd19426134c4533d -69192ef2e16591df26394791a4ecb994a24f5a7f70f1eb2604e6bfb67a452cb74ead8b0d9808f890ac386750cbac06ee03a852145094053 -b2b074b9905de5cdbf2272b67e51f159164778d6d2ef7a1ccb81df9f896ddb38ce11e8140cf6cb9c82 -''') -init_hardcoded_clean_slate=unhex(''' -06020000012c40c9d271378bc0912ef5dced69bd81b7fc16972c7b46e621af54a00e2cc6baca6eb83ea30222dfc6c925262006ae93412ea -cf482f2034ee7b13297474b7e1e91f279cac2ccb7195443e4dd3328cfd292ade073fcc2eaa8f07b77231130ba997f921b9be7b4fb6cc691 -0d2976b3e050913b27dbe73afd6e964260b9435ebab5117e71f7cb68464d4b6f8afc7e1a421f671f5854a1d0c8ab93ed3b88b2bc1a42875 -e40b15f0e78496ac40e4a4a7fd39a97534ce1866479e0384f0789bbfc2fea0ce982bf7a9df97d60b237edbe1b26c9791043a96b81e435d6 -de5971c758d374905df95b0cddabfbf531749ba191f07a6f5e2722852f137a53513a9ec6ab30c3f09aa6ce21b391e55cf81dcda6422011b -f163317a9a4382546141d45f2274bd660103bd3af705f3ed12e493bc4f834d5d7f162e2c3405cf857b00129789a3353bf7fab7796e267e3 -062d55660dbbb857911ac8e871c460dd31c56a86a5631475f0f2ee5e9ce2af0faec0931a640ba2394025f29ffeca3a7e99c15a78ce1f1f7 -808cedd7601b9b6382d72ca873257d4f6af70e29e22afea15e36e0282b8f0bfc68ffa3417d212b8bbe11bb73b363a19872e6e947d45de30 -fbc493ca083a0a4650615d8628606362081ca6df5d67527971d1776ad76a7a28c932f0317b59cb4a82a14b2bcb7b01fb662be1496d24d91 -9140ec80068b21a818daa2fb8e05f63edbd4bd5797c74a28b3e7cf81c9045248584977711341fca3f08ba91ff853b62dc24ce4bba4ed57f -47bd458545d805b6bb14fe0cde01440b60bf7be937f6444a8e2a10ed8fa9ddb8604bb95fe411b97112e78dbf5a4a0f004669c93765a9f38 -665cb55f5658895c1c06a7aedf694bfb3afa9b8b1dea5ab85c821ac20b0663b95023642fda36ad78e3e00140b966f404f7e55f0b416ea43 -b4c74c39900830abc6906a1004bef1b5b7dbbbeb5ec1b22604ac86429b9f56511b746a7124c449b8c9498f49144abc2d64f6a114f1d7f91 -aa41249faeef4d838e280cb5d6fc19cfe86c75f -''') - -class Usb(): - def __init__(self): - self.trace_enabled = False - self.queue = Queue(maxsize=10) - self.quit = None - - def purge_int_queue(self): - try: - while True: - self.queue.get_nowait() - except: - pass - - def open(self): - self.dev = ucore.find(idVendor=0x138a, idProduct=0x0097) - self.dev.default_timeout = 15000 - self.thread = Thread(target=lambda: self.int_thread()) - self.thread.daemon = True - self.thread.start() - - def send_init(self): - #self.dev.set_configuration() - - # TODO analyse responses, detect hardware type - assert_status(self.cmd(unhexlify('01'))) - assert_status(self.cmd(unhexlify('19'))) - - # 43 -- get partition header(?) (02 -- fwext partition) - # c28c745a in response is a FwextBuildtime = 0x5A748CC2 - rsp=self.cmd(unhexlify('4302')) - - assert_status(self.cmd(init_hardcoded)) - - (err,), rsp = unpack('cmd> %s' % hexlify(out).decode()) - self.dev.write(1, out) - resp = self.dev.read(129, 100*1024) - resp = bytes(resp) - self.trace('cmd> %s' % hexlify(out).decode()) + self.dev.write(1, out) + resp = self.dev.read(129, 100*1024) + resp = bytes(resp) + self.trace('. + +import argparse +import os +import subprocess +import sys +import tempfile +import urllib.request + +from enum import Enum, auto +from time import sleep +from usb import core as usb_core + +from proto9x.calibrate import calibrate +from proto9x.flash import read_flash +from proto9x.init_db import init_db +from proto9x.init_flash import init_flash +from proto9x.sensor import factory_reset +from proto9x.tls import tls as vfs_tls +from proto9x.upload_fwext import upload_fwext +from proto9x.usb import usb as vfs_usb + +class VFS(Enum): + DEV_90 = 0x0090 + DEV_97 = 0x0097 + +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/n1mgf03w.exe', + 'referral': 'https://download.lenovo.com/pccbbs/mobiles/n1mgf03w.exe' + } +} + +DEFAULT_FW_NAMES = { + VFS.DEV_90: '6_07f_Lenovo.xpfwext', + VFS.DEV_97: '6_07f_lenovo_mis.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)) + + try: + if self.args.simulate_virtualbox: + raise(Exception()) + + with open('/sys/class/dmi/id/product_name', 'r') as node: + self.product_name = node.read().strip() + with open('/sys/class/dmi/id/product_serial', 'r') as node: + self.product_serial = node.read().strip() + except: + self.product_name = 'VirtualBox' + self.product_serial = '0' + + if self.args.host_product: + self.product_name = self.args.host_product + + if self.args.host_serial: + self.product_serial = self.args.host_serial + + vfs_tls.set_hwkey(product_name=self.product_name, + serial_number=self.product_serial) + + def open_device(self, init=False): + print('Opening device',hex(self.dev_type.value)) + vfs_usb.open(product=self.dev_type.value) + + if init: + vfs_usb.send_init() + + # try to init TLS session from the flash + vfs_tls.parseTlsFlash(read_flash(1, 0, 0x1000)) + 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, fwpath): + print('Pairing the sensor with device {}'.format(self.product_name)) + + max_retries = 5 + for i in range(0, max_retries): + try: + self.open_device() + + print('Initializing flash...') + init_flash() + break + except Exception as e: + 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(fw_path=fwpath) + + self.sleep() + self.restart() + + if self.args.calibration_data: + calib_data_file = self.args.calibration_data.name + else: + calib_data_file = 'calib-data.bin' + + print('Calibrating, re-using {}, if any...'.format(calib_data_file)) + if os.path.exists(calib_data_file): + calibrate(calib_data_path=calib_data_file) + else: + try: + calib_data_file = os.path.join(tempfile.mkdtemp(), 'calib-data.bin') + calibrate(calib_data_path=calib_data_file) + print('Calibration data saved at {}'.format(calib_data_file)) + except Exception as e: + print('Calibration failed using device data ({}), ' \ + 'You can try loading a local `calib-data.bin` blob, ' \ + 'the device should work anyway, so skipping...'.format(e)) + + 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') + + 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=0x138a, idProduct=d.value) + 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) + + 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(fwpath)