diff --git a/libfprint/drivers/fpcmoh/fpcmoh.c b/libfprint/drivers/fpcmoh/fpcmoh.c new file mode 100644 index 00000000..5e4e7725 --- /dev/null +++ b/libfprint/drivers/fpcmoh/fpcmoh.c @@ -0,0 +1,1558 @@ +/* + * FPC MOH (Match-on-Host) driver for libfprint + * + * Supports FPC Disum USB fingerprint sensors (10a5:9200, 10a5:9201). + * These are "match on host" sensors: they capture raw fingerprint images + * over a TLS-PSK encrypted USB channel and rely on the host for matching. + * + * Copyright (c) 2026 Sergey Subbotin + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "drivers_api.h" +#include "fpcmoh.h" +#include "fpi-image.h" + +#include +#include +#include +#include + +#define FP_COMPONENT "fpcmoh" + +G_DEFINE_TYPE (FpiDeviceFpcMoh, fpi_device_fpcmoh, FP_TYPE_IMAGE_DEVICE); + +/* Nearest-neighbor 2x upscale — gives SIGFM more pixels for keypoint detection + * on this small sensor (112x88 → 224x176). */ +static FpImage * +fpcmoh_scale_nn_2x (FpImage *src) +{ + int sw = src->width * 2; + int sh = src->height * 2; + FpImage *dst = fp_image_new (sw, sh); + int x, y; + + for (y = 0; y < sh; y++) + for (x = 0; x < sw; x++) + dst->data[y * sw + x] = src->data[(y / 2) * src->width + (x / 2)]; + + dst->flags = src->flags; + dst->ppmm = src->ppmm; + return dst; +} + +static const FpIdEntry id_table[] = { + { .vid = 0x10A5, .pid = 0x9200 }, + { .vid = 0x10A5, .pid = 0x9201 }, + { .vid = 0, .pid = 0 }, +}; + +/* ---- Crypto helpers ---- */ + +static void +fpcmoh_sha256 (const void *data, gsize len, guint8 *out) +{ + EVP_MD_CTX *ctx = EVP_MD_CTX_new (); + unsigned int olen = 0; + + EVP_DigestInit_ex (ctx, EVP_sha256 (), NULL); + EVP_DigestUpdate (ctx, data, len); + EVP_DigestFinal_ex (ctx, out, &olen); + EVP_MD_CTX_free (ctx); +} + +static gboolean +fpcmoh_verify_tls_key_hmac (const guint8 *aad, gsize aad_len, + const guint8 *key, gsize key_len, + const guint8 *sig, gsize sig_len) +{ + guint8 hmac_key[SHA256_DIGEST_LENGTH]; + guint8 computed_sig[SHA256_DIGEST_LENGTH]; + + if (sig_len != SHA256_DIGEST_LENGTH) + return FALSE; + + /* HMAC key = SHA256("FPC_HMAC_KEY") - 13 bytes including null terminator */ + fpcmoh_sha256 ("FPC_HMAC_KEY", 13, hmac_key); + + EVP_MAC *mac = EVP_MAC_fetch (NULL, "HMAC", NULL); + EVP_MAC_CTX *mctx = EVP_MAC_CTX_new (mac); + + char digest_name[] = "SHA256"; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string ("digest", digest_name, 0), + OSSL_PARAM_construct_end () + }; + + EVP_MAC_init (mctx, hmac_key, SHA256_DIGEST_LENGTH, params); + EVP_MAC_update (mctx, aad, aad_len); + EVP_MAC_update (mctx, key, key_len); + + size_t out_len = sizeof (computed_sig); + EVP_MAC_final (mctx, computed_sig, &out_len, sizeof (computed_sig)); + + EVP_MAC_CTX_free (mctx); + EVP_MAC_free (mac); + + return CRYPTO_memcmp (computed_sig, sig, SHA256_DIGEST_LENGTH) == 0; +} + +static gboolean +fpcmoh_decrypt_tls_psk (const guint8 *encrypted, gsize enc_len, + guint8 *psk_out, gsize *psk_len_out) +{ + guint8 sealing_key[SHA256_DIGEST_LENGTH]; + EVP_CIPHER_CTX *ctx; + int out_len = 0; + + /* Sealing key = SHA256("FPC_SEALING_KEY") - 16 bytes including null terminator */ + fpcmoh_sha256 ("FPC_SEALING_KEY", 16, sealing_key); + + ctx = EVP_CIPHER_CTX_new (); + + /* Use EVP_CipherInit with NULL IV (zero IV) and direction=0 (decrypt), + * matching the reference implementation exactly. */ + if (!EVP_CipherInit (ctx, EVP_aes_256_cbc (), sealing_key, NULL, 0)) + { + EVP_CIPHER_CTX_free (ctx); + return FALSE; + } + + /* Disable PKCS7 padding — the encrypted key may not be padded. + * Must be called AFTER CipherInit so the context is fully set up. */ + EVP_CIPHER_CTX_set_padding (ctx, 0); + + gsize total_out = 0; + + out_len = 0; + if (!EVP_CipherUpdate (ctx, psk_out, &out_len, encrypted, enc_len)) + { + EVP_CIPHER_CTX_free (ctx); + return FALSE; + } + total_out = out_len; + + out_len = 0; + EVP_CipherFinal (ctx, psk_out + total_out, &out_len); + total_out += out_len; + + EVP_CIPHER_CTX_free (ctx); + + *psk_len_out = MIN (total_out, 32); + fp_dbg ("PSK decrypted: %" G_GSIZE_FORMAT " bytes from %" G_GSIZE_FORMAT " encrypted", *psk_len_out, enc_len); + return TRUE; +} + +/* ---- TLS-PSK callback ---- */ + +static unsigned int +fpcmoh_psk_server_cb (SSL *ssl, const char *identity, + unsigned char *psk, unsigned int max_psk_len) +{ + FpiDeviceFpcMoh *self = SSL_get_app_data (ssl); + + if (self->tls_psk_len == 0 || self->tls_psk_len > max_psk_len) + return 0; + + memcpy (psk, self->tls_psk, self->tls_psk_len); + return self->tls_psk_len; +} + +/* ---- USB control transfer helpers ---- */ + +static void +fpcmoh_ctrl_cmd_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + fpi_ssm_next_state (transfer->ssm); +} + +/* Like fpcmoh_ctrl_cmd_cb but ignores errors (for deactivation commands + * that may stall if no session is active). */ +static void +fpcmoh_ctrl_cmd_ignore_error_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + if (error) + { + fp_dbg ("Ignoring control transfer error: %s", error->message); + g_error_free (error); + } + + fpi_ssm_next_state (transfer->ssm); +} + +/* Fire-and-forget callback: just log errors, don't touch SSM. */ +static void +fpcmoh_ctrl_cmd_noop_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + if (error) + { + fp_dbg ("Fire-and-forget ctrl error: %s", error->message); + g_error_free (error); + } +} + +static void +fpcmoh_send_ctrl_full (FpDevice *dev, FpiSsm *ssm, + guint8 request, guint16 value, guint16 index, + const guint8 *data, gsize data_len, + FpiUsbTransferCallback callback) +{ + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + + fpi_usb_transfer_fill_control (transfer, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + request, value, index, data_len); + + if (data && data_len > 0) + memcpy (transfer->buffer, data, data_len); + + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, FPCMOH_CTRL_TIMEOUT, NULL, + callback, NULL); +} + +static void +fpcmoh_send_ctrl (FpDevice *dev, FpiSsm *ssm, + guint8 request, guint16 value, guint16 index, + const guint8 *data, gsize data_len) +{ + fpcmoh_send_ctrl_full (dev, ssm, request, value, index, data, data_len, + fpcmoh_ctrl_cmd_cb); +} + +/* ---- Bulk event reception helper ---- */ + +static void fpcmoh_capture_bulk_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error); +static void fpcmoh_tls_handshake_flush_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error); + +static void +fpcmoh_submit_bulk_read (FpDevice *dev, FpiSsm *ssm, + FpiUsbTransferCallback callback, + guint timeout_ms) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + + fpi_usb_transfer_fill_bulk (transfer, FPCMOH_EP_IN, FPCMOH_EP_IN_MAX_BUF_SIZE); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, timeout_ms, + self->interrupt_cancellable, + callback, NULL); +} + +/* ---- TLS data exchange helpers ---- */ + +/* Feed data received from device bulk IN (ev_tls payload) into SSL BIO input */ +static void +fpcmoh_tls_feed_input (FpiDeviceFpcMoh *self, + const guint8 *data, gsize len) +{ + if (len > 0) + BIO_write (self->bio_in, data, len); +} + +/* ---- Bulk event accumulation ---- + * + * Events from the device may span multiple USB bulk transfers (max 64 bytes). + * The event header's `len` field (big-endian) gives the total event size. + * We accumulate into self->bulk_buf until we have the full event. + */ + +static void fpcmoh_process_event (FpDevice *dev, + FpiSsm *ssm, + FpiUsbTransferCallback bulk_cb); + +static void +fpcmoh_open_bulk_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + if (error) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* Accumulate data into bulk_buf */ + if (self->bulk_recv_len + transfer->actual_length > sizeof (self->bulk_buf)) + { + fp_err ("Bulk buffer overflow"); + self->bulk_recv_len = 0; + self->evt_total_len = 0; + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + memcpy (self->bulk_buf + self->bulk_recv_len, + transfer->buffer, transfer->actual_length); + self->bulk_recv_len += transfer->actual_length; + + /* Do we have enough for the header? */ + if (self->bulk_recv_len < sizeof (FpcMohEvtHdr)) + { + fpcmoh_submit_bulk_read (dev, transfer->ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + /* Parse total event length from header */ + if (self->evt_total_len == 0) + { + FpcMohEvtHdr *hdr = (FpcMohEvtHdr *) self->bulk_buf; + self->evt_total_len = GUINT32_FROM_BE (hdr->len); + } + + /* Do we have the full event? */ + if (self->bulk_recv_len < self->evt_total_len) + { + fpcmoh_submit_bulk_read (dev, transfer->ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + /* Full event received, process it */ + fpcmoh_process_event (dev, transfer->ssm, fpcmoh_open_bulk_cb); +} + +/* Try to advance TLS handshake: flush output then read more */ +static void +fpcmoh_tls_handshake_step (FpDevice *dev, FpiSsm *ssm) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + int ret = SSL_do_handshake (self->ssl); + + if (ret == 1) + { + const SSL_CIPHER *cipher = SSL_get_current_cipher (self->ssl); + fp_dbg ("TLS handshake completed! cipher=%s version=%s", + cipher ? SSL_CIPHER_get_name (cipher) : "unknown", + SSL_get_version (self->ssl)); + self->tls_established = TRUE; + + /* CRITICAL: Flush any remaining TLS handshake data (e.g. Finished) + * to the device before proceeding. Without this, the sensor never + * receives our final handshake messages and doesn't consider TLS + * established, causing a reset on subsequent commands. */ + int pending_out = BIO_ctrl_pending (self->bio_out); + if (pending_out > 0) + { + fp_dbg ("Flushing %d bytes of TLS handshake output", pending_out); + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, + MIN (pending_out, FPCMOH_BULK_MAX_PKT)); + if (n > 0) + { + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + t->ssm = ssm; + /* After flushing, the flush callback will continue flushing + * or complete the handshake. */ + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_tls_handshake_flush_cb, NULL); + return; + } + } + + fpi_ssm_next_state (ssm); + return; + } + + int ssl_err = SSL_get_error (self->ssl, ret); + + if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) + { + /* Flush any pending output first */ + int pending_out = BIO_ctrl_pending (self->bio_out); + if (pending_out > 0) + { + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, + MIN (pending_out, FPCMOH_BULK_MAX_PKT)); + if (n > 0) + { + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + t->ssm = ssm; + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_tls_handshake_flush_cb, NULL); + return; + } + } + + /* Need data from device */ + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + fp_err ("TLS handshake error: ssl_err=%d: %s", ssl_err, + ERR_error_string (ERR_get_error (), NULL)); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); +} + +/* Process a fully received bulk event */ +static void +fpcmoh_process_event (FpDevice *dev, FpiSsm *ssm, + FpiUsbTransferCallback bulk_cb) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + FpcMohEvtHdr *hdr = (FpcMohEvtHdr *) self->bulk_buf; + guint32 code = GUINT32_FROM_BE (hdr->code); + guint32 len = GUINT32_FROM_BE (hdr->len); + int ssm_state = fpi_ssm_get_cur_state (ssm); + + fp_dbg ("%s event code=0x%02x len=%u state=%d", + G_STRFUNC, code, len, ssm_state); + + /* Reset accumulator for next event */ + gsize event_len = self->evt_total_len; + self->bulk_recv_len = 0; + self->evt_total_len = 0; + + switch (ssm_state) + { + case FPCMOH_OPEN_WAIT_INIT_RESULT: + if (code == FPCMOH_EVT_INIT_RESULT) + { + fp_dbg ("Got init result, proceeding to TLS key retrieval"); + fpi_ssm_next_state (ssm); + return; + } + fpcmoh_submit_bulk_read (dev, ssm, bulk_cb, FPCMOH_DATA_TIMEOUT); + return; + + case FPCMOH_OPEN_TLS_HANDSHAKE: + if (code == FPCMOH_EVT_TLS) + { + /* Feed TLS payload (after event header) into SSL BIO */ + gsize payload_off = sizeof (FpcMohEvtHdr); + + if (event_len > payload_off) + fpcmoh_tls_feed_input (self, + self->bulk_buf + payload_off, + event_len - payload_off); + + fpcmoh_tls_handshake_step (dev, ssm); + return; + } + fp_dbg ("Ignoring event 0x%02x during TLS handshake", code); + fpcmoh_submit_bulk_read (dev, ssm, bulk_cb, FPCMOH_DATA_TIMEOUT); + return; + + default: + fp_dbg ("Unexpected event 0x%02x in state %d", code, ssm_state); + fpcmoh_submit_bulk_read (dev, ssm, bulk_cb, FPCMOH_DATA_TIMEOUT); + return; + } +} + +/* Callback for the GET_STATE control transfer */ +static void +fpcmoh_open_get_state_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + if (transfer->actual_length >= 4) + { + fp_dbg ("FPC firmware version: %d.%d.%d.%d", + transfer->buffer[0], transfer->buffer[1], + transfer->buffer[2], transfer->buffer[3]); + } + + fpi_ssm_next_state (transfer->ssm); +} + +/* Callback for the GET_TLS_KEY control transfer */ +static void +fpcmoh_open_get_tls_key_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* The control response buffer starts after the setup packet (8 bytes) for + * control transfers created with fpi_usb_transfer_fill_control. + * But libfprint's fill_control puts the response directly in transfer->buffer. */ + guint8 *data = transfer->buffer; + gsize data_len = transfer->actual_length; + + fp_dbg ("%s received TLS key packet, %" G_GSIZE_FORMAT " bytes", G_STRFUNC, data_len); + + if (data_len < sizeof (FpcMohTlsKeyPkt)) + { + fp_err ("TLS key packet too short: %" G_GSIZE_FORMAT, data_len); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + FpcMohTlsKeyPkt *pkt = (FpcMohTlsKeyPkt *) data; + + if (pkt->magic != FPCMOH_TLS_KEY_MAGIC) + { + fp_err ("TLS key packet bad magic: 0x%08x (expected 0x%08x)", + pkt->magic, FPCMOH_TLS_KEY_MAGIC); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + /* All offsets in the packet are relative to the start of the packet */ + if ((pkt->aad_offset + pkt->aad_len) > data_len || + (pkt->key_offset + pkt->key_len) > data_len || + (pkt->sig_offset + pkt->sig_len) > data_len) + { + fp_err ("TLS key packet fields out of bounds (aad=%u+%u, key=%u+%u, sig=%u+%u, total=%" G_GSIZE_FORMAT ")", + pkt->aad_offset, pkt->aad_len, pkt->key_offset, pkt->key_len, + pkt->sig_offset, pkt->sig_len, data_len); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + /* Verify AAD is "FPC TLS Keys" */ + if (pkt->aad_len < 12 || + memcmp ("FPC TLS Keys", data + pkt->aad_offset, 12) != 0) + { + fp_err ("TLS key packet bad AAD"); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + /* Verify HMAC signature */ + if (!fpcmoh_verify_tls_key_hmac (data + pkt->aad_offset, pkt->aad_len, + data + pkt->key_offset, pkt->key_len, + data + pkt->sig_offset, pkt->sig_len)) + { + fp_err ("TLS key HMAC verification failed"); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + fp_dbg ("TLS key HMAC verified OK"); + + /* Decrypt PSK */ + gsize psk_len = 0; + + if (!fpcmoh_decrypt_tls_psk (data + pkt->key_offset, pkt->key_len, + self->tls_psk, &psk_len)) + { + fp_err ("TLS PSK decryption failed"); + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + self->tls_psk_len = MIN (psk_len, sizeof (self->tls_psk)); + fp_dbg ("TLS PSK decrypted, %" G_GSIZE_FORMAT " bytes", self->tls_psk_len); + + fpi_ssm_next_state (transfer->ssm); +} + +static void +fpcmoh_open_ssm_run (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + FpiUsbTransfer *transfer; + + switch (fpi_ssm_get_cur_state (ssm)) + { + case FPCMOH_OPEN_INDICATE_S_STATE: + fp_dbg ("Sending cmd_indicate_s_state (S0)"); + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_INDICATE_S_STATE, + FPCMOH_S_STATE_S0, 0, NULL, 0); + break; + + case FPCMOH_OPEN_GET_STATE: + fp_dbg ("Sending cmd_get_state"); + transfer = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (transfer, + G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_GET_STATE, 0, 0, 72); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_open_get_state_cb, NULL); + break; + + case FPCMOH_OPEN_PARSE_STATE: + /* State was parsed in callback, move on */ + fpi_ssm_next_state (ssm); + break; + + case FPCMOH_OPEN_CMD_INIT: + { + fp_dbg ("Sending cmd_init"); + guint8 init_data[FPCMOH_INIT_DATA_SIZE] = { FPCMOH_ARM_OP_INIT, 0x2f, 0x11, 0x17 }; + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_INIT, 0x0001, 0, + init_data, sizeof (init_data)); + } + break; + + case FPCMOH_OPEN_WAIT_INIT_RESULT: + fp_dbg ("Waiting for init result on bulk IN"); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + break; + + case FPCMOH_OPEN_GET_TLS_KEY: + fp_dbg ("Sending cmd_get_tls_key"); + transfer = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (transfer, + G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_GET_TLS_KEY, 0, 0, 1000); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_open_get_tls_key_cb, NULL); + break; + + case FPCMOH_OPEN_PARSE_TLS_KEY: + /* Key was parsed in callback, move on */ + fpi_ssm_next_state (ssm); + break; + + case FPCMOH_OPEN_TLS_INIT: + { + fp_dbg ("Initializing TLS-PSK"); + + /* Create SSL context - we are the server, sensor is client */ + self->ssl_ctx = SSL_CTX_new (TLS_server_method ()); + if (!self->ssl_ctx) + { + fp_err ("SSL_CTX_new failed"); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + + SSL_CTX_set_options (self->ssl_ctx, SSL_OP_NO_COMPRESSION); + SSL_CTX_use_psk_identity_hint (self->ssl_ctx, NULL); + SSL_CTX_set_psk_server_callback (self->ssl_ctx, fpcmoh_psk_server_cb); + + /* Allow PSK and ECDHE-PSK cipher suites. + * The sensor's mbedTLS prefers ECDHE-PSK-WITH-AES-128-CBC-SHA256. */ + SSL_CTX_set_cipher_list (self->ssl_ctx, + "ECDHE-PSK-AES128-CBC-SHA256:" + "ECDHE-PSK-AES256-CBC-SHA384:" + "PSK-AES128-CBC-SHA256:PSK-AES256-CBC-SHA384:" + "PSK-AES128-CBC-SHA:PSK-AES256-CBC-SHA:" + "PSK-AES128-GCM-SHA256:PSK-AES256-GCM-SHA384"); + + /* Create SSL connection */ + self->ssl = SSL_new (self->ssl_ctx); + if (!self->ssl) + { + fp_err ("SSL_new failed"); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + + SSL_set_app_data (self->ssl, self); + + /* Create memory BIOs for data exchange */ + self->bio_in = BIO_new (BIO_s_mem ()); + self->bio_out = BIO_new (BIO_s_mem ()); + BIO_set_mem_eof_return (self->bio_in, -1); + BIO_set_mem_eof_return (self->bio_out, -1); + SSL_set_bio (self->ssl, self->bio_in, self->bio_out); + + /* Set as server and start accepting */ + SSL_set_accept_state (self->ssl); + + /* Match reference: limit TLS record size to 4096 bytes */ + SSL_set_max_send_fragment (self->ssl, 4096); + + /* Send cmd_tls_init to the device to start TLS handshake */ + fp_dbg ("Sending cmd_tls_init"); + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_TLS_INIT, 0x0001, 0, NULL, 0); + } + break; + + case FPCMOH_OPEN_TLS_HANDSHAKE: + { + /* Try handshake - initially SSL will produce ServerHello etc */ + int ret = SSL_do_handshake (self->ssl); + + if (ret == 1) + { + const SSL_CIPHER *cipher = SSL_get_current_cipher (self->ssl); + fp_dbg ("TLS handshake completed! cipher=%s version=%s", + cipher ? SSL_CIPHER_get_name (cipher) : "unknown", + SSL_get_version (self->ssl)); + self->tls_established = TRUE; + + /* Flush any remaining TLS handshake data (e.g. Finished) */ + int pending_out = BIO_ctrl_pending (self->bio_out); + if (pending_out > 0) + { + fp_dbg ("Flushing %d bytes of TLS handshake output", pending_out); + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, + MIN (pending_out, FPCMOH_BULK_MAX_PKT)); + if (n > 0) + { + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + t->ssm = ssm; + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_tls_handshake_flush_cb, NULL); + return; + } + } + + fpi_ssm_next_state (ssm); + return; + } + + int ssl_err = SSL_get_error (self->ssl, ret); + + if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) + { + /* Flush output (ServerHello etc) to device, then wait for client */ + int pending = BIO_ctrl_pending (self->bio_out); + if (pending > 0) + { + /* Send TLS data to device, then read bulk for ClientHello response. + * The flush function will call fpi_ssm_next_state when done, + * but we want to stay in TLS_HANDSHAKE state. Jump back. */ + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, MIN (pending, FPCMOH_BULK_MAX_PKT)); + + if (n > 0) + { + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + t->ssm = ssm; + /* After sending, we need to read more from device. + * Use a callback that stays in this state. */ + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_tls_handshake_flush_cb, NULL); + return; + } + } + + /* Need data from device - read bulk */ + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + fp_err ("TLS handshake failed: ssl_err=%d: %s", ssl_err, + ERR_error_string (ERR_get_error (), NULL)); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + } + break; + + default: + g_assert_not_reached (); + } +} + +/* The TLS handshake flush callback needs to loop back to reading, + * not advance the SSM. Override the behavior by jumping back. */ +static void +fpcmoh_tls_handshake_flush_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* Check if more to send */ + int pending = BIO_ctrl_pending (self->bio_out); + + if (pending > 0) + { + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, MIN (pending, FPCMOH_BULK_MAX_PKT)); + + if (n > 0) + { + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + t->ssm = transfer->ssm; + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_tls_handshake_flush_cb, NULL); + return; + } + } + + /* All flushed */ + if (self->tls_established) + { + /* Post-handshake flush complete — advance SSM */ + fp_dbg ("TLS handshake flush complete, advancing SSM"); + fpi_ssm_next_state (transfer->ssm); + } + else + { + /* Mid-handshake flush — read more from device */ + fpcmoh_submit_bulk_read (dev, transfer->ssm, fpcmoh_open_bulk_cb, + FPCMOH_DATA_TIMEOUT); + } +} + +static void +fpcmoh_open_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + self->open_ssm = NULL; + + fpi_image_device_open_complete (FP_IMAGE_DEVICE (dev), error); +} + +/* ---- Capture SSM ---- + * + * After TLS is established, ALL sensor events (finger_down, image, etc.) + * arrive encrypted inside ev_tls (0x05) frames on the USB bulk endpoint. + * We must: + * 1. Read raw USB bulk data and accumulate complete events + * 2. For ev_tls events: feed payload into SSL BIO, then SSL_read + * 3. Parse the decrypted data as inner fpc_event structures + * 4. Handle inner events (finger_down → get image, image → submit) + */ + +/* Forward declarations */ +static void fpcmoh_capture_process_event (FpDevice *dev, + FpiSsm *ssm); +static void fpcmoh_process_tls_data (FpDevice *dev, + FpiSsm *ssm); + +/* Send any pending TLS output (from bio_out) to the device. + * After TLS handshake, SSL_read rarely produces output, but handle it. */ +static void +fpcmoh_flush_tls_output (FpDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + int pending = BIO_ctrl_pending (self->bio_out); + + while (pending > 0) + { + guint8 buf[FPCMOH_BULK_MAX_PKT]; + int n = BIO_read (self->bio_out, buf, MIN (pending, FPCMOH_BULK_MAX_PKT)); + + if (n <= 0) + break; + + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_TLS_DATA, 0x0001, 0, n); + memcpy (t->buffer, buf, n); + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_ctrl_cmd_noop_cb, NULL); + + pending = BIO_ctrl_pending (self->bio_out); + } +} + +static void +fpcmoh_capture_bulk_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + if (error) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + if (self->deactivating) + fpi_ssm_mark_completed (transfer->ssm); + return; + } + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + int ssm_state = fpi_ssm_get_cur_state (transfer->ssm); + gboolean waiting_finger = (ssm_state == FPCMOH_CAPTURE_WAIT_FINGER || + ssm_state == FPCMOH_CAPTURE_ARM_SENSOR); + guint timeout = waiting_finger ? FPCMOH_FINGER_TIMEOUT : FPCMOH_DATA_TIMEOUT; + + /* Accumulate data into bulk_buf (events may span multiple USB packets) */ + if (self->bulk_recv_len + transfer->actual_length > sizeof (self->bulk_buf)) + { + fp_err ("Bulk buffer overflow in capture"); + self->bulk_recv_len = 0; + self->evt_total_len = 0; + fpi_ssm_mark_failed (transfer->ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + memcpy (self->bulk_buf + self->bulk_recv_len, + transfer->buffer, transfer->actual_length); + self->bulk_recv_len += transfer->actual_length; + + /* Need at least the event header */ + if (self->bulk_recv_len < sizeof (FpcMohEvtHdr)) + { + fpcmoh_submit_bulk_read (dev, transfer->ssm, fpcmoh_capture_bulk_cb, timeout); + return; + } + + /* Parse total event length from header */ + if (self->evt_total_len == 0) + { + FpcMohEvtHdr *hdr = (FpcMohEvtHdr *) self->bulk_buf; + self->evt_total_len = GUINT32_FROM_BE (hdr->len); + } + + /* Need the full event */ + if (self->bulk_recv_len < self->evt_total_len) + { + fpcmoh_submit_bulk_read (dev, transfer->ssm, fpcmoh_capture_bulk_cb, timeout); + return; + } + + /* Full USB event received - process it */ + fpcmoh_capture_process_event (dev, transfer->ssm); +} + +/* Process a complete USB bulk event during capture. + * After TLS, all meaningful events come as ev_tls (0x05). */ +static void +fpcmoh_capture_process_event (FpDevice *dev, FpiSsm *ssm) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + FpcMohEvtHdr *hdr = (FpcMohEvtHdr *) self->bulk_buf; + guint32 code = GUINT32_FROM_BE (hdr->code); + gsize event_len = self->evt_total_len; + int state = fpi_ssm_get_cur_state (ssm); + gboolean waiting_finger = (state == FPCMOH_CAPTURE_WAIT_FINGER || + state == FPCMOH_CAPTURE_ARM_SENSOR); + guint timeout = waiting_finger ? FPCMOH_FINGER_TIMEOUT : FPCMOH_DATA_TIMEOUT; + + fp_dbg ("capture USB event: code=0x%02x len=%u state=%d", + code, (guint) event_len, state); + + /* Reset accumulator for next event */ + self->bulk_recv_len = 0; + self->evt_total_len = 0; + + if (code == FPCMOH_EVT_TLS) + { + /* Feed TLS payload into SSL BIO */ + gsize payload_off = sizeof (FpcMohEvtHdr); + + if (event_len > payload_off) + fpcmoh_tls_feed_input (self, + self->bulk_buf + payload_off, + event_len - payload_off); + + /* Decrypt and process inner events */ + fpcmoh_process_tls_data (dev, ssm); + return; + } + + /* Handle raw (non-TLS) events. + * Some firmware versions may send finger_down etc. as raw events. */ + switch (code) + { + case FPCMOH_EVT_FINGER_DOWN: + fp_dbg ("Finger detected (raw event)!"); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE); + fpi_ssm_jump_to_state (ssm, FPCMOH_CAPTURE_GET_IMAGE); + return; + + case FPCMOH_EVT_FINGER_UP: + fp_dbg ("Finger up (raw event)"); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); + break; + + case FPCMOH_EVT_ARM_RESULT: + fp_dbg ("Arm result (raw event)"); + break; + + case FPCMOH_EVT_HELLO: + fp_dbg ("Hello event (raw), sensor is alive"); + break; + + default: + fp_dbg ("Unhandled raw event 0x%02x during capture (state=%d)", code, state); + break; + } + + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, timeout); +} + +/* Read decrypted data from SSL and process inner TLS messages. + * Firmware 11.x TLS inner messages: {cmdid(4B BE), total_len(4B BE), ...metadata...} + * Only cmdid 4 (dead_pixel) and 8 (image) come through TLS. + * Other events (finger_down, arm_result) arrive as raw USB events. */ +static void +fpcmoh_process_tls_data (FpDevice *dev, FpiSsm *ssm) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + int state = fpi_ssm_get_cur_state (ssm); + gboolean waiting_finger = (state == FPCMOH_CAPTURE_WAIT_FINGER || + state == FPCMOH_CAPTURE_ARM_SENSOR); + guint timeout = waiting_finger ? FPCMOH_FINGER_TIMEOUT : FPCMOH_DATA_TIMEOUT; + guint8 ssl_buf[8192]; + int n; + + n = SSL_read (self->ssl, ssl_buf, sizeof (ssl_buf)); + + if (n <= 0) + { + int ssl_err = SSL_get_error (self->ssl, n); + + if (ssl_err == SSL_ERROR_WANT_READ) + { + /* Need more encrypted data from USB - read another bulk event */ + fpcmoh_flush_tls_output (dev); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, timeout); + return; + } + + fp_err ("SSL_read error: %d: %s", ssl_err, + ERR_error_string (ERR_get_error (), NULL)); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + fp_dbg ("SSL_read: %d decrypted bytes (state=%d, tls_in_image=%d)", + n, state, self->tls_in_image); + + /* If we're in the middle of receiving image payload data, + * all decrypted bytes are image pixels. */ + if (self->tls_in_image) + { + if (self->img_buf) + { + gsize space = self->img_expected_len - self->img_recv_len; + gsize copy = MIN ((gsize) n, space); + + memcpy (self->img_buf + self->img_recv_len, ssl_buf, copy); + self->img_recv_len += copy; + } + + if (self->img_recv_len >= self->img_expected_len) + { + fp_dbg ("Full image received (%" G_GSIZE_FORMAT " bytes)", self->img_recv_len); + self->tls_in_image = FALSE; + + g_autoptr(FpImage) img = fp_image_new (FPCMOH_IMG_WIDTH, + FPCMOH_IMG_HEIGHT); + memcpy (img->data, self->img_buf, + MIN (self->img_expected_len, (gsize) FPCMOH_IMG_PIXELS)); + img->flags = 0; + + FpImage *scaled = fpcmoh_scale_nn_2x (img); + fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), + g_steal_pointer (&scaled)); + + self->img_recv_len = 0; + fpi_ssm_mark_completed (ssm); + return; + } + + /* Need more image data */ + fpcmoh_flush_tls_output (dev); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + /* Parse inner TLS message header: {cmdid(4B BE), total_len(4B BE)} */ + if ((gsize) n < 8) + { + fp_dbg ("Partial TLS header (%d bytes), need more data", n); + fpcmoh_flush_tls_output (dev); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, timeout); + return; + } + + guint32 cmdid, total_len; + memcpy (&cmdid, ssl_buf, 4); + memcpy (&total_len, ssl_buf + 4, 4); + cmdid = GUINT32_FROM_BE (cmdid); + total_len = GUINT32_FROM_BE (total_len); + + fp_dbg ("TLS inner message: cmdid=%u total_len=%u", cmdid, total_len); + + switch (cmdid) + { + case FPCMOH_EVT_DEAD_PIXEL_REPORT: + fp_dbg ("Dead pixel report (via TLS), len=%u", total_len); + break; + + case FPCMOH_EVT_IMAGE: + { + /* total_len includes the 24-byte header; image payload follows */ + gsize img_payload = (total_len > FPCMOH_TLS_MSG_HDR_SIZE) ? + total_len - FPCMOH_TLS_MSG_HDR_SIZE : 0; + fp_dbg ("Image (via TLS), total_len=%u, payload=%lu bytes", + total_len, (unsigned long) img_payload); + + self->tls_in_image = TRUE; + self->img_recv_len = 0; + self->img_expected_len = MIN (img_payload, (gsize) FPCMOH_IMG_SIZE); + + /* Image pixels start after the 24-byte TLS message header */ + gsize img_data_off = FPCMOH_TLS_MSG_HDR_SIZE; + gsize img_data_len = (n > (int) img_data_off) ? (gsize) n - img_data_off : 0; + + if (img_data_len > 0 && self->img_buf) + { + gsize copy = MIN (img_data_len, self->img_expected_len); + memcpy (self->img_buf, ssl_buf + img_data_off, copy); + self->img_recv_len = copy; + } + + if (self->img_recv_len >= self->img_expected_len) + { + fp_dbg ("Full image in single TLS read"); + self->tls_in_image = FALSE; + + g_autoptr(FpImage) img = fp_image_new (FPCMOH_IMG_WIDTH, + FPCMOH_IMG_HEIGHT); + memcpy (img->data, self->img_buf, + MIN (self->img_expected_len, (gsize) FPCMOH_IMG_PIXELS)); + img->flags = 0; + + FpImage *scaled = fpcmoh_scale_nn_2x (img); + fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), + g_steal_pointer (&scaled)); + self->img_recv_len = 0; + fpi_ssm_mark_completed (ssm); + return; + } + + /* Need more image data */ + fpcmoh_flush_tls_output (dev); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, + FPCMOH_DATA_TIMEOUT); + return; + } + + case FPCMOH_EVT_ARM_RESULT: + fp_dbg ("Arm result (via TLS)"); + break; + + case FPCMOH_EVT_FINGER_DOWN: + fp_dbg ("Finger detected (via TLS)!"); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE); + fpi_ssm_jump_to_state (ssm, FPCMOH_CAPTURE_GET_IMAGE); + return; + + case FPCMOH_EVT_FINGER_UP: + fp_dbg ("Finger up (via TLS)"); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); + break; + + case FPCMOH_EVT_USB_LOGS: + fp_dbg ("USB logs (via TLS)"); + break; + + default: + fp_dbg ("Unknown TLS cmdid %u, ignoring", cmdid); + break; + } + + /* Continue reading */ + fpcmoh_flush_tls_output (dev); + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, timeout); +} + +static void +fpcmoh_capture_ssm_run (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case FPCMOH_CAPTURE_STOP_ARM: + { + /* Reference implementation does stop→abort→session_off→start after TLS. + * Send arm with stop flag (0x12) to reset sensor state. */ + fp_dbg ("Stopping sensor (arm 0x12)"); + guint8 stop_data[FPCMOH_INIT_DATA_SIZE] = { FPCMOH_ARM_OP_STOP, 0x2f, 0x11, 0x17 }; + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_ARM, 0x0001, 0, + stop_data, FPCMOH_INIT_DATA_SIZE); + } + break; + + case FPCMOH_CAPTURE_STOP_ABORT: + fp_dbg ("Sending abort"); + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_ABORT, 0, 0, NULL, 0); + break; + + case FPCMOH_CAPTURE_STOP_SESSION_OFF: + { + /* session_off may stall if no session is active - ignore errors */ + fp_dbg ("Sending session_off (fingerprint_off)"); + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_FINGERPRINT_OFF, 0, 0, 0); + t->ssm = ssm; + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_ctrl_cmd_ignore_error_cb, NULL); + } + break; + + case FPCMOH_CAPTURE_ARM_SENSOR: + { + fp_dbg ("Arming sensor for finger detection"); + self->img_recv_len = 0; + self->tls_in_image = FALSE; + self->bulk_recv_len = 0; + self->evt_total_len = 0; + + /* Submit bulk read BEFORE arm to catch events */ + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, + FPCMOH_FINGER_TIMEOUT); + + /* Send arm command (fire-and-forget, bulk cb handles transitions) */ + guint8 arm_data[FPCMOH_INIT_DATA_SIZE] = { FPCMOH_ARM_OP_START, 0x2f, 0x11, 0x17 }; + FpiUsbTransfer *t = fpi_usb_transfer_new (dev); + fpi_usb_transfer_fill_control (t, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + FPCMOH_CMD_ARM, 0x0001, 0, + FPCMOH_INIT_DATA_SIZE); + memcpy (t->buffer, arm_data, FPCMOH_INIT_DATA_SIZE); + fpi_usb_transfer_submit (t, FPCMOH_CTRL_TIMEOUT, NULL, + fpcmoh_ctrl_cmd_noop_cb, NULL); + } + break; + + case FPCMOH_CAPTURE_WAIT_FINGER: + fp_dbg ("Waiting for finger on sensor"); + self->bulk_recv_len = 0; + self->evt_total_len = 0; + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, + FPCMOH_FINGER_TIMEOUT); + break; + + case FPCMOH_CAPTURE_GET_IMAGE: + fp_dbg ("Requesting image capture"); + self->bulk_recv_len = 0; + self->evt_total_len = 0; + fpcmoh_send_ctrl (dev, ssm, FPCMOH_CMD_GET_IMG, 0x0000, 0, NULL, 0); + break; + + case FPCMOH_CAPTURE_RECV_IMAGE: + fp_dbg ("Receiving image data via TLS"); + self->bulk_recv_len = 0; + self->evt_total_len = 0; + fpcmoh_submit_bulk_read (dev, ssm, fpcmoh_capture_bulk_cb, + FPCMOH_DATA_TIMEOUT); + break; + + default: + g_assert_not_reached (); + } +} + +static void +fpcmoh_capture_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + self->capture_ssm = NULL; + + if (self->deactivating) + { + if (error) + g_error_free (error); + return; + } + + if (error) + { + fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); + return; + } + + /* Report finger off and restart capture loop */ + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); + + /* Start next capture cycle */ + self->capture_ssm = fpi_ssm_new (dev, fpcmoh_capture_ssm_run, + FPCMOH_CAPTURE_NUM_STATES); + fpi_ssm_start (self->capture_ssm, fpcmoh_capture_ssm_done); +} + +/* ---- Deactivate SSM ---- */ + +static void +fpcmoh_deact_ssm_run (FpiSsm *ssm, FpDevice *dev) +{ + switch (fpi_ssm_get_cur_state (ssm)) + { + case FPCMOH_DEACT_ARM_STOP: + { + fp_dbg ("Stopping sensor (arm stop)"); + guint8 stop_data[FPCMOH_INIT_DATA_SIZE] = { FPCMOH_ARM_OP_STOP, 0x2f, 0x11, 0x17 }; + fpcmoh_send_ctrl_full (dev, ssm, FPCMOH_CMD_ARM, 0x0001, 0, + stop_data, sizeof (stop_data), + fpcmoh_ctrl_cmd_ignore_error_cb); + } + break; + + case FPCMOH_DEACT_ABORT: + fp_dbg ("Sending abort"); + fpcmoh_send_ctrl_full (dev, ssm, FPCMOH_CMD_ABORT, 0, 0, NULL, 0, + fpcmoh_ctrl_cmd_ignore_error_cb); + break; + + case FPCMOH_DEACT_SESSION_OFF: + fp_dbg ("Sending fingerprint session off"); + fpcmoh_send_ctrl_full (dev, ssm, FPCMOH_CMD_FINGERPRINT_OFF, 0, 0, NULL, 0, + fpcmoh_ctrl_cmd_ignore_error_cb); + break; + + default: + g_assert_not_reached (); + } +} + +static void +fpcmoh_deact_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + if (error) + fp_err ("Deactivation error: %s", error->message); + + fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error); +} + +/* ---- FpImageDevice vfuncs ---- */ + +static void +fpcmoh_img_open (FpImageDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + GError *error = NULL; + + fp_dbg ("Opening FPC MOH device"); + + /* Claim USB interface 0 */ + if (!g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), + 0, 0, &error)) + { + fpi_image_device_open_complete (dev, error); + return; + } + + /* Allocate image buffer */ + self->img_buf = g_malloc0 (FPCMOH_IMG_SIZE); + self->img_recv_len = 0; + self->tls_established = FALSE; + self->deactivating = FALSE; + self->interrupt_cancellable = g_cancellable_new (); + + /* Start open SSM */ + self->open_ssm = fpi_ssm_new (FP_DEVICE (dev), fpcmoh_open_ssm_run, + FPCMOH_OPEN_NUM_STATES); + fpi_ssm_start (self->open_ssm, fpcmoh_open_ssm_done); +} + +static void +fpcmoh_img_close (FpImageDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + fp_dbg ("Closing FPC MOH device"); + + /* Cancel any pending operations */ + g_cancellable_cancel (self->interrupt_cancellable); + g_clear_object (&self->interrupt_cancellable); + + /* Free TLS resources */ + if (self->ssl) + { + SSL_free (self->ssl); /* also frees bio_in and bio_out */ + self->ssl = NULL; + self->bio_in = NULL; + self->bio_out = NULL; + } + if (self->ssl_ctx) + { + SSL_CTX_free (self->ssl_ctx); + self->ssl_ctx = NULL; + } + + /* Free image buffer */ + g_clear_pointer (&self->img_buf, g_free); + + /* Clear PSK */ + memset (self->tls_psk, 0, sizeof (self->tls_psk)); + self->tls_psk_len = 0; + self->tls_established = FALSE; + + /* Release USB interface */ + GError *error = NULL; + + g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), + 0, 0, &error); + + fpi_image_device_close_complete (dev, error); +} + +static void +fpcmoh_activate (FpImageDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + + fp_dbg ("Activating FPC MOH device"); + + self->deactivating = FALSE; + + if (!self->tls_established) + { + fpi_image_device_activate_complete (dev, + fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + + fpi_image_device_activate_complete (dev, NULL); + + /* Start capture loop */ + self->capture_ssm = fpi_ssm_new (FP_DEVICE (dev), fpcmoh_capture_ssm_run, + FPCMOH_CAPTURE_NUM_STATES); + fpi_ssm_start (self->capture_ssm, fpcmoh_capture_ssm_done); +} + +static void +fpcmoh_deactivate (FpImageDevice *dev) +{ + FpiDeviceFpcMoh *self = FPI_DEVICE_FPCMOH (dev); + FpiSsm *ssm; + + fp_dbg ("Deactivating FPC MOH device"); + + self->deactivating = TRUE; + + /* Cancel any pending capture */ + if (self->capture_ssm) + { + g_cancellable_cancel (self->interrupt_cancellable); + g_clear_object (&self->interrupt_cancellable); + self->interrupt_cancellable = g_cancellable_new (); + } + + /* Run deactivation sequence */ + ssm = fpi_ssm_new (FP_DEVICE (dev), fpcmoh_deact_ssm_run, + FPCMOH_DEACT_NUM_STATES); + fpi_ssm_start (ssm, fpcmoh_deact_ssm_done); +} + +/* ---- GObject boilerplate ---- */ + +static void +fpi_device_fpcmoh_init (FpiDeviceFpcMoh *self) +{ +} + +static void +fpi_device_fpcmoh_class_init (FpiDeviceFpcMohClass *klass) +{ + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass); + + dev_class->id = "fpcmoh"; + dev_class->full_name = "FPC MOH Fingerprint Sensor"; + dev_class->type = FP_DEVICE_TYPE_USB; + dev_class->id_table = id_table; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + + img_class->img_open = fpcmoh_img_open; + img_class->img_close = fpcmoh_img_close; + img_class->activate = fpcmoh_activate; + img_class->deactivate = fpcmoh_deactivate; + + img_class->img_width = FPCMOH_IMG_WIDTH * FPCMOH_IMG_SCALE; + img_class->img_height = FPCMOH_IMG_HEIGHT * FPCMOH_IMG_SCALE; + img_class->algorithm = FPI_DEVICE_ALGO_SIGFM; + img_class->score_threshold = FPCMOH_SCORE_THRESHOLD; +} diff --git a/libfprint/drivers/fpcmoh/fpcmoh.h b/libfprint/drivers/fpcmoh/fpcmoh.h new file mode 100644 index 00000000..58f3efa6 --- /dev/null +++ b/libfprint/drivers/fpcmoh/fpcmoh.h @@ -0,0 +1,188 @@ +/* + * FPC MOH (Match-on-Host) driver for libfprint + * + * Supports FPC Disum USB fingerprint sensors (10a5:9200, 10a5:9201). + * + * Copyright (c) 2026 Sergey Subbotin + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "fpi-image-device.h" +#include "fpi-ssm.h" +#include "fpi-usb-transfer.h" + +#include +#include + +/* USB parameters */ +#define FPCMOH_EP_IN (2 | FPI_USB_ENDPOINT_IN) /* 0x82 */ +#define FPCMOH_EP_IN_MAX_BUF_SIZE 2048 +#define FPCMOH_CTRL_TIMEOUT 2000 +#define FPCMOH_DATA_TIMEOUT 15000 +#define FPCMOH_FINGER_TIMEOUT 600000 /* 10 min for finger wait */ +#define FPCMOH_BULK_MAX_PKT 64 + +/* Sensor image dimensions (FPC1022: 112x88, 1 byte per pixel) */ +#define FPCMOH_IMG_WIDTH 112 +#define FPCMOH_IMG_HEIGHT 88 +#define FPCMOH_IMG_BPP 1 +#define FPCMOH_IMG_SIZE (FPCMOH_IMG_WIDTH * FPCMOH_IMG_HEIGHT * FPCMOH_IMG_BPP) +#define FPCMOH_IMG_PIXELS (FPCMOH_IMG_WIDTH * FPCMOH_IMG_HEIGHT) +#define FPCMOH_IMG_SCALE 2 + +/* TLS inner message header size (cmdid + total_len + metadata) */ +#define FPCMOH_TLS_MSG_HDR_SIZE 24 + +/* USB control transfer request types */ +#define FPCMOH_HOST_TO_DEVICE 0x40 +#define FPCMOH_DEVICE_TO_HOST 0xC0 + +/* Commands (bRequest in USB control transfer) */ +#define FPCMOH_CMD_INIT 0x01 +#define FPCMOH_CMD_ARM 0x02 +#define FPCMOH_CMD_ABORT 0x03 +#define FPCMOH_CMD_TLS_INIT 0x05 +#define FPCMOH_CMD_TLS_DATA 0x06 +#define FPCMOH_CMD_INDICATE_S_STATE 0x08 +#define FPCMOH_CMD_GET_IMG 0x09 +#define FPCMOH_CMD_GET_TLS_KEY 0x0B +#define FPCMOH_CMD_GET_KPI 0x0C +#define FPCMOH_CMD_FINGERPRINT_OFF 0x12 +#define FPCMOH_CMD_END_ENROL 0x13 +#define FPCMOH_CMD_REFRESH_SENSOR 0x20 +#define FPCMOH_CMD_GET_FW_VERSION 0x30 +#define FPCMOH_CMD_GET_HW_UNIQUE_ID 0x31 +#define FPCMOH_CMD_FLUSH_KEYS 0x32 +#define FPCMOH_CMD_GET_STATE 0x50 + +/* Events (code field in bulk IN event header) */ +#define FPCMOH_EVT_HELLO 0x01 +#define FPCMOH_EVT_INIT_RESULT 0x02 +#define FPCMOH_EVT_ARM_RESULT 0x03 +#define FPCMOH_EVT_DEAD_PIXEL_REPORT 0x04 +#define FPCMOH_EVT_TLS 0x05 +#define FPCMOH_EVT_FINGER_DOWN 0x06 +#define FPCMOH_EVT_FINGER_UP 0x07 +#define FPCMOH_EVT_IMAGE 0x08 +#define FPCMOH_EVT_USB_LOGS 0x09 +#define FPCMOH_EVT_TLS_KEY 0x0A +#define FPCMOH_EVT_REFRESH_SENSOR 0x20 + +/* TLS key packet magic */ +#define FPCMOH_TLS_KEY_MAGIC 0x0DEC0DED + +/* S-state values */ +#define FPCMOH_S_STATE_S0 0x0010 +#define FPCMOH_S_STATE_SX 0x0011 + +/* Sensor init/arm/stop data (4-byte payloads for CMD_INIT and CMD_ARM) */ +#define FPCMOH_INIT_DATA_SIZE 4 +#define FPCMOH_ARM_OP_INIT 0x10 +#define FPCMOH_ARM_OP_START 0x11 +#define FPCMOH_ARM_OP_STOP 0x12 + +/* SIGFM matching threshold (minimum number of consistent keypoint matches) */ +#define FPCMOH_SCORE_THRESHOLD 10 + +/* Event header (received on bulk IN endpoint) - network byte order */ +typedef struct __attribute__((packed)) +{ + guint32 code; + guint32 len; + guint32 unknown; +} FpcMohEvtHdr; + +/* TLS key packet header (all offsets are from start of packet) */ +typedef struct __attribute__((packed)) +{ + guint32 magic; + guint32 key_offset; + guint32 key_len; + guint32 aad_offset; + guint32 aad_len; + guint32 sig_offset; + guint32 sig_len; +} FpcMohTlsKeyPkt; + +/* SSM states for device open */ +typedef enum { + FPCMOH_OPEN_INDICATE_S_STATE = 0, + FPCMOH_OPEN_GET_STATE, + FPCMOH_OPEN_PARSE_STATE, + FPCMOH_OPEN_CMD_INIT, + FPCMOH_OPEN_WAIT_INIT_RESULT, + FPCMOH_OPEN_GET_TLS_KEY, + FPCMOH_OPEN_PARSE_TLS_KEY, + FPCMOH_OPEN_TLS_INIT, + FPCMOH_OPEN_TLS_HANDSHAKE, + FPCMOH_OPEN_NUM_STATES, +} FpcMohOpenState; + +/* SSM states for image capture */ +typedef enum { + FPCMOH_CAPTURE_STOP_ARM = 0, + FPCMOH_CAPTURE_STOP_ABORT, + FPCMOH_CAPTURE_STOP_SESSION_OFF, + FPCMOH_CAPTURE_ARM_SENSOR, + FPCMOH_CAPTURE_WAIT_FINGER, + FPCMOH_CAPTURE_GET_IMAGE, + FPCMOH_CAPTURE_RECV_IMAGE, + FPCMOH_CAPTURE_NUM_STATES, +} FpcMohCaptureState; + +/* SSM states for deactivate */ +typedef enum { + FPCMOH_DEACT_ARM_STOP = 0, + FPCMOH_DEACT_ABORT, + FPCMOH_DEACT_SESSION_OFF, + FPCMOH_DEACT_NUM_STATES, +} FpcMohDeactState; + +G_DECLARE_FINAL_TYPE (FpiDeviceFpcMoh, fpi_device_fpcmoh, FPI, + DEVICE_FPCMOH, FpImageDevice) + +struct _FpiDeviceFpcMoh +{ + FpImageDevice parent; + + /* USB bulk event reception */ + guint8 bulk_buf[FPCMOH_EP_IN_MAX_BUF_SIZE]; + gsize bulk_recv_len; + gsize evt_total_len; + + /* TLS */ + guint8 tls_psk[32]; /* SHA-256 output = 32 bytes */ + gsize tls_psk_len; + SSL_CTX *ssl_ctx; + SSL *ssl; + BIO *bio_in; /* we write device data here, SSL reads from it */ + BIO *bio_out; /* SSL writes here, we read and send to device */ + gboolean tls_established; + + /* Image buffer */ + guint8 *img_buf; + gsize img_recv_len; + gsize img_expected_len; /* actual payload size from TLS message header */ + gboolean tls_in_image; /* currently accumulating image payload from TLS */ + + /* State */ + FpiSsm *open_ssm; + FpiSsm *capture_ssm; + gboolean deactivating; + GCancellable *interrupt_cancellable; +};