import typing 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, CancelledException from .db import db, SidIdentity 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 from binascii import hexlify, unhexlify from .util import assert_status, unhex from .hw_tables import dev_info_lookup from .blobs import reset_blob from . import timeslot as prg # TODO: this should be specific to an individual device (system may have more than one sensor) calib_data_path = '/usr/share/python-validity/calib-data.bin' line_update_type1_devices = [ 0xB5, 0x885, 0xB3, 0x143B, 0x1055, 0xE1, 0x8B1, 0xEA, 0xE4, 0xED, 0x1825, 0x1FF5, 0x199 ] # TODO use more sophisticated glow patters in different cases def glow_start_scan(): cmd = unhexlify( '3920bf0200ffff0000019900200000000099990000000000000000000000000020000000000000000000000000ffff000000990020000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' ) assert_status(tls.app(cmd)) def glow_end_scan(): cmd = unhexlify( '39f4010000f401000001ff002000000000ffff0000000000000000000000000020000000000000000000000000f401000000ff0020000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' ) assert_status(tls.app(cmd)) def get_prg_status(): return tls.app(unhexlify('5100000000')) def wait_till_finished(): while True: status = get_prg_status() if status[0] in [0, 7]: break sleep(0.2) def get_prg_status2(): return tls.app(unhexlify('5100200000')) def read_hw_reg32(addr): rsp = tls.cmd(pack(' 0: raise Exception('Garbage at the end of reply') return rc def bitpack(b): l = len(b) m = min(b) x = max(b) # maximum delta which we must encode x -= m # count useful bits u = 0 while x > 0: x >>= 1 u += 1 # convert to array of binary strings with each element exactly u characters long b = [bin(i - m + 0x100)[-u:] for i in b] # combine chunks into one long text number with u*l binary digits and parse it as integer b = int(''.join(b[::-1]), 2) # convert back to bytes b = b.to_bytes((u * l + 7) // 8, 'little') return (u, m, b) class Line(): mask = None flags = None data = None v0 = 0 v1 = 0 v2 = 0 def clip(x): if x < -128: x = -128 if x > 127: x = 127 return x & 0xff def scale(x): x -= 0x80 x = int(x * 10 / 0x22) # TODO: scaling factor depends on a device return clip(x) def add(l, r): # Make signed l, r = unpack('bb', pack('BB', l, r)) return clip(l + r) def chunks(b, l): return [b[i:i + l] for i in range(0, len(b), l)] class CaptureMode(Enum): CALIBRATE = 1 IDENTIFY = 2 ENROLL = 3 class Sensor(): calib_data = b'' def open(self): self.device_info = identify_sensor() logging.info('Opening sensor: %s' % self.device_info.name) self.type_info = SensorTypeInfo.get_by_type(self.device_info.type) if self.device_info.type == 0x199: self.key_calibration_line = 0x38 # (lines_per_calibration_data/2), but hardcoded for sensor type 0x199 self.calibration_frames = 3 # TODO: workout where it's really comming from self.calibration_iterations = 3 # hardcoded for type elif self.device_info.type == 0xdb: self.key_calibration_line = 0x48 # TODO 48 is just a guess -- find it self.calibration_frames = 6 # TODO: workout where it's really comming from self.calibration_iterations = 0 else: raise Exception('Device %s is not supported (sensor type 0x%x)' % (self.device_info.name, self.device_info.type)) self.rom_info = RomInfo.get() self.hardcoded_prog = SensorCaptureProg.get(self.rom_info, self.device_info.type, 0x18, 0x19) # TODO: find where 0x18, 0x19 coming from if self.hardcoded_prog is None: raise Exception('Can\'t find initial capture program for rom %s and sensor type %x' % (repr(self.rom_info), self.device_info.type)) # Look for a "2D" chunk. It must have a 32 bit integer which represent the number of lines per frame lines_2d = [ unpack(' 1: b[i + 2] *= mult if inc_address: b[i + 1] += 1 i += 3 continue if b[i] == 0: i += 1 continue if b[i] == 7: i += 2 continue break return bytes(b) def patch_timeslot_again(self, b): b = bytearray(b) pc = 0 match = None # Look for the last Call in the script while pc < len(b): opcode, l, *operands = prg.decode_insn(b[pc:]) # End of Table, Return, End of Data if opcode == 1 or opcode == 2 or opcode == 4: break # Call if opcode == 11: match = operands[1] # destination address pc += l if match is None: return bytes(b) pc = match match = None # Look for the last Register Write to 0x8000203C while pc < len(b): opcode, l, *operands = prg.decode_insn(b[pc:]) # End of Table, Return, End of Data if opcode == 1 or opcode == 2 or opcode == 4: break # Write Register if opcode == 13 and operands[0] == 0x8000203c: match = pc pc += l if match is None: return bytes(b) # Hack the value to be taken from the factory calibration table right in the middle of a sensor b[match + 1] = self.factory_calibration_values[self.key_calibration_line] return bytes(b) def average(self, raw_calib_data): frame_size = self.lines_per_frame * self.bytes_per_line interleave_lines = self.lines_per_frame // self.type_info.lines_per_calibration_data # 2, TODO: algo is quite different when it is 1 input_frames = self.calibration_frames if interleave_lines > 1: if input_frames > 1: # skip the first frame input_frames -= 1 base_address = frame_size frame = raw_calib_data[base_address:base_address + frame_size] # split into groups of lines frame = chunks(frame, interleave_lines * self.bytes_per_line) # split group of lines into lines frame = [chunks(f, self.bytes_per_line) for f in frame] # calculate averages across interleaved lines frame = [bytes([sum(i) // len(f) for i in zip(*f)]) for f in frame] frame = b''.join(frame) else: if input_frames > 1: # skip the first frame input_frames -= 2 base_address = frame_size * 2 frames = raw_calib_data[base_address:base_address + frame_size * input_frames] frames = chunks(frames, frame_size) frame = [int(sum(i) / input_frames) for i in zip(*frames)] frame = bytes(frame) return frame def process_calibration_results(self, cooked_data): frame = chunks(cooked_data, self.bytes_per_line) # apply scaling factors frame = [f[:8] + bytes(map(scale, f[8:])) for f in frame] frame = b''.join(frame) if len(self.calib_data) > 0: # Not the first calibration run. Combine results # split previous calibration info into lines lll = chunks(self.calib_data, self.bytes_per_line) # split next calibration info into lines rrr = chunks(frame, self.bytes_per_line) # Don't touch the first 8 bytes of each line, add everything else as signed characters, clipping the values combined = [ ll[:8] + bytes([add(l, r) for l, r in zip(ll[8:], rr[8:])]) for ll, rr in zip(lll, rrr) ] self.calib_data = bytes(b''.join(combined)) else: self.calib_data = frame def get_key_line(self): if len(self.calib_data) > 0: bytes_per_calibration_line = len( self.calib_data) // self.type_info.lines_per_calibration_data key_line_offset = 8 + bytes_per_calibration_line * self.key_calibration_line key_line = self.calib_data[key_line_offset:key_line_offset + self.type_info.line_width] key_line = bytes([i - 1 if i == 5 else i for i in key_line]) else: key_line = b'\0' * self.type_info.line_width return key_line def line_update_type_1(self, mode, chunks): for c in chunks: # Timeslot Table 2D if c[0] == 0x34: # TODO: figure out when to use address increment tst = self.patch_timeslot_table(c[1], True, self.type_info.repeat_multiplier) if mode != CaptureMode.CALIBRATE: tst = self.patch_timeslot_again(tst) c[1] = self.get_key_line() + tst[self.type_info.line_width:] #---------------- Reply Configuration --------------- chunks += [[0x17, b'']] if mode == CaptureMode.IDENTIFY: # This type of fragment is not present in the debugging dump routine. # It seems to be only used for identification and it looks almost identical to Finger Detect (0x26) # Seems to be the same all the time for a given sensor and mostly hardcoded # TODO: analyse construct_wtf_4e @0000000180090BF0 chunks += [[ 0x4e, unhexlify( 'fbb20f0000000f00300000008700020067000a00018000000a0200000b1900008813b80b01091000' ) ]] # Image Reconstruction. # TODO: analyse add_image_reconstruction_cmd_02_buff_list_item @000000018008EA70 chunks += [[ 0x2e, unhexlify('0200180002000000700070004d010000a0008c003c32321e3c0a0202') ]] elif mode == CaptureMode.ENROLL: chunks += [[ 0x26, unhexlify( 'fbb20f0000000f00300000008700020067000a00018000000a0200000b19000050c360ea01091000' ) ]] # Image Reconstruction. There is only one byte difference with the "identify" version. (same is true for 0097) chunks += [[ 0x2e, unhexlify('0200180023000000700070004d010000a0008c003c32321e3c0a0202') ]] #---------------- Interleave --------------- chunks += [[0x44, pack(' 0: bytes_per_calibration_line = len( self.calib_data) // self.type_info.lines_per_calibration_data for i in range(0, 112, 4): l = Line() lines += [l] l.mask = 0xffffffff l.flags = i | (0x85 << 24) l.data = b'' for j in range(0, 112): p = 8 + j * bytes_per_calibration_line + i l.data += self.calib_data[p:p + 4] # Align to dwords, as the sensor demands it for l in lines: pad = len(l.data) % 4 if pad > 0: l.data += b'\0' * (4 - pad) #---------------- Line Update --------------- line_update = pack('> 0x14) <= 1]) chunks += [[0x30, line_update]] #---------------- Line Update Transform --------------- update_transform = b''.join([ pack('> 0x14) > 1 ]) chunks += [[0x43, update_transform]] return chunks def line_update_type_2(self, mode, chunks): for c in chunks: # patch the 2D params. # The following is only needed on some rom versions below 6.5 as reported by cmd_01 #if c[0] == 0x2f: # c[1] = pack(' 0: l.data += b'\0' * (4 - pad) #---------------- Line Update --------------- line_update = pack(' 0: tag, l = unpack(' 0: (t, l), x = unpack(' typing.Tuple[int, int, bytes]: try: stg_id = 0 # match against any storage usr_id = 0 # match against any user cmd = pack('