diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb index e40c77bd..da6113ae 100644 --- a/data/autosuspend.hwdb +++ b/data/autosuspend.hwdb @@ -171,6 +171,15 @@ usb:v04F3p0CB0* ID_AUTOSUSPEND=1 ID_PERSIST=0 +# Supported by libfprint driver elanmoc2 +usb:v04F3p0C00* +usb:v04F3p0C4C* +usb:v04F3p0C5E* +usb:v04F3p0C7C* +usb:v04F3p0C90* + ID_AUTOSUSPEND=1 + ID_PERSIST=0 + # Supported by libfprint driver etes603 usb:v1C7Ap0603* ID_AUTOSUSPEND=1 @@ -354,10 +363,7 @@ usb:v047Dp8054* usb:v047Dp8055* usb:v04E8p730B* usb:v04F3p036B* -usb:v04F3p0C00* -usb:v04F3p0C4C* usb:v04F3p0C57* -usb:v04F3p0C5E* usb:v04F3p0C5A* usb:v04F3p0C60* usb:v04F3p0C6C* diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c new file mode 100644 index 00000000..bec6aeb2 --- /dev/null +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -0,0 +1,1194 @@ +/* + * Driver for ELAN Match-On-Chip sensors + * Copyright (C) 2021-2023 Davide Depau + * + * Based on original reverse-engineering work by Davide Depau. The protocol has + * been reverse-engineered from captures of the official Windows driver, and by + * testing commands on the sensor with a multiplatform Python prototype driver: + * https://github.com/depau/Elan-Fingerprint-0c4c-PoC/ + * + * 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 + */ + +#define FP_COMPONENT "elanmoc2" + +// Library includes +#include +#include + +// Local includes +#include "drivers_api.h" + +#include "elanmoc2.h" + +struct _FpiDeviceElanMoC2 +{ + FpDevice parent; + + /* Device properties */ + unsigned int dev_type; + + /* USB response data */ + GBytes *buffer_in; + const Elanmoc2Cmd *in_flight_cmd; + + /* Command status data */ + FpiSsm *ssm; + unsigned int enrolled_num; + unsigned int enrolled_num_retries; + unsigned int print_index; + GPtrArray *list_result; + + // Enroll + int enroll_stage; + FpPrint *enroll_print; +}; + +G_DEFINE_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FP_TYPE_DEVICE); + + +static void +elanmoc2_cmd_usb_callback (FpiUsbTransfer *transfer, + FpDevice *device, + gpointer user_data, + GError *error) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + const gboolean short_is_error = GPOINTER_TO_INT (user_data); + + if (self->ssm == NULL) + { + if (self->in_flight_cmd == NULL || !self->in_flight_cmd->ssm_not_required) + fp_warn ("Received USB callback with no ongoing action"); + + self->in_flight_cmd = NULL; + + if (error) + { + fp_info ("USB callback error: %s", error->message); + g_error_free (error); + } + return; + } + + if (error) + { + fpi_ssm_mark_failed (g_steal_pointer (&self->ssm), + g_steal_pointer (&error)); + return; + } + + if (self->in_flight_cmd != NULL) + { + /* Send callback */ + const Elanmoc2Cmd *cmd = g_steal_pointer (&self->in_flight_cmd); + + if (cmd->in_len == 0) + { + /* Nothing to receive */ + fpi_ssm_next_state (self->ssm); + return; + } + + FpiUsbTransfer *transfer_in = fpi_usb_transfer_new (device); + + transfer_in->short_is_error = short_is_error; + + fpi_usb_transfer_fill_bulk (transfer_in, cmd->ep_in, + cmd->in_len); + + fpi_usb_transfer_submit (transfer_in, + ELANMOC2_USB_RECV_TIMEOUT, + cmd->is_cancellable ? + fpi_device_get_cancellable (device) : NULL, + elanmoc2_cmd_usb_callback, + NULL); + } + else + { + /* Receive callback */ + if (transfer->actual_length > 0 && transfer->buffer[0] != 0x40) + { + fpi_ssm_mark_failed (g_steal_pointer (&self->ssm), + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Error receiving data " + "from sensor")); + } + else + { + g_assert_null (self->buffer_in); + self->buffer_in = + g_bytes_new_take (g_steal_pointer (&(transfer->buffer)), + transfer->actual_length); + fpi_ssm_next_state (self->ssm); + } + } +} + +static void +elanmoc2_cmd_transceive_full (FpDevice *device, + const Elanmoc2Cmd *cmd, + GByteArray *buffer_out, + gboolean short_is_error + ) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + g_assert (buffer_out->len == cmd->out_len); + g_assert_null (self->in_flight_cmd); + self->in_flight_cmd = cmd; + + g_autoptr(FpiUsbTransfer) transfer_out = fpi_usb_transfer_new (device); + transfer_out->short_is_error = TRUE; + fpi_usb_transfer_fill_bulk_full (transfer_out, + ELANMOC2_EP_CMD_OUT, + g_byte_array_steal (buffer_out, NULL), + cmd->out_len, + g_free); + + fpi_usb_transfer_submit (g_steal_pointer (&transfer_out), + ELANMOC2_USB_SEND_TIMEOUT, + cmd->is_cancellable ? + fpi_device_get_cancellable (device) : NULL, + elanmoc2_cmd_usb_callback, + GINT_TO_POINTER (short_is_error)); +} + +static void +elanmoc2_cmd_transceive (FpDevice *device, + const Elanmoc2Cmd *cmd, + GByteArray *buffer_out) +{ + elanmoc2_cmd_transceive_full (device, cmd, buffer_out, TRUE); +} + +static GByteArray * +elanmoc2_prepare_cmd (FpiDeviceElanMoC2 *self, const Elanmoc2Cmd *cmd) +{ + if (cmd->devices != ELANMOC2_ALL_DEV && !(cmd->devices & self->dev_type)) + return NULL; + + g_assert (cmd->out_len > 0); + + GByteArray *buffer = g_byte_array_new (); + g_byte_array_set_size (buffer, cmd->out_len); + memset (buffer->data, 0, buffer->len); + + buffer->data[0] = 0x40; + memcpy (&buffer->data[1], cmd->cmd, cmd->is_single_byte_command ? 1 : 2); + + return buffer; +} + +static void +elanmoc2_print_set_data (FpPrint *print, + guchar finger_id, + guchar user_id_len, + const guchar *user_id) +{ + fpi_print_set_type (print, FPI_PRINT_RAW); + fpi_print_set_device_stored (print, TRUE); + + GVariant *user_id_v = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + user_id, user_id_len, + sizeof (guchar)); + GVariant *fpi_data = g_variant_new ("(y@ay)", finger_id, user_id_v); + g_object_set (print, "fpi-data", fpi_data, NULL); +} + +static FpPrint * +elanmoc2_print_new_with_user_id (FpiDeviceElanMoC2 *self, + guchar finger_id, + guchar user_id_len, + const guchar *user_id) +{ + FpPrint *print = fp_print_new (FP_DEVICE (self)); + + elanmoc2_print_set_data (print, finger_id, user_id_len, user_id); + return g_steal_pointer (&print); +} + +static guint +elanmoc2_get_user_id_max_length (FpiDeviceElanMoC2 *self) +{ + return self->dev_type == ELANMOC2_DEV_0C5E ? + ELANMOC2_USER_ID_MAX_LEN_0C5E : + ELANMOC2_USER_ID_MAX_LEN; +} + +static GBytes * +elanmoc2_get_user_id_string (FpiDeviceElanMoC2 *self, + GBytes *finger_info_response) +{ + GByteArray *user_id = g_byte_array_new (); + + guint offset = self->dev_type == ELANMOC2_DEV_0C5E ? 3 : 2; + guint max_len = MIN (elanmoc2_get_user_id_max_length (self), + g_bytes_get_size (finger_info_response) - offset); + + g_byte_array_set_size (user_id, max_len); + + /* The string must be copied since the input data is not guaranteed to be + * null-terminated */ + const guint8 *data = g_bytes_get_data (finger_info_response, NULL); + memcpy (user_id->data, &data[offset], max_len); + user_id->data[max_len] = '\0'; + + return g_byte_array_free_to_bytes (user_id); +} + +static FpPrint * +elanmoc2_print_new_from_finger_info (FpiDeviceElanMoC2 *self, + guint8 finger_id, + GBytes *finger_info_resp) +{ + g_autoptr(GBytes) user_id = elanmoc2_get_user_id_string (self, + finger_info_resp); + guint8 user_id_len = g_bytes_get_size (user_id); + const char *user_id_data = g_bytes_get_data (user_id, NULL); + + if (g_str_has_prefix ( user_id_data, "FP")) + { + user_id_len = strnlen (user_id_data, user_id_len); + fp_info ("Creating new print: finger %d, user id[%d]: %s", + finger_id, + user_id_len, + (char *) user_id_data); + } + else + { + fp_info ("Creating new print: finger %d, user id[%d]: raw data", + finger_id, + user_id_len); + } + + FpPrint *print = + elanmoc2_print_new_with_user_id (self, + finger_id, + user_id_len, + (const guint8 *) user_id_data); + + if (!fpi_print_fill_from_user_id (print, (const char *) user_id_data)) + /* Fingerprint matched with on-sensor print, but the on-sensor print was + * not added by libfprint. Wipe it and report a failure. */ + fp_info ("Finger info not generated by libfprint"); + else + fp_info ("Finger info with libfprint user ID"); + + return g_steal_pointer (&print); +} + +static void +elanmoc2_cancel (FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + fp_info ("Cancelling any ongoing requests"); + + g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, &cmd_abort); + elanmoc2_cmd_transceive (device, &cmd_abort, buffer_out); +} + +static void +elanmoc2_open (FpDevice *device) +{ + g_autoptr(GError) error = NULL; + FpiDeviceElanMoC2 *self; + + if (!g_usb_device_reset (fpi_device_get_usb_device (device), &error)) + return fpi_device_open_complete (device, g_steal_pointer (&error)); + + if (!g_usb_device_claim_interface ( + fpi_device_get_usb_device (FP_DEVICE (device)), 0, 0, &error)) + return fpi_device_open_complete (device, g_steal_pointer (&error)); + + self = FPI_DEVICE_ELANMOC2 (device); + self->dev_type = fpi_device_get_driver_data (FP_DEVICE (device)); + fpi_device_open_complete (device, NULL); +} + +static void +elanmoc2_close (FpDevice *device) +{ + g_autoptr(GError) error = NULL; + + fp_info ("Closing device"); + elanmoc2_cancel (device); + g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (device)), + 0, 0, &error); + fpi_device_close_complete (device, g_steal_pointer (&error)); +} + +static void +elanmoc2_ssm_completed_callback (FpiSsm *ssm, FpDevice *device, GError *error) +{ + if (error) + fpi_device_action_error (device, error); +} + +static void +elanmoc2_perform_get_num_enrolled (FpiDeviceElanMoC2 *self, FpiSsm *ssm) +{ + self->enrolled_num_retries++; + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, + &cmd_get_enrolled_count); + + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + return; + } + + fp_info ("Querying number of enrolled fingers"); + + elanmoc2_cmd_transceive_full (FP_DEVICE (self), + &cmd_get_enrolled_count, + buffer_out, + false); + fp_info ("Sent query for number of enrolled fingers"); +} + +static GError * +elanmoc2_get_num_enrolled_retry_or_error (FpiDeviceElanMoC2 *self, + FpiSsm *ssm, + int retry_state) +{ + fp_info ("Device returned no data, retrying"); + if (self->enrolled_num_retries >= ELANMOC2_MAX_RETRIES) + return fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Device refused to respond to query for " + "number of enrolled fingers"); + fpi_ssm_jump_to_state (ssm, retry_state); + return NULL; +} + +/** + * elanmoc2_get_finger_error: + * @self: #FpiDeviceElanMoC2 pointer + * @out_can_retry: Whether the current action should be retried (out) + * + * Checks a command status code and, if an error has occurred, creates a new + * error object. Returns whether the operation needs to be retried. + * + * Returns: #GError if failed, or %NULL + */ +static GError * +elanmoc2_get_finger_error (GBytes *buffer_in, gboolean *out_can_retry) +{ + g_assert_nonnull (buffer_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + const guint8 *data_in = g_bytes_get_data (buffer_in, NULL); + + /* Regular status codes never have the most-significant nibble set; + * errors do */ + if ((data_in[1] & 0xF0) == 0) + { + *out_can_retry = TRUE; + return NULL; + } + switch ((unsigned char) data_in[1]) + { + case ELANMOC2_RESP_MOVE_DOWN: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly downwards"); + + case ELANMOC2_RESP_MOVE_RIGHT: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly to the right"); + + case ELANMOC2_RESP_MOVE_UP: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly upwards"); + + case ELANMOC2_RESP_MOVE_LEFT: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly to the left"); + + case ELANMOC2_RESP_SENSOR_DIRTY: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_REMOVE_FINGER, + "Sensor is dirty or wet"); + + case ELANMOC2_RESP_NOT_ENOUGH_SURFACE: + *out_can_retry = TRUE; + return fpi_device_retry_new_msg (FP_DEVICE_RETRY_REMOVE_FINGER, + "Press your finger slightly harder on " + "the sensor"); + + case ELANMOC2_RESP_NOT_ENROLLED: + *out_can_retry = FALSE; + return fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Finger not recognized"); + + case ELANMOC2_RESP_MAX_ENROLLED_REACHED: + *out_can_retry = FALSE; + return fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_FULL, + "Maximum number of fingers already " + "enrolled"); + + default: + *out_can_retry = FALSE; + return fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Unknown error"); + } +} + +static void +elanmoc2_identify_verify_complete (FpDevice *device, GError *error) +{ + if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY) + fpi_device_identify_complete (device, error); + else + fpi_device_verify_complete (device, error); +} + +/** + * elanmoc2_identify_verify_report: + * @device: #FpDevice + * @print: Identified fingerprint + * @error: Optional error + * + * Calls the correct verify or identify report function based on the input data. + * Returns whether the action should be completed. + * + * Returns: Whether to complete the action. + */ +static gboolean +elanmoc2_identify_verify_report (FpDevice *device, FpPrint *print, + GError **error) +{ + if (*error != NULL && (*error)->domain != FP_DEVICE_RETRY) + return TRUE; + + if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY) + { + if (print != NULL) + { + GPtrArray * gallery = NULL; + fpi_device_get_identify_data (device, &gallery); + + for (int i = 0; i < gallery->len; i++) + { + FpPrint *to_match = g_ptr_array_index (gallery, i); + if (fp_print_equal (to_match, print)) + { + fp_info ("Identify: finger matches"); + fpi_device_identify_report (device, + g_steal_pointer (&to_match), + print, + NULL); + return TRUE; + } + } + fp_info ("Identify: no match"); + } + fpi_device_identify_report (device, NULL, print, *error); + return TRUE; + } + else + { + FpiMatchResult result = FPI_MATCH_FAIL; + if (print != NULL) + { + FpPrint *to_match = NULL; + fpi_device_get_verify_data (device, &to_match); + g_assert_nonnull (to_match); + + if (fp_print_equal (to_match, print)) + { + fp_info ("Verify: finger matches"); + result = FPI_MATCH_SUCCESS; + } + else + { + fp_info ("Verify: finger does not match"); + print = NULL; + } + } + fpi_device_verify_report (device, result, print, *error); + return result != FPI_MATCH_FAIL; + } +} + +static void +elanmoc2_identify_run_state (FpiSsm *ssm, FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in); + + const guint8 *data_in = + buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL; + const gsize data_in_len = + buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0; + + switch (fpi_ssm_get_cur_state (ssm)) + { + case IDENTIFY_GET_NUM_ENROLLED: { + elanmoc2_perform_get_num_enrolled (self, ssm); + break; + } + + case IDENTIFY_CHECK_NUM_ENROLLED: { + if (data_in_len == 0) + { + error = + elanmoc2_get_num_enrolled_retry_or_error (self, + ssm, + IDENTIFY_GET_NUM_ENROLLED); + if (error != NULL) + { + elanmoc2_identify_verify_complete (device, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + break; + } + + g_assert_nonnull (data_in); + g_assert (data_in_len >= 2); + + self->enrolled_num = data_in[1]; + + if (self->enrolled_num == 0) + { + fp_info ("No fingers enrolled, no need to identify finger"); + error = NULL; + elanmoc2_identify_verify_report (device, NULL, &error); + elanmoc2_identify_verify_complete (device, NULL); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + break; + } + fpi_ssm_next_state (ssm); + break; + } + + case IDENTIFY_IDENTIFY: { + g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, + &cmd_identify); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + elanmoc2_cmd_transceive (device, &cmd_identify, buffer_out); + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED); + fp_info ("Sent identification request"); + break; + } + + case IDENTIFY_GET_FINGER_INFO: { + g_assert_nonnull (buffer_in); + fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT); + gboolean can_retry = FALSE; + error = elanmoc2_get_finger_error (buffer_in, &can_retry); + if (error != NULL) + { + fp_info ("Identify failed: %s", error->message); + if (can_retry) + { + elanmoc2_identify_verify_report (device, NULL, &error); + fpi_ssm_jump_to_state (ssm, IDENTIFY_IDENTIFY); + } + else + { + elanmoc2_identify_verify_complete (device, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + break; + } + + g_assert_nonnull (data_in); + g_assert (data_in_len >= 2); + + self->print_index = data_in[1]; + + fp_info ("Identified finger %d; requesting finger info", + self->print_index); + + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, &cmd_finger_info); + + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + g_assert (buffer_out->len >= 4); + buffer_out->data[3] = self->print_index; + elanmoc2_cmd_transceive (device, &cmd_finger_info, buffer_out); + break; + } + + case IDENTIFY_CHECK_FINGER_INFO: { + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE); + + g_assert_nonnull (buffer_in); + g_autoptr(FpPrint) print = + elanmoc2_print_new_from_finger_info (self, + self->print_index, + buffer_in); + + error = NULL; + elanmoc2_identify_verify_report (device, + g_steal_pointer (&print), + &error); + elanmoc2_identify_verify_complete (device, g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + break; + } + + default: + break; + } +} + +static void +elanmoc2_identify_verify (FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + fp_info ("[elanmoc2] New identify/verify operation"); + self->ssm = fpi_ssm_new (device, elanmoc2_identify_run_state, + IDENTIFY_NUM_STATES); + self->enrolled_num_retries = 0; + fpi_ssm_start (self->ssm, elanmoc2_ssm_completed_callback); +} + +static void +elanmoc2_enroll_ssm_completed_callback (FpiSsm *ssm, FpDevice *device, + GError *error) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + /* Pointer is either stolen by fpi_device_enroll_complete() or otherwise + * unref'd by libfprint elsewhere not in this driver. */ + self->enroll_print = NULL; + elanmoc2_ssm_completed_callback (ssm, device, error); +} + +static void +elanmoc2_enroll_run_state (FpiSsm *ssm, FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in); + + const guint8 *data_in = + buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL; + const gsize data_in_len = + buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0; + + g_assert_nonnull (self->enroll_print); + + switch (fpi_ssm_get_cur_state (ssm)) + { + /* First check how many fingers are already enrolled */ + case ENROLL_GET_NUM_ENROLLED: { + elanmoc2_perform_get_num_enrolled (self, ssm); + break; + } + + case ENROLL_CHECK_NUM_ENROLLED: { + if (data_in_len == 0) + { + error = + elanmoc2_get_num_enrolled_retry_or_error (self, + ssm, + ENROLL_GET_NUM_ENROLLED); + if (error != NULL) + { + fpi_device_enroll_complete (device, + NULL, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + break; + } + + g_assert_nonnull (data_in); + g_assert (data_in_len >= 2); + self->enrolled_num = data_in[1]; + + if (self->enrolled_num >= ELANMOC2_MAX_PRINTS) + { + fp_info ("Can't enroll, sensor storage is full"); + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_FULL, + "Sensor storage is full"); + fpi_device_enroll_complete (device, + NULL, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + else if (self->enrolled_num == 0) + { + fp_info ("Enrolled count is 0, proceeding with enroll stage"); + fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL); + } + else + { + fp_info ("Fingers enrolled: %d, need to check for re-enroll", + self->enrolled_num); + fpi_ssm_next_state (ssm); + } + break; + } + + case ENROLL_EARLY_REENROLL_CHECK: { + g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, + &cmd_identify); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + elanmoc2_cmd_transceive (device, &cmd_identify, buffer_out); + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED); + fp_info ("Sent identification request"); + break; + } + + case ENROLL_GET_ENROLLED_FINGER_INFO: { + fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT); + + g_assert_nonnull (data_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + /* Not enrolled - skip to enroll stage */ + if (data_in[1] == ELANMOC2_RESP_NOT_ENROLLED) + { + fp_info ("Finger not enrolled, proceeding with enroll stage"); + fpi_device_enroll_progress (device, self->enroll_stage, NULL, + NULL); + fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL); + break; + } + + /* Identification failed (i.e. dirty) - retry */ + gboolean can_retry = FALSE; + error = elanmoc2_get_finger_error (buffer_in, &can_retry); + if (error != NULL) + { + fp_info ("Identify failed: %s", error->message); + if (can_retry) + { + fpi_device_enroll_progress (device, self->enroll_stage, NULL, + g_steal_pointer (&error)); + fpi_ssm_jump_to_state (ssm, ENROLL_EARLY_REENROLL_CHECK); + } + else + { + fpi_device_enroll_complete (device, NULL, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + self->enroll_print = NULL; + } + break; + } + + /* Finger already enrolled - fetch finger info for deletion */ + self->print_index = data_in[1]; + fp_info ("Finger enrolled as %d; fetching finger info", + self->print_index); + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, &cmd_finger_info); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + g_assert (buffer_out->len >= 4); + buffer_out->data[3] = self->print_index; + elanmoc2_cmd_transceive (device, &cmd_finger_info, buffer_out); + break; + } + + case ENROLL_ATTEMPT_DELETE: { + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE); + fp_info ("Deleting enrolled finger %d", self->print_index); + g_assert_nonnull (buffer_in); + + /* Attempt to delete the finger */ + g_autoptr(GBytes) user_id = + elanmoc2_get_user_id_string (self, buffer_in); + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, &cmd_delete); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + gsize user_id_bytes = MIN (cmd_delete.out_len - 4, + ELANMOC2_USER_ID_MAX_LEN); + g_assert (buffer_out->len >= 4 + user_id_bytes); + buffer_out->data[3] = 0xf0 | (self->print_index + 5); + memcpy (&buffer_out->data[4], + g_bytes_get_data (user_id, NULL), + user_id_bytes); + elanmoc2_cmd_transceive (device, &cmd_delete, buffer_out); + + break; + } + + case ENROLL_CHECK_DELETED: { + g_assert_nonnull (data_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + if (data_in[1] != 0) + { + fp_info ("Failed to delete finger %d, wiping sensor", + self->print_index); + fpi_ssm_jump_to_state (ssm, ENROLL_WIPE_SENSOR); + } + else + { + fp_info ("Finger %d deleted, proceeding with enroll stage", + self->print_index); + self->enrolled_num--; + fpi_device_enroll_progress (device, self->enroll_stage, NULL, + NULL); + fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL); + } + break; + } + + case ENROLL_WIPE_SENSOR: { + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, &cmd_wipe_sensor); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + elanmoc2_cmd_transceive (device, &cmd_wipe_sensor, buffer_out); + self->enrolled_num = 0; + self->print_index = 0; + fp_info ( + "Wipe sensor command sent - next operation will take a while"); + fpi_ssm_next_state (ssm); + break; + } + + case ENROLL_ENROLL: { + g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, + &cmd_enroll); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + g_assert (buffer_out->len >= 7); + buffer_out->data[3] = self->enrolled_num; + buffer_out->data[4] = ELANMOC2_ENROLL_TIMES; + buffer_out->data[5] = self->enroll_stage; + buffer_out->data[6] = 0; + elanmoc2_cmd_transceive (device, &cmd_enroll, buffer_out); + fp_info ("Enroll command sent: %d/%d", self->enroll_stage, + ELANMOC2_ENROLL_TIMES); + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED); + break; + } + + case ENROLL_CHECK_ENROLLED: { + fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT); + + g_assert_nonnull (data_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + if (data_in[1] == 0) + { + /* Stage okay */ + fp_info ("Enroll stage succeeded"); + self->enroll_stage++; + fpi_device_enroll_progress (device, self->enroll_stage, + self->enroll_print, NULL); + if (self->enroll_stage >= ELANMOC2_ENROLL_TIMES) + { + fp_info ("Enroll completed"); + fpi_ssm_next_state (ssm); + break; + } + } + else + { + /* Detection error */ + gboolean can_retry = FALSE; + error = elanmoc2_get_finger_error (buffer_in, &can_retry); + if (error != NULL) + { + fp_info ("Enroll stage failed: %s", error->message); + if (data_in[1] == ELANMOC2_RESP_NOT_ENROLLED) + { + /* Not enrolled is a fatal error for "identify" but not for + * "enroll" */ + error->domain = FP_DEVICE_RETRY; + error->code = FP_DEVICE_RETRY_TOO_SHORT; + can_retry = FALSE; + } + if (can_retry) + { + fpi_device_enroll_progress (device, self->enroll_stage, + NULL, g_steal_pointer (&error)); + } + else + { + fpi_device_enroll_complete (device, NULL, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + } + else + { + fp_info ("Enroll stage failed for unknown reasons"); + } + } + fp_info ("Performing another enroll"); + fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL); + break; + } + + case ENROLL_LATE_REENROLL_CHECK: { + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE); + g_autoptr(GByteArray) buffer_out = + elanmoc2_prepare_cmd (self, &cmd_check_enroll_collision); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + elanmoc2_cmd_transceive (device, &cmd_check_enroll_collision, buffer_out); + fp_info ("Check re-enroll command sent"); + break; + } + + case ENROLL_COMMIT: { + error = NULL; + + g_assert_nonnull (data_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + if (data_in[1] != 0) + { + fp_info ("Finger is already enrolled at position %d, cannot commit", + data_in[2]); + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_DUPLICATE, + "Finger is already enrolled"); + fpi_device_enroll_complete (device, NULL, + g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + self->enroll_print = NULL; + break; + } + + fp_info ("Finger is not enrolled, committing"); + g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, + &cmd_commit); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + g_autofree gchar *user_id = fpi_print_generate_user_id ( + self->enroll_print); + elanmoc2_print_set_data (self->enroll_print, self->enrolled_num, + strlen (user_id), (guint8 *) user_id); + + g_assert (buffer_out->len == cmd_commit.out_len); + buffer_out->data[3] = 0xf0 | (self->enrolled_num + 5); + strncpy ((gchar *) &buffer_out->data[4], user_id, cmd_commit.out_len - 4); + elanmoc2_cmd_transceive (device, &cmd_commit, buffer_out); + fp_info ("Commit command sent"); + break; + } + + case ENROLL_CHECK_COMMITTED: { + error = NULL; + + g_assert_nonnull (data_in); + g_assert (g_bytes_get_size (buffer_in) >= 2); + + if (data_in[1] != 0) + { + fp_info ("Commit failed with error code %d", data_in[1]); + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to store fingerprint for " + "unknown reasons"); + fpi_device_enroll_complete (device, NULL, error); + fpi_ssm_mark_failed (g_steal_pointer (&self->ssm), + g_steal_pointer (&error)); + } + else + { + fp_info ("Commit succeeded"); + fpi_device_enroll_complete (device, + g_object_ref (self->enroll_print), + NULL); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + break; + } + } +} + +static void +elanmoc2_enroll (FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + fp_info ("[elanmoc2] New enroll operation"); + + self->enroll_stage = 0; + fpi_device_get_enroll_data (device, &self->enroll_print); + + self->ssm = fpi_ssm_new (device, elanmoc2_enroll_run_state, + ENROLL_NUM_STATES); + self->enrolled_num_retries = 0; + fpi_ssm_start (self->ssm, elanmoc2_enroll_ssm_completed_callback); +} + +static void +elanmoc2_clear_storage_run_state (FpiSsm *ssm, FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + g_autoptr(GByteArray) buffer_out = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case CLEAR_STORAGE_WIPE_SENSOR: + buffer_out = elanmoc2_prepare_cmd (self, &cmd_wipe_sensor); + if (buffer_out == NULL) + { + fpi_ssm_next_state (ssm); + break; + } + elanmoc2_cmd_transceive (device, &cmd_wipe_sensor, buffer_out); + fp_info ("Sent sensor wipe command, sensor will hang for ~5 seconds"); + break; + + case CLEAR_STORAGE_GET_NUM_ENROLLED: + elanmoc2_perform_get_num_enrolled (self, ssm); + break; + + case CLEAR_STORAGE_CHECK_NUM_ENROLLED: { + gsize buffer_in_len = g_bytes_get_size (buffer_in); + + if (buffer_in_len == 0) + { + error = + elanmoc2_get_num_enrolled_retry_or_error (self, + ssm, + CLEAR_STORAGE_GET_NUM_ENROLLED); + if (error != NULL) + { + fpi_device_clear_storage_complete (device, g_steal_pointer (&error)); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + break; + } + + + /* It should take around 5 seconds to arrive here after the wipe sensor + * command */ + g_assert_nonnull (buffer_in); + g_assert (buffer_in_len >= 2); + + const guint8 *data_in = g_bytes_get_data (buffer_in, NULL); + self->enrolled_num = data_in[1]; + + if (self->enrolled_num == 0) + { + fpi_device_clear_storage_complete (device, NULL); + fpi_ssm_mark_completed (g_steal_pointer (&self->ssm)); + } + else + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Sensor erase requested but " + "storage is not empty"); + fpi_device_clear_storage_complete (device, error); + fpi_ssm_mark_failed (g_steal_pointer (&self->ssm), + g_steal_pointer (&error)); + break; + } + break; + } + } +} + +static void +elanmoc2_clear_storage (FpDevice *device) +{ + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); + + fp_info ("[elanmoc2] New clear storage operation"); + self->ssm = fpi_ssm_new (device, elanmoc2_clear_storage_run_state, + CLEAR_STORAGE_NUM_STATES); + self->enrolled_num_retries = 0; + fpi_ssm_start (self->ssm, elanmoc2_ssm_completed_callback); +} + +static void +fpi_device_elanmoc2_init (FpiDeviceElanMoC2 *self) +{ +} + +static const FpIdEntry elanmoc2_id_table[] = { + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c00, .driver_data = ELANMOC2_ALL_DEV}, + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c4c, .driver_data = ELANMOC2_ALL_DEV}, + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c5e, .driver_data = ELANMOC2_DEV_0C5E}, + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c7c, .driver_data = ELANMOC2_ALL_DEV}, + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c90, .driver_data = ELANMOC2_ALL_DEV}, + {.vid = 0, .pid = 0, .driver_data = 0} +}; + +static void +fpi_device_elanmoc2_class_init (FpiDeviceElanMoC2Class *klass) +{ + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + + dev_class->id = FP_COMPONENT; + dev_class->full_name = ELANMOC2_DRIVER_FULLNAME; + + dev_class->type = FP_DEVICE_TYPE_USB; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + dev_class->id_table = elanmoc2_id_table; + + dev_class->nr_enroll_stages = ELANMOC2_ENROLL_TIMES; + dev_class->temp_hot_seconds = -1; + + dev_class->open = elanmoc2_open; + dev_class->close = elanmoc2_close; + dev_class->identify = elanmoc2_identify_verify; + dev_class->verify = elanmoc2_identify_verify; + dev_class->enroll = elanmoc2_enroll; + dev_class->clear_storage = elanmoc2_clear_storage; + dev_class->cancel = elanmoc2_cancel; + + fpi_device_class_auto_initialize_features (dev_class); + dev_class->features |= FP_DEVICE_FEATURE_DUPLICATES_CHECK; + dev_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT; +} diff --git a/libfprint/drivers/elanmoc2/elanmoc2.h b/libfprint/drivers/elanmoc2/elanmoc2.h new file mode 100644 index 00000000..2a99089d --- /dev/null +++ b/libfprint/drivers/elanmoc2/elanmoc2.h @@ -0,0 +1,195 @@ +/* + * Driver for ELAN Match-On-Chip sensors + * Copyright (C) 2021-2023 Davide Depau + * + * Based on original reverse-engineering work by Davide Depau. The protocol has + * been reverse-engineered from captures of the official Windows driver, and by + * testing commands on the sensor with a multiplatform Python prototype driver: + * https://github.com/depau/Elan-Fingerprint-0c4c-PoC/ + * + * 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 + +// Stdlib includes +#include + +// Library includes +#include + +// Local includes +#include "fpi-device.h" +#include "fpi-ssm.h" + +#define ELANMOC2_DRIVER_FULLNAME "ELAN Match-on-Chip 2" +#define ELANMOC2_VEND_ID 0x04f3 + +#define ELANMOC2_ENROLL_TIMES 8 +#define ELANMOC2_CMD_MAX_LEN 2 +#define ELANMOC2_MAX_PRINTS 10 +#define ELANMOC2_MAX_RETRIES 3 + +// USB parameters +#define ELANMOC2_EP_CMD_OUT (0x1 | FPI_USB_ENDPOINT_OUT) +#define ELANMOC2_EP_CMD_IN (0x3 | FPI_USB_ENDPOINT_IN) +#define ELANMOC2_EP_MOC_CMD_IN (0x4 | FPI_USB_ENDPOINT_IN) +#define ELANMOC2_USB_SEND_TIMEOUT 10000 +#define ELANMOC2_USB_RECV_TIMEOUT 10000 + +// Response codes +#define ELANMOC2_RESP_MOVE_DOWN 0x41 +#define ELANMOC2_RESP_MOVE_RIGHT 0x42 +#define ELANMOC2_RESP_MOVE_UP 0x43 +#define ELANMOC2_RESP_MOVE_LEFT 0x44 +#define ELANMOC2_RESP_MAX_ENROLLED_REACHED 0xdd +#define ELANMOC2_RESP_SENSOR_DIRTY 0xfb +#define ELANMOC2_RESP_NOT_ENROLLED 0xfd +#define ELANMOC2_RESP_NOT_ENOUGH_SURFACE 0xfe + +// Currently only one device is supported, but I'd like to future-proof this driver for any new contributions. +#define ELANMOC2_ALL_DEV 0 +#define ELANMOC2_DEV_0C5E (1 << 0) + +// Subtract the 2-byte header +#define ELANMOC2_USER_ID_MAX_LEN (cmd_finger_info.in_len - 2) +#define ELANMOC2_USER_ID_MAX_LEN_0C5E (cmd_finger_info.in_len - 3) + +G_DECLARE_FINAL_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FPI, DEVICE_ELANMOC2, FpDevice) + +typedef struct elanmoc2_cmd +{ + unsigned char cmd[ELANMOC2_CMD_MAX_LEN]; + gboolean is_single_byte_command; + int out_len; + int in_len; + int ep_in; + unsigned short devices; + gboolean is_cancellable; + gboolean ssm_not_required; +} Elanmoc2Cmd; + + +// Cancellable commands + +static const Elanmoc2Cmd cmd_identify = { + .cmd = {0xff, 0x03}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_MOC_CMD_IN, + .is_cancellable = TRUE, +}; + +static const Elanmoc2Cmd cmd_enroll = { + .cmd = {0xff, 0x01}, + .out_len = 7, + .in_len = 2, + .ep_in = ELANMOC2_EP_MOC_CMD_IN, + .is_cancellable = TRUE, +}; + + +// Not cancellable / quick commands + +static const Elanmoc2Cmd cmd_get_fw_ver = { + .cmd = {0x19}, + .is_single_byte_command = TRUE, + .out_len = 2, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_finger_info = { + .cmd = {0xff, 0x12}, + .out_len = 4, + .in_len = 64, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_get_enrolled_count = { + .cmd = {0xff, 0x04}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_abort = { + .cmd = {0xff, 0x02}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, + .ssm_not_required = TRUE, +}; + +static const Elanmoc2Cmd cmd_commit = { + .cmd = {0xff, 0x11}, + .out_len = 72, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_check_enroll_collision = { + .cmd = {0xff, 0x10}, + .out_len = 3, + .in_len = 3, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_delete = { + .cmd = {0xff, 0x13}, + .out_len = 72, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const Elanmoc2Cmd cmd_wipe_sensor = { + .cmd = {0xff, 0x99}, + .out_len = 3, + .in_len = 0, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + + +enum IdentifyStates { + IDENTIFY_GET_NUM_ENROLLED, + IDENTIFY_CHECK_NUM_ENROLLED, + IDENTIFY_IDENTIFY, + IDENTIFY_GET_FINGER_INFO, + IDENTIFY_CHECK_FINGER_INFO, + IDENTIFY_NUM_STATES +}; + +enum EnrollStates { + ENROLL_GET_NUM_ENROLLED, + ENROLL_CHECK_NUM_ENROLLED, + ENROLL_EARLY_REENROLL_CHECK, + ENROLL_GET_ENROLLED_FINGER_INFO, + ENROLL_ATTEMPT_DELETE, + ENROLL_CHECK_DELETED, + ENROLL_WIPE_SENSOR, + ENROLL_ENROLL, + ENROLL_CHECK_ENROLLED, + ENROLL_LATE_REENROLL_CHECK, + ENROLL_COMMIT, + ENROLL_CHECK_COMMITTED, + ENROLL_NUM_STATES +}; + +enum ClearStorageStates { + CLEAR_STORAGE_WIPE_SENSOR, + CLEAR_STORAGE_GET_NUM_ENROLLED, + CLEAR_STORAGE_CHECK_NUM_ENROLLED, + CLEAR_STORAGE_NUM_STATES +}; diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c index 6040c1be..90dc3fd3 100644 --- a/libfprint/fp-device.c +++ b/libfprint/fp-device.c @@ -1647,8 +1647,9 @@ fp_device_delete_print (FpDevice *device, /* Succeed immediately if delete is not implemented. */ if (!cls->delete || !(priv->features & FP_DEVICE_FEATURE_STORAGE_DELETE)) - { - g_task_return_boolean (task, TRUE); + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED)); return; } diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c index a1be30c0..d2efce62 100644 --- a/libfprint/fpi-device.c +++ b/libfprint/fpi-device.c @@ -80,7 +80,7 @@ fpi_device_class_auto_initialize_features (FpDeviceClass *device_class) if (device_class->clear_storage) device_class->features |= FP_DEVICE_FEATURE_STORAGE_CLEAR; - if (device_class->delete && (device_class->list || device_class->clear_storage)) + if (device_class->clear_storage || (device_class->delete && device_class->list)) device_class->features |= FP_DEVICE_FEATURE_STORAGE; if (device_class->temp_hot_seconds < 0) diff --git a/libfprint/fprint-list-udev-hwdb.c b/libfprint/fprint-list-udev-hwdb.c index 65854f36..84eabce9 100644 --- a/libfprint/fprint-list-udev-hwdb.c +++ b/libfprint/fprint-list-udev-hwdb.c @@ -35,10 +35,7 @@ static const FpIdEntry allowlist_id_table[] = { { .vid = 0x047d, .pid = 0x8055 }, { .vid = 0x04e8, .pid = 0x730b }, { .vid = 0x04f3, .pid = 0x036b }, - { .vid = 0x04f3, .pid = 0x0c00 }, - { .vid = 0x04f3, .pid = 0x0c4c }, { .vid = 0x04f3, .pid = 0x0c57 }, - { .vid = 0x04f3, .pid = 0x0c5e }, { .vid = 0x04f3, .pid = 0x0c5a }, { .vid = 0x04f3, .pid = 0x0c60 }, { .vid = 0x04f3, .pid = 0x0c6c }, diff --git a/libfprint/meson.build b/libfprint/meson.build index 34494813..f4abcc8f 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -133,6 +133,8 @@ driver_sources = { [ 'drivers/elan.c' ], 'elanmoc' : [ 'drivers/elanmoc/elanmoc.c' ], + 'elanmoc2' : + [ 'drivers/elanmoc2/elanmoc2.c' ], 'elanspi' : [ 'drivers/elanspi.c' ], 'nb1010' : diff --git a/meson.build b/meson.build index baafa19c..9a0aaa4b 100644 --- a/meson.build +++ b/meson.build @@ -135,6 +135,7 @@ default_drivers = [ 'synaptics', 'elan', 'elanmoc', + 'elanmoc2', 'uru4000', 'upektc', 'upeksonly', diff --git a/tests/elanmoc-0c00/custom.py b/tests/elanmoc-0c00/custom.py new file mode 100644 index 00000000..3df8be70 --- /dev/null +++ b/tests/elanmoc-0c00/custom.py @@ -0,0 +1,48 @@ +#!/usr/bin/python3 + +import sys +import traceback + +import gi + +gi.require_version('FPrint', '2.0') +from gi.repository import FPrint, GLib + +# Exit with error on any exception, included those happening in async callbacks +sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1)) + +ctx = GLib.main_context_default() + +c = FPrint.Context() +c.enumerate() +devices = c.get_devices() + +d = devices[0] +del devices + +assert d.get_driver() == "elanmoc2" +assert not d.has_feature(FPrint.DeviceFeature.CAPTURE) +assert d.has_feature(FPrint.DeviceFeature.IDENTIFY) +assert d.has_feature(FPrint.DeviceFeature.VERIFY) +assert d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK) +assert d.has_feature(FPrint.DeviceFeature.STORAGE) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR) + +d.open_sync() + +# The test aims to stress the "get enrolled count" command. Some devices occasionally respond with +# an empty payload to this command and the driver should handle this gracefully by retrying the command. + +print("clearing device storage") +d.clear_storage_sync() + +print("ensuring device storage is empty") +stored = d.list_prints_sync() +assert len(stored) == 0 + +d.close_sync() +del d +del c +del ctx diff --git a/tests/elanmoc2/custom.pcapng b/tests/elanmoc2/custom.pcapng new file mode 100644 index 00000000..31477515 Binary files /dev/null and b/tests/elanmoc2/custom.pcapng differ diff --git a/tests/elanmoc2/custom.py b/tests/elanmoc2/custom.py new file mode 100644 index 00000000..345eeac5 --- /dev/null +++ b/tests/elanmoc2/custom.py @@ -0,0 +1,164 @@ +#!/usr/bin/python3 + +import traceback +import sys +import gi + +gi.require_version('FPrint', '2.0') +from gi.repository import FPrint, GLib + +# Exit with error on any exception, included those happening in async callbacks +sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1)) + +ctx = GLib.main_context_default() + +c = FPrint.Context() +c.enumerate() +devices = c.get_devices() + +d = devices[0] +del devices + +assert d.get_driver() == "elanmoc2" +assert not d.has_feature(FPrint.DeviceFeature.CAPTURE) +assert d.has_feature(FPrint.DeviceFeature.IDENTIFY) +assert d.has_feature(FPrint.DeviceFeature.VERIFY) +assert d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK) +assert d.has_feature(FPrint.DeviceFeature.STORAGE) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE) +assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR) + +d.open_sync() + +template = FPrint.Print.new(d) +template.set_finger(FPrint.Finger.LEFT_INDEX) + + +def dump_print(p: FPrint.Print): + print("Type: ", p.get_property("fpi-type")) + print("Finger: ", p.get_finger()) + print("Driver: ", p.get_driver()) + print("Device ID: ", p.get_device_id()) + print("FPI data: ", p.get_property("fpi-data")) + print("User ID: ", bytes(p.get_property("fpi-data")[1]).decode("utf-8")) + print("Description: ", p.get_description()) + print("Enroll date: ", p.get_enroll_date()) + print() + + +def enroll_progress(*args): + print("finger status: ", d.get_finger_status()) + print('enroll progress: ' + str(args)) + + +def identify_done(dev, res): + global identified + identified = True + identify_match, identify_print = dev.identify_finish(res) + print('identification done: ', identify_match, identify_print) + assert identify_match.equal(identify_print) + + +print("clearing device storage") +d.clear_storage_sync() + +print("ensuring device storage is empty") +stored = d.list_prints_sync() +assert len(stored) == 0 + +print("enrolling one finger") +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +enrolled = d.enroll_sync(template, None, enroll_progress, None) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +print("enroll done") +del template + +# Verify before listing since the device may not be in a good mood +print("verifying the enrolled finger") +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +verify_res, verify_print = d.verify_sync(enrolled) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +print(f"verify done, {verify_res}, {verify_print}") +assert verify_res + +print("ensuring device storage has the enrolled finger") +stored = d.list_prints_sync() +assert len(stored) == 1 +assert stored[0].equal(enrolled) +del enrolled +del verify_print + +print("attempting to enroll the same finger again") +template = FPrint.Print.new(d) +template.set_finger(FPrint.Finger.LEFT_INDEX) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +new_enrolled = d.enroll_sync(template, None, enroll_progress, None) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +print("enroll done") + +print("ensuring device storage has the enrolled finger") +stored = d.list_prints_sync() +assert len(stored) == 1 +assert stored[0].equal(new_enrolled) + +print("enrolling another finger") +template: FPrint.Print = FPrint.Print.new(d) +template.set_finger(FPrint.Finger.RIGHT_LITTLE) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +enrolled2 = d.enroll_sync(template, None, enroll_progress, None) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +print("enroll done") +del template + +print("verifying the enrolled finger") +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +verify_res, verify_print = d.verify_sync(enrolled2) +assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE +print("verify done") +assert verify_res +del verify_print + +print("ensuring device storage has both enrolled fingers") +stored = d.list_prints_sync() +assert len(stored) == 2 +for p in stored: + assert p.equal(new_enrolled) or p.equal(enrolled2) + +print("identifying the enrolled fingers") +identified = False +deserialized_prints = [] +for p in stored: + deserialized_prints.append(FPrint.Print.deserialize(p.serialize())) + assert deserialized_prints[-1].equal(p) +del stored +del p + +d.identify(deserialized_prints, callback=identify_done) +del deserialized_prints + +while not identified: + ctx.iteration(True) + +print("delete the first enrolled finger") +d.delete_print_sync(new_enrolled) + +print("ensuring device storage has only the second enrolled finger") +stored = d.list_prints_sync() +assert len(stored) == 1 +assert stored[0].equal(enrolled2) + +print("delete the second enrolled finger") +d.delete_print_sync(enrolled2) + +print("ensuring device storage is empty") +stored = d.list_prints_sync() +assert len(stored) == 0 + +del stored +del enrolled2 +del new_enrolled +d.close_sync() +del d +del c +del ctx diff --git a/tests/elanmoc2/device b/tests/elanmoc2/device new file mode 100644 index 00000000..25c58e05 --- /dev/null +++ b/tests/elanmoc2/device @@ -0,0 +1,259 @@ +P: /devices/pci0000:00/0000:00:14.0/usb3/3-9 +N: bus/usb/003/004=1201000200000008F3044C0C04030102000109025300010100A0320904000008FF0000000921100100012215000705810240000107050102400001070582024000010705020240000107058302400001070503024000010705840240000107050402400001 +E: BUSNUM=003 +E: CURRENT_TAGS=:seat: +E: DEVNAME=/dev/bus/usb/003/004 +E: DEVNUM=004 +E: DEVTYPE=usb_device +E: DRIVER=usb +E: ID_AUTOSUSPEND=1 +E: ID_BUS=usb +E: ID_FOR_SEAT=usb-pci-0000_00_14_0-usb-0_9 +E: ID_MODEL=ELAN:ARM-M4 +E: ID_MODEL_ENC=ELAN:ARM-M4 +E: ID_MODEL_ID=0c4c +E: ID_PATH=pci-0000:00:14.0-usb-0:9 +E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_9 +E: ID_PATH_WITH_USB_REVISION=pci-0000:00:14.0-usbv2-0:9 +E: ID_PERSIST=0 +E: ID_REVISION=0304 +E: ID_SERIAL=ELAN_ELAN:ARM-M4 +E: ID_USB_INTERFACES=:ff0000: +E: ID_USB_MODEL=ELAN:ARM-M4 +E: ID_USB_MODEL_ENC=ELAN:ARM-M4 +E: ID_USB_MODEL_ID=0c4c +E: ID_USB_REVISION=0304 +E: ID_USB_SERIAL=ELAN_ELAN:ARM-M4 +E: ID_USB_VENDOR=ELAN +E: ID_USB_VENDOR_ENC=ELAN +E: ID_USB_VENDOR_ID=04f3 +E: ID_VENDOR=ELAN +E: ID_VENDOR_ENC=ELAN +E: ID_VENDOR_FROM_DATABASE=Elan Microelectronics Corp. +E: ID_VENDOR_ID=04f3 +E: MAJOR=189 +E: MINOR=259 +E: NVME_HOST_IFACE=none +E: PRODUCT=4f3/c4c/304 +E: SUBSYSTEM=usb +E: TAGS=:seat: +E: TYPE=0/0/0 +A: authorized=1\n +A: avoid_reset_quirk=0\n +A: bConfigurationValue=1\n +A: bDeviceClass=00\n +A: bDeviceProtocol=00\n +A: bDeviceSubClass=00\n +A: bMaxPacketSize0=8\n +A: bMaxPower=100mA\n +A: bNumConfigurations=1\n +A: bNumInterfaces= 1\n +A: bcdDevice=0304\n +A: bmAttributes=a0\n +A: busnum=3\n +A: configuration= +H: descriptors=1201000200000008F3044C0C04030102000109025300010100A0320904000008FF0000000921100100012215000705810240000107050102400001070582024000010705020240000107058302400001070503024000010705840240000107050402400001 +A: dev=189:259\n +A: devnum=4\n +A: devpath=9\n +L: driver=../../../../../bus/usb/drivers/usb +L: firmware_node=../../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4e/device:4f/device:60 +A: idProduct=0c4c\n +A: idVendor=04f3\n +A: ltm_capable=no\n +A: manufacturer=ELAN\n +A: maxchild=0\n +A: physical_location/dock=no\n +A: physical_location/horizontal_position=left\n +A: physical_location/lid=no\n +A: physical_location/panel=top\n +A: physical_location/vertical_position=upper\n +L: port=../3-0:1.0/usb3-port9 +A: power/active_duration=74066\n +A: power/autosuspend=2\n +A: power/autosuspend_delay_ms=2000\n +A: power/connected_duration=15594864\n +A: power/control=auto\n +A: power/level=auto\n +A: power/persist=0\n +A: power/runtime_active_time=74210\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=15458081\n +A: power/wakeup=disabled\n +A: power/wakeup_abort_count=\n +A: power/wakeup_active=\n +A: power/wakeup_active_count=\n +A: power/wakeup_count=\n +A: power/wakeup_expire_count=\n +A: power/wakeup_last_time_ms=\n +A: power/wakeup_max_time_ms=\n +A: power/wakeup_total_time_ms=\n +A: product=ELAN:ARM-M4\n +A: quirks=0x0\n +A: removable=fixed\n +A: rx_lanes=1\n +A: speed=12\n +A: tx_lanes=1\n +A: urbnum=181\n +A: version= 2.00\n + +P: /devices/pci0000:00/0000:00:14.0/usb3 +N: bus/usb/003/001=12010002090001406B1D020008060302010109021900010100E0000904000001090000000705810304000C +E: BUSNUM=003 +E: CURRENT_TAGS=:seat: +E: DEVNAME=/dev/bus/usb/003/001 +E: DEVNUM=001 +E: DEVTYPE=usb_device +E: DRIVER=usb +E: ID_AUTOSUSPEND=1 +E: ID_BUS=usb +E: ID_FOR_SEAT=usb-pci-0000_00_14_0 +E: ID_MODEL=xHCI_Host_Controller +E: ID_MODEL_ENC=xHCI\x20Host\x20Controller +E: ID_MODEL_FROM_DATABASE=2.0 root hub +E: ID_MODEL_ID=0002 +E: ID_PATH=pci-0000:00:14.0 +E: ID_PATH_TAG=pci-0000_00_14_0 +E: ID_REVISION=0608 +E: ID_SERIAL=Linux_6.8.2-zen2-1-zen_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_SERIAL_SHORT=0000:00:14.0 +E: ID_USB_INTERFACES=:090000: +E: ID_USB_MODEL=xHCI_Host_Controller +E: ID_USB_MODEL_ENC=xHCI\x20Host\x20Controller +E: ID_USB_MODEL_ID=0002 +E: ID_USB_REVISION=0608 +E: ID_USB_SERIAL=Linux_6.8.2-zen2-1-zen_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_USB_SERIAL_SHORT=0000:00:14.0 +E: ID_USB_VENDOR=Linux_6.8.2-zen2-1-zen_xhci-hcd +E: ID_USB_VENDOR_ENC=Linux\x206.8.2-zen2-1-zen\x20xhci-hcd +E: ID_USB_VENDOR_ID=1d6b +E: ID_VENDOR=Linux_6.8.2-zen2-1-zen_xhci-hcd +E: ID_VENDOR_ENC=Linux\x206.8.2-zen2-1-zen\x20xhci-hcd +E: ID_VENDOR_FROM_DATABASE=Linux Foundation +E: ID_VENDOR_ID=1d6b +E: MAJOR=189 +E: MINOR=256 +E: PRODUCT=1d6b/2/608 +E: SUBSYSTEM=usb +E: TAGS=:seat: +E: TYPE=9/0/1 +A: authorized=1\n +A: authorized_default=1\n +A: avoid_reset_quirk=0\n +A: bConfigurationValue=1\n +A: bDeviceClass=09\n +A: bDeviceProtocol=01\n +A: bDeviceSubClass=00\n +A: bMaxPacketSize0=64\n +A: bMaxPower=0mA\n +A: bNumConfigurations=1\n +A: bNumInterfaces= 1\n +A: bcdDevice=0608\n +A: bmAttributes=e0\n +A: busnum=3\n +A: configuration= +H: descriptors=12010002090001406B1D020008060302010109021900010100E0000904000001090000000705810304000C +A: dev=189:256\n +A: devnum=1\n +A: devpath=0\n +L: driver=../../../../bus/usb/drivers/usb +L: firmware_node=../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4e/device:4f +A: idProduct=0002\n +A: idVendor=1d6b\n +A: interface_authorized_default=1\n +A: ltm_capable=no\n +A: manufacturer=Linux 6.8.2-zen2-1-zen xhci-hcd\n +A: maxchild=12\n +A: power/active_duration=15532764\n +A: power/autosuspend=0\n +A: power/autosuspend_delay_ms=0\n +A: power/connected_duration=15595619\n +A: power/control=auto\n +A: power/level=auto\n +A: power/runtime_active_time=15533289\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=0\n +A: power/wakeup=disabled\n +A: power/wakeup_abort_count=\n +A: power/wakeup_active=\n +A: power/wakeup_active_count=\n +A: power/wakeup_count=\n +A: power/wakeup_expire_count=\n +A: power/wakeup_last_time_ms=\n +A: power/wakeup_max_time_ms=\n +A: power/wakeup_total_time_ms=\n +A: product=xHCI Host Controller\n +A: quirks=0x0\n +A: removable=unknown\n +A: rx_lanes=1\n +A: serial=0000:00:14.0\n +A: speed=480\n +A: tx_lanes=1\n +A: urbnum=490\n +A: version= 2.00\n + +P: /devices/pci0000:00/0000:00:14.0 +E: DRIVER=xhci_hcd +E: ID_AUTOSUSPEND=1 +E: ID_MODEL_FROM_DATABASE=Tiger Lake-LP USB 3.2 Gen 2x1 xHCI Host Controller +E: ID_PATH=pci-0000:00:14.0 +E: ID_PATH_TAG=pci-0000_00_14_0 +E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller +E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI +E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller +E: ID_VENDOR_FROM_DATABASE=Intel Corporation +E: MODALIAS=pci:v00008086d0000A0EDsv0000103Csd000087F7bc0Csc03i30 +E: PCI_CLASS=C0330 +E: PCI_ID=8086:A0ED +E: PCI_SLOT_NAME=0000:00:14.0 +E: PCI_SUBSYS_ID=103C:87F7 +E: SUBSYSTEM=pci +A: ari_enabled=0\n +A: broken_parity_status=0\n +A: class=0x0c0330\n +H: config=8680EDA0060490022030030C000080000400263F6000000000000000000000000000000000000000000000003C10F787000000007000000000000000FF010000FD0134A089C27F8000000000000000003F6DDC0F000000000000000000000000316000000000000000000000000000000180C2C10800000000000000000000000590B7001806E0FE000000000000000009B014F01000400100000000C10A080000080E00001800008F50020000010000090000018680C00009001014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B50F210112000000 +A: consistent_dma_mask_bits=64\n +A: d3cold_allowed=1\n +A: device=0xa0ed\n +A: dma_mask_bits=64\n +L: driver=../../../bus/pci/drivers/xhci_hcd +A: driver_override=(null)\n +A: enable=1\n +L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4e +L: iommu=../../virtual/iommu/dmar3 +L: iommu_group=../../../kernel/iommu_groups/9 +A: irq=155\n +A: local_cpulist=0-7\n +A: local_cpus=ff\n +A: modalias=pci:v00008086d0000A0EDsv0000103Csd000087F7bc0Csc03i30\n +A: msi_bus=1\n +A: msi_irqs/155=msi\n +A: msi_irqs/156=msi\n +A: msi_irqs/157=msi\n +A: msi_irqs/158=msi\n +A: msi_irqs/159=msi\n +A: msi_irqs/160=msi\n +A: msi_irqs/161=msi\n +A: msi_irqs/162=msi\n +A: numa_node=-1\n +A: pools=poolinfo - 0.1\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 0 128 0\nbuffer-32 0 0 32 0\nxHCI 1KB stream ctx arrays 0 0 1024 0\nxHCI 256 byte stream ctx arrays 0 0 256 0\nxHCI input/output contexts 28 29 2112 29\nxHCI ring segments 93 93 4096 93\nbuffer-2048 0 0 2048 0\nbuffer-512 3 8 512 1\nbuffer-128 4 32 128 1\nbuffer-32 0 0 32 0\n +A: power/control=auto\n +A: power/runtime_active_time=15533652\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=0\n +A: power/wakeup=enabled\n +A: power/wakeup_abort_count=0\n +A: power/wakeup_active=0\n +A: power/wakeup_active_count=0\n +A: power/wakeup_count=0\n +A: power/wakeup_expire_count=0\n +A: power/wakeup_last_time_ms=0\n +A: power/wakeup_max_time_ms=0\n +A: power/wakeup_total_time_ms=0\n +A: power_state=D0\n +A: resource=0x000000603f260000 0x000000603f26ffff 0x0000000000140204\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n +A: revision=0x20\n +A: subsystem_device=0x87f7\n +A: subsystem_vendor=0x103c\n +A: vendor=0x8086\n + diff --git a/tests/meson.build b/tests/meson.build index 07c924be..34243bd0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -38,6 +38,7 @@ drivers_tests = [ 'elan', 'elan-cobo', 'elanmoc', + 'elanmoc2', 'elanspi', 'synaptics', 'upektc_img',