Incrementally pair with the device. Try downloading the firmware from postinst.

This commit is contained in:
Viktor Dragomiretskyy 2020-07-12 14:49:00 +12:00
parent b909438785
commit 600202b4d6
15 changed files with 237 additions and 332 deletions

View file

@ -1,12 +1,16 @@
# python-validity
Validity fingerprint sensor library.
Originally designed to capture some of my findings for 138a:0097, but if you manage to get it working for some other Validity sensor - pull requests are welcome.
Validity fingerprint sensor driver.
## Setting up
On a Debian-based system, to re-install from sources (useful for testing):
On Ubuntu system:
```
./setup.py install --force --install-layout deb --prefix=/usr --root=/
$ sudo apt remove fprintd
$ sudo add-apt-repository ppa:uunicorn/open-fprintd
$ sudo aptget update
$ sudo apt install open-fprintd fprintd-clients python-validity
...wait a bit...
$ fprintd-enroll
```
## Initialization

122
bin/validity-sensors-firmware Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2020 - Marco Trevisan
#
# Initializer for ThinkPad's validity sensors 138a:0090 and 138a:0097 and 06cb:009a
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import os
import subprocess
import sys
import tempfile
import urllib.request
import shutil
from enum import Enum, auto
from time import sleep
from usb import core as usb_core
python_validity_data='/usr/share/python-validity/'
# FIXME: supported usb ids are duplicated in usb.py
class VFS(Enum):
DEV_90 = (0x138a, 0x0090)
DEV_97 = (0x138a, 0x0097)
DEV_9a = (0x06cb, 0x009a)
DEFAULT_URIS = {
VFS.DEV_90: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe',
'referral': 'https://support.lenovo.com/us/en/downloads/DS120491',
},
VFS.DEV_97: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe',
'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe'
},
VFS.DEV_9a: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe',
'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe'
}
}
# FIXME: filenames are duplicated in upload_fwext.py
DEFAULT_FW_NAMES = {
VFS.DEV_90: '6_07f_Lenovo.xpfwext',
VFS.DEV_97: '6_07f_lenovo_mis_qm.xpfwext',
VFS.DEV_9a: '6_07f_lenovo_mis_qm.xpfwext'
}
def download_and_extract_fw(dev_type, fwdir, fwuri=None):
fwuri = fwuri if fwuri else DEFAULT_URIS[dev_type]['driver']
fwarchive = os.path.join(fwdir, 'fwinstaller.exe')
fwname = DEFAULT_FW_NAMES[dev_type]
print('Downloading {} to extract {}'.format(fwuri, fwname))
req = urllib.request.Request(fwuri)
req.add_header('Referer', DEFAULT_URIS[dev_type].get('referral', ''))
req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)')
with urllib.request.urlopen(req) as response:
with open(fwarchive, 'wb') as out_file:
out_file.write(response.read())
subprocess.check_call(['innoextract',
'--output-dir', fwdir,
'--include', fwname,
'--collisions', 'overwrite',
fwarchive
])
fwpath = subprocess.check_output([
'find', fwdir, '-name', fwname]).decode('utf-8').strip()
print('Found firmware at {}'.format(fwpath))
if not fwpath:
raise Exception('No {} found in the archive'.format(fwname))
return fwpath
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--driver-uri')
args = parser.parse_args()
if os.geteuid() != 0:
raise Exception('This script needs to be executed as root')
dev_type = None
for d in VFS:
dev = usb_core.find(idVendor=d.value[0], idProduct=d.value[1])
if dev:
dev_type = d
if not dev_type:
raise Exception('No supported validity device found')
try:
subprocess.check_call(['innoextract', '--version'],
stdout=subprocess.DEVNULL)
except Exception as e:
print('Impossible to run innoextract: {}'.format(e))
sys.exit(1)
with tempfile.TemporaryDirectory() as fwdir:
fwpath = download_and_extract_fw(dev_type, fwdir, fwuri=args.driver_uri)
shutil.copy(fwpath, python_validity_data)

View file

