From 4b8234bead2f8195466706d38cf60208bf8b58b3 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sat, 22 Aug 2026 17:41:07 -0400 Subject: [PATCH] Add 53xc family driver (confirmed working for USB PID 0x533c) Reverse-engineered from the closed libfprint-tod blob shipped for Ubuntu/Canonical OEM images (libfprint-2-tod1-goodix, "53xc" TOD module), plus a live USB capture of that blob's own traffic against real 0x533c hardware. Reuses this repo's existing wire protocol (goodix.py) entirely -- only the model-specific constants (PSK, DEVICE_CONFIG, sensor dimensions) and three protocol-quirk workarounds are new. This chip doesn't reply with a fixed [ack][data] pair for reset, mcu_switch_to_fdt_mode, write_sensor_register, or mcu_get_image's ack -- responses were observed in both orders and sometimes duplicated. tolerant_* helpers classify each read by decoded command instead of assuming position, used only for those commands; everything else goes through the existing Device methods unmodified. Confirmed against real hardware for PID 0x533c only (produces a real, non-degenerate 108x88 image via run_533c.py). 0x530c/0x538c share the same firmware/command set per the same .so but weren't tested -- the five other named DEVICE_CONFIG templates found alongside "MilanFn" in that binary are documented but unused here. PSK writing is intentionally not implemented: this unit's PSK was already correctly provisioned from the factory, and its COMMAND_PRESET_PSK_WRITE_R success convention was found to be inverted relative to every other model in this repo (documented in the driver as a note for anyone implementing it later). Full methodology and static-analysis findings: https://github.com/daemonhorn/goodix-533c-re Closes/references #31. --- driver_53xc.py | 317 +++++++++++++++++++++++++++++++++++++++++++++++++ run_533c.py | 3 + 2 files changed, 320 insertions(+) create mode 100644 driver_53xc.py create mode 100644 run_533c.py diff --git a/driver_53xc.py b/driver_53xc.py new file mode 100644 index 0000000..a07904c --- /dev/null +++ b/driver_53xc.py @@ -0,0 +1,317 @@ +""" +Driver for the Goodix 53xc GTLS sensor family (USB PID 0x530c/0x533c/0x538c). + +Reverse-engineered from the closed libfprint-tod blob shipped for Ubuntu/ +Canonical OEM images (`libfprint-tod-goodix-53xc`, package +`libfprint-2-tod1-goodix`), plus a live USB capture of that blob's own +traffic against real hardware. Full methodology, static analysis notes, +and a working end-to-end capture script: +https://github.com/daemonhorn/goodix-533c-re + +Confirmed against real hardware for PID 0x533c only. 0x530c/0x538c share +the same firmware/command set in the same .so (five other named +DEVICE_CONFIG templates exist alongside the "MilanFn" one used here -- see +goodix-533c-re/findings/device-config.md), but their constants were not +extracted or tested; treat this file as a starting point for those PIDs, +not a working driver for them. + +Device-specific quirk (533c): unlike every other model in this repo, this +firmware does not reply with a fixed [ack][data] pair for `reset`, +`mcu_switch_to_fdt_mode`, `write_sensor_register`, or `mcu_get_image`'s +ack -- responses have been observed in ack-then-data order, data-then-ack +order, and with duplicated packets. goodix.Device's equivalent methods +assume the fixed order and raise ValueError against this chip. The +tolerant_* helpers below classify each USB read by its decoded command +rather than assuming position, and are used in place of the equivalent +Device methods for just those commands. +""" +import select +import socket +import struct +import subprocess +import time + +import goodix +import protocol +import tool + +TARGET_FIRMWARE = "GF5288_GM168SEC_APP_13016" + +# Confirmed via a live capture of the closed blob's own traffic: this +# unit's PSK was already correctly provisioned to all-zero from the +# factory -- COMMAND_PRESET_PSK_WRITE_R was never observed in that +# capture, and this all-zero PSK's PMK_HASH matched what the device +# already had stored. This driver does not implement PSK writing. +PSK = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000000") + +PMK_HASH = bytes.fromhex( + "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925") + +# NOTE for anyone implementing PSK writing for this chip: its +# COMMAND_PRESET_PSK_WRITE_R response uses the OPPOSITE success +# convention from every other model in this repo -- message[0] == 0x01 +# means success here, not the 0x00 that goodix.Device.preset_psk_write() +# checks for. Found by observing a write that goodix.py logged as +# "failed" actually take effect (the PSK read back correctly afterward, +# and the real vendor driver never rewrote it on a later run). Do not +# reuse Device.preset_psk_write()'s return value unmodified for this chip. + +# Extracted from libfprint-tod-goodix-53xc-0.0.4.so's rodata, offset +# 0x103380 (the "MilanFn" config template; five other named sibling +# templates exist in the same blob for other 53xc PIDs/variants -- see +# goodix-533c-re/findings/device-config.md). This is a template, not a +# ready-to-send config: its last 2 bytes are a placeholder checksum, not +# a valid one, matching the pattern in driver_53x5.py's +# DEFAULT_CONFIG/fix_config_checksum. No OTP-derived calibration data is +# spliced in here (unlike 53x5's TCODE_TAG/DAC_L_TAG patching) -- a +# checksum fix alone was confirmed sufficient to get upload_config_mcu() +# accepted and produce a real image, but calibration quality with a +# finger actually present has not been evaluated. +DEFAULT_CONFIG = bytes.fromhex( + "6011607124952cc114d510e500e514f9030402000008001111ba000180ca000700" + "8400c0b38600bbc48800baba8a00b2b28c00aaaa8e00c1c19000bbbb9200b1b194" + "0000a8960000b6980000bf9a0000ba50000105d000000070000000720078567400" + "34122600001220001040120003042a0102002200012024003200800001005c0080" + "00560008205800010032002c028200800cba000180ca0007002a01820320001040" + "2200012024001400800005005c0000015600082058000300820080152a0108005c" + "0080006200090364001800220000202a0108005c00800052000800000000000000" + "00000000000000fdf02e2e2f73656e736f722f4d696c616e46") + + +def fix_config_checksum(config: bytearray): + """Seed 0xa5a5, running 16-bit LE sum over bytes [0:254], negated mod + 0x10000, stored LE in bytes [254:256]. Matches driver_53x5.py's + fix_config_checksum exactly; confirmed via disassembly of this .so + (VA 0x2d0a0) and required since DEFAULT_CONFIG's stored checksum is + stale template data, not a checksum of the template as-is.""" + checksum = 0xa5a5 + for i in range(0, 254, 2): + checksum = (checksum + int.from_bytes(config[i:i + 2], "little")) & 0xffff + checksum = (0x10000 - checksum) & 0xffff + config[254:256] = checksum.to_bytes(2, "little") + + +_config = bytearray(DEFAULT_CONFIG) +fix_config_checksum(_config) +DEVICE_CONFIG = bytes(_config) + +# Not a confirmed firmware constant -- static analysis of this .so found +# no SENSOR_WIDTH/HEIGHT for this PID (see goodix-533c-re/findings/ +# dims-and-inventory.md). Strongly shape-corroborated from a live capture +# instead: of the 12 plausible (width, height) factorizations of the +# resulting 9504-pixel decoded image, only width=108 reshapes into a +# coherent 2D shape (a rounded vignette matching a capacitive sensor's +# physical active area); every other candidate is pure horizontal banding +# with no 2D structure -- the signature of reshaping row-major data at +# the wrong row length. This also exactly matches driver_53x5.py's +# SENSOR_WIDTH=108/SENSOR_HEIGHT=88, suggesting 530c/533c/538c may share +# that sensor's physical die. Treat as likely, not certain. +SENSOR_WIDTH = 108 +SENSOR_HEIGHT = 88 + + +def init_device(product: int): + device = goodix.Device(product, protocol.USBProtocol) + device.nop() + return device + + +def check_psk(device: goodix.Device): + ok, flags, pmk_hash = device.preset_psk_read(0xbb020001, len(PMK_HASH), 0) + return ok and pmk_hash == PMK_HASH + + +def _tolerant_read(device: goodix.Device, command: int, expect_data: bool): + """Drain packets until one ACK and (if expect_data) one data response + for `command` have been seen, discarding any duplicate/orphaned + extras -- order-agnostic, classifying each read by decoded command + rather than assuming position. Returns the data payload (or None).""" + ack_payload = None + data_payload = None + misses = 0 + while misses < 2 and (ack_payload is None or + (expect_data and data_payload is None)): + try: + raw = device.protocol.read(timeout=1) + except Exception: + misses += 1 + continue + + try: + inner, _flags, _length = goodix.decode_message_pack(raw) + payload, cmd, _plen = goodix.decode_message_protocol(inner) + except Exception: + continue + + if cmd == goodix.COMMAND_ACK and ack_payload is None: + ack_payload = payload + elif cmd == command and data_payload is None: + data_payload = payload + + if ack_payload is not None: + goodix.check_ack(ack_payload, command) + + return data_payload + + +def tolerant_reset(device: goodix.Device, reset_sensor: bool, + soft_reset_mcu: bool, sleep_time: int): + payload = (struct.pack("