* Roll back async changes for enroll & identify.

* Add a couple of tweaks to stop the sensor from crashing.
* A few more comments and TODOs
This commit is contained in:
Viktor Dragomiretskyy 2020-07-16 23:13:46 +12:00
parent 229fffe14c
commit 75c6ddb5b3
9 changed files with 263 additions and 332 deletions

View file

@ -10,6 +10,7 @@ import dbus
import dbus.mainloop.glib
import dbus.service
from binascii import hexlify, unhexlify
from threading import Thread
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
from gi.repository import GLib
import logging
@ -92,19 +93,21 @@ class Device(dbus.service.Object):
def VerifyStart(self, user, finger):
logging.debug('In VerifyStart %s' % finger)
def complete_cb(rsp, e):
if e is not None:
self.VerifyStatus('verify-no-match', True)
else:
self.VerifyStatus('verify-match', True)
usrid, subtype, hsh = rsp
# TODO pass down the user DB id
# check that a correct finger was identified
def update_cb(e):
self.VerifyStatus('verify-retry-scan', False)
sensor.identify(update_cb, complete_cb)
def run():
try:
usrid, subtype, hsh = sensor.identify(update_cb)
self.VerifyStatus('verify-match', True)
except Exception as e:
logging.exception('Oops', e)
self.VerifyStatus('verify-no-match', True)
thread = Thread(target=run)
thread.daemon = True
thread.start()
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='',
@ -112,18 +115,6 @@ class Device(dbus.service.Object):
def Cancel(self):
sensor.cancel()
def do_enroll(self, finger_name, uid):
# it is pointless to try and remember username passed in claim as Gnome does not seem to be passing anything useful anyway
try:
# TODO hardcode the username and finger for now
z=enroll(uid2identity(uid), 0xf5)
logging.debug('Enroll was successfull')
self.EnrollStatus('enroll-completed', True)
except Exception as e:
self.EnrollStatus('enroll-failed', True)
#loop.quit();
raise e
@dbus.service.method(dbus_interface=INTERFACE_NAME,
in_signature='ss',
out_signature='')
@ -137,13 +128,18 @@ class Device(dbus.service.Object):
else:
self.EnrollStatus('enroll-stage-passed', False)
def complete_cb(rsp, e):
if e is not None:
self.EnrollStatus('enroll-failed', True)
else:
def run():
try:
sensor.enroll(uid2identity(uid), 0xf5, update_cb) # TODO parse the finger name
self.EnrollStatus('enroll-completed', True)
except Exception as e:
logging.exception('Oops', e)
self.EnrollStatus('enroll-failed', True)
sensor.enroll(uid2identity(uid), 0xf5, update_cb, complete_cb) # TODO parse the finger name
thread = Thread(target=run)
thread.daemon = True
thread.start()
@dbus.service.signal(dbus_interface=INTERFACE_NAME, signature='sb')
def VerifyStatus(self, result, done):

View file

@ -14,69 +14,21 @@ import code
#tls.trace_enabled = True
def identify():
cv=Condition()
result=[]
def update_cb(e):
print('Capture error: %s, try again' % repr(e))
def complete_cb(rsp, e):
cv.acquire()
try:
result.append((rsp, e))
cv.notify()
finally:
cv.release()
sensor.identify(update_cb, complete_cb)
cv.acquire()
try:
cv.wait()
rsp, e = result[0]
if e is not None: raise e
except:
sensor.cancel()
raise
finally:
cv.release()
usrid, subtype, hsh = rsp
usrid, subtype, hsh = sensor.identify(update_cb)
print('Got finger %x for user recordid %d. Hash: %s' % (subtype, usrid, hexlify(hsh).decode()))
def enroll(sid, finger):
cv=Condition()
result=[]
def update_cb(x, e):
if e is not None:
print('Enroll error: %s, try again' % repr(e))
else:
print('Enroll progress: %s' % hexlify(x).decode())
def complete_cb(rsp, e):
cv.acquire()
try:
result.append((rsp, e))
cv.notify()
finally:
cv.release()
sensor.enroll(sid, finger, update_cb, complete_cb)
cv.acquire()
try:
cv.wait()
recid, e = result[0]
if e is not None: raise e
except:
sensor.cancel()
raise
finally:
cv.release()
recid = sensor.enroll(sid, finger, update_cb)
print('Created a finger record with dbid %d' % recid)