@ -1,239 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2020 - Marco Trevisan
#
# Initializer for ThinkPad's validity sensors 138a:0090 and 138a:0097 and 06cb:009a
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import os
import subprocess
import sys
import tempfile
import urllib.request
import shutil
from enum import Enum, auto
from time import sleep
from usb import core as usb_core
from validitysensor.flash import read_tls_flash
from validitysensor.init_db import init_db
from validitysensor.init_flash import init_flash
from validitysensor.sensor import sensor as vfs_sensor
from validitysensor.sensor import factory_reset
from validitysensor.tls import tls as vfs_tls
from validitysensor.upload_fwext import upload_fwext
from validitysensor.usb import usb as vfs_usb
class VFS(Enum):
DEV_90 = (0x138a, 0x0090)
DEV_97 = (0x138a, 0x0097)
DEV_9a = (0x06cb, 0x009a)
DEFAULT_URIS = {
VFS.DEV_90: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/n1cgn08w.exe',
'referral': 'https://support.lenovo.com/us/en/downloads/DS120491',
},
VFS.DEV_97: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe',
'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe'
},
VFS.DEV_9a: {
'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe',
'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe'
}
}
DEFAULT_FW_NAMES = {
VFS.DEV_90: '6_07f_Lenovo.xpfwext',
VFS.DEV_97: '6_07f_lenovo_mis_qm.xpfwext',
VFS.DEV_9a: '6_07f_lenovo_mis_qm.xpfwext'
}
class VFSInitializer():
def __init__(self, args, usb_dev, dev_type):
self.args = args
self.usb_dev = usb_dev
self.dev_type = dev_type
self.dev_str = repr(usb_dev)
print('Found device {}'.format(self.dev_str))
if self.args.host_product or self.args.host_serial:
vfs_tls.set_hwkey(product_name=self.args.host_product,
serial_number=self.args.host_serial)
def open_device(self, init=False):
print('Opening device',hex(self.dev_type.value[1]))
vfs_usb.open(vendor=self.dev_type.value[0], product=self.dev_type.value[1])
if init:
vfs_usb.send_init()
# try to init TLS session from the flash
vfs_tls.parseTlsFlash(read_tls_flash())
vfs_tls.open()
def restart(self):
vfs_tls.reset()
self.open_device(init=True)
def download_and_extract_fw(self, fwdir, fwuri=None):
fwuri = fwuri if fwuri else DEFAULT_URIS[self.dev_type]['driver']
fwarchive = os.path.join(fwdir, 'fwinstaller.exe')
fwname = DEFAULT_FW_NAMES[self.dev_type]
print('Downloading {} to extract {}'.format(fwuri, fwname))
req = urllib.request.Request(fwuri)
req.add_header('Referer', DEFAULT_URIS[self.dev_type].get('referral', ''))
req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)')
with urllib.request.urlopen(req) as response:
with open(fwarchive, 'wb') as out_file:
out_file.write(response.read())
subprocess.check_call(['innoextract',
'--output-dir', fwdir,
'--include', fwname,
'--collisions', 'overwrite',
fwarchive
])
fwpath = subprocess.check_output([
'find', fwdir, '-name', fwname]).decode('utf-8').strip()
print('Found firmware at {}'.format(fwpath))
if not fwpath:
raise Exception('No {} found in the archive'.format(fwname))
return fwpath
def sleep(self, sec=3):
print('Sleeping...')
sleep(sec)
def try_factory_reset(self):
self.open_device()
try:
print('Factory reset...')
factory_reset()
except Exception as e:
print('Factory reset failed with {}, this should not happen, but ' \
'we can ignore it, if pairing works...'.format(e))
def pair(self):
print('Pairing the sensor with device')
max_retries = 5
err = None
for i in range(0, max_retries):
try:
self.open_device()
print('Initializing flash...')
init_flash()
break
except Exception as e:
if err is None:
err = e
self.sleep()
print('Try {} failed with error: {}'.format(i+1, e))
finally:
max_retries -= 1
if max_retries == 0:
print('Device didn\'t show up after reset, retry...')
raise(err)
self.sleep()
self.restart()
print('Uploading firmware...')
upload_fwext()
self.sleep()
self.restart()
print('Calibrating...')
vfs_sensor.open()
vfs_sensor.calibrate()
print('Init database...')
init_db()
vfs_tls.reset()
print('That\'s it, pairing with {} finished'.format(self.dev_str))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--driver-uri')
parser.add_argument('--firmware-path', type=argparse.FileType('r'))
parser.add_argument('--calibration-data', type=argparse.FileType('r'))
parser.add_argument('--host-product')
parser.add_argument('--host-serial')
parser.add_argument('--simulate-virtualbox', action='store_true')
parser.add_argument('--download-fw-only', action='store_true')
args = parser.parse_args()
if args.simulate_virtualbox and (args.host_product or args.host_serial):
parser.error("--simulate-virtualbox is incompatible with host params.")
if os.geteuid() != 0:
raise Exception('This script needs to be executed as root')
usb_dev = None
for d in VFS:
dev = usb_core.find(idVendor=d.value[0], idProduct=d.value[1])
if dev:
dev_type = d
usb_dev = dev
if not usb_dev:
raise Exception('No supported validity device found')
try:
subprocess.check_call(['innoextract', '--version'],
stdout=subprocess.DEVNULL)
except Exception as e:
print('Impossible to run innoextract: {}'.format(e))
sys.exit(1)
vfs_initializer = VFSInitializer(args, usb_dev, dev_type)
with tempfile.TemporaryDirectory() as fwdir:
if args.firmware_path:
fwpath = args.firmware_path.name
else:
fwpath = vfs_initializer.download_and_extract_fw(fwdir,
fwuri=args.driver_uri)
shutil.copy(fwpath, '/usr/share/python-validity/')
if not args.download_fw_only:
input('The device will be now reset to factory and associated to the ' \
'current laptop.\nPress Enter to continue (or Ctrl+C to cancel)...')
vfs_initializer.try_factory_reset()
vfs_initializer.sleep()
vfs_initializer.pair()

