├── .gitignore ├── README.md ├── dbus-service.py ├── factory-reset.py ├── holdthedoor.py ├── pair.py ├── proto9x ├── blobs.py ├── blobs_90.py ├── blobs_97.py ├── calibrate.py ├── db.py ├── flash.py ├── hw_tables.py ├── init_db.py ├── init_flash.py ├── sensor.py ├── sid.py ├── tls.py ├── upload_fwext.py ├── usb.py └── util.py ├── prototype.py ├── requirements.txt ├── setup.py ├── snap ├── local │ └── snap-launcher.sh └── snapcraft.yaml └── validity-sensors-tools /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | tls.dict 3 | *.bin 4 | *.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-validity 2 | Validity fingerprint sensor library. 3 | 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. 4 | 5 | ## Setting up 6 | 7 | To install Python dependencies run 8 | ``` 9 | $ pip3 install -r requirements.txt 10 | ``` 11 | 12 | ## Initialization 13 | 14 | ### Automatic factory reset, pairing and firmware flashing 15 | 16 | This repo includes `validity-sensors-tools.py`, a simple collection of 17 | tools that helps initializing Validity fingerprint readers under linux, 18 | loading their binary firmware and initializing them. 19 | 20 | This tool currently only supports these sensors: 21 | - 138a:0090 Validity Sensors, Inc. VFS7500 Touch Fingerprint Sensor 22 | - 138a:0097 Validity Sensors, Inc. 23 | Which are present in various ThinkPad and HP laptops. 24 | 25 | These devices communicate with the laptop via an encrypted protocol and they 26 | need to be paired with the host computer in order to work and compute the 27 | TLS keys. 28 | Such initialization is normally done by the Windows driver, however thanks to 29 | the amazing efforts of Viktor Dragomiretskyy (uunicorn), and previously of 30 | Nikita Mikhailov, we have reverse-engineerd the pairing process, and so it's 31 | possible to do it under Linux with only native tools as well. 32 | 33 | The procedure is quite simple: 34 | - Device is factory-reset and its flash repartitioned 35 | - A TLS key is negotiated, generated via host hw ID and serial 36 | - Windows driver is downloaded from Lenovo to extract the device firmware 37 | - The device firmware is uploaded to the device 38 | - The device is calibrated 39 | 40 | For 138a:0097 it's also possible to enroll fingers in the internal storage 41 | doing: 42 | `validity-sensors-tools.py --tool enroll --finger-id [0-9]` 43 | 44 | Once the chip is paired with the computer via this tool, it's possible to use 45 | it in libfprint using the driver at 46 | - https://github.com/3v1n0/libfprint/ 47 | 48 | #### Installing it as [snap](https://snapcraft.io/) 49 | 50 | This tool can be easily installed [almost every linux distribution](https://snapcraft.io/docs/installing-snapd) 51 | with all its dependencies as snap. 52 | 53 | To do so: 54 | 55 | ```bash 56 | sudo snap install validity-sensors-tools 57 | 58 | # Give it access to the usb devices 59 | sudo snap connect validity-sensors-tools:raw-usb 60 | sudo snap connect validity-sensors-tools:hardware-observe 61 | 62 | # Initialize the device 63 | sudo validity-sensors-tools.initializer 64 | 65 | # Test the device 66 | sudo validity-sensors-tools.led-test 67 | 68 | # See other avilable tools 69 | validity-sensors-tools --help 70 | ``` 71 | 72 | [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/validity-sensors-tools) 73 | 74 | #### Installing from AUR on Arch Linux 75 | 76 | Install [validity-sensors-tools-git[AUR]](https://aur.archlinux.org/packages/validity-sensors-tools-git/). 77 | 78 | You can run it as `validity-sensors-tools`: 79 | 80 | ```bash 81 | sudo validity-sensors-tools -t led-dance 82 | ``` 83 | 84 | --- 85 | 86 | ### Getting the firmware 87 | 88 | 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`. 89 | 90 | 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. 91 | 92 | innoextract n1mgf03w.exe -e -I 6_07f_lenovo_mis.xpfwext # vfs0097 93 | innoextract n1cgn08w.exe -e -I 6_07f_Lenovo.xpfwext # vfs0090 94 | 95 | ### Factory reset 96 | If your device was previously paired with another OS or computer, you need to do a factory reset. 97 | This will erase all fingers from the internal database and make the device ready for pairing. 98 | ``` 99 | $ python3 factory-reset.py 100 | $ 101 | ``` 102 | 103 | ### Pairing 104 | After performing a factory reset you need to pair your device with a host computer. 105 | This must be done only once, before you can enroll/identify/verify fingers. 106 | ``` 107 | $ python3 pair.py 108 | Initializing flash... 109 | Detected Flash IC: W25Q80B, 1048576 bytes 110 | Clean slate 111 | Uploading firmware... 112 | Sensor: VSI 55E FM209-001 113 | Loaded FWExt version 1.1 (Sat Feb 3 05:07:30 2018), 8 modules 114 | Calibrating... 115 | Sensor: VSI 55E FM209-001 116 | FWExt version 1.1 (Sat Feb 3 05:07:30 2018), 8 modules 117 | Calibration data loaded from the file. 118 | Init database... 119 | Creating a new user storage object 120 | Creating a host machine GUID record 121 | That's it, pairing's finished 122 | $ 123 | ``` 124 | 125 | ## Examples 126 | Here is a couple of examples of how you can use this library. All examples assume that your device is already paired. 127 | 128 | ### Initialize a session 129 | Before talking to a device you will need to open it and start a new TLS session 130 | ``` 131 | $ python3 132 | Python 3.6.7 (default, Oct 22 2018, 11:32:17) 133 | [GCC 8.2.0] on linux 134 | Type "help", "copyright", "credits" or "license" for more information. 135 | >>> from prototype import * 136 | >>> open97() 137 | >>> 138 | ``` 139 | Or load previosly saved TLS session (see comments in [holdthedoor.py](holdthedoor.py)) 140 | ``` 141 | $ python3 142 | Python 3.6.7 (default, Oct 22 2018, 11:32:17) 143 | [GCC 8.2.0] on linux 144 | Type "help", "copyright", "credits" or "license" for more information. 145 | >>> from prototype import * 146 | >>> load97() 147 | >>> 148 | ``` 149 | 150 | ### Enroll a new user 151 | Note: 0xf5 == WINBIO_FINGER_UNSPECIFIED_POS_01 (see [ms docs](https://docs.microsoft.com/en-us/windows/desktop/SecBioMet/winbio-ansi-381-pos-fingerprint-constants)) 152 | ``` 153 | >>> db.dump_all() 154 | 8: User S-1-5-21-111111111-1111111111-1111111111-1000 with 1 fingers: 155 | 9: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 156 | >>> enroll(sid_from_string('S-1-5-21-394619333-3876782012-1672975908-3333'), 0xf5) 157 | Waiting for a finger... 158 | Progress: 14 % done 159 | Progress: 28 % done 160 | Progress: 42 % done 161 | Progress: 57 % done 162 | Progress: 71 % done 163 | Progress: 85 % done 164 | Progress: 100 % done 165 | All done 166 | 11 167 | >>> db.dump_all() 168 | 8: User S-1-5-21-111111111-1111111111-1111111111-1000 with 1 fingers: 169 | 9: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 170 | 10: User S-1-5-21-394619333-3876782012-1672975908-3333 with 1 fingers: 171 | 11: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 172 | >>> 173 | ``` 174 | 175 | ### Delete database record (user/finger/whatever) 176 | ``` 177 | >>> db.dump_all() 178 | 8: User S-1-5-21-111111111-1111111111-1111111111-1000 with 1 fingers: 179 | 9: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 180 | 10: User S-1-5-21-394619333-3876782012-1672975908-3333 with 1 fingers: 181 | 11: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 182 | >>> db.del_record(11) 183 | >>> db.dump_all() 184 | 8: User S-1-5-21-111111111-1111111111-1111111111-1000 with 1 fingers: 185 | 9: f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) 186 | 10: User S-1-5-21-394619333-3876782012-1672975908-3333 with 0 fingers: 187 | >>> 188 | ``` 189 | 190 | ### Identify a finger (scan) 191 | ``` 192 | >>> identify() 193 | Recognised finger f5 (WINBIO_FINGER_UNSPECIFIED_POS_01) from user S-1-5-21-111111111-1111111111-1111111111-1000 194 | Template hash: 36bc1fe077e59a3090c816fcf2798c30a85d8a8fbe000ead5c6a946c3bacef7b 195 | ``` 196 | ## DBus service 197 | Sources contain a simple DBus service which can impersonate [fprint](https://www.freedesktop.org/wiki/Software/fprint/) daemon. 198 | Install fprint, edit /usr/share/dbus-1/system-services/net.reactivated.Fprint.service by commenting out activation info: 199 | ``` 200 | #Exec=/usr/lib/fprintd/fprintd 201 | #User=root 202 | #SystemdService=fprintd.service 203 | ``` 204 | start a fake service with something like this: 205 | ``` 206 | while sleep 1; do 207 | python3 dbus-service.py 208 | echo "====restart===" 209 | done 210 | ``` 211 | 212 | 213 | ## Debugging 214 | If you are curious you can enable tracing to see what flows in and out of device before and after encryption 215 | ``` 216 | >>> tls.trace_enabled=True 217 | >>> usb.trace_enabled=True 218 | >>> db.dump_all() 219 | >tls> 17: 4b00000b0053746757696e64736f7200 220 | >cmd> 1703030050c00a7ff1cf76e90f168141b4bc519ca9598eacb575ff01b7552a3707be8506b246d5272cb119e7b8b3eccd991cb7d8387245953ff1da62cebfb07fae7e47b9b536fb1a82185cc9399d30625ee3c1451f 221 | tls> 17: 4a080000000000 224 | >cmd> 1703030040ef982e5d6c403ff636c44cd53e7d0f98c21f67ff3b5b80f53555e4547028bd4d17cf5b0539ac0489238f1f066b8ba849120380cf979088d6c63249c873868c95 225 | tls> 17: 4a0a0000000000 228 | >cmd> 1703030040b522c55b73480e0d71a322abf8b65d97c9b55e9930206c463f998886cda4410d1b00ab41ec5b213d2ac18bf3bf61ce817446f27d643f99aba5a1d4cb80d18461 229 | >> 235 | ``` 236 | -------------------------------------------------------------------------------- /dbus-service.py: -------------------------------------------------------------------------------- 1 | 2 | from threading import Thread 3 | from gi.repository import GLib 4 | from pydbus import SystemBus 5 | from pydbus.generic import signal 6 | import pkg_resources 7 | from time import sleep 8 | from prototype import * 9 | from proto9x.db import subtype_to_string 10 | from proto9x.sensor import cancel_capture 11 | import pwd 12 | 13 | print("Starting up") 14 | 15 | loop = GLib.MainLoop() 16 | 17 | usb.quit = lambda e: loop.quit() 18 | 19 | def uname2identity(uname): 20 | if uname == '': 21 | # For some reason Gnome enrollment UI does not send the user name 22 | # (it also ignores our num-enroll-stages attribute). I probably should upgrade Gnome 23 | print('No username specified. ') 24 | uname = 'unicorn' 25 | pw=pwd.getpwnam(uname) 26 | sidstr='S-1-5-21-111111111-1111111111-1111111111-%d' % pw.pw_uid 27 | return sid_from_string(sidstr) 28 | 29 | class AlreadyInUse(Exception): 30 | __name__ = 'net.reactivated.Fprint.Error.AlreadyInUse' 31 | 32 | class Device(): 33 | name = 'Validity sensor' 34 | 35 | def __init__(self): 36 | setattr(self, 'num-enroll-stages', 7) 37 | setattr(self, 'scan-type', 'press') 38 | self.capturing = False 39 | 40 | def Claim(self, usr): 41 | print('In Claim %s' % usr) 42 | 43 | def Release(self): 44 | print('In Release') 45 | self.caimed = False 46 | 47 | def ListEnrolledFingers(self, usr): 48 | try: 49 | print('In ListEnrolledFingers %s' % usr) 50 | 51 | usr=db.lookup_user(uname2identity(usr)) 52 | 53 | if usr == None: 54 | print('User not found on this device') 55 | return [] 56 | 57 | rc = [subtype_to_string(f['subtype']) for f in usr.fingers] 58 | print(repr(rc)) 59 | return rc 60 | except Exception as e: 61 | loop.quit() 62 | raise e 63 | 64 | def do_scan(self): 65 | if self.capturing: 66 | return 67 | 68 | try: 69 | self.capturing = True 70 | z=identify() 71 | self.VerifyStatus('verify-match', True) 72 | except Exception as e: 73 | self.VerifyStatus('verify-no-match', True) 74 | #loop.quit(); 75 | raise e 76 | finally: 77 | self.capturing = False 78 | 79 | def VerifyStart(self, finger_name): 80 | print('In VerifyStart %s' % finger_name) 81 | Thread(target=lambda: self.do_scan()).start() 82 | #self.do_scan() 83 | 84 | def VerifyStop(self): 85 | print('In VerifyStop') 86 | cancel_capture() 87 | 88 | def DeleteEnrolledFingers(self, user): 89 | print('In DeleteEnrolledFingers %s' % user) 90 | usr=db.lookup_user(uname2identity(user)) 91 | 92 | if usr == None: 93 | print('User not found on this device') 94 | return 95 | 96 | db.del_record(usr.dbid) 97 | 98 | def do_enroll(self, finger_name): 99 | # it is pointless to try and remember username passed in claim as Gnome does not seem to be passing anything useful anyway 100 | try: 101 | # hardcode the username and finger for now 102 | z=enroll(uname2identity('unicorn'), 0xf5) 103 | print('Enroll was successfull') 104 | self.EnrollStatus('enroll-completed', True) 105 | except Exception as e: 106 | self.EnrollStatus('enroll-failed', True) 107 | #loop.quit(); 108 | raise e 109 | 110 | def EnrollStart(self, finger_name): 111 | print('In EnrollStart %s' % finger_name) 112 | Thread(target=lambda: self.do_enroll(finger_name)).start() 113 | 114 | 115 | def EnrollStop(self): 116 | print('In EnrollStop') 117 | 118 | VerifyFingerSelected = signal() 119 | VerifyStatus = signal() 120 | EnrollStatus = signal() 121 | 122 | class Manager(): 123 | def GetDevices(self): 124 | print('In GetDevices') 125 | return ['/net/reactivated/Fprint/Device/0'] 126 | 127 | def GetDefaultDevice(self): 128 | print('In GetDefaultDevice') 129 | return '/net/reactivated/Fprint/Device/0' 130 | 131 | 132 | def readif(fn): 133 | with open('/usr/share/dbus-1/interfaces/' + fn, 'rb') as f: 134 | # for some reason Gio can't seem to handle XML entities declared inline 135 | return f.read().decode('utf-8') \ 136 | .replace('&ERROR_CLAIM_DEVICE;', 'net.reactivated.Fprint.Error.ClaimDevice') \ 137 | .replace('&ERROR_ALREADY_IN_USE;', 'net.reactivated.Fprint.Error.AlreadyInUse') \ 138 | .replace('&ERROR_INTERNAL;', 'net.reactivated.Fprint.Error.Internal') \ 139 | .replace('&ERROR_PERMISSION_DENIED;', 'net.reactivated.Fprint.Error.PermissionDenied') \ 140 | .replace('&ERROR_NO_ENROLLED_PRINTS;', 'net.reactivated.Fprint.Error.NoEnrolledPrints') \ 141 | .replace('&ERROR_NO_ACTION_IN_PROGRESS;', 'net.reactivated.Fprint.Error.NoActionInProgress') \ 142 | .replace('&ERROR_INVALID_FINGERNAME;', 'net.reactivated.Fprint.Error.InvalidFingername') \ 143 | .replace('&ERROR_NO_SUCH_DEVICE;', 'net.reactivated.Fprint.Error.NoSuchDevice') 144 | 145 | Device.dbus=[readif('net.reactivated.Fprint.Device.xml')] 146 | Manager.dbus=[readif('net.reactivated.Fprint.Manager.xml')] 147 | 148 | 149 | bus = SystemBus() 150 | bus.publish('net.reactivated.Fprint', 151 | ('/net/reactivated/Fprint/Manager', Manager()), 152 | ('/net/reactivated/Fprint/Device/0', Device()) 153 | ) 154 | 155 | open97() 156 | 157 | usb.trace_enabled = True 158 | tls.trace_enabled = True 159 | 160 | loop.run() 161 | 162 | print("Normal exit") 163 | -------------------------------------------------------------------------------- /factory-reset.py: -------------------------------------------------------------------------------- 1 | 2 | from proto9x.usb import usb 3 | from proto9x.sensor import factory_reset 4 | 5 | usb.open() 6 | factory_reset() 7 | -------------------------------------------------------------------------------- /holdthedoor.py: -------------------------------------------------------------------------------- 1 | # There seems to be either a bug or a feature which renders the scanner unusable if 2 | # there were too many attemps to establish TLS connection with it. While device 3 | # seems to magically fix itself after a while (8 hours?) it is extremely annoying 4 | # when hacking. 5 | # 6 | # It is probably not that bad if all communication is done by a single service process which 7 | # only initiates TLS connection once during system startup and then simply serves 8 | # requests via DBus, DCOM or whatever. It is not that nice when you have a 9 | # standalone program which you are hacking and restarting all the time. 10 | # 11 | # So, to workaround the problem I'm trying to save and restore the TLS state 12 | # between the prototype invocation. This works fine with one exception. As soon as 13 | # you close the last file descriptor associated with a USB device, the kernel automatically 14 | # resets the device, effectively killing the established TLS state. 15 | # 16 | # This script helps to work around this last problem. It keeps an open descriptor which 17 | # prevents kernel from resetting the device configuration. It does not interfere with 18 | # the main process and does not hold the claim on the inface. It just sits there 19 | # doing nothing until you decide to quit. 20 | # 21 | # The same can be achived by running something like "read 4>> 6f 000e 000000000000 63 | # <<< 0000 880d 0000 07000000 64 | # 0800 0000 9400 0e00 0300 0080 07000000 7e7f807f808080808080808080808080808080808080818081808180818080808080818081808080818081808180818081808180818081808180808081808180808081807f80808180808081808180818080808180818081808180818081808080818081808180818081808180818081808180818081808180818081808080808080808080807f807f807f807f7f7e7e 65 | # a400 0000 0800 0e00 0200 0000 00000000 0d007100 66 | # b400 0000 0800 0e00 0800 0080 db000000 00000000 67 | # c400 0000 0400 0e00 0500 0080 1c6f0400 68 | # d000 0000 9400 0e00 0700 0080 07000000 2b23203c2d182e1e30182e1c321d341d341e321c301e1e241e201f201d1c321a301e1c211e21341f1e202024201f1e20201f212221221d221e23341e1d1e1d20341f1d193b341c1d1e35201e201c20221f341c1e1e1c221f201d21201e1c1f34242221201f20221f201e241e241d2020221e2420231d221e211e1f1e1e341c321e3220301d2d302f2d2c2b23223a211c 69 | # 6c01 0000 1400 0e00 0f00 0080 05550007 7701002805720000080100020811e107 70 | # 8801 0000 0c00 0e00 1200 0080 07000000 7002 7800 7002 7800 71 | # 72 | # Empty reply: 73 | # >>> 6f 000a 000000000000 74 | # <<< 0000 880d 0000 00000000 75 | 76 | rsp=tls.cmd(calibrate_prg) 77 | assert_status(rsp) 78 | # ^ TODO check what the rest of the rsp means 79 | 80 | calib_data=usb.read_82() 81 | print('len=%d' % len(calib_data)) 82 | with open(calib_data_path, 'wb') as f: 83 | f.write(calib_data) 84 | 85 | lines=[calib_data[i:i+0x90+8] for i in range(0, len(calib_data), 0x90+8)] # TODO work out where "bytes per line" constant is comming from 86 | lines=[Line(i) for i in lines] 87 | frame4=[i.serialize() for i in lines if i.frame == 4] # why 4? 88 | frame4=b''.join(frame4) 89 | calib_data=pack('' % (self.dbid, repr(self.name), repr(self.users)) 19 | 20 | class User(): 21 | def __init__(self, dbid, identity): 22 | self.dbid=dbid 23 | self.identity=identity 24 | self.fingers=[] 25 | 26 | def __repr__(self): 27 | return '' % (self.dbid, repr(self.identity), repr(self.fingers)) 28 | 29 | def subtype_to_string(s): 30 | if s < 0xf5 or s > 0xfe: 31 | return 'Unknown' 32 | 33 | return 'WINBIO_FINGER_UNSPECIFIED_POS_%02d' % (s - 0xf5 + 1) 34 | 35 | def parse_user_storage(rsp): 36 | rc, = unpack(' 0: 50 | raise Exception('Junk at the end of the storage info response: %s' % rsp.hex()) 51 | 52 | storage=UserStorage(recid, name) 53 | 54 | while len(usrtab) > 0: 55 | rec, usrtab = usrtab[:4], usrtab[4:] 56 | urid, valsz = unpack(' 0: 82 | raise Exception('Junk at the end of the user info response: %s' % rsp.hex()) 83 | 84 | identity = parse_identity(identity) 85 | user=User(recid, identity) 86 | 87 | while len(fingertab) > 0: 88 | rec, fingertab = fingertab[:8], fingertab[8:] 89 | frid, subtype, stgid, valsz = unpack('' % ( 118 | self.dbid, 119 | self.type, 120 | self.storage, 121 | repr(self.value), 122 | repr(self.children) 123 | ) 124 | 125 | 126 | class Db(): 127 | def get_user_storage(self, dbid=0, name=''): 128 | name=name.encode() 129 | 130 | if len(name) > 0: 131 | name += b'\0' 132 | 133 | return parse_user_storage(tls.cmd(pack('>> 4302 -- get partition header (get fwext info) 47 | # b004 -- no fw detected 48 | # 0000 49 | # 0100 (major) 0100 (minor) 0800 (modules) c28c745a (buildtime) 50 | # type subtype major minor size 51 | # 0100 3446 0200 0700 d03e0000 52 | # 0100 8408 0100 0700 00040000 53 | # 0200 8428 0300 1200 e0100000 54 | # 0200 7636 0100 0c00 100a0000 55 | # 0100 8647 0000 0100 505a0000 56 | # 0200 2377 0000 0100 802f0000 57 | # 0200 6637 0100 0c00 f0220200 58 | # 0100 2556 0000 0100 60040000 59 | class ModuleInfo(): 60 | def __init__(self, type, subtype, major, minor, size): 61 | self.type, self.subtype, self.major, self.minor, self.size = type, subtype, major, minor, size 62 | 63 | def __repr__(self): 64 | return 'ModuleInfo(0x%04x, 0x%04x, %d, %d, %d)' % (self.type, self.subtype, self.major, self.minor, self.size) 65 | 66 | class FirmwareInfo(): 67 | def __init__(self, major, minor, buildtime, modules): 68 | self.major, self.minor, self.buildtime, self.modules = major, minor, buildtime, modules 69 | 70 | def __repr__(self): 71 | return 'FirmwareInfo(%d, %d, %d, %s)' % (self.major, self.minor, self.buildtime, repr(self.modules)) 72 | 73 | def get_fw_info(partition): 74 | rsp=tls.cmd(pack(' 0: 117 | chunk, buf = buf[:bs], buf[bs:] 118 | write_flash(partition, ptr, chunk) 119 | ptr += len(chunk) 120 | 121 | def read_flash_all(partition, start, end): 122 | bs = 0x1000 123 | blocks = [read_flash(partition, addr, bs) for addr in range(start, end, bs)] 124 | return b''.join(blocks) 125 | 126 | def write_fw_signature(partition, signature): 127 | rsp=tls.cmd(pack(' 0: 75 | raise Exception('Flash is already partitioned') 76 | 77 | cmd = unhex('4f 0000 0000') 78 | cmd += with_hdr(0, serialize_flash_params(info.ic)) 79 | cmd += with_hdr(1, b''.join([serialize_partition(p) for p in layout])) 80 | cmd += with_hdr(5, make_cert(client_public)) 81 | cmd += with_hdr(3, crt_hardcoded) 82 | rsp = tls.cmd(cmd) 83 | assert_status(rsp) 84 | rsp = rsp[2:] 85 | crt_len, rsp=rsp[:4], rsp[4:] 86 | crt_len, = unpack(' 0: 189 | (t, l), x = unpack(' 1: 248 | raise Exception('Expected only one child record for finger') 249 | 250 | print('Recognised finger %02x (%s) from user %s' % (subtype, subtype_to_string(subtype), repr(usr.identity))) 251 | print('Template hash: %s' % hexlify(hsh).decode()) 252 | 253 | if len(finger_record.children) > 0: 254 | if finger_record.children[0]['type'] != 8: 255 | raise Exception('Expected data blob as a finger child') 256 | 257 | blob_id = finger_record.children[0]['dbid'] 258 | blob = db.get_record_value(blob_id).value 259 | 260 | tag, sz = unpack('BBHL', self.revision, len(self.subauth), self.auth >> 32, self.auth & 0xffffffff) 12 | for i in self.subauth: 13 | b += pack(' 0: 42 | res += hmac.new(secret, a+seed, sha256).digest() 43 | a = hmac.new(secret, a, sha256).digest() 44 | n -= 1 45 | 46 | return res[:length] 47 | 48 | def hs_key(): 49 | key=password_hardcoded[:0x10] 50 | seed=password_hardcoded[0x10:] + b'\xaa'*2 51 | hs_key=prf(key, b'HS_KEY_PAIR_GEN' + seed, 0x20) 52 | return int(hs_key[::-1].hex(), 16) 53 | 54 | def with_2bytes_size(chunk): 55 | return pack('>H', len(chunk)) + chunk 56 | 57 | def with_3bytes_size(chunk): 58 | return pack('>BH', len(chunk) >> 16, len(chunk)) + chunk 59 | 60 | def with_1byte_size(chunk): 61 | return pack('>B', len(chunk)) + chunk 62 | 63 | def to_bytes(n): 64 | b=b'' 65 | while n: 66 | b += (n & 0xff).to_bytes(1, 'big') 67 | n >>= 8 68 | 69 | return b 70 | 71 | def pad(b): 72 | l = 16 - (len(b) % 16) 73 | return b + bytes([l-1])*l 74 | 75 | def unpad(b): 76 | return b[:-1-b[-1]] 77 | 78 | 79 | # TODO assert the right state transitions 80 | class Tls(): 81 | 82 | def __init__(self, usb): 83 | self.usb = usb 84 | self.reset() 85 | self.set_hwkey(product_name='VirtualBox', serial_number='0') 86 | 87 | def reset(self): 88 | self.trace_enabled = False 89 | self.secure_rx = False 90 | self.secure_tx = False 91 | 92 | # Info about the host computer 93 | def set_hwkey(self, product_name, serial_number): 94 | hw_key = bytes(product_name, 'ascii') + b'\0' + \ 95 | bytes(serial_number, 'ascii') + b'\0' 96 | 97 | # pre-TLS keys 98 | self.psk_encryption_key = prf(password_hardcoded, b'GWK' + hw_key, 0x20) 99 | self.psk_validation_key = prf(self.psk_encryption_key, 100 | b'GWK_SIGN' + gwk_sign_hardcoded, 0x20) 101 | 102 | def cmd(self, cmd): 103 | if self.secure_rx and self.secure_tx: 104 | rsp=self.app(cmd) 105 | else: 106 | rsp=self.usb.cmd(cmd) 107 | 108 | return rsp 109 | 110 | def open(self): 111 | self.secure_rx = False 112 | self.secure_tx = False 113 | 114 | self.handshake_hash = sha256() 115 | 116 | rsp=self.usb.cmd(unhexlify('44000000') + self.make_handshake(self.make_client_hello())) 117 | self.parse_tls_response(rsp) 118 | 119 | self.make_keys() 120 | 121 | rsp=self.usb.cmd( 122 | unhexlify('44000000') + 123 | self.make_handshake( 124 | self.make_certs() + 125 | self.make_client_kex() + 126 | self.make_cert_verify()) + 127 | self.make_change_cipher_spec() + 128 | self.make_handshake(self.make_finish())) 129 | 130 | self.parse_tls_response(rsp) 131 | 132 | def trace(self, s): 133 | if self.trace_enabled: 134 | print(s) 135 | 136 | def app(self, b): 137 | b = b() if callable(b) else b 138 | return self.parse_tls_response(self.usb.cmd(self.make_app_data(b))) 139 | 140 | def update_neg(self, b): 141 | self.handshake_hash.update(b) 142 | 143 | def make_keys(self): 144 | #self.session_private=0x2E38AFE3D563398E5962D2CDEA7FE16D3CFEA36656A9DEC412C648EE3A232D21 145 | self.session_private = gen_private_key(P256) 146 | self.session_public = get_public_key(self.session_private, P256) 147 | 148 | pre_master_secret = self.session_private*self.ecdh_q 149 | pre_master_secret = pre_master_secret.x 150 | pre_master_secret = to_bytes(pre_master_secret)[::-1] 151 | 152 | seed = self.client_random + self.server_random 153 | self.master_secret = prf(pre_master_secret, b'master secret'+seed, 0x30) 154 | 155 | key_block = prf(self.master_secret, b'key expansion'+seed, 0x120) 156 | self.sign_key = key_block[0x00:0x20] 157 | self.validation_key = key_block[0x20:0x20+0x20] 158 | self.encryption_key = key_block[0x40:0x40+0x20] 159 | self.decryption_key = key_block[0x60:0x60+0x20] 160 | 161 | def save(self): 162 | with open('tls.dict', 'wb') as f: 163 | pickle.dump({ 164 | 'sign_key': self.sign_key, 165 | 'validation_key': self.validation_key, 166 | 'encryption_key': self.encryption_key, 167 | 'decryption_key': self.decryption_key, 168 | 'secure_rx': self.secure_rx, 169 | 'secure_tx': self.secure_tx 170 | }, f) 171 | 172 | def load(self): 173 | with open('tls.dict', 'rb') as f: 174 | d=pickle.load(f) 175 | self.sign_key = d['sign_key'] 176 | self.validation_key = d['validation_key'] 177 | self.encryption_key = d['encryption_key'] 178 | self.decryption_key = d['decryption_key'] 179 | self.secure_rx = d['secure_rx'] 180 | self.secure_tx = d['secure_tx'] 181 | 182 | def decrypt(self, c): 183 | iv, c = c[:0x10], c[0x10:] 184 | aes=AES.new(self.decryption_key, AES.MODE_CBC, iv) 185 | m=aes.decrypt(c) 186 | m=unpad(m) 187 | return m 188 | 189 | def encrypt(self, b): 190 | #iv = unhexlify('454849acdd075174d6b9e713a957c2e7') 191 | iv = get_random_bytes(0x10) 192 | aes=AES.new(self.encryption_key, AES.MODE_CBC, iv) 193 | b=pad(b) 194 | c=aes.encrypt(b) 195 | return iv + c 196 | 197 | def validate(self, t, b): 198 | b, hs = b[:-0x20], b[-0x20:] 199 | 200 | hdr = pack('>BBBH', t, 3, 3, len(b)) 201 | sig=hmac.new(self.validation_key, hdr+b, sha256).digest() 202 | 203 | if sig != hs: 204 | raise Exception('Packet signature validation check failed') 205 | 206 | self.trace('tls> %02x: %s' % (t, hexlify(b).decode())) 211 | 212 | hdr = pack('>BBBH', t, 3, 3, len(b)) 213 | sig=hmac.new(self.sign_key, hdr+b, sha256).digest() 214 | return b + sig 215 | 216 | def make_finish(self): 217 | self.secure_tx = True 218 | hs_hash = self.handshake_hash.copy().digest() 219 | verify_data = prf(self.master_secret, b'client finished'+hs_hash, 0xc) 220 | return b'\x14' + with_3bytes_size(verify_data) 221 | 222 | def make_change_cipher_spec(self): 223 | return unhexlify('140303000101') 224 | 225 | def make_certs(self): 226 | cert = self.tls_cert 227 | cert = unhexlify('ac16') + cert # what's this? 228 | cert = pack('>BH', 0, len(self.tls_cert)) + cert # this seems to violate the standard (should be len(cert)) 229 | cert = pack('>BH', 0, len(self.tls_cert)) + cert # same 230 | return self.with_neg_hdr(0x0b, cert) 231 | 232 | def with_neg_hdr(self, t, b): 233 | b = pack('>B', t) + with_3bytes_size(b) 234 | self.update_neg(b) 235 | return b 236 | 237 | def make_client_kex(self): 238 | b = b'\x04' + to_bytes(self.session_public.x)[::-1] + to_bytes(self.session_public.y)[::-1] 239 | return self.with_neg_hdr(0x10, b) 240 | 241 | def make_cert_verify(self): 242 | buf=self.handshake_hash.copy().digest() 243 | s=sign(hexlify(buf).decode(), self.priv_key, prehashed=True) 244 | b=DEREncoder().encode_signature(s[0], s[1]) 245 | return self.with_neg_hdr(0x0f, b) 246 | 247 | def handle_server_hello(self, p): 248 | if p[:2] != unhexlify('0303'): 249 | raise Exception('unexpected TLS version %s' % hexlify(p[:2]).decode()) 250 | 251 | p = p[2:] 252 | 253 | self.server_random, p = p[:0x20], p[0x20:] 254 | l = p[0] 255 | self.server_sessid, p = p[1:1+l], p[1+l:] 256 | 257 | (suite,), p = unpack('>H', p[:2]), p[2:] 258 | 259 | if suite != 0xc005: 260 | raise Exception('Server accepted unsupported cipher suite %04x' % suite) 261 | 262 | if p[0] != 0: 263 | raise Exception('Server selected to enable compression, which we don''t support %02x' % p[0]) 264 | 265 | p = p[1:] 266 | 267 | if p != b'': 268 | raise Exception('Not expecting any more data') 269 | 270 | def handle_cert_req(self, p): 271 | (sign_and_hash_algo,), p = unpack('>H', p[:2]), p[2:] 272 | if sign_and_hash_algo != 0x140: 273 | raise Exception('Server requested a cert with an unsupported sign and hash algo combination %04x' % sign_and_hash_algo) 274 | 275 | (l,), p = unpack('>H', p[:2]), p[2:] 276 | if l != 0: 277 | raise Exception('Server requested a cert with non-empty list of CAs') 278 | 279 | if p != b'': 280 | raise Exception('Not expecting any more data') 281 | 282 | def handle_server_hello_done(self, p): 283 | if p != b'': 284 | raise Exeception('Not expecting any body for "server hello done" pkt: %s' % hexlify(p).decode()) 285 | 286 | def handle_finish(self, b): 287 | hs_hash = self.handshake_hash.copy().digest() 288 | verify_data = prf(self.master_secret, b'server finished'+hs_hash, 0xc) 289 | if verify_data != b: 290 | raise Exception('Final handshake check failed') 291 | 292 | def handle_app_data(self, b): 293 | if not self.secure_rx: 294 | raise Exception('App payload before secure connection established') 295 | 296 | return self.validate(0x17, self.decrypt(b)) 297 | 298 | def handle_handshake(self, handshake): 299 | if self.secure_rx: 300 | handshake = self.validate(0x16, self.decrypt(handshake)) 301 | 302 | while len(handshake) > 0: 303 | while len(handshake) < 4: 304 | handshake += b'\0' 305 | 306 | hdr, handshake = handshake[:4], handshake[4:] 307 | t, l12, l3 = unpack('>BHB', hdr) 308 | l = (l12 << 8) | l3 309 | p, handshake = handshake[:l], handshake[l:] 310 | 311 | if t == 2: 312 | self.handle_server_hello(p) 313 | elif t == 0xd: 314 | self.handle_cert_req(p) 315 | elif t == 0xe: 316 | self.handle_server_hello_done(p) 317 | elif t == 0x14: 318 | self.handle_finish(p) 319 | else: 320 | raise Exception('Unknown handshake packet %02x' % t) 321 | 322 | self.update_neg(hdr+p) 323 | 324 | def parse_tls_response(self, rsp): 325 | app_data=b'' 326 | 327 | while len(rsp) > 0: 328 | while len(rsp) < 5: 329 | rsp += b'\0' 330 | 331 | hdr, rsp = rsp[:5], rsp[5:] 332 | t, mj, mn, sz = unpack('>BBBH', hdr) 333 | pkt, rsp = rsp[:sz], rsp[sz:] 334 | 335 | if mj != 3 or mn != 3: 336 | raise Exception('Unexpected TLS version %d %d' % (mj, mn)) 337 | 338 | if t == 0x16: 339 | self.handle_handshake(pkt) 340 | 341 | elif t == 0x14: 342 | if pkt != unhexlify('01'): 343 | raise Exception('Unexpected ChangeCipherSpec payload') 344 | 345 | self.secure_rx = True 346 | 347 | elif t == 0x17: 348 | app_data += self.handle_app_data(pkt) 349 | 350 | else: 351 | raise Exception('Dont know how to handle message type %02x' % t) 352 | 353 | return app_data 354 | 355 | def make_app_data(self, b): 356 | if not self.secure_tx: 357 | raise Exception('App payload before secure connection established') 358 | 359 | b=self.encrypt(self.sign(0x17, b)) 360 | 361 | return unhexlify('170303') + with_2bytes_size(b) 362 | 363 | def make_handshake(self, b): 364 | if self.secure_tx: 365 | b=self.encrypt(self.sign(0x16, b)) 366 | 367 | return unhexlify('160303') + with_2bytes_size(b) 368 | 369 | def make_client_hello(self): 370 | h = unhexlify('0303') # TLS 1.2 371 | #self.client_random = unhexlify('bc349559ac16c8f8362191395b4d04a435d870315f519eed8777488bc2b9600c') 372 | self.client_random = get_random_bytes(0x20) 373 | h += self.client_random # client's random 374 | h += with_1byte_size(unhexlify('00000000000000')) # session ID 375 | 376 | suits = b'' 377 | suits += pack('>H', 0xc005) # TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA 378 | suits += pack('>H', 0x003d) # TLS_RSA_WITH_AES_256_CBC_SHA256 379 | suits += pack('>H', 0x008d) # TLS_RSA_WITH_AES_256_CBC_SHA256 380 | h += with_2bytes_size(suits) 381 | 382 | h += with_1byte_size(b'') # no compression options 383 | 384 | exts = b'' 385 | exts += self.make_ext(0x004, pack('>H', 0x0017)) # truncated_hmac = 0x17 386 | exts += self.make_ext(0x00b, with_1byte_size(unhexlify('00'))) # EC points format = uncompressed 387 | # h += with_2bytes_size(exts) 388 | h += pack('>H', len(exts)-2) + exts # -2? WHY?!... 389 | 390 | return self.with_neg_hdr(0x01, h) 391 | 392 | def make_ext(self, id, b): 393 | return pack('>H', id) + with_2bytes_size(b) 394 | 395 | def parseTlsFlash(self, reply): 396 | while len(reply) > 0: 397 | hdr, reply = reply[:4], reply[4:] 398 | hs, reply = reply[:0x20], reply[0x20:] 399 | id, sz = unpack('cmd> %s' % hexlify(out).decode()) 61 | self.dev.write(1, out) 62 | resp = self.dev.read(129, 100*1024) 63 | resp = bytes(resp) 64 | self.trace(' /dev/null; then 10 | echo "Unable to access to USB devices" 11 | echo " $SNAP_NAME is installed as a snap." 12 | echo " To allow it to function correctly you may need to run:" 13 | echo " sudo snap connect $SNAP_NAME:raw-usb" 14 | echo " sudo snap connect $SNAP_NAME:hardware-observe" 15 | exit 1 16 | fi 17 | 18 | run_tool() { 19 | [ -n "$VFS_TOOL" ] && \ 20 | local args=(--tool "$VFS_TOOL") 21 | 22 | $SNAP/vfs-tools/validity-sensors-tools "${args[@]}" "$@" 23 | } 24 | 25 | run_tool "$@" 26 | ret=$? 27 | 28 | if [ "$ret" -eq 0 ] && [[ "$VFS_TOOL" == 'initializer' ]]; then 29 | unset VFS_TOOL 30 | echo "May the leds be with you...!" 31 | (run_tool "$@" --tool=led-dance &> /dev/null) & 32 | fi 33 | 34 | exit $ret 35 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: validity-sensors-tools 2 | base: core20 3 | version: git 4 | summary: A Linux tool to flash and pair Validity fingerprint sensors 009x 5 | description: | 6 | A simple tool that helps initializing Validity fingerprint readers under 7 | linux, loading their binary firmware and initializing them. 8 | 9 | This tool currently only supports these sensors: 10 | - 138a:0090 Validity Sensors, Inc. VFS7500 Touch Fingerprint Sensor 11 | - 138a:0097 Validity Sensors, Inc. 12 | Which are present in various ThinkPad and HP laptops. 13 | 14 | These devices communicate with the laptop via an encrypted protocol and they 15 | need to be paired with the host computer in order to work and compute the 16 | TLS keys. 17 | Such initialization is normally done by the Windows driver, however thanks to 18 | the amazing efforts of Viktor Dragomiretskyy (uunicorn), and previously of 19 | Nikita Mikhailov, we have reverse-engineerd the pairing process, and so it's 20 | possible to do it under Linux with only native tools as well. 21 | 22 | The procedure is quite simple: 23 | - Device is factory-reset and its flash repartitioned 24 | - A TLS key is negotiated, generated via host hw ID and serial 25 | - Windows driver is downloaded from Lenovo to extract the device firmware 26 | - The device firmware is uploaded to the device 27 | - The device is calibrated 28 | 29 | For 138a:0097 it's also possible to enroll fingers in the internal storage 30 | doing: 31 | `validity-sensors-tools.enroll --finger-id [0-9]` 32 | 33 | Once the chip is paired with the computer via this tool, it's possible to use 34 | it in libfprint using the driver at 35 | - https://github.com/3v1n0/libfprint/ 36 | 37 | grade: stable 38 | confinement: strict 39 | 40 | parts: 41 | python-validity: 42 | plugin: python 43 | source: . 44 | python-packages: 45 | - wheel 46 | requirements: 47 | - requirements.txt 48 | build-packages: 49 | - gcc 50 | - git 51 | - libgmp-dev 52 | stage-packages: 53 | - innoextract 54 | - libusb-1.0-0 55 | - usbutils 56 | override-build: | 57 | set -x 58 | snapcraftctl build 59 | git clone . $SNAPCRAFT_PART_INSTALL/vfs-tools 60 | rm -rf $SNAPCRAFT_PART_INSTALL/vfs-tools/.git* 61 | rm -rf $SNAPCRAFT_PART_INSTALL/vfs-tools/snap 62 | 63 | snap-launcher: 64 | plugin: dump 65 | source: snap/local 66 | organize: 67 | snap-launcher.sh: bin/snap-launcher.sh 68 | 69 | apps: 70 | validity-sensors-tools: 71 | command: bin/snap-launcher.sh 72 | plugs: 73 | - raw-usb 74 | - hardware-observe 75 | - network 76 | 77 | initializer: 78 | command: bin/snap-launcher.sh 79 | environment: 80 | VFS_TOOL: initializer 81 | plugs: 82 | - raw-usb 83 | - hardware-observe 84 | - network 85 | 86 | led-test: 87 | command: bin/snap-launcher.sh 88 | environment: 89 | VFS_TOOL: led-dance 90 | plugs: 91 | - raw-usb 92 | - hardware-observe 93 | 94 | erase-db: 95 | command: bin/snap-launcher.sh 96 | environment: 97 | VFS_TOOL: erase-db 98 | plugs: 99 | - raw-usb 100 | - hardware-observe 101 | 102 | factory-reset: 103 | command: bin/snap-launcher.sh 104 | environment: 105 | VFS_TOOL: factory-reset 106 | plugs: 107 | - raw-usb 108 | - hardware-observe 109 | 110 | pair: 111 | command: bin/snap-launcher.sh 112 | environment: 113 | VFS_TOOL: pair 114 | plugs: 115 | - raw-usb 116 | - hardware-observe 117 | 118 | calibrate: 119 | command: bin/snap-launcher.sh 120 | environment: 121 | VFS_TOOL: calibrate 122 | plugs: 123 | - raw-usb 124 | - hardware-observe 125 | 126 | enroll: 127 | command: bin/snap-launcher.sh 128 | environment: 129 | VFS_TOOL: enroll 130 | plugs: 131 | - raw-usb 132 | - hardware-observe 133 | -------------------------------------------------------------------------------- /validity-sensors-tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 2020 - Marco Trevisan 5 | # 6 | # Initializer for ThinkPad's validity sensors 0090 and 0097 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | import argparse 22 | import io 23 | import os 24 | import subprocess 25 | import sys 26 | import tempfile 27 | import urllib.request 28 | 29 | from binascii import unhexlify 30 | from enum import Enum, auto 31 | from time import sleep 32 | from usb import core as usb_core 33 | 34 | from proto9x.calibrate import calibrate 35 | from proto9x.db import db 36 | from proto9x.flash import read_flash 37 | from proto9x.init_db import init_db 38 | from proto9x.init_flash import init_flash 39 | from proto9x.sensor import factory_reset, glow_start_scan, glow_end_enroll, enroll 40 | from proto9x.sid import sid_from_string 41 | from proto9x.tls import tls as vfs_tls 42 | from proto9x.upload_fwext import upload_fwext 43 | from proto9x.usb import usb as vfs_usb 44 | from proto9x.util import assert_status 45 | 46 | 47 | VALIDITY_VENDOR_ID = 0x138a 48 | 49 | class VFS(Enum): 50 | DEV_90 = 0x0090 51 | DEV_97 = 0x0097 52 | 53 | DEFAULT_URIS = { 54 | VFS.DEV_90: { 55 | 'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe', 56 | 'referral': 'https://support.lenovo.com/us/en/downloads/DS120491', 57 | }, 58 | VFS.DEV_97: { 59 | 'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1mgf03w.exe', 60 | 'referral': 'https://download.lenovo.com/pccbbs/mobiles/n1mgf03w.exe' 61 | } 62 | } 63 | 64 | DEFAULT_FW_NAMES = { 65 | VFS.DEV_90: '6_07f_Lenovo.xpfwext', 66 | VFS.DEV_97: '6_07f_lenovo_mis.xpfwext', 67 | } 68 | 69 | 70 | class VFSTools(): 71 | def __init__(self, args, usb_dev, dev_type): 72 | self.args = args 73 | self.usb_dev = usb_dev 74 | self.dev_type = dev_type 75 | self.dev_str = repr(usb_dev) 76 | 77 | print('Found device {}'.format(self.dev_str)) 78 | 79 | try: 80 | if self.args.simulate_virtualbox: 81 | raise(Exception()) 82 | 83 | with open('/sys/class/dmi/id/product_name', 'r') as node: 84 | self.product_name = node.read().strip() 85 | with open('/sys/class/dmi/id/product_serial', 'r') as node: 86 | self.product_serial = node.read().strip() 87 | except: 88 | self.product_name = 'VirtualBox' 89 | self.product_serial = '0' 90 | 91 | if self.args.host_product: 92 | self.product_name = self.args.host_product 93 | 94 | if self.args.host_serial: 95 | self.product_serial = self.args.host_serial 96 | 97 | vfs_tls.set_hwkey(product_name=self.product_name, 98 | serial_number=self.product_serial) 99 | 100 | def retry_command(self, command, max_retries=3): 101 | for i in range(max_retries): 102 | try: 103 | command() 104 | break 105 | except Exception as e: 106 | err = e 107 | self.sleep() 108 | print('Try {} failed with error: {}'.format(i+1, e)) 109 | 110 | if i == max_retries-1: 111 | print('Device didn\'t reply in time...') 112 | raise(err) 113 | 114 | def open_device(self, init=False): 115 | print('Opening device',hex(self.dev_type.value)) 116 | try: 117 | vfs_usb.dev.reset() 118 | except: 119 | pass 120 | 121 | vfs_usb.open(product=self.dev_type.value) 122 | 123 | if init: 124 | self.retry_command(vfs_usb.send_init) 125 | 126 | # try to init TLS session from the flash 127 | vfs_tls.parseTlsFlash(read_flash(1, 0, 0x1000)) 128 | vfs_tls.open() 129 | 130 | def restart(self, init=True): 131 | vfs_tls.reset() 132 | self.open_device(init=init) 133 | 134 | def download_and_extract_fw(self, fwdir, fwuri=None): 135 | fwuri = fwuri if fwuri else DEFAULT_URIS[self.dev_type]['driver'] 136 | fwarchive = os.path.join(fwdir, 'fwinstaller.exe') 137 | fwname = DEFAULT_FW_NAMES[self.dev_type] 138 | 139 | try: 140 | subprocess.check_call(['innoextract', '--version'], 141 | stdout=subprocess.DEVNULL) 142 | except Exception as e: 143 | print('Impossible to run innoextract: {}'.format(e)) 144 | raise(e) 145 | 146 | print('Downloading {} to extract {}'.format(fwuri, fwname)) 147 | 148 | req = urllib.request.Request(fwuri) 149 | req.add_header('Referer', DEFAULT_URIS[self.dev_type].get('referral', '')) 150 | req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)') 151 | 152 | with urllib.request.urlopen(req) as response: 153 | with open(fwarchive, 'wb') as out_file: 154 | out_file.write(response.read()) 155 | 156 | subprocess.check_call(['innoextract', 157 | '--output-dir', fwdir, 158 | '--include', fwname, 159 | '--collisions', 'overwrite', 160 | fwarchive 161 | ]) 162 | 163 | fwpath = subprocess.check_output([ 164 | 'find', fwdir, '-name', fwname]).decode('utf-8').strip() 165 | print('Found firmware at {}'.format(fwpath)) 166 | 167 | if not fwpath: 168 | raise Exception('No {} found in the archive'.format(fwname)) 169 | 170 | return fwpath 171 | 172 | def sleep(self, sec=3): 173 | print('Sleeping...') 174 | sleep(sec) 175 | 176 | def factory_reset(self): 177 | print('Factory reset...') 178 | self.retry_command(factory_reset) 179 | 180 | def flash_firmware(self, fwpath): 181 | print('Uploading firmware...') 182 | upload_fwext(fw_path=fwpath) 183 | 184 | def calibrate(self, calib_data=None): 185 | if isinstance(calib_data, io.IOBase): 186 | calib_data_file = calib_data.name 187 | elif calib_data: 188 | calib_data_file = calib_data 189 | else: 190 | calib_data_file = 'calib-data.bin' 191 | 192 | use_device = False 193 | if os.path.exists(calib_data_file): 194 | print('Calibrating, using data from {}'.format(calib_data_file)) 195 | else: 196 | print('Calibrating using device data') 197 | calib_data_file = os.path.join(tempfile.mkdtemp(), 'calib-data.bin') 198 | use_device = True 199 | 200 | calibrate(calib_data_path=calib_data_file) 201 | 202 | if use_device: 203 | print('Calibration data saved at {}'.format(calib_data_file)) 204 | 205 | def init_db(self): 206 | print('Init database...') 207 | init_db() 208 | 209 | def dump_db(self): 210 | print('Dumping database...') 211 | db.dump_all() 212 | 213 | def pair(self, fwpath, calib_data=None): 214 | print('Pairing the sensor with device {}'.format(self.product_name)) 215 | 216 | def init_flash_command(): 217 | self.open_device() 218 | print('Initializing flash...') 219 | init_flash() 220 | self.retry_command(init_flash_command, max_retries=5) 221 | 222 | self.sleep() 223 | self.restart() 224 | 225 | self.flash_firmware(fwpath) 226 | 227 | self.sleep() 228 | self.restart() 229 | 230 | self.calibrate(calib_data) 231 | 232 | self.init_db() 233 | 234 | print('That\'s it, pairing with {} finished'.format(self.dev_str)) 235 | 236 | def initialize(self, fwpath, calib_data=None): 237 | self.open_device() 238 | 239 | try: 240 | self.factory_reset() 241 | except Exception as e: 242 | print('Factory reset failed with {}, this should not happen, but ' \ 243 | 'we can ignore it, if pairing works...'.format(e)) 244 | 245 | vfs_tls.reset() 246 | vfs_usb.dev.reset() 247 | self.sleep() 248 | 249 | self.pair(fwpath, calib_data) 250 | 251 | def led_dance(self): 252 | print('Let\'s glow the led!') 253 | 254 | for i in range(10): 255 | glow_start_scan() 256 | sleep(0.05) 257 | glow_end_enroll() 258 | sleep(0.05) 259 | 260 | led_script = unhexlify( 261 | '39ff100000ff03000001ff002000000000ffff0000ffff0000ff03000001ff00' \ 262 | '200000000000000000ffff0000ff03000001ff002000000000ffff0000000000' \ 263 | '0000000000000000000000000000000000000000000000000000000000000000' \ 264 | '0000000000000000000000000000000000000000000000000000000000') 265 | 266 | assert_status(vfs_tls.app(led_script)) 267 | 268 | def enroll(self, finger=0): 269 | if self.dev_type.value != 0x97: 270 | raise Exception('Enroll not supported yet for device {}'.format( 271 | hex(self.dev_type.value))) 272 | 273 | sid = sid_from_string('S-1-5-21-394619333-3876782012-1672975908-3333') 274 | enroll(sid, finger + 0xf5) 275 | 276 | 277 | if __name__ == "__main__": 278 | parser = argparse.ArgumentParser() 279 | parser.add_argument('-d', '--driver-uri') 280 | parser.add_argument('-f', '--firmware-path', type=argparse.FileType('r')) 281 | parser.add_argument('-c', '--calibration-data', type=argparse.FileType('r')) 282 | parser.add_argument('--host-product') 283 | parser.add_argument('--host-serial') 284 | parser.add_argument('--simulate-virtualbox', action='store_true') 285 | parser.add_argument('-s', '--finger-id', type=int, choices=range(0, 10)) 286 | parser.add_argument('-t', '--tool', 287 | choices=( 288 | 'initializer', 289 | 'factory-reset', 290 | 'flash-firmware', 291 | 'pair', 292 | 'calibrate', 293 | 'dump-db', 294 | 'erase-db', 295 | 'led-dance', 296 | 'enroll', 297 | ), 298 | help='Tool to launch (default: %(default)s)') 299 | 300 | args = parser.parse_args() 301 | 302 | if not args.tool: 303 | parser.print_help() 304 | sys.exit(1) 305 | 306 | if os.geteuid() != 0: 307 | raise Exception('This script needs to be executed as root') 308 | 309 | if args.simulate_virtualbox and (args.host_product or args.host_serial): 310 | parser.error("--simulate-virtualbox is incompatible with host params.") 311 | 312 | usb_dev = None 313 | for d in VFS: 314 | dev = usb_core.find(idVendor=VALIDITY_VENDOR_ID, idProduct=d.value) 315 | if dev: 316 | dev_type = d 317 | usb_dev = dev 318 | 319 | if not usb_dev: 320 | raise Exception('No supported validity device found') 321 | 322 | vfs_tools = VFSTools(args, usb_dev, dev_type) 323 | 324 | if args.tool == 'initializer' or args.tool == 'pair': 325 | with tempfile.TemporaryDirectory() as fwdir: 326 | if args.firmware_path: 327 | fwpath = args.firmware_path.name 328 | else: 329 | fwpath = vfs_tools.download_and_extract_fw(fwdir, 330 | fwuri=args.driver_uri) 331 | 332 | input('The device will be now reset to factory and associated to ' \ 333 | 'the current laptop.\nPress Enter to continue (or Ctrl+C to ' \ 334 | 'cancel)...') 335 | 336 | if args.tool == 'pair': 337 | vfs_tools.pair(fwpath, args.calibration_data) 338 | else: 339 | vfs_tools.initialize(fwpath, args.calibration_data) 340 | 341 | elif args.tool == 'factory-reset': 342 | input('The device will be now reset to factory\n' \ 343 | 'Press Enter to continue (or Ctrl+C to cancel)...') 344 | vfs_tools.open_device() 345 | vfs_tools.factory_reset() 346 | 347 | elif args.tool == 'flash-firmware': 348 | with tempfile.TemporaryDirectory() as fwdir: 349 | if args.firmware_path: 350 | fwpath = args.firmware_path.name 351 | else: 352 | fwpath = vfs_tools.download_and_extract_fw(fwdir, 353 | fwuri=args.driver_uri) 354 | 355 | input('The device will be now flashed with {} firmware.\n' \ 356 | 'Press Enter to continue (or Ctrl+C to cancel)...'.format( 357 | fwpath)) 358 | 359 | vfs_tools.open_device(init=True) 360 | vfs_tools.flash_firmware(fwpath) 361 | 362 | elif args.tool == 'calibrate': 363 | vfs_tools.open_device(init=True) 364 | vfs_tools.calibrate(args.calibration_data) 365 | 366 | elif args.tool == 'erase-db': 367 | vfs_tools.open_device(init=True) 368 | vfs_tools.init_db() 369 | 370 | elif args.tool == 'dump-db': 371 | vfs_tools.open_device(init=True) 372 | vfs_tools.dump_db() 373 | 374 | elif args.tool == 'led-dance': 375 | vfs_tools.open_device(init=True) 376 | vfs_tools.led_dance() 377 | 378 | elif args.tool == 'enroll': 379 | vfs_tools.open_device(init=True) 380 | vfs_tools.enroll(finger=args.finger_id) 381 | 382 | else: 383 | parser.error('No valid tool selected') 384 | --------------------------------------------------------------------------------