View file

@ -6,7 +6,7 @@ from .util import assert_status
from struct import pack, unpack
from binascii import hexlify, unhexlify
from .blobs import db_write_enable
from .flash import flush_changes
from .flash import call_cleanups
from .sid import *
class UserStorage():
@ -125,6 +125,18 @@ class DbRecord():
class Db():
class Info():
def __init__(self, total, used, free, records, roots):
self.total = total # partition size
self.used = used # used (not deleted)
self.free = free # unallocated space
self.records = records # total number, including deleted
self.roots = roots
def __repr__(self):
return 'Db.Info(total=%d, used=%d, free=%d, records=%d, roots=%s)' % (
self.total, self.used, self.free, self.records, repr(self.roots))
def get_user_storage(self, dbid=0, name=''):
name=name.encode()
@ -175,7 +187,7 @@ class Db():
rsp = rsp[14:]
rec.children=[]
for i in range(0, cnt):
dbid, typ = unpack('<HH', rsp[i:i+4])
dbid, typ = unpack('<HH', rsp[i*4:i*4+4])
rec.children += [{ 'dbid': dbid, 'type': typ }]
return rec
@ -183,14 +195,27 @@ class Db():
def del_record(self, dbid):
assert_status(tls.cmd(pack('<BH', 0x48, dbid)))
def db_info(self):
rsp = tls.cmd(b'\x45')
assert_status(rsp)
rsp = rsp[2:]
unknown1, unknown0, total, used, free, records, nroots = unpack('<LLLLLHH', rsp[:0x18])
# Seems to always be unknown1 == 1, unknown0 == 0
rsp = rsp[0x18:]
roots = [unpack('<H', rsp[i*2:i*2+2])[0] for i in range(0, nroots)]
return Db.Info(total, used, free, records, roots)
def new_record(self, parent, typ, storage, data):
assert_status(tls.cmd(b'\x45'))
self.db_info() # TODO check free space, compact the partition when out of storage
assert_status(tls.cmd(db_write_enable))
rsp = tls.cmd(pack('<BHHHH', 0x47, parent, typ, storage, len(data)) + data)
assert_status(rsp)
recid, = unpack('<H', rsp[2:])
flush_changes()
try:
rsp = tls.cmd(pack('<BHHHH', 0x47, parent, typ, storage, len(data)) + data)
assert_status(rsp)
recid, = unpack('<H', rsp[2:])
finally:
call_cleanups()
return recid
def new_user(self, identity):
@ -213,6 +238,17 @@ class Db():
rec = self.new_record(parent, 0x8, stg.dbid, data)
return rec
def dump_raw(self, root=3, depth=0):
rec = self.get_record_value(root)
val = hexlify(rec.value).decode()
if len(val) > 80:
val = val[:80] + '...'
print('%s%d (type %d) %s' % (' '*depth, rec.dbid, rec.type, val))
rec = self.get_record_children(root)
for c in rec.children:
self.dump_raw(c['dbid'], depth+1)
def dump_all(self):
stg = self.get_user_storage(name='StgWindsor')
usrs = [self.get_user(u['dbid']) for u in stg.users]

View file

@ -91,13 +91,19 @@ def get_fw_info(partition):
def write_enable():
assert_status(tls.cmd(db_write_enable))
def flush_changes():
assert_status(tls.cmd(b'\x1a'))
def call_cleanups():
rsp = tls.cmd(b'\x1a')
err = unpack('<H', rsp[:2])[0]
if err == 0x0491: # Nothing to commit
return # don't throw an exception, just ignore
assert_status(rsp)
def erase_flash(partition):
assert_status(tls.cmd(db_write_enable))
assert_status(tls.cmd(pack('<BB', 0x3f, partition)))
flush_changes()
try:
assert_status(tls.cmd(pack('<BB', 0x3f, partition)))
finally:
call_cleanups()
def read_flash(partition, addr, size):
cmd = pack('<BBBHLL', 0x40, partition, 1, 0, addr, size)
@ -110,9 +116,11 @@ def read_flash(partition, addr, size):
def write_flash(partition, addr, buf):
tls.cmd(db_write_enable)
cmd = pack('<BBBHLL', 0x41, partition, 1, 0, addr, len(buf)) + buf
rsp = tls.cmd(cmd)
assert_status(rsp)
flush_changes()
try:
rsp = tls.cmd(cmd)
assert_status(rsp)
finally:
call_cleanups()
def write_flash_all(partition, ptr, buf):
bs = 0x1000

