import logging import os.path import typing from binascii import hexlify, unhexlify from enum import Enum from hashlib import sha256 from struct import pack, unpack from time import sleep from usb import core as usb_core from . import timeslot as prg from .blobs import reset_blob from .db import db, SidIdentity from .flash import write_enable, call_cleanups, read_flash, erase_flash, write_flash_all, read_flash_all from .hw_tables import dev_info_lookup from .table_types import SensorTypeInfo, SensorCaptureProg from .tls import tls from .usb import usb, CancelledException from .util import assert_status, unhex # 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('