diff --git a/README.md b/README.md index aec2e0b64..f4bd57e1d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is an experimental libfprint driver implementation for Goodix drivers. Currently in the works: - 27c6x5110 (80x64 resolution) +- 27c6:5e0a (Goodix TLS, 64x80 native / 128x160 scaled, e.g. Realme Book) *LibFPrint is part of the **[FPrint][Website]** project.* diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb index d476f7e1a..6df2af6e3 100644 --- a/data/autosuspend.hwdb +++ b/data/autosuspend.hwdb @@ -190,6 +190,11 @@ usb:v27C6p6A94* ID_AUTOSUSPEND=1 ID_PERSIST=0 +# Supported by libfprint driver goodixtls5e0a +usb:v27C6p5E0A* + ID_AUTOSUSPEND=1 + ID_PERSIST=0 + # Supported by libfprint driver nb1010 usb:v298Dp1010* ID_AUTOSUSPEND=1 @@ -367,7 +372,6 @@ usb:v27C6p55A2* usb:v27C6p55A4* usb:v27C6p55B4* usb:v27C6p5740* -usb:v27C6p5E0A* usb:v27C6p581A* usb:v2808p9338* usb:v2808p93A9* diff --git a/libfprint/drivers/goodixtls/goodix.c b/libfprint/drivers/goodixtls/goodix.c new file mode 100644 index 000000000..5ab42343c --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix.c @@ -0,0 +1,1798 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 "fpi-log.h" +#include "fpi-ssm.h" +#include "fpi-usb-transfer.h" +#define FP_COMPONENT "goodixtls" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "drivers_api.h" +#include "goodix.h" +#include "goodix_proto.h" +#include "goodixtls.h" + +typedef struct +{ + GoodixTlsServer *tls_hop; + + GSource *timeout; + + guint8 cmd; + + gboolean ack; + gboolean reply; + + GoodixCmdCallback callback; + gpointer user_data; + + guint8 *data; + guint32 length; + + GoodixCallbackInfo *tls_ready_callback; + + /* Stale-activation guard: bumped on (re)activation start and teardown; + * TLS completion callbacks drop on mismatch. */ + guint activation_gen; + + /* Boot sequence counter coupled to actual USB reset. */ + guint boot_seq; + + /* Conditional USB reset flag: TRUE only when previous session closed cleanly. + * FALSE is the safe direction (reset taken on open). */ + gboolean clean_close; + + /* USB device identity snapshot to detect kernel re-enumeration. */ + guint8 last_usb_bus; + guint8 last_usb_addr; + guint8 last_usb_port; + guint16 last_usb_vid; + guint16 last_usb_pid; + gboolean usb_identity_valid; + + GCancellable *transfer_cancel_tkn; + gboolean inited; +} FpiDeviceGoodixTlsPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (FpiDeviceGoodixTls, fpi_device_goodixtls, + FP_TYPE_IMAGE_DEVICE); + +static GoodixCallbackInfo * +make_cb_info (GCallback callback, gpointer user_data) +{ + GoodixCallbackInfo *cb_info = g_new0 (GoodixCallbackInfo, 1); + + cb_info->callback = callback; + cb_info->user_data = user_data; + + return cb_info; +} + +/* Defined below; needed early by goodix_receive_data_cb. */ +static void goodix_receive_data (FpDevice *dev); + +gchar * +data_to_str (guint8 *data, guint32 length) +{ + gchar *string; + + if (data == NULL || length == 0 || length > 4096) + return g_strdup (""); + + string = g_malloc ((length * 2) + 1); + + for (guint32 i = 0; i < length; i++) + g_snprintf (string + i * 2, 3, "%02x", data[i]); + + return string; +} + +static void +goodix_receive_done (FpDevice *dev, guint8 *data, guint16 length, + GError *error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + GoodixCmdCallback callback = priv->callback; + gpointer user_data = priv->user_data; + + if (!(priv->ack || priv->reply)) + { + if (error) + g_error_free (error); + return; + } + + goodix_reset_state (dev); + if (!error) + fp_dbg ("Completed command: 0x%02x", priv->cmd); + + if (callback) + callback (dev, data, length, user_data, error); + else if (error) + g_error_free (error); +} + +void +goodix_receive_none (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixNoneCallback callback = (GoodixNoneCallback) cb_info->callback; + + callback (dev, cb_info->user_data, error); +} + +static void +goodix_receive_none_tolerant (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixNoneCallback callback = (GoodixNoneCallback) cb_info->callback; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + g_clear_error (&error); /* Buffer already empty. */ + + callback (dev, cb_info->user_data, error); +} + +void +goodix_receive_default (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixDefaultCallback callback = (GoodixDefaultCallback) cb_info->callback; + + callback (dev, data, length, cb_info->user_data, error); +} + +static void +goodix_receive_success (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixSuccessCallback callback = (GoodixSuccessCallback) cb_info->callback; + + if (error) + { + callback (dev, FALSE, cb_info->user_data, error); + return; + } + + if (length != sizeof (guint8) * 2) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid success reply length: %d", length); + callback (dev, FALSE, cb_info->user_data, error); + return; + } + + callback (dev, data[0] == 0x00 ? FALSE : TRUE, cb_info->user_data, NULL); +} + +static void +goodix_receive_preset_psk_read (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + guint32 psk_len; + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixPresetPskReadCallback callback = + (GoodixPresetPskReadCallback) cb_info->callback; + + if (error) + { + callback (dev, FALSE, 0x00000000, NULL, 0, cb_info->user_data, error); + return; + } + + if (length < sizeof (guint8)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid preset PSK read reply length: %d", length); + callback (dev, FALSE, 0x00000000, NULL, 0, cb_info->user_data, error); + return; + } + + if (data[0] != 0x00) + { + callback (dev, FALSE, 0x00000000, NULL, 0, cb_info->user_data, NULL); + return; + } + + if (length < sizeof (guint8) + sizeof (GoodixPresetPsk)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid preset PSK read reply length: %d", length); + callback (dev, FALSE, 0x00000000, NULL, 0, cb_info->user_data, error); + return; + } + + { + memcpy (&psk_len, data + sizeof (guint8) + G_STRUCT_OFFSET (GoodixPresetPsk, length), sizeof (psk_len)); + psk_len = GUINT32_FROM_LE (psk_len); + } + + if (length < psk_len + sizeof (guint8) + sizeof (GoodixPresetPsk)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid preset PSK read reply length: %d", length); + callback (dev, FALSE, 0x00000000, NULL, 0, cb_info->user_data, error); + return; + } + + { + guint32 psk_flags; + memcpy (&psk_flags, data + sizeof (guint8) + G_STRUCT_OFFSET (GoodixPresetPsk, flags), sizeof (psk_flags)); + callback (dev, TRUE, GUINT32_FROM_LE (psk_flags), + data + sizeof (guint8) + sizeof (GoodixPresetPsk), psk_len, + cb_info->user_data, NULL); + } +} + +static void +goodix_receive_firmware_version (FpDevice *dev, guint8 *data, + guint16 length, gpointer user_data, + GError *error) +{ + g_autofree gchar *payload = g_malloc (length + sizeof (gchar)); + g_autofree GoodixCallbackInfo *cb_info = user_data; + GoodixFirmwareVersionCallback callback = + (GoodixFirmwareVersionCallback) cb_info->callback; + + if (error) + { + callback (dev, NULL, cb_info->user_data, error); + return; + } + + memcpy (payload, data, length); + + /* Some devices send the firmware without the null terminator. */ + payload[length] = 0x00; + + callback (dev, payload, cb_info->user_data, NULL); +} + +static void +goodix_receive_ack (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + guint8 cmd, flags; + + if (length != sizeof (GoodixAck)) + { + fp_warn ("Invalid ACK length: %u", length); + return; + } + + if (data == NULL) + { + fp_warn ("Invalid ACK: NULL payload"); + return; + } + + /* Byte ops, not bitfields: layout is wire-defined. */ + cmd = data[0]; + flags = data[1]; + + if (!(flags & 0x01)) + { + fp_warn ("Invalid ACK flags: 0x%02x", flags); + return; + } + + if (flags & 0x02) + fp_warn ("MCU has no config"); + + if (priv->cmd != cmd) + { + fp_warn ("Invalid ACK command: 0x%02x", cmd); + return; + } + + if (!priv->ack) + { + fp_warn ("Didn't expect an ACK for command: 0x%02x", priv->cmd); + return; + } + + if (!priv->reply) + { + G_DEBUG_HERE (); + goodix_receive_done (dev, NULL, 0, NULL); + return; + } + + priv->ack = FALSE; +} + +static void +goodix_receive_protocol (FpDevice *dev, guint8 *data, guint32 length) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + guint8 cmd; + g_autofree guint8 *payload = NULL; + guint16 payload_len; + gboolean valid_checksum, valid_null_checksum; + + if (!goodix_decode_protocol (data, length, &cmd, &payload, &payload_len, + &valid_checksum, &valid_null_checksum)) + { + fp_dbg ("Dropping short protocol frame, size: %u", length); + return; + } + + /* Either the standard checksum or the null checksum is accepted; some + * replies use the null value. Checksum mismatches are logged but do not + * block delivery: the calculation is not yet trusted against hardware + * quirks, and dropping here turns into command timeouts. */ + if (!valid_checksum && !valid_null_checksum) + { + fp_dbg ("protocol frame with bad checksum"); + } + + if (cmd == GOODIX_CMD_ACK) + { + fp_dbg ("got ack"); + goodix_receive_ack (dev, payload, payload_len, NULL, NULL); + return; + } + + if (priv->cmd != cmd) + { + fp_warn ("Invalid protocol command: 0x%02x", cmd); + return; + } + + if (!priv->reply) + { + fp_warn ("Didn't expect a reply for command: 0x%02x", priv->cmd); + return; + } + + if (priv->ack) + fp_warn ("Missing ACK for command: 0x%02x", priv->cmd); + + goodix_receive_done (dev, payload, payload_len, NULL); +} + +static void +goodix_receive_pack (FpDevice *dev, guint8 *data, guint32 length) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + guint8 flags; + g_autofree guint8 *payload = NULL; + guint16 payload_len; + gboolean valid_checksum; + + if (length == 0) + return; + + priv->data = g_realloc (priv->data, priv->length + length); + memcpy (priv->data + priv->length, data, length); + priv->length += length; + + if (!goodix_decode_pack (priv->data, priv->length, &flags, &payload, + &payload_len, &valid_checksum)) + { + fp_dbg ("not full packet"); + return; + } + + if (!valid_checksum) + { + /* Logged only: the checksum calculation is not yet trusted + * against hardware quirks. */ + fp_dbg ("pack with bad checksum"); + } + + switch (flags) + { + case GOODIX_FLAGS_MSG_PROTOCOL: + fp_dbg ("Got protocol msg"); + goodix_receive_protocol (dev, payload, payload_len); + break; + + case GOODIX_FLAGS_TLS: + case GOODIX_FLAGS_TLS_DATA: + fp_dbg ("Got TLS msg (%u bytes)", payload_len); + if (priv->cmd == GOODIX_CMD_MCU_GET_IMAGE || + priv->cmd == GOODIX_CMD_REQUEST_TLS_CONNECTION || + (priv->reply && priv->callback != NULL && priv->cmd == 0)) + goodix_receive_done (dev, payload, payload_len, NULL); + else + fp_dbg ("Discarding stale TLS msg while waiting for cmd 0x%02x", + priv->cmd); + break; + + default: + fp_dbg ("Unknown flags: 0x%02x", flags); + break; + } + + g_clear_pointer (&priv->data, g_free); + priv->length = 0; +} + +static void +goodix_receive_data_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if ((priv->transfer_cancel_tkn && g_cancellable_is_cancelled (priv->transfer_cancel_tkn)) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + fp_dbg ("transfer cancelled, aborting read loop..."); + if (error) + g_error_free (error); + return; + } + if (error) + { + fp_warn ("Receive data error: %s", error->message); + g_error_free (error); + + goodix_receive_data (dev); + return; + } + + goodix_receive_pack (dev, transfer->buffer, transfer->actual_length); + + goodix_receive_data (dev); +} + +static void +goodix_receive_timeout_cb (FpDevice *dev, gpointer user_data) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + GError *error = NULL; + + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, + "Command timed out: 0x%02x", priv->cmd); + goodix_receive_done (dev, NULL, 0, error); +} + +void +goodix_start_read_loop (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if (priv->inited) + return; + + if (priv->transfer_cancel_tkn && g_cancellable_is_cancelled (priv->transfer_cancel_tkn)) + g_cancellable_reset (priv->transfer_cancel_tkn); + + priv->inited = TRUE; + g_clear_pointer (&priv->data, g_free); + priv->length = 0; + + goodix_receive_data (dev); +} + +void +goodix_stop_read_loop (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + fp_dbg ("Stopping read loop"); + if (priv->transfer_cancel_tkn) + g_cancellable_cancel (priv->transfer_cancel_tkn); + + priv->inited = FALSE; + g_clear_pointer (&priv->data, g_free); + priv->length = 0; +} + +static void +goodix_receive_data (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsClass *class = FPI_DEVICE_GOODIXTLS_GET_CLASS (self); + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + transfer->short_is_error = FALSE; + + fpi_usb_transfer_fill_bulk (transfer, class->ep_in, + GOODIX_EP_IN_MAX_BUF_SIZE); + + fpi_usb_transfer_submit (transfer, 0, priv->transfer_cancel_tkn, + goodix_receive_data_cb, NULL); +} + +static gboolean +goodix_send_data (FpDevice *dev, guint8 *data, guint32 length, + GDestroyNotify free_func, GError **error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsClass *class = FPI_DEVICE_GOODIXTLS_GET_CLASS (self); + + for (guint32 i = 0; i < length; i += GOODIX_EP_OUT_MAX_BUF_SIZE) + { + guint32 chunk_len = MIN ((guint32) GOODIX_EP_OUT_MAX_BUF_SIZE, length - i); + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + transfer->short_is_error = TRUE; + + fpi_usb_transfer_fill_bulk_full (transfer, class->ep_out, data + i, + chunk_len, NULL); + + if (!fpi_usb_transfer_submit_sync (transfer, GOODIX_TIMEOUT, + error)) + { + if (free_func) + free_func (data); + fpi_usb_transfer_unref (transfer); + return FALSE; + } + fpi_usb_transfer_unref (transfer); + } + + if (free_func) + free_func (data); + return TRUE; +} + +static gboolean +goodix_send_pack (FpDevice *dev, guint8 flags, guint8 *payload, + guint16 length, GDestroyNotify free_func, + GError **error) +{ + guint8 *data; + guint32 data_len; + + goodix_encode_pack (flags, payload, length, TRUE, &data, &data_len); + if (free_func) + free_func (payload); + + return goodix_send_data (dev, data, data_len, g_free, error); +} + +void +goodix_send_protocol ( + FpDevice *dev, guint8 cmd, const guint8 *payload, guint16 length, + GDestroyNotify free_func, gboolean calc_checksum, guint timeout_ms, + gboolean reply, GoodixCmdCallback callback, gpointer user_data) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + GError *error = NULL; + guint8 *data; + guint32 data_len; + + if (priv->ack || priv->reply || priv->timeout) + { + /* A command is already running. */ + guint8 busy_cmd = priv->cmd; + GError *collision_error; + fp_warn ("A command is already running: 0x%02x", busy_cmd); + if (free_func) + free_func ((void *) payload); + collision_error = + g_error_new (G_IO_ERROR, G_IO_ERROR_BUSY, + "A command is already running: 0x%02x", busy_cmd); + goodix_receive_done (dev, NULL, 0, collision_error); + /* Fail the incoming waiter as well, otherwise its SSM stalls. */ + if (callback) + { + GError *incoming_error = + g_error_new (G_IO_ERROR, G_IO_ERROR_BUSY, + "A command is already running: 0x%02x", busy_cmd); + callback (dev, NULL, 0, user_data, incoming_error); + } + return; + } + + fp_dbg ("Running command: 0x%02x", cmd); + + if (timeout_ms) + priv->timeout = fpi_device_add_timeout ( + dev, timeout_ms, goodix_receive_timeout_cb, NULL, NULL); + priv->cmd = cmd; + priv->ack = TRUE; + priv->reply = reply; + priv->callback = callback; + priv->user_data = user_data; + + goodix_encode_protocol (cmd, payload, length, calc_checksum, FALSE, + &data, &data_len); + if (free_func) + free_func ((void *) payload); + + if (!goodix_send_pack (dev, GOODIX_FLAGS_MSG_PROTOCOL, data, data_len, + g_free, &error)) + { + goodix_receive_done (dev, NULL, 0, error); + return; + } +} +void +goodix_send_nop (FpDevice *dev, GoodixNoneCallback callback, + gpointer user_data) +{ + GoodixNop payload = {.unknown = 0x00000000}; + GoodixCallbackInfo *cb_info; + + /* Flush command: silence from the MCU indicates success. */ + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_NOP, (guint8 *) &payload, + sizeof (payload), NULL, FALSE, GOODIX_NOP_TIMEOUT, FALSE, + goodix_receive_none_tolerant, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_NOP, (guint8 *) &payload, sizeof (payload), + NULL, FALSE, GOODIX_NOP_TIMEOUT, FALSE, NULL, NULL); +} + +static void +goodix_send_mcu_get_image (FpDevice *dev, GoodixImageCallback callback, + gpointer user_data) +{ + FpiDeviceGoodixTlsClass *class = FPI_DEVICE_GOODIXTLS_GET_CLASS (dev); + GoodixCallbackInfo *cb_info; + GoodixDefault payload_default = {.unused_flags = 0x01}; + const guint8 *payload = (const guint8 *) &payload_default; + guint16 len = sizeof (payload_default); + + if (class->capture_payload != NULL && class->capture_payload_len > 0) + { + payload = class->capture_payload; + len = class->capture_payload_len; + } + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_MCU_GET_IMAGE, payload, + len, NULL, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_MCU_GET_IMAGE, payload, + len, NULL, TRUE, GOODIX_TIMEOUT, TRUE, + NULL, NULL); +} + +void +goodix_send_mcu_switch_to_fdt_down (FpDevice *dev, const guint8 *mode, guint16 length, + GDestroyNotify free_func, + GoodixDefaultCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixDefaultCallback cb = NULL; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + cb = goodix_receive_default; + } + + if (mode && length > 0 && mode[0] == 0x01) + { + guint8 *payload = g_malloc (sizeof (guint8) * (length + 1)); + memcpy (payload + 1, mode, length); + payload[0] = 0xc; + if (free_func) + free_func ((void *) mode); + goodix_send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN, payload, length + 1, + g_free, TRUE, 0, TRUE, cb, cb_info); + } + else + { + goodix_send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN, mode, length, + free_func, TRUE, 0, TRUE, cb, cb_info); + } +} + +void +goodix_send_mcu_switch_to_fdt_up (FpDevice *dev, const guint8 *mode, guint16 length, + GDestroyNotify free_func, + GoodixDefaultCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixDefaultCallback cb = NULL; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + cb = goodix_receive_default; + } + + if (mode && length > 0 && mode[0] == 0x01) + { + guint8 *payload = g_malloc (sizeof (guint8) * (length + 1)); + memcpy (payload + 1, mode, length); + payload[0] = 0xe; + if (free_func) + free_func ((void *) mode); + goodix_send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, payload, length + 1, + g_free, TRUE, 0, TRUE, cb, cb_info); + } + else + { + goodix_send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, mode, length, + free_func, TRUE, 0, TRUE, cb, cb_info); + } +} + +void +goodix_send_mcu_switch_to_fdt_mode (FpDevice *dev, const guint8 *mode, + guint16 length, GDestroyNotify free_func, + GoodixNoneCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixCmdCallback cb = NULL; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + cb = goodix_receive_none; + } + + goodix_send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_MODE, mode, length, + free_func, TRUE, GOODIX_TIMEOUT, FALSE, cb, + cb_info); + +} + +void +goodix_send_nav_0 (FpDevice *dev, GoodixDefaultCallback callback, + gpointer user_data) +{ + GoodixDefault payload = {.unused_flags = 0x01}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_NAV_0, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_NAV_0, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, NULL, + NULL); +} + +void +goodix_send_read_sensor_register (FpDevice *dev, guint16 address, + guint8 length, + GoodixDefaultCallback callback, + gpointer user_data) +{ + /* Only support one address. */ + + GoodixReadSensorRegister payload = { + .multiples = FALSE, .address = GUINT16_TO_LE (address), .length = length + }; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_READ_SENSOR_REGISTER, + (guint8 *) &payload, sizeof (payload), NULL, TRUE, + GOODIX_TIMEOUT, TRUE, goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_READ_SENSOR_REGISTER, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, NULL, + NULL); +} + +void +goodix_send_upload_config_mcu (FpDevice *dev, guint8 *config, + guint16 length, GDestroyNotify free_func, + GoodixSuccessCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_UPLOAD_CONFIG_MCU, config, length, + free_func, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_success, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_UPLOAD_CONFIG_MCU, config, length, + free_func, TRUE, GOODIX_TIMEOUT, TRUE, NULL, NULL); +} + +void +goodix_send_enable_chip (FpDevice *dev, gboolean enable, + GoodixNoneCallback callback, gpointer user_data) +{ + GoodixEnableChip payload = {.enable = enable ? TRUE : FALSE}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_ENABLE_CHIP, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, FALSE, + goodix_receive_none, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_ENABLE_CHIP, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, FALSE, NULL, + NULL); +} + +void +goodix_send_query_firmware_version (FpDevice *dev, + GoodixFirmwareVersionCallback callback, + gpointer user_data) +{ + GoodixNone payload = {}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_FIRMWARE_VERSION, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_firmware_version, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_FIRMWARE_VERSION, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, NULL, + NULL); +} + +void +goodix_send_query_mcu_state (FpDevice *dev, GoodixNoneCallback callback, + gpointer user_data) +{ + GoodixQueryMcuState payload = {.unused_flags = 0x55}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_QUERY_MCU_STATE, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, FALSE, + goodix_receive_none, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_QUERY_MCU_STATE, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, FALSE, NULL, + NULL); +} + +static void +goodix_send_request_tls_connection (FpDevice *dev, + GoodixDefaultCallback callback, + gpointer user_data) +{ + GoodixNone payload = {}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_REQUEST_TLS_CONNECTION, + (guint8 *) &payload, sizeof (payload), NULL, TRUE, 0, + TRUE, goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_REQUEST_TLS_CONNECTION, + (guint8 *) &payload, sizeof (payload), NULL, TRUE, + GOODIX_TIMEOUT, TRUE, NULL, NULL); +} + +static void +goodix_send_tls_successfully_established (FpDevice *dev, + GoodixNoneCallback callback, + gpointer user_data) +{ + GoodixNone payload = {}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + /* Timeout needs to be at least 10ms. */ + + goodix_send_protocol (dev, GOODIX_CMD_TLS_SUCCESSFULLY_ESTABLISHED, + (guint8 *) &payload, sizeof (payload), NULL, TRUE, + 2000, TRUE, goodix_receive_none, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_TLS_SUCCESSFULLY_ESTABLISHED, + (guint8 *) &payload, sizeof (payload), NULL, TRUE, + 2000, TRUE, NULL, NULL); +} + +void +goodix_send_read_otp (FpDevice *dev, GoodixDefaultCallback callback, + gpointer user_data) +{ + GoodixNone payload = {}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_protocol (dev, GOODIX_CMD_READ_OTP, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_READ_OTP, (guint8 *) &payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + NULL, NULL); +} + +/* Shared 0xe4 PSK-read sender. The two public wrappers below build + * differently-shaped request bodies (generic flags+length vs 5e0a + * length+offset+flags+reserved) around this single protocol call. */ +static void +goodix_send_preset_psk_read_payload (FpDevice *dev, + const guint8 *payload, + guint16 length, + GoodixPresetPskReadCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixCmdCallback cb = NULL; + + if (callback) + { + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + cb = goodix_receive_preset_psk_read; + } + + goodix_send_protocol (dev, GOODIX_CMD_PRESET_PSK_READ, payload, length, + NULL, TRUE, GOODIX_TIMEOUT, TRUE, cb, cb_info); +} + +void +goodix_send_preset_psk_read (FpDevice *dev, guint32 flags, guint16 length, + GoodixPresetPskReadCallback callback, + gpointer user_data) +{ + GoodixPresetPsk payload = {.flags = GUINT32_TO_LE (flags), + .length = GUINT32_TO_LE (length)}; + + goodix_send_preset_psk_read_payload (dev, (const guint8 *) &payload, + sizeof (payload), callback, user_data); +} + +void +goodix_send_preset_psk_read_5e0a (FpDevice *dev, + guint32 flags, + guint32 length, + guint32 offset, + GoodixPresetPskReadCallback callback, + gpointer user_data) +{ + /* 5e0a CMD 0xe4 payload: length + offset + flags + reserved (4B LE each). */ + struct __attribute__((__packed__)) { + guint32 length; + guint32 offset; + guint32 flags; + guint32 reserved; + } payload = { + .length = GUINT32_TO_LE (length), + .offset = GUINT32_TO_LE (offset), + .flags = GUINT32_TO_LE (flags), + .reserved = 0, + }; + + goodix_send_preset_psk_read_payload (dev, (const guint8 *) &payload, + sizeof (payload), callback, user_data); +} + +gboolean +goodix_dev_init (FpDevice *dev, GError **error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsClass *class = FPI_DEVICE_GOODIXTLS_GET_CLASS (self); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + priv->timeout = NULL; + priv->ack = FALSE; + priv->reply = FALSE; + priv->callback = NULL; + priv->user_data = NULL; + priv->data = NULL; + priv->length = 0; + priv->transfer_cancel_tkn = g_cancellable_new (); + + /* Conditional USB reset: skip reset only when previous session on + * this USB device closed cleanly. Re-enumeration or dirty state forces reset. */ + { + GUsbDevice *usb = fpi_device_get_usb_device (dev); + gboolean reenumerated = FALSE; + gboolean take_reset; + + if (usb != NULL) + { + guint8 bus = g_usb_device_get_bus (usb); + guint8 addr = g_usb_device_get_address (usb); + guint8 port = g_usb_device_get_port_number (usb); + guint16 vid = g_usb_device_get_vid (usb); + guint16 pid = g_usb_device_get_pid (usb); + + /* Address is unstable across suspend and re-enumeration; bus + * identifies the host controller and port the topology, so compare + * bus+port+vid+pid. The address snapshot is kept for diagnostics + * only. */ + if (priv->usb_identity_valid + && (bus != priv->last_usb_bus || port != priv->last_usb_port + || vid != priv->last_usb_vid || pid != priv->last_usb_pid)) + { + reenumerated = TRUE; + priv->clean_close = FALSE; + } + priv->last_usb_bus = bus; + priv->last_usb_addr = addr; + priv->last_usb_port = port; + priv->last_usb_vid = vid; + priv->last_usb_pid = pid; + priv->usb_identity_valid = TRUE; + } + else + { + reenumerated = TRUE; + priv->clean_close = FALSE; + } + + take_reset = !priv->clean_close; + if (take_reset) + { + priv->boot_seq++; + if (reenumerated) + fp_dbg ("USB reset taken (re-enumerated device, boot_seq=%u)", + priv->boot_seq); + else + fp_dbg ("USB reset taken (dirty close, boot_seq=%u)", + priv->boot_seq); + g_usb_device_reset (fpi_device_get_usb_device (dev), NULL); + } + else + { + fp_dbg ("USB reset skipped (clean close, boot_seq=%u)", + priv->boot_seq); + } + } + + { + gboolean ok = g_usb_device_claim_interface (fpi_device_get_usb_device (dev), + class->interface, 0, error); + + if (!ok) + priv->clean_close = FALSE; + return ok; + } +} +void +goodix_reset_state (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if (priv->timeout) + g_clear_pointer (&priv->timeout, g_source_destroy); + priv->ack = FALSE; + priv->reply = FALSE; + priv->cmd = 0; + priv->callback = NULL; + priv->user_data = NULL; + g_clear_pointer (&priv->data, g_free); + priv->length = 0; +} + +/* Stale-activation guard counter (live paths untouched). */ +guint +goodix_activation_gen_get (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + return priv->activation_gen; +} + +guint +goodix_activation_gen_bump (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + return ++priv->activation_gen; +} + +guint +goodix_boot_seq_get (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + return priv->boot_seq; +} + +void +goodix_session_mark_clean (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + priv->clean_close = TRUE; +} + +void +goodix_session_mark_dirty (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + priv->clean_close = FALSE; +} + +gboolean +goodix_session_is_clean (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + return priv->clean_close; +} + +gboolean +goodix_dev_deinit (FpDevice *dev, GError **error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsClass *class = FPI_DEVICE_GOODIXTLS_GET_CLASS (self); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + gboolean clean_close = priv->clean_close; + gboolean released; + + if (!clean_close) + goodix_activation_gen_bump (dev); + + if (priv->transfer_cancel_tkn) + g_cancellable_cancel (priv->transfer_cancel_tkn); + g_clear_object (&priv->transfer_cancel_tkn); + + if (!clean_close) + { + g_autoptr(GError) tls_err = NULL; + + goodix_shutdown_tls (dev, &tls_err); + if (tls_err) + fp_warn ("TLS shutdown warning: %s", tls_err->message); + } + + goodix_reset_state (dev); + priv->inited = FALSE; + + released = g_usb_device_release_interface (fpi_device_get_usb_device (dev), + class->interface, 0, error); + if (!released) + { + priv->clean_close = FALSE; + if (clean_close) + { + goodix_activation_gen_bump (dev); + goodix_shutdown_tls (dev, NULL); + } + } + + return released; +} + +static void +goodix_read_tls (FpDevice *dev, GoodixTlsCallback callback, + gpointer user_data) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + fp_dbg ("goodix_read_tls()"); + priv->callback = callback; + priv->user_data = user_data; + priv->reply = TRUE; + priv->cmd = 0; +} + +enum tls_states { + TLS_SERVER_INIT, + TLS_SERVER_HANDSHAKE_INIT, + TLS_NUM_STATES, +}; + + +static void +on_goodix_tls_read_handshake (FpDevice *dev, guint8 *data, + guint16 length, gpointer user_data, + GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodixTls *self; + FpiDeviceGoodixTlsPrivate *priv; + int sent; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + self = FPI_DEVICE_GOODIXTLS (fpi_ssm_get_data (user_data)); + priv = fpi_device_goodixtls_get_instance_private (self); + + sent = goodix_tls_client_write (priv->tls_hop, data, length); + + if (sent < 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (g_io_error_quark (), sent, + "failed to sent data to " + "tls server")); + return; + } + fpi_ssm_next_state (ssm); +} + +enum goodix_tls_handshake_stages { + TLS_HANDSHAKE_STAGE_HELLO_S, + TLS_HANDSHAKE_STAGE_KH_EXCHANGE, + TLS_HANDSHAKE_STAGE_CHANGE_CIPHER_C, + TLS_HANDSHAKE_STAGE_HANDSHAKE_C, + TLS_HANDSHAKE_STAGE_CHANGE_CIPHER_S, + + TLS_HANDSHAKE_STAGE_NUM, +}; + +static void +on_tls_successfully_established (FpDevice *dev, gpointer user_data, + GError *error) +{ + FpiDeviceGoodixTls * self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate * priv = + fpi_device_goodixtls_get_instance_private (self); + + fp_dbg ("HANDSHAKE DONE"); + if (!priv->tls_ready_callback) + { + if (error) + g_error_free (error); + return; + } + /* The MCU never ACKs the notification; a timeout here reports success. */ + if (error) + g_error_free (error); + ((GoodixNoneCallback) priv->tls_ready_callback->callback)( + dev, priv->tls_ready_callback->user_data, NULL); + g_clear_pointer (&priv->tls_ready_callback, g_free); +} + +/* Wait briefly for the TLS serve thread to finish SSL_accept, then report + * whether the device completed the handshake. In the healthy case accept + * returns right after the proxied client Finished, well before the proxy + * SSM ends, so this returns immediately; the deadline only bounds genuinely + * stuck handshakes. If the outcome is still unknown at the deadline, return + * TRUE to preserve the previous behavior (never fail a slow-but-healthy + * handshake on a missing signal). */ +static gboolean +tls_accept_wait_ok (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if (!priv->tls_hop) + return TRUE; + + for (int i = 0; + i < 200 && !g_atomic_int_get (&priv->tls_hop->accept_done); + i++) + g_usleep (10000); + + if (!g_atomic_int_get (&priv->tls_hop->accept_done)) + { + fp_dbg ("TLS accept outcome not ready yet, proceeding"); + return TRUE; + } + + if (priv->tls_hop->accept_ret <= 0) + { + fp_err ("TLS not accepted by device: %s", + priv->tls_hop->accept_err); + return FALSE; + } + + return TRUE; +} + +static void +tls_handshake_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if (error) + { + fp_err ("failed to do tls handshake: %s (code: %d)", error->message, + error->code); + if (priv->tls_ready_callback) + { + ((GoodixNoneCallback) priv->tls_ready_callback->callback)( + dev, priv->tls_ready_callback->user_data, error); + g_clear_pointer (&priv->tls_ready_callback, g_free); + } + return; + } + + /* The proxy SSM completing does not prove the openssl server accepted: + * check the serve thread outcome before declaring the session live. + * Without this, an accept failure (e.g. peer Finished bad-record-mac from + * a device key the host does not expect) went unnoticed and activation + * proceeded on a dead session, hanging later at FDT with a command + * timeout instead of failing here with a clear TLS error. */ + if (!tls_accept_wait_ok (dev)) + { + GError *accept_error; + + accept_error = fpi_device_error_new_msg ( + FP_DEVICE_ERROR_GENERAL, + "TLS handshake failed: device did not complete the handshake " + "(likely PSK mismatch; see log for accept error)"); + fp_err ("%s", accept_error->message); + if (priv->tls_ready_callback) + { + ((GoodixNoneCallback) priv->tls_ready_callback->callback)( + dev, priv->tls_ready_callback->user_data, accept_error); + g_clear_pointer (&priv->tls_ready_callback, g_free); + } + else + { + g_error_free (accept_error); + } + return; + } + + goodix_send_tls_successfully_established ( + dev, on_tls_successfully_established, NULL); +} + +/* Read one relay flight from the TLS server side. + * + * The server emits back-to-back TLS records (ServerHello through HelloDone, + * ChangeCipherSpec and Finished) on the socket pair. A single read returns + * whatever is pending and truncates the flight, which desynchronises the + * relay, so whole records are read (5-byte header, then the body) until + * poll reports the socket idle. Bounded by the caller buffer. + * + * Returns the flight size, or -1 when nothing was read or the flight does + * not fit: a truncated record must never be relayed. */ +static int +goodix_tls_read_flight (GoodixTlsServer *tls, guint8 *buf, int buf_size) +{ + struct pollfd pfd; + int total = 0; + + if (!tls || !buf || buf_size <= 0 || tls->client_fd < 0) + return -1; + + pfd.fd = tls->client_fd; + pfd.events = POLLIN; + + for (;;) + { + int hdr_got = 0; + int rec_len; + + while (hdr_got < 5) + { + ssize_t n; + + if (total + 5 > buf_size) + { + fp_dbg ("TLS relay flight does not fit, failing"); + return -1; + } + n = read (tls->client_fd, buf + total + hdr_got, 5 - hdr_got); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + return total > 0 ? total : -1; + hdr_got += n; + } + + rec_len = (buf[total + 3] << 8) | buf[total + 4]; + fp_dbg ("TLS relay record type=0x%02x len=%d", + buf[total], rec_len); + total += 5; + + while (rec_len > 0) + { + ssize_t n; + + if (total + rec_len > buf_size) + { + fp_dbg ("TLS relay flight does not fit, failing"); + return -1; + } + n = read (tls->client_fd, buf + total, rec_len); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + return -1; + total += n; + rec_len -= n; + } + + if (poll (&pfd, 1, 50) <= 0 || !(pfd.revents & POLLIN)) + break; + } + + return total > 0 ? total : -1; +} + +static void +tls_handshake_run (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + int stage = fpi_ssm_get_cur_state (ssm); + guint8 buff[4096]; + int size; + GError *err = NULL; + + if (stage == TLS_HANDSHAKE_STAGE_HELLO_S) + { + size = goodix_tls_read_flight (priv->tls_hop, buff, sizeof (buff)); + if (size <= 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (g_io_error_quark (), size, + "failed to read tls server " + "hello")); + return; + } + if (!goodix_send_pack (dev, GOODIX_FLAGS_TLS, buff, size, NULL, &err)) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + fpi_ssm_next_state (ssm); + } + else if (stage < TLS_HANDSHAKE_STAGE_CHANGE_CIPHER_S) + { + /* Still proxying from hardware. */ + fpi_ssm_set_data (ssm, dev, NULL); + goodix_read_tls (dev, on_goodix_tls_read_handshake, ssm); + } + else if (stage == TLS_HANDSHAKE_STAGE_CHANGE_CIPHER_S) + { + fp_dbg ("Reading to proxy back"); + size = goodix_tls_read_flight (priv->tls_hop, buff, sizeof (buff)); + if (size <= 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (g_io_error_quark (), size, + "failed to read server " + "handshake")); + + return; + } + if (!goodix_send_pack (dev, GOODIX_FLAGS_TLS, buff, size, NULL, &err)) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + fpi_ssm_next_state (ssm); + } +} + +static void +do_tls_handshake (FpDevice *dev) +{ + fpi_ssm_start (fpi_ssm_new (dev, tls_handshake_run, TLS_HANDSHAKE_STAGE_NUM), + tls_handshake_done); +} + +static void +on_goodix_request_tls_connection (FpDevice *dev, guint8 *data, + guint16 length, gpointer user_data, + GError *error) +{ + FpiDeviceGoodixTls *self; + FpiDeviceGoodixTlsPrivate *priv; + int sent; + + if (error) + { + fp_err ("failed to get tls handshake: %s", error->message); + /* Forward the error instead of faking success on a dead channel. */ + self = FPI_DEVICE_GOODIXTLS (dev); + priv = fpi_device_goodixtls_get_instance_private (self); + if (priv->tls_ready_callback) + { + ((GoodixNoneCallback) priv->tls_ready_callback->callback)( + dev, priv->tls_ready_callback->user_data, error); + g_clear_pointer (&priv->tls_ready_callback, g_free); + } + return; + } + self = FPI_DEVICE_GOODIXTLS (user_data); + priv = fpi_device_goodixtls_get_instance_private (self); + + sent = goodix_tls_client_write (priv->tls_hop, data, length); + + if (sent < 0) + { + GError *write_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "failed to send data to tls server"); + if (priv->tls_ready_callback) + { + ((GoodixNoneCallback) priv->tls_ready_callback->callback)( + dev, priv->tls_ready_callback->user_data, write_error); + g_clear_pointer (&priv->tls_ready_callback, g_free); + } + else + { + g_error_free (write_error); + } + return; + } + + do_tls_handshake (dev); +} + +void +goodix_tls_init (FpDevice *dev, GoodixNoneCallback callback, gpointer user_data) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + GoodixTlsServer *s; + GError *err = NULL; + + fp_dbg ("Starting up goodix tls server"); + g_assert (priv->tls_hop == NULL); + priv->tls_hop = g_new0 (GoodixTlsServer, 1); + + g_clear_pointer (&priv->tls_ready_callback, g_free); + priv->tls_ready_callback = make_cb_info (G_CALLBACK (callback), user_data); + s = priv->tls_hop; + s->user_data = self; + if (!goodix_tls_server_init (priv->tls_hop, &err)) + { + fp_err ("failed to init tls server, error: %s, code: %d", + err ? err->message : "unknown", + err ? err->code : 0); + if (priv->tls_ready_callback) + { + ((GoodixNoneCallback) priv->tls_ready_callback->callback) ( + dev, priv->tls_ready_callback->user_data, err); + g_clear_pointer (&priv->tls_ready_callback, g_free); + } + else + { + g_clear_error (&err); + } + g_clear_pointer (&priv->tls_hop, g_free); + return; + } + + goodix_send_request_tls_connection (dev, + on_goodix_request_tls_connection, dev); +} + +gboolean +goodix_shutdown_tls (FpDevice *dev, GError **error) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + if (priv->tls_hop) + { + gboolean rs = goodix_tls_server_deinit (priv->tls_hop, error); + g_free (priv->tls_hop); + priv->tls_hop = NULL; + /* Drop any stranded handshake waiter without invoking it. */ + g_clear_pointer (&priv->tls_ready_callback, g_free); + return rs; + } + g_clear_pointer (&priv->tls_ready_callback, g_free); + return TRUE; +} + +gboolean +goodix_tls_is_alive (FpDevice *dev) +{ + FpiDeviceGoodixTls *self = FPI_DEVICE_GOODIXTLS (dev); + FpiDeviceGoodixTlsPrivate *priv = + fpi_device_goodixtls_get_instance_private (self); + + return priv->tls_hop != NULL; +} + +static void +goodix_tls_ready_image_handler (FpDevice *dev, guint8 *data, + guint16 length, gpointer user_data, + GError *error) +{ + GoodixCallbackInfo *cb_info = user_data; + GoodixImageCallback callback = (GoodixImageCallback) cb_info->callback; + FpiDeviceGoodixTls *self; + FpiDeviceGoodixTlsPrivate *priv; + guint8 *tls_data; + guint16 tls_len; + int sent; + guint32 size; + guint8 *buff; + GError *err = NULL; + int read_size; + + if (error) + { + callback (dev, NULL, 0, cb_info->user_data, error); + g_free (cb_info); + return; + } + self = FPI_DEVICE_GOODIXTLS (dev); + priv = fpi_device_goodixtls_get_instance_private (self); + + tls_data = data; + tls_len = length; + + if (length > 9 && data[0] == 0x00 && data[1] == 0x20) + { + tls_data = data + 9; + tls_len = length - 9; + } + else if (length > 5 && data[0] != 0x17) + { + for (guint16 i = 0; i + 5 < length; i++) + { + if (data[i] == 0x17 && data[i + 1] == 0x03 && data[i + 2] == 0x03) + { + tls_data = data + i; + tls_len = length - i; + break; + } + } + } + + sent = goodix_tls_client_write (priv->tls_hop, tls_data, tls_len); + + if (sent < 0) + { + GError *write_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "failed to send data to tls server"); + callback (dev, NULL, 0, cb_info->user_data, write_error); + g_free (cb_info); + return; + } + + size = 65535; + buff = g_malloc (size); + read_size = goodix_tls_server_read (priv->tls_hop, buff, size, &err); + + if (read_size <= 0) + { + callback (dev, NULL, 0, cb_info->user_data, err); + g_free (buff); + g_free (cb_info); + return; + } + + callback (dev, buff, read_size, cb_info->user_data, NULL); + g_free (buff); + g_free (cb_info); +} + +void +goodix_tls_read_image (FpDevice *dev, GoodixImageCallback callback, + gpointer user_data) +{ + GoodixCallbackInfo *cb_info; + + g_assert (callback); + cb_info = make_cb_info (G_CALLBACK (callback), user_data); + + goodix_send_mcu_get_image (dev, goodix_tls_ready_image_handler, cb_info); +} + +static void +fpi_device_goodixtls_init (FpiDeviceGoodixTls *self) +{ +} + +static void +fpi_device_goodixtls_class_init (FpiDeviceGoodixTlsClass *class) +{ + /* Generic GoodixDefault MCU_GET_IMAGE payload unless a subclass + * provides capture_payload/capture_payload_len. */ + class->capture_payload = NULL; + class->capture_payload_len = 0; +} diff --git a/libfprint/drivers/goodixtls/goodix.h b/libfprint/drivers/goodixtls/goodix.h new file mode 100644 index 000000000..5f98eec5d --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix.h @@ -0,0 +1,428 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 "drivers_api.h" + +/* 1 second USB timeout, applied per USB chunk in goodix_send_data */ +#define GOODIX_TIMEOUT (1000) + +/* NOP is a flush operation where silence from the MCU is expected. */ +#define GOODIX_NOP_TIMEOUT (200) + +G_DECLARE_DERIVABLE_TYPE (FpiDeviceGoodixTls, fpi_device_goodixtls, FPI, + DEVICE_GOODIXTLS, FpImageDevice) + +#define FPI_TYPE_DEVICE_GOODIXTLS (fpi_device_goodixtls_get_type ()) + +struct _FpiDeviceGoodixTlsClass +{ + FpImageDeviceClass parent; + + gint interface; + guint8 ep_in; + guint8 ep_out; + + /* MCU_GET_IMAGE capture payload override. NULL/0 selects the generic + * GoodixDefault payload; subclasses with device-specific capture bytes + * (e.g. 5e0a) set both fields in class_init. */ + const guint8 *capture_payload; + guint16 capture_payload_len; +}; + +typedef struct _GoodixCallbackInfo +{ + GCallback callback; + gpointer user_data; +} GoodixCallbackInfo; + +typedef void (*GoodixCmdCallback)(FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +typedef void (*GoodixFirmwareVersionCallback)(FpDevice *dev, + gchar *firmware, + gpointer user_data, + GError *error); + +typedef void (*GoodixPresetPskReadCallback)(FpDevice *dev, + gboolean success, + guint32 flags, + guint8 *psk, + guint16 length, + gpointer user_data, + GError *error); + +typedef void (*GoodixSuccessCallback)(FpDevice *dev, + gboolean success, + gpointer user_data, + GError *error); + +typedef void (*GoodixResetCallback)(FpDevice *dev, + gboolean success, + guint16 number, + gpointer user_data, + GError *error); + +typedef void (*GoodixNoneCallback)(FpDevice *dev, + gpointer user_data, + GError *error); + +typedef void (*GoodixDefaultCallback)(FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); +typedef GoodixDefaultCallback GoodixTlsCallback; + +typedef void (*GoodixImageCallback)(FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +gchar *data_to_str (guint8 *data, + guint32 length); + +/** + * @defgroup goodixrecv Goodix receive functions + * These functions are callbacks for receiving data from the device + * and should not be called from driver code directly + * @{ + * + */ +void goodix_receive_none (FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +void goodix_receive_default (FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +/** @} */ + +/** + * @brief Start the read loop that lets us get data from the device asynchronously. Should be called upon device activation + * + * @param dev + */ +void goodix_start_read_loop (FpDevice *dev); +void goodix_stop_read_loop (FpDevice *dev); + +/** + * @brief Low level function to send a protocol message to the device + * @note You should never need to call this directly from your driver! + * + * @param dev + * @param cmd command identifier + * @param payload command payload + * @param length length of payload + * @param free_func function to free the payload or NULL + * @param calc_checksum calculate and add the checksum to the data? + * @param timeout_ms timeout for recieving data back or 0 for none + * @param reply do we expect a reply from the device? + * @param callback + * @param user_data + */ +void goodix_send_protocol (FpDevice *dev, + guint8 cmd, + const guint8 *payload, + guint16 length, + GDestroyNotify free_func, + gboolean calc_checksum, + guint timeout_ms, + gboolean reply, + GoodixCmdCallback callback, + gpointer user_data); + +/** + * @brief Send nop to the device + * + * @param dev + * @param callback + * @param user_data + */ +void goodix_send_nop (FpDevice *dev, + GoodixNoneCallback callback, + gpointer user_data); + +/** + * @brief Tell the device we want to wait for the user to present their finger + * + * @param dev + * @param mode magic bytes + * @param free_func function to free the mode bytes or NULL + * @param callback called once the user has presented their finger or with an error. Note may be called at once if the mode bytes are wrong + * @param user_data + */ +void goodix_send_mcu_switch_to_fdt_down (FpDevice *dev, + const guint8 *mode, + guint16 length, + GDestroyNotify free_func, + GoodixDefaultCallback callback, + gpointer user_data); + + +/** + * @brief Tell the device we want to wait for the user to lift their finger off + * + * @param dev + * @param mode magic bytes + * @param free_func function to free the mode bytes or NULL + * @param callback called once the user has presented their finger or with an error. Note may be called at once if the mode bytes are wrong + * @param user_data + */ +void goodix_send_mcu_switch_to_fdt_up (FpDevice *dev, + const guint8 * mode, + guint16 length, + GDestroyNotify free_func, + GoodixDefaultCallback callback, + gpointer user_data); + +/** + * @brief Prep the device for fdt down and fdt up commands + * + * @param dev + * @param mode magic bytes + * @param free_func function to free mode bytes or NULL + * @param callback + * @param user_data + */ +void goodix_send_mcu_switch_to_fdt_mode (FpDevice *dev, + const guint8 * mode, + guint16 length, + GDestroyNotify free_func, + GoodixNoneCallback callback, + gpointer user_data); + +void goodix_send_nav_0 (FpDevice *dev, + GoodixDefaultCallback callback, + gpointer user_data); + +void goodix_send_read_sensor_register (FpDevice *dev, + guint16 address, + guint8 length, + GoodixDefaultCallback callback, + gpointer user_data); + +/** + * @brief Upload an MCU config to the device. Config may vary by device + * + * @param dev + * @param config config buffer + * @param length length of buffer + * @param free_func free function or NULL + * @param callback + * @param user_data + */ +void goodix_send_upload_config_mcu (FpDevice *dev, + guint8 *config, + guint16 length, + GDestroyNotify free_func, + GoodixSuccessCallback callback, + gpointer user_data); + +/** + * @brief Turn the chip on + * + * @param dev + * @param enable + * @param callback + * @param user_data + */ +void goodix_send_enable_chip (FpDevice *dev, + gboolean enable, + GoodixNoneCallback callback, + gpointer user_data); + +/** + * @brief Ask the device what firmware version it is running. Response is null-terminated string + * + * @param dev + * @param callback + * @param user_data + */ +void goodix_send_query_firmware_version (FpDevice *dev, + GoodixFirmwareVersionCallback callback, + gpointer user_data); + +/** + * @brief Ask the device what the current mcu state is + * + * @param dev + * @param callback + * @param user_data + */ +void goodix_send_query_mcu_state (FpDevice *dev, + GoodixNoneCallback callback, + gpointer user_data); + +/** + * @brief Ask the device what preset psk it has + * + * @param dev + * @param flags flags for the command, possibly device specific? + * @param length + * @param callback + * @param user_data + */ +void goodix_send_preset_psk_read (FpDevice *dev, + guint32 flags, + guint16 length, + GoodixPresetPskReadCallback callback, + gpointer user_data); +/** + * @brief Read PSK from the device using the 5e0a / Geneva wire framing. + * + * The 5e0a CMD 0xe4 payload reverses the order compared to CMD 0x06: + * length (4B LE) + offset (4B LE) + flags (4B LE) + reserved (4B LE). + * + * @param dev + * @param flags + * @param length + * @param offset byte offset into the PSK slot + * @param callback + * @param user_data + */ +void goodix_send_preset_psk_read_5e0a (FpDevice *dev, + guint32 flags, + guint32 length, + guint32 offset, + GoodixPresetPskReadCallback callback, + gpointer user_data); +/** + * @brief Request the OTP (One Time Password) from the device + * + * @param dev + * @param callback + * @param user_data + */ +void goodix_send_read_otp (FpDevice *dev, + GoodixDefaultCallback callback, + gpointer user_data); + +/** + * @brief Claim the resources used for communcation with the device + * + * @param dev + * @param error + * @return gboolean + */ +gboolean goodix_dev_init (FpDevice *dev, + GError **error); + +/** + * @brief Cleanup the resources used to communicate with the device + * + * @param dev + * @param error + * @return gboolean + */ +gboolean goodix_dev_deinit (FpDevice *dev, + GError **error); + +/** + * @brief Reset the internal state of the communication with the device. Use this e.g. on device deactivation (where it may be reactivated again in future) + * + * @param dev + */ +void goodix_reset_state (FpDevice *dev); + +/** + * @brief Stale-activation guard generation. + * + * Counter bumped on (re)activation start and teardown; TLS completion + * callbacks drop on mismatch without touching hardware. + * + * @param dev + * @return current generation (get) or new generation after bump (bump) + */ +guint goodix_activation_gen_get (FpDevice *dev); +guint goodix_activation_gen_bump (FpDevice *dev); + +/** + * @brief Warm-activation boot sequence counter. + * + * Bumped in goodix_dev_init when a USB reset is actually performed. + * + * @param dev + * @return current boot sequence number + */ +guint goodix_boot_seq_get (FpDevice *dev); + +/** + * @brief Clean-vs-dirty session lifetime tracking for conditional USB reset. + * + * Mark clean only when a session completes deactivate cleanly with a live + * TLS session; mark dirty on any error or full teardown. + * + * @param dev + */ +void goodix_session_mark_clean (FpDevice *dev); +void goodix_session_mark_dirty (FpDevice *dev); +gboolean goodix_session_is_clean (FpDevice *dev); + +/** + * @brief Initialise TLS with the device. Performs handshaking and such for you + * + * @param dev + * @param callback + * @param user_data + */ +void goodix_tls_init (FpDevice *dev, + GoodixNoneCallback callback, + gpointer user_data); + +/** + * @brief Shutdown TLS communication with the device + * + * @param dev + * @param error + * @return gboolean TRUE if ok, FALSE otherwise + */ +gboolean goodix_shutdown_tls (FpDevice *dev, + GError **error); + +/** + * @brief Check whether a negotiated TLS session context is currently alive. + * + * @param dev + * @return TRUE when priv->tls_hop != NULL, FALSE otherwise + */ +gboolean goodix_tls_is_alive (FpDevice *dev); + +/** + * @brief Read a TLS encrypted image from the device and decrypt it + * + * @param dev + * @param callback Called when the image is decrypted + * @param user_data + */ +void goodix_tls_read_image (FpDevice *dev, + GoodixImageCallback callback, + gpointer user_data); diff --git a/libfprint/drivers/goodixtls/goodix5e0a.c b/libfprint/drivers/goodixtls/goodix5e0a.c new file mode 100644 index 000000000..8645db3e7 --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix5e0a.c @@ -0,0 +1,1570 @@ +/* + * Goodix driver for USB devices 27c6:5e0a + * + * Copyright (C) 2026 The libfprint Goodix 5e0a contributors + * + * 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/goodixtls/goodix5xx.h" +#include "fp-device.h" +#include "fp-image-device.h" +#include "fp-image.h" +#include "fpi-assembling.h" +#include "fpi-context.h" +#include "fpi-image-device.h" +#include "fpi-image.h" +#include "fpi-ssm.h" +#include "glibconfig.h" +#include "gusb/gusb-device.h" +#include +#include +#include + +#define FP_COMPONENT "goodixtls5e0a" + +#include +#include + +#include "drivers_api.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include "nbis/include/lfs.h" +#pragma GCC diagnostic pop +#include "goodix.h" +#include "goodix_proto.h" +#include "goodix5e0a.h" + +struct _FpiDeviceGoodixTls5e0a +{ + FpiDeviceGoodixTls5xx parent; + + gboolean session_started; + FpiSsm *scan_ssm; + guint scan_gen; + guint scan_timeout_gen; + GSource *down_timeout; + + /* TLS session parking state across deactivate/activate cycles */ + gboolean tls_parked; + gint64 tls_parked_at; + guint tls_parked_gen; + + /* Warm activation state to avoid redundant sensor reset/configuration */ + gboolean warm_ok; + gint64 last_clean_mono; + guint warm_boot_seq; + const char *warm_down_reason; + gboolean warm_attempted; + gboolean warm_retried; + + /* Multi-frame burst capture and best-frame selection */ + guint frame_count; + FpImage *best_img; + guint best_minutiae; + guint best_frame_no; + + /* Verify retry guard against rapid retry burn on continuous touch */ + gboolean retry_guard; + gint64 retry_guard_mono; +}; + +G_DECLARE_FINAL_TYPE (FpiDeviceGoodixTls5e0a, fpi_device_goodixtls5e0a, FPI, + DEVICE_GOODIXTLS5E0A, FpiDeviceGoodixTls5xx); + +G_DEFINE_TYPE (FpiDeviceGoodixTls5e0a, fpi_device_goodixtls5e0a, + FPI_TYPE_DEVICE_GOODIXTLS5XX); + +/* Session command used during activation. */ +#define GOODIX_CMD_SESSION_D6 (0xd6) + +/* Pre-shared key for TLS_PSK_WITH_AES_128_CBC_SHA256. */ +static const guint8 goodix_5e0a_psk[32] = { + 0xd8, 0x53, 0xad, 0x19, 0x41, 0xb2, 0xdc, 0x53, + 0x50, 0xc7, 0x66, 0xcd, 0x72, 0x6e, 0xf7, 0xa5, + 0xdf, 0x7d, 0x5f, 0xa3, 0x90, 0x53, 0xbf, 0xac, + 0x26, 0x9c, 0xe7, 0x52, 0xd7, 0xa8, 0xb2, 0xab +}; + +/* Sensor configuration blob uploaded during activation. */ +static const guint8 goodix_5e0a_config[256] = { + 0xb0, 0x11, 0x60, 0x71, 0x2c, 0x9d, 0x2c, 0xc9, 0x1c, 0xe5, 0x18, 0xfd, 0x00, 0xfd, 0x00, 0xfd, + 0x03, 0xba, 0x00, 0x01, 0x80, 0xca, 0x00, 0x04, 0x00, 0x84, 0x00, 0x15, 0xb3, 0x86, 0x00, 0x00, + 0xc4, 0x88, 0x00, 0x00, 0xba, 0x8a, 0x00, 0x00, 0xb2, 0x8c, 0x00, 0x00, 0xaa, 0x8e, 0x00, 0x00, + 0xc1, 0x90, 0x00, 0xbb, 0xbb, 0x92, 0x00, 0xb1, 0xb1, 0x94, 0x00, 0x00, 0xa8, 0x96, 0x00, 0x00, + 0xb6, 0x98, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, + 0x00, 0xd6, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0x50, 0x00, 0x01, 0x05, 0xd0, 0x00, 0x00, + 0x00, 0x70, 0x00, 0x00, 0x00, 0x72, 0x00, 0x78, 0x56, 0x74, 0x00, 0x34, 0x12, 0x20, 0x00, 0x10, + 0x40, 0x2a, 0x01, 0x02, 0x04, 0x22, 0x00, 0x01, 0x20, 0x24, 0x00, 0x32, 0x00, 0x80, 0x00, 0x01, + 0x00, 0x5c, 0x00, 0x80, 0x00, 0x56, 0x00, 0x24, 0x20, 0x58, 0x00, 0x03, 0x02, 0x32, 0x00, 0x0c, + 0x02, 0x66, 0x00, 0x03, 0x00, 0x7c, 0x00, 0x00, 0x58, 0x82, 0x00, 0x80, 0x15, 0x2a, 0x01, 0x82, + 0x03, 0x22, 0x00, 0x01, 0x20, 0x24, 0x00, 0x14, 0x00, 0x80, 0x00, 0x01, 0x00, 0x5c, 0x00, 0x00, + 0x01, 0x56, 0x00, 0x04, 0x20, 0x58, 0x00, 0x03, 0x02, 0x32, 0x00, 0x0c, 0x02, 0x66, 0x00, 0x03, + 0x00, 0x7c, 0x00, 0x00, 0x58, 0x82, 0x00, 0x80, 0x15, 0x2a, 0x01, 0x08, 0x00, 0x5c, 0x00, 0x80, + 0x00, 0x54, 0x00, 0x10, 0x01, 0x62, 0x00, 0x04, 0x03, 0x64, 0x00, 0x19, 0x00, 0x66, 0x00, 0x03, + 0x00, 0x7c, 0x00, 0x01, 0x58, 0x2a, 0x01, 0x08, 0x00, 0x5c, 0x00, 0x00, 0x01, 0x52, 0x00, 0x08, + 0x00, 0x54, 0x00, 0x00, 0x01, 0x66, 0x00, 0x03, 0x00, 0x7c, 0x00, 0x01, 0x58, 0x00, 0x53, 0x0e +}; + +/* Session initialization commands */ +static const guint8 goodix_5e0a_query_ae[3] = {0x00, 0x01, 0x00}; +static const guint8 goodix_5e0a_session_d6[2] = {0x00, 0x00}; + +/* Image capture payload, also published via capture_payload. */ +static const guint8 goodix_5e0a_img_payload[10] = { + 0x05, 0x00, 0xb0, 0x00, 0xb2, 0x00, 0xb0, 0x00, 0xb1, 0x00 +}; + +/* Finger-detect down table. */ +static const guint8 goodix_5e0a_down_s12[35] = { + 0x1c, 0x01, 0xb0, 0x00, 0xb2, 0x00, 0xb0, 0x00, 0xb1, 0x00, + 0x80, 0xb7, 0x80, 0xce, 0x80, 0xaa, 0x80, 0xbe, 0x80, 0xb1, 0x80, 0xc2, + 0x00, 0x00, 0x00, 0x00, + 0xb0, 0x00, 0xb2, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0x00 +}; + +/* Finger-detect up table. */ +static const guint8 goodix_5e0a_up_u01[35] = { + 0x0e, 0x01, 0xb0, 0x00, 0xb2, 0x00, 0xb0, 0x00, 0xb1, 0x00, + 0x80, 0x94, 0x80, 0xc2, 0x80, 0x97, 0x80, 0xb1, 0x80, 0xa5, 0x80, 0x21, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const FpIdEntry goodix_5e0a_id_table[] = { + {.vid = 0x27c6, .pid = 0x5e0a}, + {.vid = 0, .pid = 0, .driver_data = 0}, +}; + +static void goodix5e0a_reset_touch_frames (FpiDeviceGoodixTls5e0a *self); + +#define GOODIX_5E0A_TLS_PARK_TTL_US (G_USEC_PER_SEC * 30) +#define GOODIX_5E0A_TLS_PARK_HEALTH_TIMEOUT_MS 500 +#define GOODIX_5E0A_WARM_TTL_US (G_USEC_PER_SEC * 60) + +enum activate_states { + ACTIVATE_READ_AND_NOP, + ACTIVATE_RESET, + ACTIVATE_READ_CHIP_ID, + ACTIVATE_READ_OTP, + ACTIVATE_CHECK_FW_VER, + ACTIVATE_UPLOAD_CONFIG, + ACTIVATE_CHECK_PSK, + ACTIVATE_NUM_STATES, +}; + +static void activate_complete (FpiSsm *ssm, FpDevice *dev, GError *error); + +/* Read the PSK slot before TLS to latch the MCU crypto state. Best effort: + * failure falls through to TLS and surfaces there. */ +static void +on_psk_hash_read (FpDevice *dev, gboolean success, guint32 flags, + guint8 *psk, guint16 length, gpointer user_data, + GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fp_warn ("PSK hash read failed: %s", error->message); + g_error_free (error); + } + else + { + fp_dbg ("PSK hash read: success=%d, len=%u", success, length); + } + fpi_ssm_next_state (ssm); +} + +static void +activate_run_state (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case ACTIVATE_READ_AND_NOP: + goodix_start_read_loop (dev); + goodix_send_nop (dev, goodixtls5xx_check_none, ssm); + break; + + case ACTIVATE_RESET: + /* The reset command is not part of the activation sequence. */ + fpi_ssm_jump_to_state (ssm, ACTIVATE_CHECK_FW_VER); + break; + + case ACTIVATE_READ_CHIP_ID: + if (self->warm_attempted) + { + fpi_ssm_jump_to_state (ssm, ACTIVATE_CHECK_FW_VER); + return; + } + goodix_send_read_sensor_register (dev, 0x0000, 4, goodixtls5xx_check_none_cmd, ssm); + break; + + case ACTIVATE_READ_OTP: + if (self->warm_attempted) + { + fpi_ssm_jump_to_state (ssm, ACTIVATE_CHECK_FW_VER); + return; + } + goodix_send_read_otp (dev, goodixtls5xx_check_none_cmd, ssm); + break; + + case ACTIVATE_CHECK_FW_VER: + goodix_send_query_firmware_version (dev, goodixtls5xx_check_firmware_version, ssm); + break; + + case ACTIVATE_UPLOAD_CONFIG: + if (self->warm_attempted) + { + fpi_ssm_jump_to_state (ssm, ACTIVATE_NUM_STATES); + return; + } + /* Config is uploaded after TLS completes; advance to the PSK read. */ + fpi_ssm_next_state (ssm); + break; + + case ACTIVATE_CHECK_PSK: + if (self->warm_attempted) + { + fpi_ssm_jump_to_state (ssm, ACTIVATE_NUM_STATES); + return; + } + goodix_send_preset_psk_read_5e0a (dev, GOODIX_5E0A_PSK_FLAGS, 32, 0, + on_psk_hash_read, ssm); + break; + } +} + +static void +on_chip_enabled (FpDevice *dev, gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + if (error) + { + self->warm_ok = FALSE; + self->warm_down_reason = "failed-last"; + self->warm_attempted = FALSE; + goodix_session_mark_dirty (dev); + fp_err ("failed to enable chip: %s (code: %d)", error->message, error->code); + fpi_image_device_activate_complete (FP_IMAGE_DEVICE (dev), error); + return; + } + self->warm_ok = TRUE; + self->last_clean_mono = g_get_monotonic_time (); + self->warm_boot_seq = goodix_boot_seq_get (dev); + self->warm_attempted = FALSE; + fp_dbg ("Chip enabled! Activation complete."); + fpi_image_device_activate_complete (FP_IMAGE_DEVICE (dev), NULL); +} + +static gboolean +goodix5e0a_warm_fresh (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + return self->warm_ok + && self->warm_boot_seq == goodix_boot_seq_get (dev) + && (g_get_monotonic_time () - self->last_clean_mono) < GOODIX_5E0A_WARM_TTL_US; +} + +static void +goodix5e0a_log_warm_taken (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + fp_dbg ("warm activation: reusing MCU config (age=%.1fs, boot_seq=%u)", + (g_get_monotonic_time () - self->last_clean_mono) / (gdouble) G_USEC_PER_SEC, + self->warm_boot_seq); +} + +static void +goodix5e0a_start_warm_activation (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + self->session_started = FALSE; + self->scan_ssm = NULL; + self->down_timeout = NULL; + self->warm_attempted = TRUE; + + fp_dbg ("warm path: skipping RESET + config upload, entry=CHECK_FW_VER"); + fpi_ssm_start (fpi_ssm_new (dev, activate_run_state, ACTIVATE_NUM_STATES), + activate_complete); +} + +static void +goodix5e0a_start_full_activation (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + self->session_started = FALSE; + self->scan_ssm = NULL; + self->down_timeout = NULL; + self->warm_attempted = FALSE; + + fpi_ssm_start (fpi_ssm_new (dev, activate_run_state, ACTIVATE_NUM_STATES), + activate_complete); +} + +static void +on_parked_health_reply (FpDevice *dev, gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + guint gen = GPOINTER_TO_UINT (user_data); + + if (gen != goodix_activation_gen_get (dev)) + { + fp_dbg ("dropping stale parked-TLS health reply"); + if (error) + g_error_free (error); + return; + } + + if (error) + { + const char *reason = "tls-error"; + gboolean transport_miss = FALSE; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + { + reason = "timeout"; + transport_miss = TRUE; + } + g_error_free (error); + goodix_shutdown_tls (dev, NULL); + goodix_reset_state (dev); + if (transport_miss) + { + self->warm_ok = FALSE; + self->warm_down_reason = "transport-miss"; + } + else if (goodix5e0a_warm_fresh (dev)) + { + goodix5e0a_log_warm_taken (dev); + goodix5e0a_start_warm_activation (dev); + return; + } + fp_dbg ("parked TLS session unhealthy (%s), full re-handshake", reason); + goodix5e0a_start_full_activation (dev); + return; + } + + fp_dbg ("TLS session reused (parked %.1fs, gen=%u)", + (g_get_monotonic_time () - self->tls_parked_at) / (gdouble) G_USEC_PER_SEC, + gen); + fp_dbg ("parked TLS session healthy, confirming chip enable"); + goodix_send_enable_chip (dev, TRUE, on_chip_enabled, NULL); +} + +static void +on_post_tls_config_uploaded (FpDevice *dev, gboolean success, + gpointer user_data, GError *error) +{ + if (error) + { + fp_err ("failed to upload config after TLS: %s", error->message); + fpi_image_device_activate_complete (FP_IMAGE_DEVICE (dev), error); + return; + } + if (!success) + { + fp_err ("MCU rejected config upload after TLS"); + fpi_image_device_activate_complete ( + FP_IMAGE_DEVICE (dev), + g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO, + "failed to upload mcu config after TLS")); + return; + } + goodix_send_enable_chip (dev, TRUE, on_chip_enabled, NULL); +} + +static void +on_tls_activation_complete (FpDevice *dev, gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + /* Drop this completion if a deactivate bumped the generation while the TLS handshake was in flight. */ + if (GPOINTER_TO_UINT (user_data) != goodix_activation_gen_get (dev)) + { + fp_dbg ("dropping stale TLS activation completion"); + if (error) + g_error_free (error); + return; + } + + if (error) + { + goodix_session_mark_dirty (dev); + if (self->warm_attempted && !self->warm_retried) + { + self->warm_ok = FALSE; + self->warm_down_reason = "failed-last"; + self->warm_attempted = FALSE; + self->warm_retried = TRUE; + fp_dbg ("warm attempt failed (%s), retrying full ladder", error->message); + g_error_free (error); + goodix_shutdown_tls (dev, NULL); + goodix_reset_state (dev); + goodix5e0a_start_full_activation (dev); + return; + } + self->warm_ok = FALSE; + self->warm_down_reason = "failed-last"; + self->warm_attempted = FALSE; + fp_err ("failed during TLS activation: %s (code: %d)", error->message, error->code); + fpi_image_device_activate_complete (FP_IMAGE_DEVICE (dev), error); + return; + } + + fp_dbg ("TLS connection ready!"); + + /* Upload config after TLS on the cold path; warm reuse keeps its config. */ + if (self->warm_attempted) + { + goodix_send_enable_chip (dev, TRUE, on_chip_enabled, NULL); + } + else + { + goodix_send_upload_config_mcu (dev, (guint8 *) goodix_5e0a_config, + sizeof (goodix_5e0a_config), NULL, + on_post_tls_config_uploaded, NULL); + } +} + +static void +activate_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + G_DEBUG_HERE (); + if (!error) + { + /* Capture the activation generation for the staleness guard. */ + goodix_tls_init (dev, on_tls_activation_complete, + GUINT_TO_POINTER (goodix_activation_gen_get (dev))); + } + else + { + goodix_session_mark_dirty (dev); + if (self->warm_attempted && !self->warm_retried) + { + self->warm_ok = FALSE; + self->warm_down_reason = "failed-last"; + self->warm_attempted = FALSE; + self->warm_retried = TRUE; + fp_dbg ("warm attempt failed (%s), retrying full ladder", error->message); + g_error_free (error); + goodix5e0a_start_full_activation (dev); + return; + } + self->warm_ok = FALSE; + self->warm_down_reason = "failed-last"; + self->warm_attempted = FALSE; + fp_err ("failed during activation: %s (code: %d)", error->message, error->code); + fpi_image_device_activate_complete (FP_IMAGE_DEVICE (dev), error); + } +} + +static void +dev_activate (FpImageDevice *img_dev) +{ + FpDevice *dev = FP_DEVICE (img_dev); + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + guint pre_gen = goodix_activation_gen_get (dev); + guint new_gen = goodix_activation_gen_bump (dev); + + goodix5e0a_reset_touch_frames (self); + self->warm_retried = FALSE; + + if (self->tls_parked && self->tls_parked_gen == pre_gen + && goodix_tls_is_alive (dev) + && (g_get_monotonic_time () - self->tls_parked_at) < GOODIX_5E0A_TLS_PARK_TTL_US) + { + GoodixCallbackInfo *cb_info; + GoodixQueryMcuState payload; + + self->tls_parked = FALSE; + self->scan_ssm = NULL; + self->down_timeout = NULL; + fp_dbg ("parked TLS session candidate fresh, health-checking (gen=%u)", new_gen); + goodix_start_read_loop (dev); + + cb_info = g_new0 (GoodixCallbackInfo, 1); + cb_info->callback = G_CALLBACK (on_parked_health_reply); + cb_info->user_data = GUINT_TO_POINTER (new_gen); + payload.unused_flags = 0x55; + goodix_send_protocol (dev, GOODIX_CMD_QUERY_MCU_STATE, + (guint8 *) &payload, sizeof (payload), + NULL, TRUE, + GOODIX_5E0A_TLS_PARK_HEALTH_TIMEOUT_MS, + FALSE, goodix_receive_none, cb_info); + return; + } + + if (self->tls_parked) + { + const char *reason; + + if (self->tls_parked_gen != pre_gen) + reason = "gen-mismatch"; + else if (!goodix_tls_is_alive (dev)) + reason = "tls-error"; + else + reason = "expired"; + self->tls_parked = FALSE; + fp_dbg ("parked TLS session unhealthy (%s), full re-handshake", reason); + goodix_shutdown_tls (dev, NULL); + } + + if (goodix5e0a_warm_fresh (dev)) + { + goodix5e0a_log_warm_taken (dev); + goodix5e0a_start_warm_activation (dev); + return; + } + + { + const char *reason; + + if (self->warm_ok && self->warm_boot_seq == goodix_boot_seq_get (dev)) + { + reason = "ttl-expired"; + self->warm_ok = FALSE; + self->warm_down_reason = "ttl-expired"; + } + else if (self->warm_ok) + { + reason = "cold-start"; + self->warm_ok = FALSE; + self->warm_down_reason = "cold-start"; + } + else + { + reason = self->warm_down_reason ? self->warm_down_reason : "cold-start"; + } + self->warm_attempted = FALSE; + fp_dbg ("warm expired: reason=%s", reason); + goodix5e0a_start_full_activation (dev); + } +} + +enum goodix5e0a_scan_states { + SCAN_5E0A_SESSION_AE, + SCAN_5E0A_SESSION_D6, + SCAN_5E0A_FDT_DOWN, + SCAN_5E0A_GET_IMAGE, + SCAN_5E0A_FDT_UP_1, + SCAN_5E0A_UP_AE, + SCAN_5E0A_FDT_UP_2, + SCAN_5E0A_NUM_STATES, +}; + +static void +send_cmd_noreply (FpDevice *dev, guint8 cmd, const guint8 *payload, guint16 len, + GoodixNoneCallback cb, gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixCmdCallback callback = NULL; + + if (cb) + { + cb_info = g_new0 (GoodixCallbackInfo, 1); + cb_info->callback = G_CALLBACK (cb); + cb_info->user_data = user_data; + callback = goodix_receive_none; + } + + goodix_send_protocol (dev, cmd, payload, len, NULL, TRUE, GOODIX_TIMEOUT, + FALSE, callback, cb_info); +} + +static void +send_cmd_reply (FpDevice *dev, guint8 cmd, const guint8 *payload, guint16 len, + guint timeout_ms, GoodixDefaultCallback cb, gpointer user_data) +{ + GoodixCallbackInfo *cb_info = NULL; + GoodixCmdCallback callback = NULL; + + if (cb) + { + cb_info = g_new0 (GoodixCallbackInfo, 1); + cb_info->callback = G_CALLBACK (cb); + cb_info->user_data = user_data; + callback = goodix_receive_default; + } + + goodix_send_protocol (dev, cmd, payload, len, NULL, TRUE, timeout_ms, + TRUE, callback, cb_info); +} + +static void +goodix5e0a_step_cb (FpDevice *dev, gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fp_dbg ("ignoring expected command error: %s", error->message); + g_error_free (error); + } + fpi_ssm_next_state (ssm); +} + +static void +goodix5e0a_on_d6_reply (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + if (err) + { + fp_warn ("session d6 reply error: %s", err->message); + g_error_free (err); + } + else + { + fp_dbg ("session d6 replied successfully (len=%u)", len); + } + self->session_started = TRUE; + if (self->retry_guard) + fpi_ssm_jump_to_state (ssm, SCAN_5E0A_FDT_UP_1); + else + fpi_ssm_next_state (ssm); +} + +static void goodix5e0a_on_fdt_down_reply (FpDevice *dev, + guint8 *data, + guint16 len, + gpointer ssm, + GError *err); + +static void +goodix5e0a_on_down_poll_timeout (FpDevice *dev, gpointer user_data) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + FpiSsm *ssm = user_data; + + self->down_timeout = NULL; + + if (self->scan_ssm != ssm) + return; + if (self->scan_timeout_gen != self->scan_gen) + return; + + send_cmd_reply (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN, + goodix_5e0a_down_s12, sizeof (goodix_5e0a_down_s12), + 0, goodix5e0a_on_fdt_down_reply, ssm); +} + +static void +goodix5e0a_on_fdt_down_reply (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + guint8 status; + guint32 channel_energy = 0; + gboolean touch; + + if (err) + { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + fpi_ssm_mark_failed (ssm, err); + return; + } + + status = (len > 0) ? data[0] : 0x00; + fp_dbg ("finger-detect reply: status=0x%02x len=%u", status, len); + + if (len >= 4) + for (guint16 i = 4; i + 1 < len; i += 2) + channel_energy += (guint32) data[i] | ((guint32) data[i + 1] << 8); + + /* Gating rule: touch = channel-byte energy (data[2] != 0xff and channel_energy > 0), never byte0 */ + touch = (len >= 4 && data[2] != 0xff && channel_energy > 0); + + if (touch) + { + if (self->down_timeout) + { + g_source_destroy (self->down_timeout); + self->down_timeout = NULL; + } + fp_dbg ("touch confirmed: mask=0x%02x energy=%u", + (data && len >= 3) ? data[2] : 0, channel_energy); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE); + fpi_ssm_next_state (ssm); + return; + } + + /* No touch (empty air or poor contact): pace re-sampling silently after 50ms */ + if (self->down_timeout) + { + g_source_destroy (self->down_timeout); + self->down_timeout = NULL; + } + self->scan_timeout_gen = self->scan_gen; + self->down_timeout = fpi_device_add_timeout (dev, 50, goodix5e0a_on_down_poll_timeout, ssm, NULL); +} + +static FpImage * process_raw_frame (GoodixTls5xxPix * pix); +static guint goodix5e0a_count_minutiae (FpImage *img); + +static guint32 +goodix5e0a_decode_frame (GoodixTls5xxPix *out_row_major, const guint8 *data, guint16 len) +{ + g_autofree guint8 *packed = NULL; + guint32 packed_len = 0; + guint32 pixel_idx = 0; + + if (!out_row_major || !data) + return 0; + + packed = g_new0 (guint8, GOODIX_5E0A_ACT_BYTES); + + /* Each frame is 80 blocks of 132 bytes followed by a four-byte footer. + * Each block carries 96 packed pixel bytes and 36 padding bytes. */ + for (guint32 block = 0; block < GOODIX_5E0A_FRAME_BLOCKS; block++) + { + guint32 src = block * GOODIX_5E0A_BLOCK_BYTES; + if (src + GOODIX_5E0A_BLOCK_ACTIVE_BYTES > len) + break; + + memcpy (packed + packed_len, data + src, GOODIX_5E0A_BLOCK_ACTIVE_BYTES); + packed_len += GOODIX_5E0A_BLOCK_ACTIVE_BYTES; + } + + for (guint32 i = 0; i + 6 <= packed_len && pixel_idx + 4 <= GOODIX_5E0A_FRAME_SIZE; i += 6) + { + const guint8 *c = packed + i; + out_row_major[pixel_idx++] = ((c[0] & 0x0f) << 8) | c[1]; + out_row_major[pixel_idx++] = (c[3] << 4) | (c[0] >> 4); + out_row_major[pixel_idx++] = ((c[5] & 0x0f) << 8) | c[2]; + out_row_major[pixel_idx++] = (c[4] << 4) | (c[5] >> 4); + } + + return pixel_idx; +} + +static void +goodix5e0a_reset_touch_frames (FpiDeviceGoodixTls5e0a *self) +{ + if (self->best_img != NULL) + { + g_object_unref (self->best_img); + self->best_img = NULL; + } + self->frame_count = 0; + self->best_minutiae = 0; + self->best_frame_no = 0; +} + +static FpImage * +goodix5e0a_claim_best_frame (FpiDeviceGoodixTls5e0a *self) +{ + FpImage *best; + + if (self->best_img == NULL) + return NULL; + best = self->best_img; + fp_dbg ("best frame %u/%u: minutiae=%u (submitting)", + self->best_frame_no, (guint) GOODIX_5E0A_FRAMES_PER_TOUCH, + self->best_minutiae); + self->best_img = NULL; + self->best_minutiae = 0; + self->best_frame_no = 0; + return best; +} + +static gboolean +goodix5e0a_keep_best_frame (FpDevice *dev, gpointer ssm, FpImage *img, + guint16 declen, guint active, guint range); + +static void +goodix5e0a_on_read_img (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + FpiDeviceAction action = fpi_device_get_current_action (dev); + g_autofree GoodixTls5xxPix *raw_frame = NULL; + FpImage *img; + guint frame_active = 0; + guint16 frame_min = 65535, frame_max = 0; + guint frame_range = 0; + + if (self->scan_ssm != ssm) + { + if (err) + g_error_free (err); + return; + } + + /* Fall back to the best frame captured so far if a mid-burst read fails. */ + if (err) + { + if (action != FPI_DEVICE_ACTION_ENROLL && self->best_img != NULL) + { + g_error_free (err); + img = goodix5e0a_claim_best_frame (self); + goto deliver; + } + fpi_ssm_mark_failed (ssm, err); + return; + } + + /* Submit the best frame captured so far if a mid-burst read is truncated. */ + if (action != FPI_DEVICE_ACTION_ENROLL && self->best_img != NULL + && (data == NULL || len < GOODIX_5E0A_FRAME_WIRE_BYTES)) + { + fp_dbg ("frame %u/%u: short declen=%u, submitting best-so-far %u/%u", + self->frame_count + 1, (guint) GOODIX_5E0A_FRAMES_PER_TOUCH, + len, self->best_frame_no, + (guint) GOODIX_5E0A_FRAMES_PER_TOUCH); + img = goodix5e0a_claim_best_frame (self); + goto deliver; + } + + raw_frame = g_new0 (GoodixTls5xxPix, GOODIX_5E0A_FRAME_SIZE); + goodix5e0a_decode_frame (raw_frame, data, len); + + for (guint32 i = 0; i < GOODIX_5E0A_FRAME_SIZE; i++) + { + if (raw_frame[i] > 30) + { + frame_active++; + if (raw_frame[i] < frame_min) + frame_min = raw_frame[i]; + if (raw_frame[i] > frame_max) + frame_max = raw_frame[i]; + } + } + frame_range = (frame_min != 65535 && frame_max > frame_min) + ? (guint) (frame_max - frame_min) : 0; + + img = process_raw_frame (raw_frame); + + if (action == FPI_DEVICE_ACTION_ENROLL) + { + guint minutiae_count; + + if (img == NULL) + { + fp_dbg ("enrollment touch rejected: poor frame quality (press firmer)"); + fpi_image_device_retry_scan (FP_IMAGE_DEVICE (dev), FP_DEVICE_RETRY_TOO_SHORT); + fpi_ssm_next_state (ssm); + return; + } + minutiae_count = goodix5e0a_count_minutiae (img); + fp_dbg ("enrollment quality check: minutiae_count=%u (floor=%d)", + minutiae_count, GOODIX_5E0A_ENROLL_MIN_MINUTIAE); + if (minutiae_count < GOODIX_5E0A_ENROLL_MIN_MINUTIAE) + { + fp_dbg ("enrollment touch rejected: minutiae_count=%u < %d (press firmer)", + minutiae_count, GOODIX_5E0A_ENROLL_MIN_MINUTIAE); + g_object_unref (img); + fpi_image_device_retry_scan (FP_IMAGE_DEVICE (dev), FP_DEVICE_RETRY_TOO_SHORT); + fpi_ssm_next_state (ssm); + return; + } + } + + if (action != FPI_DEVICE_ACTION_ENROLL) + { + if (goodix5e0a_keep_best_frame (dev, ssm, img, len, frame_active, frame_range)) + return; + img = goodix5e0a_claim_best_frame (self); + if (img == NULL) + { + fpi_image_device_retry_scan (FP_IMAGE_DEVICE (dev), FP_DEVICE_RETRY_TOO_SHORT); + goto deliver_done; + } + } + +deliver: + fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), img); + +deliver_done: + if (action != FPI_DEVICE_ACTION_ENROLL) + { + self->scan_ssm = NULL; + self->retry_guard = TRUE; + self->retry_guard_mono = g_get_monotonic_time (); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); + fpi_ssm_mark_completed (ssm); + } + else + { + fpi_ssm_next_state (ssm); + } +} + +static gboolean +goodix5e0a_keep_best_frame (FpDevice *dev, gpointer ssm, FpImage *img, + guint16 declen G_GNUC_UNUSED, guint active, guint range) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + guint minutiae = img ? goodix5e0a_count_minutiae (img) : 0; + + self->frame_count++; + fp_dbg ("frame %u/%u: active=%u range=%u minutiae=%u", + self->frame_count, (guint) GOODIX_5E0A_FRAMES_PER_TOUCH, + active, range, minutiae); + + if (img != NULL) + { + if (self->best_img == NULL || minutiae > self->best_minutiae) + { + if (self->best_img != NULL) + g_object_unref (self->best_img); + self->best_img = img; + self->best_minutiae = minutiae; + self->best_frame_no = self->frame_count; + } + else + { + g_object_unref (img); + } + } + + if (self->frame_count < GOODIX_5E0A_FRAMES_PER_TOUCH) + { + goodix_tls_read_image (dev, goodix5e0a_on_read_img, ssm); + return TRUE; + } + return FALSE; +} + +static void +goodix5e0a_on_fdt_up_reply (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + if (self->scan_ssm != ssm) + { + if (err) + g_error_free (err); + return; + } + + if (err) + { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + fp_dbg ("finger-detect command failed: %s", err->message); + if (self->retry_guard) + { + /* A timeout here means the finger is still down, not released. + * Re-issue while the guard is held; stop after 30s. */ + if (g_get_monotonic_time () - self->retry_guard_mono > 30 * G_USEC_PER_SEC) + { + self->retry_guard = FALSE; + fp_dbg ("retry guard: orphaned hold past 30s, failing claim"); + fpi_ssm_mark_failed (ssm, err); + return; + } + g_error_free (err); + fp_dbg ("retry guard: finger still present, re-issuing FDT UP"); + send_cmd_reply (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, + goodix_5e0a_up_u01, sizeof (goodix_5e0a_up_u01), + 2000, goodix5e0a_on_fdt_up_reply, ssm); + return; + } + g_error_free (err); + } + else + { + fp_dbg ("finger release detected"); + } + + if (self->retry_guard) + { + self->retry_guard = FALSE; + fp_dbg ("retry guard: release ok, arming FDT DOWN"); + fpi_ssm_jump_to_state (ssm, SCAN_5E0A_FDT_DOWN); + return; + } + + /* Mark current scan SSM completed before notifying libfprint, + * so that when libfprint synchronously requests AWAIT_FINGER_ON, + * the concurrency guard does not block the new scan SSM. */ + self->scan_ssm = NULL; + fpi_ssm_next_state (ssm); + fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); +} + +static void +goodix5e0a_scan_run_state (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case SCAN_5E0A_SESSION_AE: + send_cmd_noreply (dev, GOODIX_CMD_QUERY_MCU_STATE, + goodix_5e0a_query_ae, sizeof (goodix_5e0a_query_ae), + goodix5e0a_step_cb, ssm); + break; + + case SCAN_5E0A_SESSION_D6: + if (self->session_started) + { + if (self->retry_guard) + fpi_ssm_jump_to_state (ssm, SCAN_5E0A_FDT_UP_1); + else + fpi_ssm_jump_to_state (ssm, SCAN_5E0A_FDT_DOWN); + return; + } + send_cmd_reply (dev, GOODIX_CMD_SESSION_D6, + goodix_5e0a_session_d6, sizeof (goodix_5e0a_session_d6), + GOODIX_TIMEOUT, goodix5e0a_on_d6_reply, ssm); + break; + + case SCAN_5E0A_FDT_DOWN: + /* Blocking wait for the MCU capacitive touch interrupt: timeout 0 + * installs no libfprint timer, so the await survives until a real + * touch, client cancel, or deactivate. Any finite timeout turns an + * idle wait into "Command timed out: 0x32". */ + send_cmd_reply (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN, + goodix_5e0a_down_s12, sizeof (goodix_5e0a_down_s12), + 0, goodix5e0a_on_fdt_down_reply, ssm); + break; + + case SCAN_5E0A_GET_IMAGE: + goodix_tls_read_image (dev, goodix5e0a_on_read_img, ssm); + break; + + case SCAN_5E0A_FDT_UP_1: + send_cmd_noreply (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, + goodix_5e0a_up_u01, sizeof (goodix_5e0a_up_u01), + goodix5e0a_step_cb, ssm); + break; + + case SCAN_5E0A_UP_AE: + send_cmd_noreply (dev, GOODIX_CMD_QUERY_MCU_STATE, + goodix_5e0a_query_ae, sizeof (goodix_5e0a_query_ae), + goodix5e0a_step_cb, ssm); + break; + + case SCAN_5E0A_FDT_UP_2: + send_cmd_reply (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, + goodix_5e0a_up_u01, sizeof (goodix_5e0a_up_u01), + self->retry_guard ? 2000 : 5000, goodix5e0a_on_fdt_up_reply, ssm); + break; + } +} + +static void +goodix5e0a_scan_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + self->scan_gen++; + self->scan_ssm = NULL; + goodix5e0a_reset_touch_frames (self); + if (self->down_timeout) + { + g_source_destroy (self->down_timeout); + self->down_timeout = NULL; + } + + if (error) + { + goodix_session_mark_dirty (dev); + self->warm_ok = FALSE; + self->retry_guard = FALSE; + fp_err ("failed to scan: %s (code: %d)", error->message, error->code); + fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); + return; + } + fp_dbg ("finished scan stage"); +} + +static void +goodix5e0a_scan_start (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + + if (self->scan_ssm != NULL) + { + fp_dbg ("scan SSM already active, ignoring start request"); + return; + } + + if (self->retry_guard) + { + gint64 delta_us = g_get_monotonic_time () - self->retry_guard_mono; + if (delta_us > 2 * G_USEC_PER_SEC) + { + fp_dbg ("retry guard expired (delta=%ld ms), clearing", (long) (delta_us / 1000)); + self->retry_guard = FALSE; + } + else + { + fp_dbg ("retry guard active (delta=%ld ms): awaiting finger release", (long) (delta_us / 1000)); + } + } + + goodix5e0a_reset_touch_frames (self); + + self->scan_gen++; + self->scan_ssm = fpi_ssm_new (dev, goodix5e0a_scan_run_state, SCAN_5E0A_NUM_STATES); + fpi_ssm_start (self->scan_ssm, goodix5e0a_scan_complete); +} + +static void +goodix5e0a_change_state (FpImageDevice *img_dev, FpiImageDeviceState state) +{ + if (state == FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON) + goodix5e0a_scan_start (FP_DEVICE (img_dev)); +} + +static void +goodix5e0a_deactivate (FpImageDevice *img_dev) +{ + FpDevice *dev = FP_DEVICE (img_dev); + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + gboolean scan_was_active; + g_autoptr(GError) tls_err = NULL; + + goodix5e0a_reset_touch_frames (self); + goodix_activation_gen_bump (dev); + + self->session_started = FALSE; + self->scan_gen++; + if (self->down_timeout) + { + g_source_destroy (self->down_timeout); + self->down_timeout = NULL; + } + + goodix_reset_state (dev); + scan_was_active = (self->scan_ssm != NULL); + if (self->scan_ssm != NULL) + { + fpi_ssm_free (self->scan_ssm); + self->scan_ssm = NULL; + } + + /* Only park when deactivation arrived idle; a scan torn down mid-flight + * can leave dangling replies that poison the next reuse. */ + if (scan_was_active && goodix_tls_is_alive (dev) && self->warm_ok) + fp_dbg ("park invalidated: scan active at deactivate"); + if (goodix_tls_is_alive (dev) && self->warm_ok && !scan_was_active) + { + goodix_stop_read_loop (dev); + self->tls_parked = TRUE; + self->tls_parked_at = g_get_monotonic_time (); + self->tls_parked_gen = goodix_activation_gen_get (dev); + goodix_session_mark_clean (dev); + fp_dbg ("parking live TLS session (gen=%u)", self->tls_parked_gen); + fpi_image_device_deactivate_complete (img_dev, NULL); + return; + } + + self->tls_parked = FALSE; + self->retry_guard = FALSE; + self->retry_guard_mono = 0; + goodix_session_mark_dirty (dev); + goodix_shutdown_tls (dev, &tls_err); + goodix_stop_read_loop (dev); + fpi_image_device_deactivate_complete (img_dev, g_steal_pointer (&tls_err)); +} + +static void +fpi_device_goodixtls5e0a_init (FpiDeviceGoodixTls5e0a *self) +{ + self->session_started = FALSE; + self->scan_ssm = NULL; + self->scan_gen = 0; + self->scan_timeout_gen = 0; + self->down_timeout = NULL; + self->tls_parked = FALSE; + self->tls_parked_at = 0; + self->tls_parked_gen = 0; + self->warm_ok = FALSE; + self->last_clean_mono = 0; + self->warm_boot_seq = 0; + self->warm_down_reason = "cold-start"; + self->warm_attempted = FALSE; + self->warm_retried = FALSE; + self->frame_count = 0; + self->best_img = NULL; + self->best_minutiae = 0; + self->best_frame_no = 0; + self->retry_guard = FALSE; + self->retry_guard_mono = 0; +} + +static double +goodix5e0a_axis_correlation (const GoodixTls5xxPix *pix, + int width, + int height, + int dx, + int dy) +{ + double sum_a = 0.0, sum_b = 0.0; + guint count = 0; + double mean_a, mean_b; + double covariance = 0.0, variance_a = 0.0, variance_b = 0.0; + double denominator; + + for (int y = 0; y + dy < height; y++) + for (int x = 0; x + dx < width; x++) + { + sum_a += pix[y * width + x]; + sum_b += pix[(y + dy) * width + x + dx]; + count++; + } + + if (count == 0) + return 0.0; + + mean_a = sum_a / count; + mean_b = sum_b / count; + + for (int y = 0; y + dy < height; y++) + for (int x = 0; x + dx < width; x++) + { + double a = pix[y * width + x] - mean_a; + double b = pix[(y + dy) * width + x + dx] - mean_b; + covariance += a * b; + variance_a += a * a; + variance_b += b * b; + } + + denominator = sqrt (variance_a * variance_b); + return denominator > 1e-6 ? covariance / denominator : 0.0; +} + +static FpImage * +process_raw_frame (GoodixTls5xxPix * pix) +{ + const int W = GOODIX_5E0A_WIDTH; + const int H = GOODIX_5E0A_HEIGHT; + const int dst_w = GOODIX_5E0A_SCALED_WIDTH; + const int dst_h = GOODIX_5E0A_SCALED_HEIGHT; + + guint16 min_v = 65535, max_v = 0; + guint active = 0; + guint16 range = 0; + double horizontal_corr = 0.0, vertical_corr = 0.0, horizontal_lag4_corr = 0.0; + GString *active_cols = NULL; + g_autofree float *residual = NULL; + float residual_min = G_MAXFLOAT; + float residual_max = -G_MAXFLOAT; + float residual_range = 0.0f; + g_autofree guint8 *normalized = NULL; + FpImage *scaled = NULL; + + for (int r = 0; r < H; ++r) + { + for (int c = 0; c < W; ++c) + { + guint16 v = pix[r * W + c]; + if (v > 30) + { + active++; + if (v < min_v) + min_v = v; + if (v > max_v) + max_v = v; + } + } + } + + if (min_v == 65535) + min_v = 0; + range = (max_v > min_v) ? (max_v - min_v) : 1; + + horizontal_corr = goodix5e0a_axis_correlation (pix, W, H, 1, 0); + vertical_corr = goodix5e0a_axis_correlation (pix, W, H, 0, 1); + horizontal_lag4_corr = goodix5e0a_axis_correlation (pix, W, H, 4, 0); + + active_cols = g_string_new (""); + for (int c = 0; c < W; ++c) + { + guint32 c_sum = 0; + for (int r = 0; r < H; ++r) + c_sum += pix[r * W + c]; + if (c_sum > 0) + g_string_append_printf (active_cols, "%d ", c); + } + if (active_cols->len > 0) + fp_dbg ("active cols: %s", active_cols->str); + else + fp_dbg ("active cols: NONE (all 0)"); + g_string_free (active_cols, TRUE); + + fp_dbg ("frame stats: active=%u, min_v=%u, max_v=%u, range=%u, h_corr=%.3f, v_corr=%.3f, h_lag4_corr=%.3f (native %dx%d WxH)", + active, min_v, max_v, range, + horizontal_corr, vertical_corr, horizontal_lag4_corr, W, H); + + if (active < 64 || range < 8) + return NULL; + + /* Remove the slowly varying pressure/offset field before global scaling. + * A 3x3 local mean is the smallest window that removes this field without + * averaging across a full ridge period. */ + residual = g_new (float, GOODIX_5E0A_FRAME_SIZE); + for (int y = 0; y < H; y++) + { + for (int x = 0; x < W; x++) + { + guint32 local_sum = 0; + guint local_count = 0; + float value; + for (int yy = MAX (0, y - 1); yy <= MIN (H - 1, y + 1); yy++) + for (int xx = MAX (0, x - 1); xx <= MIN (W - 1, x + 1); xx++) + { + local_sum += pix[yy * W + xx]; + local_count++; + } + + value = pix[y * W + x] - (float) local_sum / local_count; + residual[y * W + x] = value; + residual_min = MIN (residual_min, value); + residual_max = MAX (residual_max, value); + } + } + + residual_range = residual_max - residual_min; + fp_dbg ("local contrast: min=%.2f max=%.2f range=%.2f window=3x3 gain=%.2f", + residual_min, residual_max, residual_range, GOODIX_5E0A_CONTRAST_GAIN); + if (residual_range < 1.0f) + return NULL; + + normalized = g_new (guint8, GOODIX_5E0A_FRAME_SIZE); + for (guint i = 0; i < GOODIX_5E0A_FRAME_SIZE; i++) + { + int value = (int) roundf (128.0f + residual[i] * GOODIX_5E0A_CONTRAST_GAIN); + normalized[i] = (guint8) CLAMP (value, 0, 255); + } + + /* Create the scaled 128x160 image directly via bilinear upscaling. + * Use FPI_IMAGE_COLORS_INVERTED for capacitive ridges (high ADC = black). + * Omit FPI_IMAGE_PARTIAL so remove_perimeter_pts=0 retains edge minutiae. */ + scaled = fp_image_new (dst_w, dst_h); + scaled->flags = FPI_IMAGE_COLORS_INVERTED; + scaled->ppmm = 500.0 / 25.4; + + for (int y = 0; y < dst_h; y++) + { + float src_y = (y + 0.5f) * 0.5f - 0.5f; + int y0, y1; + float y_frac; + + if (src_y < 0.0f) + src_y = 0.0f; + y0 = (int) src_y; + y1 = (y0 + 1 < H) ? y0 + 1 : y0; + y_frac = src_y - (float) y0; + + for (int x = 0; x < dst_w; x++) + { + float src_x = (x + 0.5f) * 0.5f - 0.5f; + int x0, x1; + float x_frac, top, bot, val; + int norm; + + if (src_x < 0.0f) + src_x = 0.0f; + x0 = (int) src_x; + x1 = (x0 + 1 < W) ? x0 + 1 : x0; + x_frac = src_x - (float) x0; + + top = (float) normalized[y0 * W + x0] * (1.0f - x_frac) + (float) normalized[y0 * W + x1] * x_frac; + bot = (float) normalized[y1 * W + x0] * (1.0f - x_frac) + (float) normalized[y1 * W + x1] * x_frac; + val = top * (1.0f - y_frac) + bot * y_frac; + norm = (int) roundf (val); + scaled->data[y * dst_w + x] = (guint8) CLAMP (norm, 0, 255); + } + } + + fp_dbg ("scaled image: %dx%d (WxH) flags=0x%02x active=%u range=%u ppmm=%.3f", + scaled->width, scaled->height, scaled->flags, active, range, scaled->ppmm); + return scaled; +} + +static guint +goodix5e0a_count_minutiae (FpImage *img) +{ + int w, h; + unsigned char *buf; + LFSPARMS parms = g_lfsparms_V2; + double ppmm; + MINUTIAE *minutiae = NULL; + int *qmap = NULL, *dmap = NULL, *lcmap = NULL, *lfmap = NULL, *hcmap = NULL; + int mw, mh, bw, bh, bd; + unsigned char *bdata = NULL; + int ret; + guint count; + + if (!img || !img->data) + return 0; + + w = img->width; + h = img->height; + buf = g_memdup2 (img->data, w * h); + + if (img->flags & FPI_IMAGE_COLORS_INVERTED) + for (int i = 0; i < w * h; i++) + buf[i] = 255 - buf[i]; + + parms.remove_perimeter_pts = 0; + ppmm = img->ppmm > 0 ? img->ppmm : (500.0 / 25.4); + + ret = get_minutiae (&minutiae, &qmap, &dmap, &lcmap, &lfmap, &hcmap, + &mw, &mh, &bdata, &bw, &bh, &bd, + buf, w, h, 8, ppmm, &parms); + count = (ret == 0 && minutiae) ? minutiae->num : 0; + + g_free (buf); + if (minutiae) + free_minutiae (minutiae); + if (qmap) + g_free (qmap); + if (dmap) + g_free (dmap); + if (lcmap) + g_free (lcmap); + if (lfmap) + g_free (lfmap); + if (hcmap) + g_free (hcmap); + if (bdata) + g_free (bdata); + + return count; +} + +static void +goodix5e0a_suspend (FpDevice *dev) +{ + FpiDeviceGoodixTls5e0a *self = FPI_DEVICE_GOODIXTLS5E0A (dev); + FpiDeviceAction action = fpi_device_get_current_action (dev); + g_autoptr(GError) tls_err = NULL; + + fp_dbg ("suspend requested during action: %d", action); + + /* Orphan any in-flight TLS handshake/activation; its completion will drop. */ + goodix_activation_gen_bump (dev); + + self->tls_parked = FALSE; + goodix_session_mark_dirty (dev); + self->warm_ok = FALSE; + self->warm_down_reason = "suspended"; + self->warm_attempted = FALSE; + goodix5e0a_reset_touch_frames (self); + self->retry_guard = FALSE; + self->retry_guard_mono = 0; + self->session_started = FALSE; + self->scan_gen++; + if (self->down_timeout) + { + g_source_destroy (self->down_timeout); + self->down_timeout = NULL; + } + + /* Reset in-flight protocol commands and timeout */ + goodix_reset_state (dev); + + /* Free in-flight scan state machine */ + if (self->scan_ssm != NULL) + { + fpi_ssm_free (self->scan_ssm); + self->scan_ssm = NULL; + } + + /* Terminate background read loop and cancel transfers */ + goodix_stop_read_loop (dev); + + /* Tear down TLS context. Synchronous (joins the serve thread); log but + * do not propagate failures so the NOT_SUPPORTED suspend completion below + * still triggers clean core deactivation. */ + if (!goodix_shutdown_tls (dev, &tls_err)) + { + fp_warn ("suspend: TLS shutdown failed: %s", + tls_err ? tls_err->message : "unknown error"); + g_clear_error (&tls_err); + } + + /* Complete suspend with NOT_SUPPORTED to trigger clean core deactivation + * of the active task before sleep. */ + fpi_device_suspend_complete (dev, fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED)); +} + +static void +goodix5e0a_resume (FpDevice *dev) +{ + fp_dbg ("resume requested"); + + /* Device state was cleaned up during suspend; complete resume immediately. + * Subsequent user claims will trigger clean open/activate and hardware re-priming. */ + fpi_device_resume_complete (dev, NULL); +} + +static void +fpi_device_goodixtls5e0a_class_init (FpiDeviceGoodixTls5e0aClass * class) +{ + FpiDeviceGoodixTlsClass * gx_class = FPI_DEVICE_GOODIXTLS_CLASS (class); + FpDeviceClass * dev_class = FP_DEVICE_CLASS (class); + FpImageDeviceClass * img_dev_class = FP_IMAGE_DEVICE_CLASS (class); + FpiDeviceGoodixTls5xxClass * xx_cls = FPI_DEVICE_GOODIXTLS5XX_CLASS (class); + + xx_cls->process_raw_frame = process_raw_frame; + xx_cls->scan_height = GOODIX_5E0A_HEIGHT; + xx_cls->scan_width = GOODIX_5E0A_WIDTH; + xx_cls->psk = goodix_5e0a_psk; + xx_cls->psk_flags = GOODIX_5E0A_PSK_FLAGS; + xx_cls->psk_len = sizeof (goodix_5e0a_psk); + xx_cls->firmware_version = GOODIX_5E0A_FIRMWARE_VERSION; + xx_cls->reset_number = GOODIX_5E0A_RESET_NUMBER; + xx_cls->has_calibration = FALSE; + + gx_class->interface = GOODIX_5E0A_INTERFACE; + gx_class->ep_in = GOODIX_5E0A_EP_IN; + gx_class->ep_out = GOODIX_5E0A_EP_OUT; + gx_class->capture_payload = goodix_5e0a_img_payload; + gx_class->capture_payload_len = sizeof (goodix_5e0a_img_payload); + + dev_class->id = "goodixtls5e0a"; + dev_class->full_name = "Goodix TLS Fingerprint Sensor 5e0a"; + dev_class->type = FP_DEVICE_TYPE_USB; + dev_class->id_table = goodix_5e0a_id_table; + dev_class->nr_enroll_stages = 5; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + dev_class->temp_hot_seconds = -1; /* Disable thermal watchdog */ + dev_class->suspend = goodix5e0a_suspend; + dev_class->resume = goodix5e0a_resume; + + img_dev_class->activate = dev_activate; + img_dev_class->change_state = goodix5e0a_change_state; + img_dev_class->deactivate = goodix5e0a_deactivate; + img_dev_class->bz3_threshold = 14; + img_dev_class->img_width = GOODIX_5E0A_SCALED_WIDTH; + img_dev_class->img_height = GOODIX_5E0A_SCALED_HEIGHT; + + fpi_device_class_auto_initialize_features (dev_class); +} diff --git a/libfprint/drivers/goodixtls/goodix5e0a.h b/libfprint/drivers/goodixtls/goodix5e0a.h new file mode 100644 index 000000000..b9eebd0fc --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix5e0a.h @@ -0,0 +1,56 @@ +/* + * Goodix driver for USB devices 27c6:5e0a + * + * Copyright (C) 2026 The libfprint Goodix 5e0a contributors + * + * 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 + +#define GOODIX_5E0A_INTERFACE (0) +#define GOODIX_5E0A_EP_IN (0x3 | FPI_USB_ENDPOINT_IN) +#define GOODIX_5E0A_EP_OUT (0x1 | FPI_USB_ENDPOINT_OUT) + +#define GOODIX_5E0A_FIRMWARE_VERSION ("GFUSB_GM168SEC_APP_10036") +#define GOODIX_5E0A_PSK_FLAGS (0xbb020001) +#define GOODIX_5E0A_RESET_NUMBER (2048) + +#define GOODIX_5E0A_WIDTH (64) +#define GOODIX_5E0A_HEIGHT (80) +#define GOODIX_5E0A_SCALED_WIDTH (128) +#define GOODIX_5E0A_SCALED_HEIGHT (160) +#define GOODIX_5E0A_SCAN_WIDTH (64) +#define GOODIX_5E0A_SCAN_HEIGHT (80) +#define GOODIX_5E0A_FRAME_SIZE (GOODIX_5E0A_WIDTH * GOODIX_5E0A_HEIGHT) +#define GOODIX_5E0A_FRAME_BLOCKS (80) +#define GOODIX_5E0A_BLOCK_BYTES (132) +#define GOODIX_5E0A_BLOCK_ACTIVE_BYTES (96) +#define GOODIX_5E0A_ACT_BYTES (GOODIX_5E0A_FRAME_BLOCKS * GOODIX_5E0A_BLOCK_ACTIVE_BYTES) /* 7680 */ +#define GOODIX_5E0A_FRAME_WIRE_BYTES (GOODIX_5E0A_FRAME_BLOCKS * GOODIX_5E0A_BLOCK_BYTES + 4) /* 10564 */ + +#define GOODIX_5E0A_CONTRAST_GAIN (1.0f) +#define GOODIX_5E0A_ENROLL_MIN_MINUTIAE (12) +#define GOODIX_5E0A_FRAMES_PER_TOUCH 3 + + +/* Sensor Analog Front-End (AFE) Gain/Exposure Register Configuration */ +#define GOODIX_5E0A_REG_GAIN_EXPOSURE (0x022c) +#define GOODIX_5E0A_REG_GAIN_EXPOSURE_VAL (0x0305) /* Little-endian 16-bit: \x05\x03 */ +#define GOODIX_5E0A_REG_GAIN_EXPOSURE_CALIB_VAL (0x030a) /* Little-endian 16-bit: \x0a\x03 */ +#define GOODIX_5E0A_REG_GAIN_EXPOSURE_RESET_VAL (0x020a) /* Little-endian 16-bit: \x0a\x02 */ + + +/* Wire tables and PSK live in goodix5e0a.c as file-static consts. */ diff --git a/libfprint/drivers/goodixtls/goodix5xx.c b/libfprint/drivers/goodixtls/goodix5xx.c new file mode 100644 index 000000000..65fc35546 --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix5xx.c @@ -0,0 +1,690 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 "fp-image-device.h" +#include "fpi-image-device.h" +#include "fpi-ssm.h" +#define FP_COMPONENT "goodixtls5xx" + +#include "drivers/goodixtls/goodix5xx.h" +#include "drivers_api.h" +#include "goodix.h" +#include +#include +#include + + +typedef struct +{ + GoodixTls5xxPix * calibration_img; +} FpiDeviceGoodixTls5xxPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (FpiDeviceGoodixTls5xx, fpi_device_goodixtls5xx, FPI_TYPE_DEVICE_GOODIXTLS) + +enum CALIBRATION_STAGES { + CALIBRATION_STAGE_FDT_UP, + CALIBRATION_STAGE_NAV0, + CALIBRATION_STAGE_GET_IMG, + + CALIBRATION_STAGE_NUM, + +}; + +enum SCAN_STAGES { + SCAN_STAGE_QUERY_MCU, + SCAN_STAGE_SWITCH_TO_FDT_MODE, + SCAN_STAGE_CALIBRATE, + SCAN_STAGE_SWITCH_TO_FDT_DOWN_ARM, + SCAN_STAGE_SWITCH_TO_FDT_DOWN, + SCAN_STAGE_GET_IMG, + SCAN_STAGE_SWITCH_TO_FTD_UP, + SCAN_STAGE_SWITCH_TO_FTD_DONE, + + SCAN_STAGE_NUM, +}; + + +/* Base class provides no default MCU config; 5e0a overrides change_state + * so this guard only covers direct base-class scans. */ +static gboolean +get_mcu_cfg_or_skip (FpDevice *dev, FpiSsm *ssm, GoodixTls5xxMcuConfig *cfg) +{ + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + if (!cls->get_mcu_cfg) + { + fpi_ssm_next_state (ssm); + return FALSE; + } + *cfg = cls->get_mcu_cfg (); + return TRUE; +} + +static void +send_switch_mode (FpDevice * dev, FpiSsm * ssm, void (*mode_switch)(FpDevice *, + const guint8 *, + guint16, + GDestroyNotify, + GoodixDefaultCallback, + gpointer)) +{ + GoodixTls5xxMcuConfig cfg; + if (!get_mcu_cfg_or_skip (dev, ssm, &cfg)) + return; + + mode_switch (dev, cfg.data, cfg.data_len, cfg.free_fn, goodixtls5xx_check_none_cmd, ssm); +} +static void +on_calibrate_scan (FpDevice * dev, guint8 * data, guint16 len, gpointer ssm, GError * err) +{ + FpiDeviceGoodixTls5xx * self = FPI_DEVICE_GOODIXTLS5XX (dev); + FpiDeviceGoodixTls5xxPrivate * priv = fpi_device_goodixtls5xx_get_instance_private (self); + FpiDeviceGoodixTls5xxClass * cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (self); + + if (err) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + if (!priv->calibration_img) + priv->calibration_img = g_try_new0 (GoodixTls5xxPix, cls->scan_height * cls->scan_width); + if (!priv->calibration_img) + { + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + goodixtls5xx_decode_frame (priv->calibration_img, cls->scan_height * cls->scan_width, len, data); + + fpi_ssm_next_state (ssm); +} +static void +calibrate_run (FpiSsm * ssm, FpDevice * dev) +{ + switch (fpi_ssm_get_cur_state (ssm)) + { + case CALIBRATION_STAGE_FDT_UP: + { + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + if (cls->get_fdt_up_cfg) + { + GoodixTls5xxMcuConfig cfg = cls->get_fdt_up_cfg (); + goodix_send_mcu_switch_to_fdt_up (dev, cfg.data, cfg.data_len, cfg.free_fn, goodixtls5xx_check_none_cmd, ssm); + } + else + { + send_switch_mode (dev, ssm, goodix_send_mcu_switch_to_fdt_up); + } + } + break; + + case CALIBRATION_STAGE_NAV0: + goodix_send_nav_0 (dev, goodixtls5xx_check_none_cmd, ssm); + break; + + case CALIBRATION_STAGE_GET_IMG: + goodix_tls_read_image (dev, on_calibrate_scan, ssm); + } +} + +static void +do_calibration (FpDevice * dev, FpiSsm * parent) +{ + fpi_ssm_start_subsm (parent, fpi_ssm_new (dev, calibrate_run, CALIBRATION_STAGE_NUM)); +} + + +void +goodixtls5xx_check_none (FpDevice *dev, gpointer user_data, GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (user_data, error); + return; + } + + fpi_ssm_next_state (user_data); +} + +void +goodixtls5xx_check_none_cmd (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + if (err) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + fpi_ssm_next_state (ssm); +} + +void +goodixtls5xx_check_firmware_version (FpDevice *dev, gchar *firmware, + gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls5xxClass * cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (FPI_DEVICE_GOODIXTLS5XX (dev)); + + if (error) + { + fpi_ssm_mark_failed (user_data, error); + return; + } + + fp_dbg ("Device firmware: \"%s\"", firmware); + + if (strcmp (firmware, cls->firmware_version)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid device firmware: \"%s\"", firmware); + fpi_ssm_mark_failed (user_data, error); + return; + } + + fpi_ssm_next_state (user_data); +} + + +void +goodixtls5xx_check_preset_psk_read (FpDevice *dev, gboolean success, + guint32 flags, guint8 *psk, guint16 length, + gpointer user_data, GError *error) +{ + g_autofree gchar *psk_str = data_to_str (psk, length); + FpiDeviceGoodixTls5xxClass * cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + + if (error) + { + fpi_ssm_mark_failed (user_data, error); + return; + } + + if (!success) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to read PSK from device"); + fpi_ssm_mark_failed (user_data, error); + return; + } + + fp_dbg ("Device PSK: 0x%s", psk_str); + fp_dbg ("Device PSK flags: 0x%08x", flags); + + if (flags != cls->psk_flags) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid device PSK flags: 0x%08x", flags); + fpi_ssm_mark_failed (user_data, error); + return; + } + + if (length != cls->psk_len) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid device PSK: 0x%s", psk_str); + fpi_ssm_mark_failed (user_data, error); + return; + } + + if (memcmp (psk, cls->psk, cls->psk_len)) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid device PSK: 0x%s", psk_str); + fpi_ssm_mark_failed (user_data, error); + return; + } + + fpi_ssm_next_state (user_data); +} + +void +goodixtls5xx_check_idle (FpDevice *dev, gpointer user_data, GError *err) +{ + + if (err) + { + fpi_ssm_mark_failed (user_data, err); + return; + } + fpi_ssm_next_state (user_data); +} +void +goodixtls5xx_check_config_upload (FpDevice *dev, gboolean success, + gpointer user_data, GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (user_data, error); + } + else if (!success) + { + fpi_ssm_mark_failed (user_data, + g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO, + "failed to upload mcu config")); + } + else + { + fpi_ssm_next_state (user_data); + } +} +void +goodixtls5xx_check_reset (FpDevice *dev, gboolean success, guint16 number, + gpointer user_data, GError *error) +{ + FpiDeviceGoodixTls5xxClass * cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + + if (error) + { + fpi_ssm_mark_failed (user_data, error); + return; + } + + if (!success) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to reset device"); + fpi_ssm_mark_failed (user_data, error); + return; + } + + fp_dbg ("Device reset number: %d", number); + + if (number != cls->reset_number) + { + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid device reset number: %d", number); + fpi_ssm_mark_failed (user_data, error); + return; + } + + fpi_ssm_next_state (user_data); +} + +void +goodixtls5xx_check_powerdown_scan_freq (FpDevice *dev, gboolean success, + gpointer user_data, GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (user_data, error); + } + else if (!success) + { + fpi_ssm_mark_failed (user_data, + g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO, + "failed to set powerdown freq")); + } + else + { + fpi_ssm_next_state (user_data); + } +} + +void +goodixtls5xx_squash_frame_linear (GoodixTls5xxPix *frame, guint8 *squashed, guint16 frame_size) +{ + GoodixTls5xxPix min = 0xffff; + GoodixTls5xxPix max = 0; + + for (int i = 0; i != frame_size; ++i) + { + const GoodixTls5xxPix pix = frame[i]; + if (pix < min) + min = pix; + if (pix > max) + max = pix; + } + + for (int i = 0; i != frame_size; ++i) + { + const GoodixTls5xxPix pix = frame[i]; + if (pix - min == 0 || max - min == 0) + squashed[i] = 0; + else + squashed[i] = (pix - min) * 0xff / (max - min); + } +} +static void +linear_subtract_inplace (GoodixTls5xxPix * src, GoodixTls5xxPix * by, guint16 len) +{ + for (guint16 n = 0; n != len; ++n) + src[n] = (src[n] > by[n]) ? (src[n] - by[n]) : 0; +} + +static void +scan_on_read_img (FpDevice *dev, guint8 *data, guint16 len, + gpointer ssm, GError *err) +{ + FpImageDevice * img_dev = FP_IMAGE_DEVICE (dev); + FpiDeviceGoodixTls5xx * self = FPI_DEVICE_GOODIXTLS5XX (dev); + FpiDeviceGoodixTls5xxPrivate * priv = fpi_device_goodixtls5xx_get_instance_private (self); + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + GoodixTls5xxPix * raw_frame; + FpImage * img = NULL; + + if (err) + { + fpi_ssm_mark_failed (ssm, err); + return; + } + + raw_frame = g_try_new0 (GoodixTls5xxPix, cls->scan_width * cls->scan_height); + if (!raw_frame) + { + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + goodixtls5xx_decode_frame (raw_frame, cls->scan_width * cls->scan_height, len, data); + if (priv->calibration_img) + linear_subtract_inplace (raw_frame, priv->calibration_img, cls->scan_width * cls->scan_height); + + if (cls->process_raw_frame) + { + img = cls->process_raw_frame (raw_frame); + } + else if (cls->process_frame) + { + guint8 * squashed = g_try_malloc0 (cls->scan_height * cls->scan_width); + if (!squashed) + { + g_free (raw_frame); + fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } + goodixtls5xx_squash_frame_linear (raw_frame, squashed, cls->scan_height * cls->scan_width); + img = cls->process_frame (squashed); + g_free (squashed); + } + g_free (raw_frame); + + fpi_image_device_image_captured (img_dev, img); + + fpi_ssm_next_state (ssm); +} + +static void +scan_get_img (FpDevice * dev, FpiSsm * ssm) +{ + goodix_tls_read_image (dev, scan_on_read_img, ssm); +} + +static void +scan_run_state (FpiSsm * ssm, FpDevice * dev) +{ + FpImageDevice *img_dev = FP_IMAGE_DEVICE (dev); + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case SCAN_STAGE_QUERY_MCU: + goodix_send_query_mcu_state (dev, goodixtls5xx_check_none, ssm); + break; + + case SCAN_STAGE_SWITCH_TO_FDT_MODE: + { + GoodixTls5xxMcuConfig cfg; + if (!get_mcu_cfg_or_skip (dev, ssm, &cfg)) + break; + goodix_send_mcu_switch_to_fdt_mode (dev, cfg.data, cfg.data_len, cfg.free_fn, goodixtls5xx_check_none, ssm); + } + break; + + case SCAN_STAGE_CALIBRATE: + { + if (cls->has_calibration) + do_calibration (dev, ssm); + else + fpi_ssm_next_state (ssm); + } + break; + + case SCAN_STAGE_SWITCH_TO_FDT_DOWN_ARM: + fpi_ssm_next_state (ssm); + break; + + case SCAN_STAGE_SWITCH_TO_FDT_DOWN: + { + if (cls->get_fdt_down_cfg) + { + GoodixTls5xxMcuConfig cfg = cls->get_fdt_down_cfg (); + goodix_send_mcu_switch_to_fdt_down (dev, cfg.data, cfg.data_len, cfg.free_fn, goodixtls5xx_check_none_cmd, ssm); + } + else + { + send_switch_mode (dev, ssm, goodix_send_mcu_switch_to_fdt_down); + } + } + break; + + case SCAN_STAGE_GET_IMG: + fpi_image_device_report_finger_status (img_dev, TRUE); + scan_get_img (dev, ssm); + break; + + case SCAN_STAGE_SWITCH_TO_FTD_UP: + { + /* 27-byte merge layout is scoped to the 511-era base-class devices + * (mode header + bytes 10-25 from the FDT-DOWN table). The 5e0a + * bypasses this path entirely via its own scan SSM in goodix5e0a.c + * and never sets get_mcu_cfg/get_fdt_down_cfg, so no virtual + * merge hook is needed unless a future 5xx device conflicts. */ + if (cls->get_mcu_cfg && cls->get_fdt_down_cfg) + { + GoodixTls5xxMcuConfig mode = cls->get_mcu_cfg (); + GoodixTls5xxMcuConfig down = cls->get_fdt_down_cfg (); + if (mode.data && down.data && mode.data_len == 27 && down.data_len >= 27) + { + guint8 post[27]; + memcpy (post, mode.data, sizeof (post)); + memcpy (post + 10, down.data + 10, 16); + post[26] = 0x00; + goodix_send_mcu_switch_to_fdt_mode (dev, post, sizeof (post), NULL, goodixtls5xx_check_none, ssm); + break; + } + } + if (cls->get_fdt_up_cfg) + { + GoodixTls5xxMcuConfig cfg = cls->get_fdt_up_cfg (); + goodix_send_mcu_switch_to_fdt_up (dev, cfg.data, cfg.data_len, cfg.free_fn, goodixtls5xx_check_none_cmd, ssm); + } + else + { + send_switch_mode (dev, ssm, goodix_send_mcu_switch_to_fdt_up); + } + } + break; + + case SCAN_STAGE_SWITCH_TO_FTD_DONE: + fpi_image_device_report_finger_status (img_dev, FALSE); + fpi_ssm_next_state (ssm); + break; + } +} + +static void +scan_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + if (error) + { + fp_err ("failed to scan: %s (code: %d)", error->message, error->code); + fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); + return; + } + fp_dbg ("finished scan"); +} + + +void +goodixtls5xx_scan_start (FpiDeviceGoodixTls5xx * dev) +{ + fpi_ssm_start (fpi_ssm_new (FP_DEVICE (dev), scan_run_state, SCAN_STAGE_NUM), scan_complete); +} + +void +goodixtls5xx_decode_frame (GoodixTls5xxPix * frame, guint32 max_pixels, guint32 frame_size, const guint8 *raw_frame) +{ + GoodixTls5xxPix *pix = frame; + guint32 start = 0; + guint32 end = (frame_size >= 4) ? (frame_size - 4) : frame_size; + guint32 pixel_idx = 0; + + if (!frame || !raw_frame || max_pixels == 0) + return; + + if (frame_size >= 13 && (frame_size - 13) % 6 == 0) + { + start = 8; + end = frame_size - 5; + } + + for (guint32 i = start; i + 6 <= end && pixel_idx + 4 <= max_pixels; i += 6) + { + const guint8 *chunk = raw_frame + i; + pix[pixel_idx++] = ((chunk[0] & 0xf) << 8) + chunk[1]; + pix[pixel_idx++] = (chunk[3] << 4) + (chunk[0] >> 4); + pix[pixel_idx++] = ((chunk[5] & 0xf) << 8) + chunk[2]; + pix[pixel_idx++] = (chunk[4] << 4) + (chunk[5] >> 4); + } +} + +static void +dev_change_state (FpImageDevice * img_dev, FpiImageDeviceState state) +{ + if (state == FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON) + goodixtls5xx_scan_start (FPI_DEVICE_GOODIXTLS5XX (img_dev)); +} +static void +dev_deinit (FpImageDevice * img_dev) +{ + FpDevice *dev = FP_DEVICE (img_dev); + GError *error = NULL; + + if (!goodix_dev_deinit (dev, &error)) + { + fpi_image_device_close_complete (img_dev, error); + return; + } + + fpi_image_device_close_complete (img_dev, NULL); +} +static void +dev_init (FpImageDevice *img_dev) +{ + FpDevice *dev = FP_DEVICE (img_dev); + GError *error = NULL; + + if (!goodix_dev_init (dev, &error)) + { + fpi_image_device_open_complete (img_dev, error); + return; + } + + fpi_image_device_open_complete (img_dev, NULL); +} + +static void +dev_deactivate (FpImageDevice *img_dev) +{ + FpDevice *dev = FP_DEVICE (img_dev); + GError *error = NULL; + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + + /* Orphan any in-flight TLS activation; its completion will drop. */ + goodix_activation_gen_bump (dev); + + goodix_reset_state (dev); + + goodix_shutdown_tls (dev, &error); + + goodixtls5xx_cleanup (FPI_DEVICE_GOODIXTLS5XX (dev)); + + if (cls->reset_state) + cls->reset_state (dev); + goodix_stop_read_loop (dev); + fpi_image_device_deactivate_complete (img_dev, error); +} + +static void +tls_activation_complete (FpDevice *dev, gpointer user_data, + GError *error) +{ + FpImageDevice *image_dev; + + /* Drop orphaned completions without touching hardware. */ + if (GPOINTER_TO_UINT (user_data) != goodix_activation_gen_get (dev)) + { + fp_dbg ("dropping stale TLS activation completion"); + if (error) + g_error_free (error); + return; + } + + image_dev = FP_IMAGE_DEVICE (dev); + + if (error) + { + fp_err ("failed to complete tls activation: %s", error->message); + fpi_image_device_activate_complete (image_dev, error); + return; + } + + fpi_image_device_activate_complete (image_dev, error); +} + +void +goodixtls5xx_init_tls (FpDevice * dev) +{ + /* Capture the activation generation for the staleness guard. */ + goodix_tls_init (dev, tls_activation_complete, + GUINT_TO_POINTER (goodix_activation_gen_get (dev))); +} + +void +fpi_device_goodixtls5xx_class_init (FpiDeviceGoodixTls5xxClass * self) +{ + FpImageDeviceClass *img_cls = FP_IMAGE_DEVICE_CLASS (self); + + self->get_mcu_cfg = NULL; + self->get_fdt_down_cfg = NULL; + self->get_fdt_up_cfg = NULL; + self->process_frame = NULL; + self->scan_height = 0; + self->scan_width = 0; + self->reset_state = NULL; + + img_cls->change_state = dev_change_state; + img_cls->deactivate = dev_deactivate; + img_cls->img_close = dev_deinit; + img_cls->img_open = dev_init; +} + +void +fpi_device_goodixtls5xx_init (FpiDeviceGoodixTls5xx * self) +{ + FpiDeviceGoodixTls5xxPrivate * priv = fpi_device_goodixtls5xx_get_instance_private (self); + + priv->calibration_img = NULL; +} + +void +goodixtls5xx_cleanup (FpiDeviceGoodixTls5xx * dev) +{ + FpiDeviceGoodixTls5xxPrivate * priv = fpi_device_goodixtls5xx_get_instance_private (dev); + + g_free (priv->calibration_img); + priv->calibration_img = NULL; +} \ No newline at end of file diff --git a/libfprint/drivers/goodixtls/goodix5xx.h b/libfprint/drivers/goodixtls/goodix5xx.h new file mode 100644 index 000000000..c99dda803 --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix5xx.h @@ -0,0 +1,251 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 "drivers_api.h" +#include "goodix.h" + +#define FPI_TYPE_DEVICE_GOODIXTLS5XX (fpi_device_goodixtls5xx_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (FpiDeviceGoodixTls5xx, fpi_device_goodixtls5xx, FPI, DEVICE_GOODIXTLS5XX, FpiDeviceGoodixTls); + +/** + * @brief API for the common parts of communication between the goodixtls 5xx device + * @details This API is designed to make it easier to write and maintain + * drivers for the goodixtls 5xx (usb) devices. For an example out goodix511.c. + * + * @par The bare minimum needed is to provide get_mcu_cfg and process_frame to + * FpiDeviceGoodixTls5xxClass and activate to FpImageDeviceClass (activate + * varies from device to device) + * + * @par There are also quite a few helper functions in the goodixtls5xx_* + * namespace, the check functions expect a state machine as the user data and + * will advance it or mark it as failed as appropriate + * @struct FpiDeviceGoodixTls5xx + * + */ + +typedef guint16 GoodixTls5xxPix; + + +typedef struct +{ + guint16 data_len; + void (*free_fn)(void *); + const guint8 * data; +} GoodixTls5xxMcuConfig; + +typedef FpImage *(*GoodixTls5xxProcessFrameFn)(guint8 * frame); +typedef FpImage *(*GoodixTls5xxProcessRawFrameFn)(GoodixTls5xxPix * frame); +typedef GoodixTls5xxMcuConfig (*GoodixTls5xxGetMcuFn)(void); +typedef void (*GoodixTls5xxResetStateFn)(FpDevice *); + +struct _FpiDeviceGoodixTls5xxClass +{ + FpiDeviceGoodixTlsClass parent; + + GoodixTls5xxGetMcuFn get_mcu_cfg; /* Provide MCU config before FDT commands */ + GoodixTls5xxGetMcuFn get_fdt_down_cfg; + GoodixTls5xxGetMcuFn get_fdt_up_cfg; + GoodixTls5xxProcessFrameFn process_frame; /* Process a frame after decoding (e.g. crop) */ + GoodixTls5xxProcessRawFrameFn process_raw_frame; /* Process raw 12-bit ADC frame directly */ + GoodixTls5xxResetStateFn reset_state; /* Callback to reset state, may be NULL */ + + guint16 scan_width; /* Width of raw scanner image */ + guint16 scan_height; /* Height of raw scanner image */ + + const char * firmware_version; /* For goodixtls5xx_check_firmware_version() */ + + /* For goodixtls5xx_check_preset_psk_read() */ + int psk_flags; + guint16 psk_len; + const guint8 * psk; + + int reset_number; /* For goodixtls5xx_check_reset() */ + gboolean has_calibration; /* TRUE if device requires calibration step (e.g. 511) */ +}; + +/** + * @brief Check the reply to a reset command + * + * @param dev + * @param success + * @param number + * @param user_data + * @param error + */ +void goodixtls5xx_check_reset (FpDevice *dev, + gboolean success, + guint16 number, + gpointer user_data, + GError *error); + +/** + * @brief Check the reply to a firmware version query matches configured firmware + * @note Requires firmware_version field to be set + * + * @param dev + * @param firmware + * @param user_data + * @param error + */ +void goodixtls5xx_check_firmware_version (FpDevice *dev, + gchar *firmware, + gpointer user_data, + GError *error); + + +/** + * @brief Check the reply to a preset psk query matched configured psk + * @note Requires psk_flags, psk_len, and psk fields to be set + * + * @param dev + * @param success + * @param flags + * @param psk + * @param length + * @param user_data + * @param error + */ +void goodixtls5xx_check_preset_psk_read (FpDevice *dev, + gboolean success, + guint32 flags, + guint8 *psk, + guint16 length, + gpointer user_data, + GError *error); + +/** + * @brief Check the reply to an idle command + * + * @param dev + * @param user_data + * @param err + */ +void goodixtls5xx_check_idle (FpDevice *dev, + gpointer user_data, + GError *err); + +/** + * @brief Check the reply to uploading an mcu config + * + * @param dev + * @param success + * @param user_data + * @param error + */ +void goodixtls5xx_check_config_upload (FpDevice *dev, + gboolean success, + gpointer user_data, + GError *error); + +/** + * @brief Check the reply to a powerdown_scan_freq command + * + * @param dev + * @param success + * @param user_data + * @param error + */ +void goodixtls5xx_check_powerdown_scan_freq (FpDevice *dev, + gboolean success, + gpointer user_data, + GError *error); + +/** + * @brief General check for GoodixNoneCallback replies + * + * @param dev + * @param user_data state machine to advance + * @param error + */ +void goodixtls5xx_check_none (FpDevice *dev, + gpointer user_data, + GError *error); + +/** + * @brief General callback for GoodixDefaultCallback replies + * + * @param dev + * @param data + * @param len + * @param ssm State machine to advance (userdata) + * @param err + */ +void goodixtls5xx_check_none_cmd (FpDevice *dev, + guint8 *data, + guint16 len, + gpointer ssm, + GError *err); + +/** + * @brief Start a scan + * @note This is called automatically for you unless you overwrote change_state in FpImageDeviceClass + * + * @param dev + */ +void goodixtls5xx_scan_start (FpiDeviceGoodixTls5xx * dev); + +/** + * @brief Decode a goodixtls frame + * @details Decodes the weird 4/6 byte packing: https://blog.th0m.as/misc/fingerprint-reversing/ + * @note Doesn't decrypt it + * + * @param frame + * @param max_pixels + * @param frame_size + * @param raw_frame + */ +void goodixtls5xx_decode_frame (GoodixTls5xxPix * frame, + guint32 max_pixels, + guint32 frame_size, + const guint8 *raw_frame); + + +/** + * @brief Initalise the TLS for the device + * @note You probably want to call this directly after device activation + * + * @param dev + */ +void goodixtls5xx_init_tls (FpDevice * dev); + +/** + * @brief Squashes the 2 byte pixels of a raw frame into the 1 byte pixels used + * by libfprint. + * @details Borrowed from the elan driver. We reduce frames to + * within the max and min. + * + * @param frame + * @param squashed + */ +void goodixtls5xx_squash_frame_linear (GoodixTls5xxPix *frame, + guint8 *squashed, + guint16 frame_size); + +/** + * @brief Cleans up the state after activation. If you replaced the deactivate callback + * then you will need to call this, otherwise don't worry its done for you + * + * @param dev device to cleanup the state for + */ +void goodixtls5xx_cleanup (FpiDeviceGoodixTls5xx * dev); \ No newline at end of file diff --git a/libfprint/drivers/goodixtls/goodix_proto.c b/libfprint/drivers/goodixtls/goodix_proto.c new file mode 100644 index 000000000..1cc2f945a --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix_proto.c @@ -0,0 +1,163 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 +#include + +#include "fpi-compat.h" +#include "goodix_proto.h" + +static guint8 +goodix_calc_checksum (guint8 *data, guint16 length) +{ + guint8 checksum = 0; + + for (guint16 i = 0; i < length; i++) + checksum += data[i]; + + return checksum; +} + +void +goodix_encode_pack (guint8 flags, guint8 *payload, guint16 payload_len, + gboolean pad_data, guint8 **data, guint32 *data_len) +{ + GoodixPack *pack; + + *data_len = sizeof (GoodixPack) + sizeof (guint8) + payload_len; + + if (pad_data && *data_len % GOODIX_EP_OUT_MAX_BUF_SIZE) + *data_len += + GOODIX_EP_OUT_MAX_BUF_SIZE - *data_len % GOODIX_EP_OUT_MAX_BUF_SIZE; + + *data = g_malloc0 (*data_len); + pack = (GoodixPack *) *data; + + pack->flags = flags; + pack->length = GUINT16_TO_LE (payload_len); + (*data)[sizeof (GoodixPack)] = goodix_calc_checksum (*data, sizeof (GoodixPack)); + + memcpy (*data + sizeof (GoodixPack) + sizeof (guint8), payload, payload_len); +} + +void +goodix_encode_protocol (guint8 cmd, const guint8 *payload, guint16 payload_len, + gboolean calc_checksum, gboolean pad_data, + guint8 **data, guint32 *data_len) +{ + GoodixProtocol *protocol; + + *data_len = sizeof (GoodixProtocol) + payload_len + sizeof (guint8); + + if (pad_data && *data_len % GOODIX_EP_OUT_MAX_BUF_SIZE) + *data_len += + GOODIX_EP_OUT_MAX_BUF_SIZE - *data_len % GOODIX_EP_OUT_MAX_BUF_SIZE; + + *data = g_malloc0 (*data_len); + protocol = (GoodixProtocol *) *data; + + protocol->cmd = cmd; + protocol->length = GUINT16_TO_LE (payload_len + sizeof (guint8)); + + memcpy (*data + sizeof (GoodixProtocol), payload, payload_len); + + if (calc_checksum) + (*data)[sizeof (GoodixProtocol) + payload_len] = + 0xaa - + goodix_calc_checksum (*data, sizeof (GoodixProtocol) + payload_len); + else + (*data)[sizeof (GoodixProtocol) + payload_len] = GOODIX_NULL_CHECKSUM; +} + +gboolean +goodix_decode_pack (guint8 *data, guint32 data_len, guint8 *flags, + guint8 **payload, guint16 *payload_len, + gboolean *valid_checksum) +{ + guint16 length; + guint16 wire_len; + + if (data == NULL || flags == NULL || payload == NULL || + payload_len == NULL || valid_checksum == NULL) + return FALSE; + + if (data_len < sizeof (GoodixPack) + sizeof (guint8)) + return FALSE; + + memcpy (&wire_len, data + sizeof (guint8), sizeof (wire_len)); + length = GUINT16_FROM_LE (wire_len); + + if (data_len < (guint32) length + sizeof (GoodixPack) + sizeof (guint8)) + return FALSE; + + *flags = data[0]; + if (length > 0) + *payload = g_memdup2 (data + sizeof (GoodixPack) + sizeof (guint8), length); + else + *payload = NULL; + *payload_len = length; + *valid_checksum = goodix_calc_checksum (data, sizeof (GoodixPack)) == + data[sizeof (GoodixPack)]; + + return TRUE; +} + +gboolean +goodix_decode_protocol (guint8 *data, guint32 data_len, guint8 *cmd, + guint8 **payload, guint16 *payload_len, + gboolean *valid_checksum, + gboolean *valid_null_checksum) +{ + guint16 wire_len, length; + + if (data == NULL || cmd == NULL || payload == NULL || + payload_len == NULL || valid_checksum == NULL || + valid_null_checksum == NULL) + return FALSE; + + if (data_len < sizeof (GoodixProtocol) + sizeof (guint8)) + return FALSE; + + memcpy (&wire_len, data + sizeof (guint8), sizeof (wire_len)); + wire_len = GUINT16_FROM_LE (wire_len); + + /* Wire length includes the trailing checksum byte. */ + if (wire_len < sizeof (guint8)) + return FALSE; + length = wire_len - sizeof (guint8); + + if (data_len < (guint32) length + sizeof (GoodixProtocol) + sizeof (guint8)) + return FALSE; + + *cmd = data[0]; + if (length > 0) + *payload = g_memdup2 (data + sizeof (GoodixProtocol), length); + else + *payload = NULL; + *payload_len = length; + *valid_checksum = + 0xaa - goodix_calc_checksum (data, sizeof (GoodixProtocol) + length) == + data[sizeof (GoodixProtocol) + length]; + *valid_null_checksum = + GOODIX_NULL_CHECKSUM == data[sizeof (GoodixProtocol) + length]; + + return TRUE; +} diff --git a/libfprint/drivers/goodixtls/goodix_proto.h b/libfprint/drivers/goodixtls/goodix_proto.h new file mode 100644 index 000000000..965eae8d8 --- /dev/null +++ b/libfprint/drivers/goodixtls/goodix_proto.h @@ -0,0 +1,177 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 + +#define GOODIX_EP_IN_MAX_BUF_SIZE (0x10000) +#define GOODIX_EP_OUT_MAX_BUF_SIZE (0x40) + +#define GOODIX_NULL_CHECKSUM (0x88) + +#define GOODIX_FLAGS_MSG_PROTOCOL (0xa0) +#define GOODIX_FLAGS_TLS (0xb0) +#define GOODIX_FLAGS_TLS_DATA (0xb2) + +#define GOODIX_CMD_NOP (0x00) +#define GOODIX_CMD_MCU_GET_IMAGE (0x20) +#define GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN (0x32) +#define GOODIX_CMD_MCU_SWITCH_TO_FDT_UP (0x34) +#define GOODIX_CMD_MCU_SWITCH_TO_FDT_MODE (0x36) +#define GOODIX_CMD_NAV_0 (0x50) +#define GOODIX_CMD_MCU_SWITCH_TO_IDLE_MODE (0x70) +#define GOODIX_CMD_WRITE_SENSOR_REGISTER (0x80) +#define GOODIX_CMD_READ_SENSOR_REGISTER (0x82) +#define GOODIX_CMD_UPLOAD_CONFIG_MCU (0x90) +#define GOODIX_CMD_SET_POWERDOWN_SCAN_FREQUENCY (0x94) +#define GOODIX_CMD_ENABLE_CHIP (0x96) +#define GOODIX_CMD_RESET (0xa2) +#define GOODIX_CMD_READ_OTP (0xa6) +#define GOODIX_CMD_FIRMWARE_VERSION (0xa8) +#define GOODIX_CMD_SET_POV_CONFIG (0xac) +#define GOODIX_CMD_QUERY_MCU_STATE (0xae) +#define GOODIX_CMD_ACK (0xb0) +#define GOODIX_CMD_SET_DRV_STATE (0xc4) +#define GOODIX_CMD_REQUEST_TLS_CONNECTION (0xd0) +#define GOODIX_CMD_MCU_GET_POV_IMAGE (0xd2) +#define GOODIX_CMD_TLS_SUCCESSFULLY_ESTABLISHED (0xd4) +#define GOODIX_CMD_PRESET_PSK_WRITE (0xe0) +#define GOODIX_CMD_PRESET_PSK_READ (0xe4) + +typedef struct __attribute__((__packed__)) _GoodixPack +{ + guint8 flags; + guint16 length; +} GoodixPack; + +typedef struct __attribute__((__packed__)) _GoodixProtocol +{ + guint8 cmd; + guint16 length; +} GoodixProtocol; + +typedef struct __attribute__((__packed__)) _GoodixAck +{ + guint8 cmd; + guint8 flags; +} GoodixAck; + +G_STATIC_ASSERT (sizeof (GoodixPack) == 3); +G_STATIC_ASSERT (sizeof (GoodixProtocol) == 3); +G_STATIC_ASSERT (sizeof (GoodixAck) == 2); + +typedef struct __attribute__((__packed__)) _GoodixNop +{ + guint32 unknown; +} GoodixNop; + +typedef struct __attribute__((__packed__)) _GoodixMcuSwitchToIdleMode +{ + guint8 sleep_time; + guint8 : 8; +} GoodixMcuSwitchToIdleMode; + +typedef struct __attribute__((__packed__)) _GoodixWriteSensorRegister +{ + guint8 multiples; + guint16 address; + guint16 value; +} GoodixWriteSensorRegister; + +typedef struct __attribute__((__packed__)) _GoodixReadSensorRegister +{ + guint8 multiples; + guint16 address; + guint8 length; + guint8 : 8; +} GoodixReadSensorRegister; + +typedef struct __attribute__((__packed__)) _GoodixSetPowerdownScanFrequency +{ + guint16 powerdown_scan_frequency; +} GoodixSetPowerdownScanFrequency; + +typedef struct __attribute__((__packed__)) _GoodixEnableChip +{ + guint8 enable; + guint8 : 8; +} GoodixEnableChip; + +typedef struct __attribute__((__packed__)) _GoodixReset +{ + guint8 reset_sensor : 1; + guint8 soft_reset_mcu : 1; + guint8 : 6; + guint8 sleep_time; +} GoodixReset; + +typedef struct __attribute__((__packed__)) _GoodixQueryMcuState +{ + guint8 unused_flags; +} GoodixQueryMcuState; + +typedef struct __attribute__((__packed__)) _GoodixPresetPsk +{ + guint32 flags; + guint32 length; +} GoodixPresetPsk; + +typedef struct __attribute__((__packed__)) _GoodixDefault +{ + guint8 unused_flags; + guint8 : 8; +} GoodixDefault; + +typedef struct __attribute__((__packed__)) _GoodixNone +{ + guint16 : 16; +} GoodixNone; + +void goodix_encode_pack (guint8 flags, + guint8 *payload, + guint16 payload_len, + gboolean pad_data, + guint8 **data, + guint32 *data_len); + +void goodix_encode_protocol (guint8 cmd, + const guint8 *payload, + guint16 payload_len, + gboolean calc_checksum, + gboolean pad_data, + guint8 **data, + guint32 *data_len); + +gboolean goodix_decode_pack (guint8 *data, + guint32 data_len, + guint8 *flags, + guint8 **payload, + guint16 *payload_len, + gboolean *valid_checksum); + +gboolean goodix_decode_protocol (guint8 *data, + guint32 data_len, + guint8 *cmd, + guint8 **payload, + guint16 *payload_len, + gboolean *valid_checksum, + gboolean *valid_null_checksum); diff --git a/libfprint/drivers/goodixtls/goodixtls.c b/libfprint/drivers/goodixtls/goodixtls.c new file mode 100644 index 000000000..b6939349b --- /dev/null +++ b/libfprint/drivers/goodixtls/goodixtls.c @@ -0,0 +1,364 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "drivers_api.h" +#include "fp-device.h" +#include "fpi-device.h" +#include "goodix.h" +#include "goodix5xx.h" +#include "goodixtls.h" + +#ifndef fpi_device_emulation_mode_enabled +#define fpi_device_emulation_mode_enabled(dev) (g_getenv ("FP_DEVICE_EMULATION") != NULL) +#endif + +static GError * +err_from_ssl (void) +{ + unsigned long code = ERR_get_error (); + const char *msg = code ? ERR_reason_error_string (code) : NULL; + + if (code == 0) + return g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL, + "TLS connection closed by peer"); + return g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL, + "SSL error (0x%lx): %s", code, msg ? msg : "unknown SSL error"); +} + +#define GOODIX_TLS_CIPHERS "PSK-AES128-CBC-SHA256:@SECLEVEL=1" + +static unsigned int +tls_server_psk_server_callback (SSL *ssl, + const char *identity, + unsigned char *psk, + unsigned int max_psk_len) +{ + GoodixTlsServer *server = SSL_get_app_data (ssl); + + if (server && server->user_data) + { + FpDevice *dev = FP_DEVICE (server->user_data); + if (FPI_IS_DEVICE_GOODIXTLS5XX (dev)) + { + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + if (cls && cls->psk && cls->psk_len > 0) + { + if (cls->psk_len > max_psk_len) + { + fp_err ("max psk length (%d) too short (needs %d)", max_psk_len, cls->psk_len); + return 0; + } + memcpy (psk, cls->psk, cls->psk_len); + fp_dbg ("PSK callback: using device-specific PSK (%d bytes, identity='%s')", + cls->psk_len, identity ? identity : ""); + return cls->psk_len; + } + } + else + { + fp_dbg ("PSK callback: unexpected device type"); + } + } + else + { + fp_dbg ("PSK callback: missing server context"); + } + + fp_err ("PSK callback: no valid device PSK available"); + return 0; +} + +static SSL_CTX * +tls_server_create_ctx (void) +{ + const SSL_METHOD *method; + + method = TLS_server_method (); + + SSL_CTX *ctx = SSL_CTX_new (method); + + if (!ctx) + return NULL; + + return ctx; +} + +static void +tls_server_config_ctx (SSL_CTX *ctx) +{ + (void) SSL_CTX_set_ecdh_auto (ctx, 1); + SSL_CTX_set_dh_auto (ctx, 1); + if (SSL_CTX_set_cipher_list (ctx, GOODIX_TLS_CIPHERS) != 1) + fp_warn ("TLS: failed to set cipher list '%s'", GOODIX_TLS_CIPHERS); + SSL_CTX_set_min_proto_version (ctx, TLS1_2_VERSION); + SSL_CTX_set_max_proto_version (ctx, TLS1_2_VERSION); + SSL_CTX_set_psk_server_callback (ctx, tls_server_psk_server_callback); +} + +int +goodix_tls_client_write (GoodixTlsServer *self, guint8 *data, guint16 length) +{ + if (!self || !data || self->client_fd < 0) + return -1; + + size_t total_written = 0; + + while (total_written < length) + { + ssize_t ret = write (self->client_fd, data + total_written, length - total_written); + + if (ret < 0) + { + if (errno == EINTR) + continue; + return -1; + } + if (ret == 0) + return -1; + total_written += ret; + } + + return (int) total_written; +} + +int +goodix_tls_server_read (GoodixTlsServer *self, guint8 *data, + guint32 length, GError **error) +{ + int retr; + + if (!self || !self->ssl_layer) + { + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL, + "TLS server not initialised"); + return -1; + } + + retr = SSL_read (self->ssl_layer, data, length); + + if (retr <= 0) + { + int ssl_err = SSL_get_error (self->ssl_layer, retr); + + if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK, + "TLS read would block"); + } + else if (error && *error == NULL) + { + *error = err_from_ssl (); + } + } + return retr; +} + +static void +tls_config_ssl (SSL *ssl) +{ + SSL_set_min_proto_version (ssl, TLS1_2_VERSION); + SSL_set_max_proto_version (ssl, TLS1_2_VERSION); + SSL_set_psk_server_callback (ssl, tls_server_psk_server_callback); + if (SSL_set_cipher_list (ssl, GOODIX_TLS_CIPHERS) != 1) + fp_warn ("TLS: failed to set cipher list '%s'", GOODIX_TLS_CIPHERS); +} + +static void * +goodix_tls_init_serve (void *me) +{ + GoodixTlsServer *self = me; + + fp_dbg ("TLS server waiting to accept..."); + int retr = SSL_accept (self->ssl_layer); + + fp_dbg ("TLS server accept done"); + self->accept_ret = retr; + if (retr <= 0) + { + unsigned long err_code; + gboolean first = TRUE; + while ((err_code = ERR_get_error ()) != 0) + { + const char *err_str = ERR_error_string (err_code, NULL); + if (first) + { + g_snprintf (self->accept_err, sizeof (self->accept_err), + "%s (0x%lx)", err_str ? err_str : "unknown error", + err_code); + first = FALSE; + } + fp_warn ("TLS accept failed: %s (0x%lx, cipher: %s)", + err_str, err_code, + SSL_get_cipher_name (self->ssl_layer)); + } + if (first) + g_snprintf (self->accept_err, sizeof (self->accept_err), + "SSL_accept returned %d with no queued error", retr); + } + else + { + fp_dbg ("TLS connection ready (cipher: %s, proto: %s)", + SSL_get_cipher_name (self->ssl_layer), + SSL_get_version (self->ssl_layer)); + } + g_atomic_int_set (&self->accept_done, 1); + return NULL; +} + +gboolean +goodix_tls_server_deinit (GoodixTlsServer *self, GError **error G_GNUC_UNUSED) +{ + if (!self) + return TRUE; + + /* First shutdown both socket descriptors. + * This immediately unblocks any thread in SSL_accept() or read() with EOF. */ + if (self->client_fd >= 0) + shutdown (self->client_fd, SHUT_RDWR); + if (self->sock_fd >= 0) + shutdown (self->sock_fd, SHUT_RDWR); + + /* Now join the serve thread which unblocks instantly */ + if (self->serve_thread_started) + { + pthread_join (self->serve_thread, NULL); + self->serve_thread_started = FALSE; + } + + /* Close file descriptors after the worker thread has safely exited */ + if (self->client_fd >= 0) + { + close (self->client_fd); + self->client_fd = -1; + } + if (self->sock_fd >= 0) + { + close (self->sock_fd); + self->sock_fd = -1; + } + + if (self->ssl_layer) + { + SSL_shutdown (self->ssl_layer); + SSL_free (self->ssl_layer); + self->ssl_layer = NULL; + } + + if (self->ssl_ctx) + { + SSL_CTX_free (self->ssl_ctx); + self->ssl_ctx = NULL; + } + + return TRUE; +} + +gboolean +goodix_tls_server_init (GoodixTlsServer *self, GError **error) +{ + self->sock_fd = -1; + self->client_fd = -1; + self->serve_thread_started = FALSE; + self->ssl_layer = NULL; + self->ssl_ctx = NULL; + self->accept_done = 0; + self->accept_ret = 0; + self->accept_err[0] = '\0'; + + if (self->user_data && fpi_device_emulation_mode_enabled (FP_DEVICE (self->user_data))) + { + static const unsigned char fixed_seed[32] = "goodix5e0a_deterministic_seed_01"; + RAND_seed (fixed_seed, sizeof (fixed_seed)); + } + + SSL_load_error_strings (); + OpenSSL_add_ssl_algorithms (); + SSL_library_init (); + self->ssl_ctx = tls_server_create_ctx (); + if (self->ssl_ctx == NULL) + { + fp_dbg ("Unable to create TLS server context\n"); + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL, + "Unable to create TLS server context"); + return FALSE; + } + tls_server_config_ctx (self->ssl_ctx); + + int socks[2] = {-1, -1}; + if (socketpair (AF_UNIX, SOCK_STREAM, 0, socks) != 0) + { + g_set_error (error, G_FILE_ERROR, errno, + "failed to create socket pair: %s", strerror (errno)); + SSL_CTX_free (self->ssl_ctx); + self->ssl_ctx = NULL; + return FALSE; + } + self->sock_fd = socks[0]; + self->client_fd = socks[1]; + + self->ssl_layer = SSL_new (self->ssl_ctx); + if (!self->ssl_layer) + { + g_propagate_error (error, err_from_ssl ()); + if (self->sock_fd >= 0) + { + close (self->sock_fd); + self->sock_fd = -1; + } + if (self->client_fd >= 0) + { + close (self->client_fd); + self->client_fd = -1; + } + if (self->ssl_ctx) + { + SSL_CTX_free (self->ssl_ctx); + self->ssl_ctx = NULL; + } + return FALSE; + } + SSL_set_app_data (self->ssl_layer, self); + tls_config_ssl (self->ssl_layer); + SSL_set_fd (self->ssl_layer, self->sock_fd); + + if (pthread_create (&self->serve_thread, NULL, goodix_tls_init_serve, self) == 0) + self->serve_thread_started = TRUE; + else + { + g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL, + "Failed to create TLS serve thread"); + goodix_tls_server_deinit (self, NULL); + return FALSE; + } + + return TRUE; +} diff --git a/libfprint/drivers/goodixtls/goodixtls.h b/libfprint/drivers/goodixtls/goodixtls.h new file mode 100644 index 000000000..e8ea2da07 --- /dev/null +++ b/libfprint/drivers/goodixtls/goodixtls.h @@ -0,0 +1,109 @@ +/* Goodix TLS driver for libfprint + * + * Copyright (C) 2021 Alexander Meiler + * Copyright (C) 2021 Matthieu CHARETTE + * Copyright (C) 2021 Natasha England-Elbro + * + * 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 +#include +#include + +struct _GoodixTlsServer; + + +/** + * @brief TLS server context for goodix devices + * @details This works by creating an openssl TLS server and a socket for the client and server end. + * Raw data is read and written on the client-side and is intended to be passed directly to and from + * the sensor (so it acts as the client). The server-side meanwhile provides a way to decrypt the data + * from the sensor by reading out of the server socket with openssl + * @struct GoodixTlsServer + */ +typedef struct _GoodixTlsServer +{ + gpointer user_data; /* Passed to all callbacks */ + + SSL_CTX *ssl_ctx; + SSL *ssl_layer; + + int sock_fd; + int client_fd; + + pthread_t serve_thread; + gboolean serve_thread_started; + + /* SSL_accept outcome, recorded by the serve thread before it exits. + * accept_done is an atomic flag; accept_ret/accept_err are valid once set. + * Lets the handshake completion path fail activation loudly instead of + * running on a dead session when the device negotiates with a key the + * host does not expect (e.g. peer Finished bad-record-mac). */ + volatile gint accept_done; + int accept_ret; + char accept_err[256]; +} GoodixTlsServer; + +/** + * @brief Initalise the server + * + * @param self context to init + * @param error output error + * @return gboolean TRUE on success, FALSE otherwise + */ +gboolean goodix_tls_server_init (GoodixTlsServer *self, + GError **error); + +/** + * @brief Read a message from the server side of the TLS connection. + * i.e. decrypt the message last written with goodix_tls_client_write() + * + * @param self server context + * @param data buffer to read the data into + * @param length length of the data expected, buffer should be at least this size + * @param error output error + * @return int bytes read or <=0 in case of error + */ +int goodix_tls_server_read (GoodixTlsServer *self, + guint8 *data, + guint32 length, + GError **error); + +/** + * @brief Write a message directly to the client end of the TLS connection. + * This should be used to dump encrypted data directly from the device + * to be decrypted by goodix_tls_server_read() + * + * @param self + * @param data buffer to write + * @param length length of data to be written + * @return int bytes written or -1 in case of error + */ +int goodix_tls_client_write (GoodixTlsServer *self, + guint8 *data, + guint16 length); + +/** + * @brief Shutdown the TLS server + * + * @param self context to shutdown + * @param error output error + * @return gboolean TRUE on success, FALSE otherwise + */ +gboolean goodix_tls_server_deinit (GoodixTlsServer *self, + GError **error); diff --git a/libfprint/fprint-list-udev-hwdb.c b/libfprint/fprint-list-udev-hwdb.c index cf1c4ecff..4ee77ac3e 100644 --- a/libfprint/fprint-list-udev-hwdb.c +++ b/libfprint/fprint-list-udev-hwdb.c @@ -118,7 +118,6 @@ static const FpIdEntry whitelist_id_table[] = { { .vid = 0x27c6, .pid = 0x55a4 }, { .vid = 0x27c6, .pid = 0x55b4 }, { .vid = 0x27c6, .pid = 0x5740 }, - { .vid = 0x27c6, .pid = 0x5e0a }, { .vid = 0x27c6, .pid = 0x581a }, { .vid = 0x2808, .pid = 0x9338 }, { .vid = 0x2808, .pid = 0x93a9 }, diff --git a/libfprint/meson.build b/libfprint/meson.build index d3c8b034c..7b74b6885 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -139,6 +139,8 @@ driver_sources = { [ 'drivers/synaptics/synaptics.c', 'drivers/synaptics/bmkt_message.c' ], 'goodixmoc' : [ 'drivers/goodixmoc/goodix.c', 'drivers/goodixmoc/goodix_proto.c' ], + 'goodixtls5e0a' : + [ 'drivers/goodixtls/goodix5e0a.c' ], 'fpcmoc' : [ 'drivers/fpcmoc/fpc.c' ], } @@ -150,6 +152,10 @@ helper_sources = { [ 'drivers/aesx660.c' ], 'aes3k' : [ 'drivers/aes3k.c' ], + 'goodixtls' : + [ 'drivers/goodixtls/goodix_proto.c', 'drivers/goodixtls/goodix.c', 'drivers/goodixtls/goodixtls.c', 'drivers/goodixtls/goodix5xx.c' ], + 'openssl' : + [ ], 'nss' : [ ], 'udev' : diff --git a/meson.build b/meson.build index 1badb1644..3c73621b0 100644 --- a/meson.build +++ b/meson.build @@ -124,6 +124,7 @@ default_drivers = [ 'upeksonly', 'upekts', 'goodixmoc', + 'goodixtls5e0a', 'nb1010', 'fpcmoc', @@ -157,6 +158,7 @@ driver_helper_mapping = { 'aes3500' : [ 'aeslib', 'aes3k' ], 'aes4000' : [ 'aeslib', 'aes3k' ], 'uru4000' : [ 'nss' ], + 'goodixtls5e0a' : [ 'goodixtls', 'openssl' ], 'elanspi' : [ 'udev' ], 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], @@ -220,6 +222,13 @@ foreach i : driver_helpers endif optional_deps += nss_dep + elif i == 'openssl' + openssl_dep = dependency('openssl', version: '>= 3.0', required: false) + if not openssl_dep.found() + error('OpenSSL is required for @0@ and possibly others'.format(driver)) + endif + + optional_deps += openssl_dep elif i == 'udev' install_udev_rules = true