View file

@ -13,8 +13,8 @@ from cryptography.hazmat.backends import default_backend
from .tls import tls, hs_key, crt_hardcoded
from .usb import usb
from .flash import write_flash, erase_flash, flush_changes, PartitionInfo, get_flash_info
from .sensor import reboot
from .flash import write_flash, erase_flash, call_cleanups, PartitionInfo, get_flash_info
from .sensor import reboot, RomInfo
from .util import assert_status, unhex
from .blobs import reset_blob
@ -121,13 +121,14 @@ def init_flash():
partition_flash(info, flash_layout_hardcoded, client_public)
rsp=usb.cmd(unhex('01'))
assert_status(rsp)
# ^ get device info, contains firmware version which is needed to lookup pubkey for server cert validation
RomInfo.get()
# ^ TODO: use the firmware version which to lookup pubkey for server cert validation
rsp=usb.cmd(unhex('50'))
assert_status(rsp)
flush_changes()
try:
rsp=usb.cmd(unhex('50'))
assert_status(rsp)
finally:
call_cleanups()
rsp=rsp[2:]
l,=unpack('<L', rsp[:4])

View file

@ -3,10 +3,11 @@ from enum import Enum
from hashlib import sha256
import os.path
import logging
from usb import core as usb_core
from .tls import tls
from .usb import usb
from .usb import usb, CancelledException
from .db import db, subtype_to_string
from .flash import write_enable, flush_changes, read_flash, erase_flash, write_flash_all, read_flash_all
from .flash import write_enable, call_cleanups, 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
@ -190,14 +191,10 @@ class CaptureMode(Enum):
IDENTIFY=2
ENROLL=3
class CancelledException(Exception):
pass
class Sensor():
calib_data=b''
def open(self):
self.interrupt_cb = None
self.device_info = identify_sensor()
logging.info('Opening sensor: %s' % self.device_info.name)
@ -625,58 +622,32 @@ class Sensor():
self.save()
def cancel(self):
cb=usb.interrupt_cb
if cb is not None:
cb(None)
usb.cancel = True
def capture(self, mode, complete_cb):
if usb.interrupt_cb is not None:
raise Exception('Wrong state for capture')
def capture(self, mode):
try:
assert_status(tls.app(self.build_cmd_02(mode)))
def next(cb):
if cb is None:
usb.interrupt_cb = None
return
def run(b):
try:
if b is None:
raise CancelledException()
cb(b)
except Exception as e:
usb.interrupt_cb = None
tls.app(unhexlify('04')) # capture stop if still running, cleanup
complete_cb(None, e)
usb.interrupt_cb = run
def wait_start(b):
# start
b = usb.wait_int()
if b[0] != 0:
raise Exception('wait_start: Unexpected interrupt type %s' % hexlify(b).decode())
next(wait_finger)
def wait_finger(b):
if b[0] == 2:
next(wait_capture_complete)
# TODO: report status?
def wait_capture_complete(b):
if b[0] != 3:
raise Exception('wait_finger: Unexpected interrupt type %s' % hexlify(b).decode())
if b[2] & 4:
capture_complete()
return
# wait for finger
while True:
b = usb.wait_int()
if b[0] == 2:
break;
# TODO: report status?
def capture_complete():
next(None)
# wait capture complete
while True:
b = usb.wait_int()
if b[0] != 3:
raise Exception('Unexpected interrupt type %s' % hexlify(b).decode())
if b[2] & 4:
break
res = get_prg_status2()
assert_status(res)
@ -693,88 +664,69 @@ class Sensor():
if error != 0:
raise Exception('Scanning problem: %04x' % error)
complete_cb((x, y, w1, w2), None)
return (x, y, w1, w2)
next(wait_start)
finally:
tls.app(unhexlify('04')) # capture stop if still running, cleanup
assert_status(tls.app(self.build_cmd_02(mode)))
def enrollment_update_start(self, key, complete_cb):
if usb.interrupt_cb is not None:
raise Exception('Wrong state, sensor\'s busy')
def wait(b):
if b is None:
raise Exception('Cancelling while enrollment is being updated is not a good idea.')
usb.interrupt_cb = None
complete_cb(new_key, b)
usb.interrupt_cb = wait
def enrollment_update_start(self, key):
rsp=tls.app(pack('<BLL', 0x68, key, 0))
assert_status(rsp)
new_key, = unpack('<L', rsp[2:])
usb.wait_int()
return new_key
def create_enrollment(self):
assert_status(tls.app(pack('<BL', 0x69, 1)))
def enrollment_update_end(self):
assert_status(tls.app(pack('<BL', 0x69, 0)))
# Generates interrupt
def enrollment_update(self, prev):
write_enable()
rsp=tls.app(b'\x6b' + prev)
assert_status(rsp)
flush_changes()
try:
rsp=tls.app(b'\x6b' + prev)
assert_status(rsp)
finally:
call_cleanups()
return rsp[2:]
def append_new_image(self, prev, complete_cb):
if usb.interrupt_cb is not None:
raise Exception('Wrong state, sensor\'s busy')
def finished(b):
res = self.enrollment_update(prev)
l, res = res[:2], res[2:]
l, = unpack('<H', l)
if l != len(res):
raise Exception('Response size does not match %d != %d', l, len(res))
magic_len = 0x38 # hardcoded in the DLL
template = header = tid = None
while len(res) > 0:
tag, l = unpack('<HH', res[:4])
if tag == 0:
template = res[:magic_len+l]
elif tag == 1:
header = res[magic_len:magic_len+l]
elif tag == 3:
tid = res[magic_len:magic_len+l]
else:
logging.warning('Ignoring unknown tag %x' % tag)
res=res[magic_len+l:]
return (header, template, tid)
def wait(b):
if b is None:
raise Exception('Cancelling while enrollment is being updated is not a good idea.')
usb.interrupt_cb = None
try:
complete_cb(finished(b), None)
except Exception as e:
complete_cb(None, e)
usb.interrupt_cb = wait
# Start the work. Interrupt will be generated when it is finished.
def append_new_image(self, prev):
self.enrollment_update(prev)
usb.wait_int()
res = self.enrollment_update(prev)
l, res = res[:2], res[2:]
l, = unpack('<H', l)
if l != len(res):
raise Exception('Response size does not match %d != %d', l, len(res))
magic_len = 0x38 # hardcoded in the DLL
template = header = tid = None
while len(res) > 0:
tag, l = unpack('<HH', res[:4])
if tag == 0:
template = res[:magic_len+l]
elif tag == 1:
header = res[magic_len:magic_len+l]
elif tag == 3:
tid = res[magic_len:magic_len+l]
else:
logging.warning('Ignoring unknown tag %x' % tag)
res=res[magic_len+l:]
return (header, template, tid)
def make_finger_data(self, subtype, template, tid):
template = pack('<HH', 1, len(template)) + template
@ -787,70 +739,51 @@ class Sensor():
return tinfo
def enroll(self, identity, subtype, update_cb, complete_cb):
def enroll(self, identity, subtype, update_cb):
def do_create_finger(final_template, tid):
tinfo = self.make_finger_data(subtype, final_template, tid)
usr=db.lookup_user(identity)
if usr == None:
usr = db.new_user(identity)
else:
usr = usr.dbid
recid = db.new_finger(usr, tinfo)
usb.wait_int()
glow_end_scan()
return recid
key=0
template=b''
self.create_enrollment()
while True:
try:
tinfo = self.make_finger_data(subtype, final_template, tid)
usr=db.lookup_user(identity)
if usr == None:
usr = db.new_user(identity)
else:
usr = usr.dbid
recid = db.new_finger(usr, tinfo)
glow_start_scan()
self.capture(CaptureMode.ENROLL)
key = self.enrollment_update_start(key)
rsp = self.append_new_image(template)
header, template, tid = rsp
update_cb(header, None)
if tid:
break
except usb_core.USBError as e:
raise e
except CancelledException as e:
glow_end_scan()
complete_cb(recid, None)
raise e
except Exception as e:
complete_cb(None, e)
print(e)
update_cb(None, e)
# sleep, so we don't end up in a busy loop spaming the sensor with requests in case of unrecoverable error
finally:
self.enrollment_update_end()
def start_iteration(key=0, template=b''):
def wrap_cb(f):
def r(res, ex):
try:
f(res, ex)
except CancelledException as e:
glow_end_scan()
complete_cb(None, e)
except Exception as e:
update_cb(None, e)
# sleep, so we don't end up in a busy loop spaming the sensor with requests in case of unrecoverable error
sleep(1)
self.enrollment_update_end()
start_iteration(key, template)
return r
def capture_cb(res, e):
if e is not None: raise e
def enrollment_update_start_cb(new_key, b):
def append_new_image_cb(rsp, e):
if e is not None: raise e
self.enrollment_update_end()
header, new_template, tid = rsp
update_cb(header, None)
if tid:
do_create_finger(new_template, tid)
else:
start_iteration(new_key, new_template)
## end of append_new_image_cb
self.append_new_image(template, wrap_cb(append_new_image_cb))
## end of enrollment_update_start_cb
self.enrollment_update_start(key, wrap_cb(enrollment_update_start_cb))
## end of capture_cb
glow_start_scan()
self.capture(CaptureMode.ENROLL, wrap_cb(capture_cb))
start_iteration()
self.enrollment_update_end() # done twice for some reason
return do_create_finger(template, tid)
def parse_dict(self, x):
rc={}
@ -861,74 +794,56 @@ class Sensor():
return rc
def match_finger(self, complete_cb):
if usb.interrupt_cb is not None:
raise Exception('Wrong state for capture')
def match_finger(self):
try:
stg_id=0 # match against any storage
usr_id=0 # match against any user
cmd=pack('<BBBHHHHH', 0x5e, 2, 0xff, stg_id, usr_id, 1, 0,0)
rsp=tls.app(cmd)
assert_status(rsp)
def wait(b):
if b is None:
raise Exception('Cancelling while finger match is running is not a good idea.')
b = usb.wait_int()
if b[0] != 3:
raise Exception('Finger not recognized: %s' % hexlify(b).decode())
try:
if b[0] != 3:
raise Exception('Finger not recognized: %s' % hexlify(b).decode())
# get results
rsp = tls.app(unhexlify('6000000000'))
assert_status(rsp)
rsp = rsp[2:]
# get results
rsp = tls.app(unhexlify('6000000000'))
assert_status(rsp)
rsp = rsp[2:]
(l,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if l != len(rsp):
raise Exception('Response size does not match')
(l,), rsp = unpack('<H', rsp[:2]), rsp[2:]
if l != len(rsp):
raise Exception('Response size does not match')
rsp=self.parse_dict(rsp)
rsp=self.parse_dict(rsp)
usrid, subtype, hsh = rsp[1], rsp[3], rsp[4]
usrid, = unpack('<L', usrid)
subtype, = unpack('<H', subtype)
usrid, subtype, hsh = rsp[1], rsp[3], rsp[4]
usrid, = unpack('<L', usrid)
subtype, = unpack('<H', subtype)
complete_cb((usrid, subtype, hsh), None)
except Exception as e:
complete_cb(None, e)
finally:
usb.interrupt_cb = None
# cleanup, ignore any errors
tls.app(unhexlify('6200000000'))
usb.interrupt_cb = wait
stg_id=0 # match against any storage
usr_id=0 # match against any user
cmd=pack('<BBBHHHHH', 0x5e, 2, 0xff, stg_id, usr_id, 1, 0,0)
rsp=tls.app(cmd)
assert_status(rsp)
return (usrid, subtype, hsh)
finally:
# cleanup, ignore any errors
tls.app(unhexlify('6200000000'))
def identify(self, update_cb, complete_cb):
def start():
def identify(self, update_cb):
while True:
try:
glow_start_scan()
self.capture(CaptureMode.IDENTIFY, capture_cb)
except Exception as e:
# failed to start the capture, pointless to retry
glow_end_scan()
complete_cb(None, e)
def capture_cb(_, e):
try:
if e is not None: raise e
self.match_finger(complete_cb)
self.capture(CaptureMode.IDENTIFY)
break
except usb_core.USBError as e:
raise e
except CancelledException as e:
glow_end_scan()
complete_cb(None, e)
raise e
except Exception as e:
# Capture failed, retry
update_cb(e)
sleep(1)
start()
start()
return self.match_finger()
def get_finger_blobs(self, usrid, subtype):
usr = db.get_user(usrid)

View file

@ -7,7 +7,7 @@ from os.path import basename
from .tls import tls
from .usb import usb
from .sensor import reboot, write_hw_reg32, read_hw_reg32, identify_sensor
from .flash import flush_changes, read_flash, erase_flash, write_flash_all, write_fw_signature, get_fw_info
from .flash import read_flash, erase_flash, write_flash_all, write_fw_signature, get_fw_info
from .util import assert_status
firmware_home='/usr/share/python-validity'
@ -55,7 +55,6 @@ def upload_fwext(fw_path=None):
fwext=fwext[fwext.index(b'\x1a')+1:]
fwext, signature = fwext[:-0x100], fwext[-0x100:]
#flush_changes()
write_flash_all(2, 0, fwext)
write_fw_signature(2, signature)

View file

@ -1,5 +1,6 @@
import logging
import errno
import usb.core as ucore
from binascii import *
from .util import assert_status, unhex
@ -15,6 +16,8 @@ supported_devices=[
(0x06cb, 0x009a),
]
class CancelledException(Exception):
pass
class Usb():
def __init__(self):
@ -22,6 +25,7 @@ class Usb():
self.trace_enabled = False
self.quit = None
self.dev = None
self.cancel = False
def open(self, vendor=None, product=None):
if vendor is not None and product is not None:
@ -48,9 +52,6 @@ class Usb():
self.dev = dev
self.dev.default_timeout = 15000
self.thread = Thread(target=lambda: self.int_thread())
self.thread.daemon = True
self.thread.start()
def close(self):
if self.dev is not None:
@ -59,8 +60,6 @@ class Usb():
self.dev = None
except:
pass
finally:
self.thread.join()
def usb_dev(self):
return self.dev
@ -69,12 +68,12 @@ class Usb():
#self.dev.set_configuration()
# TODO analyse responses, detect hardware type
assert_status(self.cmd(unhexlify('01')))
assert_status(self.cmd(unhexlify('01'))) # RomInfo.get()
assert_status(self.cmd(unhexlify('19')))
# 43 -- get partition header(?) (02 -- fwext partition)
# c28c745a in response is a FwextBuildtime = 0x5A748CC2
rsp=self.cmd(unhexlify('4302'))
rsp=self.cmd(unhexlify('4302')) # get_fw_info()
assert_status(self.cmd(init_hardcoded))
@ -106,6 +105,27 @@ class Usb():
self.trace('<130< Error: %s' % repr(e))
return None
# FIXME There is a chance of a race condition here
def cancel(self):
self.cancel = True
def wait_int(self):
self.cancel = False
while True:
try:
resp = self.dev.read(131, 1024, timeout=100)
resp = bytes(resp)
self.trace('<int< %s' % hexlify(resp).decode())
return resp
except USBError as e:
if e.errno == errno.ETIMEDOUT:
if self.cancel:
raise CancelledException()
else:
raise e
def int_thread(self):
try:
while True:

View file

@ -6,8 +6,12 @@ from binascii import unhexlify
def assert_status(b):
s,=unpack('<H', b[:2])
if s != 0:
if s == 0x44f:
raise Exception('Signature validation failed: %04x' % s)
raise Exception('Failed: %04x' % s)
def unhex(x):
return unhexlify(re.sub('\W', '', x))