View file

@ -2,6 +2,7 @@
import argparse
import re
import sys
import dbus
import dbus.mainloop.glib
import dbus.service
@ -15,7 +16,7 @@ from validitysensor.tls import tls
from validitysensor.usb import usb
from validitysensor.sid import sid_from_string
from validitysensor.db import subtype_to_string, db
from validitysensor.sensor import sensor, reboot
from validitysensor.sensor import sensor, reboot, RebootException
import pwd
GLib.threads_init()
@ -166,14 +167,18 @@ if __name__ == '__main__':
usb.trace_enabled = True
tls.trace_enabled = True
if args.devpath is None:
init.open()
else:
z = re.match('^usb-(\d+)-(\d+)$', args.devpath)
if not z:
parser.error('Option --devpath should look like this: usb-<busnum>-<address>')
try:
if args.devpath is None:
init.open()
else:
z = re.match('^usb-(\d+)-(\d+)$', args.devpath)
if not z:
parser.error('Option --devpath should look like this: usb-<busnum>-<address>')
init.open_devpath(*map(int, z.groups()))
init.open_devpath(*map(int, z.groups()))
except RebootException:
print('Initialization ended up in rebooting the sensor. Normal exit.')
sys.exit(0)
bus = dbus.SystemBus()

View file

@ -4,6 +4,7 @@ set -e
#DEBHELPER#
if [ "$1" = "configure" ]; then
validity-sensors-firmware || true
systemctl daemon-reload || true
udevadm control --reload-rules || true
udevadm trigger || true

View file

@ -5,5 +5,6 @@ Description=python-validity driver dbus service for %I
[Service]
Type=simple
ExecStart=/usr/lib/python-validity/dbus-service --devpath %i
StandardOutput=syslog
Restart=no

View file

@ -1,6 +1,9 @@
from validitysensor.usb import usb
from validitysensor.sensor import factory_reset
from validitysensor.sensor import factory_reset, RebootException
usb.open()
factory_reset()
try:
usb.open()
factory_reset()
except RebootException:
pass

View file

@ -1,44 +0,0 @@
from time import sleep
from validitysensor.usb import usb
from validitysensor.tls import tls
from validitysensor.flash import read_tls_flash
from validitysensor.init_flash import init_flash
from validitysensor.upload_fwext import upload_fwext
from validitysensor.init_db import init_db
from validitysensor.sensor import sensor, reboot
#usb.trace_enabled=True
#tls.trace_enabled=True
def restart():
print('Sleeping...')
sleep(3)
tls.reset()
usb.open()
usb.send_init()
tls.parseTlsFlash(read_tls_flash())
tls.open()
try:
usb.open()
print('Initializing flash...')
init_flash()
restart()
print('Uploading firmware...')
upload_fwext()
restart()
print('Calibrating...')
sensor.open(False)
sensor.calibrate()
print('Init database...')
init_db()
print('That\'s it, pairing\'s finished')
finally:
sleep(1)
reboot()

View file

@ -8,7 +8,7 @@ setup(name='python-validity',
packages=['validitysensor'],
scripts=[
'bin/validity-led-dance',
'bin/validity-sensors-initializer',
'bin/validity-sensors-firmware',
],
install_requires=[
'cryptography >= 2.1.4',
@ -21,7 +21,6 @@ setup(name='python-validity',
'scripts/dbus-cmd.py',
'scripts/lsdbus.py',
'scripts/factory-reset.py',
'scripts/pair.py',
'scripts/prototype.py'
]),
]

View file

