├── .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 | [](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 |
--------------------------------------------------------------------------------