@ -121,10 +121,10 @@ def write_flash_all(partition, ptr, buf):
write_flash(partition, ptr, chunk)
ptr += len(chunk)
def read_flash_all(partition, start, end):
def read_flash_all(partition, start, size):
bs = 0x1000
blocks = [read_flash(partition, addr, bs) for addr in range(start, end, bs)]
return b''.join(blocks)
blocks = [read_flash(partition, addr, bs) for addr in range(start, start+size, bs)]
return b''.join(blocks)[:size]
def write_fw_signature(partition, signature):
rsp=tls.cmd(pack('<BBxH', 0x42, partition, len(signature)) + signature)

View file

@ -6,6 +6,9 @@ from validitysensor.tls import tls
from validitysensor.sensor import sensor
from validitysensor.sensor import reboot
from validitysensor.flash import read_tls_flash
from validitysensor.init_flash import init_flash
from validitysensor.upload_fwext import upload_fwext
from validitysensor.init_db import init_db
def close():
if usb.dev is not None:
@ -14,19 +17,26 @@ def close():
# The reboot command may fail if we're shutting down because the device is already gone.
try:
reboot()
except RebootException:
pass
finally:
usb.close()
def open_common():
init_flash()
usb.send_init()
# We must register atexit only after we opened usb device,
# so that our handler is called before pyusb's one and we can still talk to the device
atexit.register(close)
tls.parseTlsFlash(read_tls_flash())
tls.open()
upload_fwext()
sensor.open()
init_db()
# We must register atexit only after we opened usb device,
# so that our atexit handler is called before pyusb's one and we can still talk to the device
#
# Don't attempt to autoreboot unless we finished initializing the sensor successfully.
# Otherwise if there is a bug in initialization, we may end up in a loop constantly rebooting the device.
atexit.register(close)
def open():
usb.open()

View file

@ -11,8 +11,9 @@ def machine_id_rec_value(b):
b = b + b'\0' * (0x94 - len(b))
return pack('<HH', 0x102, len(b)) + b # 0x102 = Machine ID/GUID?
# TODO: make this GUID unique!
def init_db(machine_guid='e7260876-58db-4d27-8c40-8d13110d6a71'):
stg=db.get_user_storage(name='StgWindsor')
stg = db.get_user_storage(name='StgWindsor')
if stg == None:
print('Creating a new user storage object')
db.new_user_storate()
@ -21,7 +22,7 @@ def init_db(machine_guid='e7260876-58db-4d27-8c40-8d13110d6a71'):
if rc == []:
print('Creating a host machine GUID record')
stg=db.get_user_storage(name='StgWindsor')
stg = db.get_user_storage(name='StgWindsor')
db.new_record(stg.dbid, 0x8, stg.dbid, machine_id_rec_value(machine_guid))
rc = db.get_storage_data()

View file

@ -85,14 +85,9 @@ def serialize_partition(p):
b = b + b'\0'*4 + sha256(b).digest()
return b
def partition_flash(layout, client_public):
info = get_flash_info()
def partition_flash(info, layout, client_public):
print('Detected Flash IC: %s, %d bytes' % (info.ic.name, info.ic.size))
if len(info.partitions) > 0:
raise Exception('Flash is already partitioned')
cmd = unhex('4f 0000 0000')
cmd += with_hdr(0, serialize_flash_params(info.ic))
cmd += with_hdr(1, b''.join([serialize_partition(p) for p in layout]) + get_partition_signature())
@ -108,6 +103,14 @@ def partition_flash(layout, client_public):
# ^ TODO - figure out what the rest of rsp means
def init_flash():
info = get_flash_info()
if len(info.partitions) > 0:
print('Flash has %d partitions.' % len(info.partitions))
return
else:
print('Flash was not initialized yet. Formatting...')
assert_status(usb.cmd(reset_blob))
skey = ec.generate_private_key(ec.SECP256R1(), crypto_backend)
@ -115,7 +118,7 @@ def init_flash():
client_private = snums.private_value
client_public = snums.public_numbers
partition_flash(flash_layout_hardcoded, client_public)
partition_flash(info, flash_layout_hardcoded, client_public)
rsp=usb.cmd(unhex('01'))
assert_status(rsp)
@ -150,5 +153,6 @@ def init_flash():
# Persist certs and keys on cert partition.
write_flash(1, 0, tls.makeTlsFlash())
# Reboot
# Reboot.
# The device will disconnect and our service will be started by udev as soon as it is connected again.
reboot()

View file

@ -5,7 +5,7 @@ import os.path
from .tls import tls
from .usb import usb
from .db import db, subtype_to_string
from .flash import write_enable, flush_changes, read_flash, erase_flash, write_flash_all
from .flash import write_enable, flush_changes, read_flash, erase_flash, write_flash_all, read_flash_all
from time import sleep
from struct import pack, unpack
from .table_types import SensorTypeInfo, SensorCaptureProg
@ -55,9 +55,12 @@ def write_hw_reg32(addr, val):
rsp=tls.cmd(pack('<BLLB', 8, addr, val, 4))
assert_status(rsp)
class RebootException(Exception):
pass
def reboot():
assert_status(tls.cmd(unhex('050200')))
raise RebootException()
def factory_reset():
assert_status(usb.cmd(reset_blob))
@ -192,7 +195,7 @@ class CancelledException(Exception):
class Sensor():
calib_data=b''
def open(self, load_calib_data=True):
def open(self):
self.interrupt_cb = None
self.device_info = identify_sensor()
@ -227,13 +230,7 @@ class Sensor():
if 7 in factory_bits:
self.factory_calib_data = factory_bits[7][4:]
if load_calib_data and os.path.isfile(calib_data_path):
with open(calib_data_path, 'rb') as f:
self.calib_data = f.read()
print('Calibration data loaded from a file.')
else:
self.calib_data = b''
print('Warning: no calibration data was loaded. Consider calibrating the sensor.')
self.calibrate()
def save(self):
with open(calib_data_path, 'wb') as f:
@ -569,7 +566,43 @@ class Sensor():
write_flash_all(6, 0, clean_slate)
def check_clean_slate(self):
start = read_flash(6, 0, 0x44)
magic, l = unpack('<HH', start[:4])
start = start[4:]
if magic != 0x5002:
return False
hs, zeroes = start[0:0x20], start[0x20:0x40]
if zeroes != b'\0'*0x20:
print('Unexpected contents in calibration flash partition')
return False
img = read_flash_all(6, 0x44, l)
if hs != sha256(img).digest():
print('Calibration flash hash mismatch')
print(hexlify(hs), hexlify(img))
return False
return True
def calibrate(self):
if os.path.isfile(calib_data_path):
with open(calib_data_path, 'rb') as f:
self.calib_data = f.read()
print('Calibration data loaded from a file.')
if self.check_clean_slate():
return
else:
print('No calibration data on the flash. Calibrating...')
else:
self.calib_data = b''
print('No calibration data was loaded. Calibrating...')
for i in range(0, self.calibration_iterations):
print('Calibration iteration %d...' % i)
rsp = tls.cmd(self.build_cmd_02(CaptureMode.CALIBRATE))
@ -586,7 +619,7 @@ class Sensor():
clean_slate = pack('<H', len(clean_slate)) + clean_slate
clean_slate = clean_slate + pack('<H', 0) # TODO: still don't know what this zero is for
clean_slate = pack('<H', len(clean_slate)) + sha256(clean_slate).digest() + b'\0'*0x20 + clean_slate
clean_slate = unhexlify('0250') + clean_slate
clean_slate = pack('<H', 0x5002) + clean_slate
self.persist_clean_slate(clean_slate)
self.save()

View file

@ -23,6 +23,13 @@ def default_fwext_name():
return '6_07f_lenovo_mis_qm.xpfwext'
def upload_fwext(fw_path=None):
fwi=get_fw_info(2)
if fwi != None:
print('Detected firmware version %d.%d (%s))' % (fwi.major, fwi.minor, ctime(fwi.buildtime)))
return
else:
print('No firmware detected. Uploading...')
# no idea what this is:
write_hw_reg32(0x8000205c, 7)
if read_hw_reg32(0x80002080) not in [2, 3]:
@ -33,6 +40,7 @@ def upload_fwext(fw_path=None):
# ^ TODO -- what is the real reason to detect HW at this stage?
# just a guess: perhaps it is used to construct fwext filename
default_name = firmware_home + '/' + default_fwext_name()
if not fw_path:
fw_path = default_name
@ -46,10 +54,6 @@ def upload_fwext(fw_path=None):
fwext=fwext[fwext.index(b'\x1a')+1:]
fwext, signature = fwext[:-0x100], fwext[-0x100:]
fwi=get_fw_info(2)
if fwi != None:
raise Exception('FW is already present (version %d.%d (%s))' % (fwi.major, fwi.minor, ctime(fwi.buildtime)))
#flush_changes()
write_flash_all(2, 0, fwext)
write_fw_signature(2, signature)
@ -62,3 +66,4 @@ def upload_fwext(fw_path=None):
# Reboot
reboot()
raise Exception('Reboot')