validity: Iteration 6 — Enrollment, Verification, and DB Management

Add core fingerprint operations: enrollment, verification, identification,
print listing, print deletion, and storage clearing.

New files:
- validity_db.h/c: On-chip template database operations — command builders
  for all DB commands (0x45-0x4B, 0x47-0x48, 0x51, 0x5E, 0x60, 0x62, 0x63,
  0x64, 0x68, 0x69, 0x6B), response parsers for DB info/user storage/user/
  record value/record children/new record ID, identity builder (UUID→VCSFW
  binary), finger data builder, and db_write_enable blob accessor.
- validity_enroll.c: 31-state enrollment SSM with interrupt-driven finger
  detection (EP 0x83), capture command orchestration via build_cmd_02(),
  enrollment session management (create/update/commit), DB record creation
  (user + finger), and LED glow feedback.
- validity_verify.c: 17-state verify/identify SSM with match command
  dispatching (cmd 0x5E for verify, cmd 0x60 for identify), 6-state list
  SSM for enumerating enrolled prints via GPtrArray, 8-state delete SSM,
  and clear_storage stub.

Modified files:
- validity.h: Added DB header include, 5 new state enums (CalibState,
  EnrollState, VerifyState, ListState, DeleteState), new struct fields
  for enrollment/verification/list/delete state, function declarations.
- validity.c: Replaced all operation stubs with real implementations,
  added cleanup for new fields in dev_close, wired all FpDevice methods.
- meson.build: Added 3 new source files to driver.
- tests/meson.build: Added test-validity-db executable.
- tests/validity/custom.py: Updated feature assertions (STORAGE,
  STORAGE_LIST, STORAGE_CLEAR now enabled).

Tests: 29 new unit tests in test-validity-db.c covering all command
builders, response parsers, identity/finger data builders, and blob
accessor. All 37 tests pass (0 fail, 2 skip).
This commit is contained in:
Leonardo Francisco 2026-04-06 00:51:35 -04:00
parent 1abd64a577
commit 0ba7d71d5f
10 changed files with 3530 additions and 42 deletions

View file

@ -729,6 +729,12 @@ dev_close (FpDevice *device)
g_clear_pointer (&self->cmd_response_data, g_free);
self->cmd_response_len = 0;
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
validity_user_storage_clear (&self->list_storage);
validity_capture_state_clear (&self->capture);
validity_sensor_state_clear (&self->sensor);
validity_tls_free (&self->tls);
@ -741,41 +747,13 @@ dev_close (FpDevice *device)
}
/* ================================================================
* Enroll / Verify / Identify / Delete stubs
* Enroll / Verify / Identify / Delete / List
* ================================================================
*
* These are stubs for Iteration 1. Real implementations come in
* Iteration 5 (Flash/DB Management).
* Real implementations now in validity_enroll.c and validity_verify.c.
* These thin wrappers call the external SSM starters.
*/
static void
enroll (FpDevice *device)
{
fpi_device_enroll_complete (device, NULL,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
}
static void
verify (FpDevice *device)
{
fpi_device_verify_complete (device,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
}
static void
identify (FpDevice *device)
{
fpi_device_identify_complete (device,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
}
static void
delete_print (FpDevice *device)
{
fpi_device_delete_complete (device,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
}
static void
cancel (FpDevice *device)
{
@ -811,10 +789,12 @@ fpi_device_validity_class_init (FpiDeviceValidityClass *klass)
dev_class->probe = dev_probe;
dev_class->open = dev_open;
dev_class->close = dev_close;
dev_class->enroll = enroll;
dev_class->verify = verify;
dev_class->identify = identify;
dev_class->delete = delete_print;
dev_class->enroll = validity_enroll;
dev_class->verify = validity_verify;
dev_class->identify = validity_identify;
dev_class->delete = validity_delete;
dev_class->list = validity_list;
dev_class->clear_storage = validity_clear_storage;
dev_class->cancel = cancel;
fpi_device_class_auto_initialize_features (dev_class);

View file

@ -23,6 +23,7 @@
#include "fpi-device.h"
#include "fpi-ssm.h"
#include "validity_capture.h"
#include "validity_db.h"
#include "validity_sensor.h"
#include "validity_tls.h"
@ -85,6 +86,104 @@ typedef enum {
VALIDITY_CLOSE_NUM_STATES,
} ValidityCloseState;
/* Calibration SSM states (runs during open, after capture_setup) */
typedef enum {
CALIB_BUILD_CMD = 0,
CALIB_SEND_CMD,
CALIB_SEND_CMD_RECV,
CALIB_READ_DATA,
CALIB_AVERAGE_FRAMES,
CALIB_PROCESS,
CALIB_LOOP_CHECK,
CALIB_BUILD_CLEAN_SLATE_CMD,
CALIB_CLEAN_SLATE_SEND,
CALIB_CLEAN_SLATE_RECV,
CALIB_CLEAN_SLATE_READ,
CALIB_SAVE_CLEAN_SLATE,
CALIB_DONE,
CALIB_NUM_STATES,
} ValidityCalibState;
/* Enrollment SSM states */
typedef enum {
ENROLL_START = 0,
ENROLL_START_RECV,
ENROLL_LED_ON,
ENROLL_LED_ON_RECV,
ENROLL_BUILD_CAPTURE,
ENROLL_CAPTURE_SEND,
ENROLL_CAPTURE_RECV,
ENROLL_WAIT_FINGER,
ENROLL_WAIT_SCAN_COMPLETE,
ENROLL_UPDATE_START,
ENROLL_UPDATE_START_RECV,
ENROLL_DB_WRITE_ENABLE,
ENROLL_DB_WRITE_ENABLE_RECV,
ENROLL_APPEND_IMAGE,
ENROLL_APPEND_IMAGE_RECV,
ENROLL_CLEANUPS,
ENROLL_CLEANUPS_RECV,
ENROLL_UPDATE_END,
ENROLL_UPDATE_END_RECV,
ENROLL_LOOP_CHECK,
ENROLL_DB_WRITE_ENABLE2,
ENROLL_DB_WRITE_ENABLE2_RECV,
ENROLL_CREATE_USER,
ENROLL_CREATE_USER_RECV,
ENROLL_CREATE_FINGER,
ENROLL_CREATE_FINGER_RECV,
ENROLL_FINAL_CLEANUPS,
ENROLL_FINAL_CLEANUPS_RECV,
ENROLL_LED_OFF,
ENROLL_LED_OFF_RECV,
ENROLL_DONE,
ENROLL_NUM_STATES,
} ValidityEnrollState;
/* Verify/Identify SSM states */
typedef enum {
VERIFY_LED_ON = 0,
VERIFY_LED_ON_RECV,
VERIFY_BUILD_CAPTURE,
VERIFY_CAPTURE_SEND,
VERIFY_CAPTURE_RECV,
VERIFY_WAIT_FINGER,
VERIFY_WAIT_SCAN_COMPLETE,
VERIFY_MATCH_START,
VERIFY_MATCH_START_RECV,
VERIFY_WAIT_MATCH_INT,
VERIFY_GET_RESULT,
VERIFY_GET_RESULT_RECV,
VERIFY_CLEANUP,
VERIFY_CLEANUP_RECV,
VERIFY_LED_OFF,
VERIFY_LED_OFF_RECV,
VERIFY_DONE,
VERIFY_NUM_STATES,
} ValidityVerifyState;
/* List prints SSM states */
typedef enum {
LIST_GET_STORAGE = 0,
LIST_GET_STORAGE_RECV,
LIST_GET_USER,
LIST_GET_USER_RECV,
LIST_DONE,
LIST_NUM_STATES,
} ValidityListState;
/* Delete print SSM states */
typedef enum {
DELETE_GET_STORAGE = 0,
DELETE_GET_STORAGE_RECV,
DELETE_LOOKUP_USER,
DELETE_LOOKUP_USER_RECV,
DELETE_DEL_RECORD,
DELETE_DEL_RECORD_RECV,
DELETE_DONE,
DELETE_NUM_STATES,
} ValidityDeleteState;
#define FPI_TYPE_DEVICE_VALIDITY (fpi_device_validity_get_type ())
G_DECLARE_FINAL_TYPE (FpiDeviceValidity, fpi_device_validity,
FPI, DEVICE_VALIDITY, FpDevice)
@ -109,14 +208,48 @@ struct _FpiDeviceValidity
/* Firmware extension status */
gboolean fwext_loaded;
/* Calibration state */
gboolean calibrated;
guint calib_iteration;
/* Enrollment state */
guint32 enroll_key;
guint8 *enroll_template;
gsize enroll_template_len;
guint enroll_stage;
/* Verify/identify mode flag: TRUE = identify, FALSE = verify */
gboolean identify_mode;
/* List prints state */
ValidityUserStorage list_storage;
guint list_user_idx;
/* Delete state */
guint16 delete_storage_dbid;
/* Command SSM: manages the send-cmd/recv-response cycle */
FpiSsm *cmd_ssm;
/* Open SSM: back-pointer for non-subsm child SSMs */
/* Parent SSM: back-pointer for non-subsm child SSMs */
FpiSsm *open_ssm;
/* Pending response data stashed for higher-level SSM consumption */
guint16 cmd_response_status;
guint8 *cmd_response_data;
gsize cmd_response_len;
/* Bulk data buffer (EP 0x82 reads during capture/calibration) */
guint8 *bulk_data;
gsize bulk_data_len;
};
/* Enrollment SSM (validity_enroll.c) */
void validity_enroll (FpDevice *device);
/* Verify/Identify SSMs (validity_verify.c) */
void validity_verify (FpDevice *device);
void validity_identify (FpDevice *device);
void validity_list (FpDevice *device);
void validity_delete (FpDevice *device);
void validity_clear_storage (FpDevice *device);

View file

@ -0,0 +1,793 @@
/*
* Database operations for Validity/Synaptics VCSFW sensors
*
* Implements on-chip template database management: command builders,
* response parsers, identity encoding, and finger data formatting.
*
* Reference: python-validity db.py, flash.py, sensor.py
*
* Copyright (C) 2024 libfprint 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
*/
#define FP_COMPONENT "validity"
#include "drivers_api.h"
#include "fpi-byte-reader.h"
#include "fpi-byte-utils.h"
#include "validity_db.h"
#include "vcsfw_protocol.h"
/* Include the db_write_enable blob for 009a */
#include "validity_blob_dbe_009a.inc"
/* ================================================================
* Structure cleanup helpers
* ================================================================ */
void
validity_db_info_clear (ValidityDbInfo *info)
{
g_clear_pointer (&info->roots, g_free);
memset (info, 0, sizeof (*info));
}
void
validity_user_storage_clear (ValidityUserStorage *storage)
{
g_clear_pointer (&storage->name, g_free);
g_clear_pointer (&storage->user_dbids, g_free);
g_clear_pointer (&storage->user_val_sizes, g_free);
memset (storage, 0, sizeof (*storage));
}
void
validity_user_clear (ValidityUser *user)
{
g_clear_pointer (&user->identity, g_free);
g_clear_pointer (&user->fingers, g_free);
memset (user, 0, sizeof (*user));
}
void
validity_db_record_clear (ValidityDbRecord *record)
{
g_clear_pointer (&record->value, g_free);
memset (record, 0, sizeof (*record));
}
void
validity_record_children_clear (ValidityRecordChildren *children)
{
g_clear_pointer (&children->children, g_free);
memset (children, 0, sizeof (*children));
}
/* ================================================================
* Command builders
*
* Each function allocates and returns a binary command payload.
* The caller must g_free() the result.
* ================================================================ */
/* cmd 0x45: DB info */
guint8 *
validity_db_build_cmd_info (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 1);
cmd[0] = VCSFW_CMD_DB_INFO;
*out_len = 1;
return cmd;
}
/* cmd 0x4B: Get user storage
* Format: 0x4B | dbid(2LE) | name_len(2LE) | name(NUL-terminated) */
guint8 *
validity_db_build_cmd_get_user_storage (const gchar *name,
gsize *out_len)
{
gsize name_len = 0;
gsize cmd_len;
guint8 *cmd;
if (name && name[0] != '\0')
name_len = strlen (name) + 1; /* include NUL terminator */
cmd_len = 1 + 2 + 2 + name_len;
cmd = g_new0 (guint8, cmd_len);
cmd[0] = VCSFW_CMD_GET_USER_STORAGE;
FP_WRITE_UINT16_LE (&cmd[1], 0); /* dbid = 0 (lookup by name) */
FP_WRITE_UINT16_LE (&cmd[3], name_len);
if (name_len > 0)
memcpy (&cmd[5], name, name_len);
*out_len = cmd_len;
return cmd;
}
/* cmd 0x4A: Get user by dbid
* Format: 0x4A | dbid(2LE) | 0(2LE) | 0(2LE) */
guint8 *
validity_db_build_cmd_get_user (guint16 dbid,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 7);
cmd[0] = VCSFW_CMD_GET_USER;
FP_WRITE_UINT16_LE (&cmd[1], dbid);
FP_WRITE_UINT16_LE (&cmd[3], 0);
FP_WRITE_UINT16_LE (&cmd[5], 0);
*out_len = 7;
return cmd;
}
/* cmd 0x4A: Lookup user by identity within a storage
* Format: 0x4A | 0(2LE) | storage_dbid(2LE) | identity_len(2LE) | identity */
guint8 *
validity_db_build_cmd_lookup_user (guint16 storage_dbid,
const guint8 *identity,
gsize identity_len,
gsize *out_len)
{
gsize cmd_len = 1 + 2 + 2 + 2 + identity_len;
guint8 *cmd = g_new0 (guint8, cmd_len);
cmd[0] = VCSFW_CMD_GET_USER;
FP_WRITE_UINT16_LE (&cmd[1], 0); /* dbid = 0 (lookup by identity) */
FP_WRITE_UINT16_LE (&cmd[3], storage_dbid);
FP_WRITE_UINT16_LE (&cmd[5], identity_len);
if (identity_len > 0)
memcpy (&cmd[7], identity, identity_len);
*out_len = cmd_len;
return cmd;
}
/* cmd 0x49: Get record value
* Format: 0x49 | dbid(2LE) */
guint8 *
validity_db_build_cmd_get_record_value (guint16 dbid,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 3);
cmd[0] = VCSFW_CMD_GET_RECORD_VALUE;
FP_WRITE_UINT16_LE (&cmd[1], dbid);
*out_len = 3;
return cmd;
}
/* cmd 0x46: Get record children
* Format: 0x46 | dbid(2LE) */
guint8 *
validity_db_build_cmd_get_record_children (guint16 dbid,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 3);
cmd[0] = VCSFW_CMD_GET_RECORD_CHILDREN;
FP_WRITE_UINT16_LE (&cmd[1], dbid);
*out_len = 3;
return cmd;
}
/* cmd 0x47: New record
* Format: 0x47 | parent(2LE) | type(2LE) | storage(2LE) | data_len(2LE) | data */
guint8 *
validity_db_build_cmd_new_record (guint16 parent,
guint16 type,
guint16 storage,
const guint8 *data,
gsize data_len,
gsize *out_len)
{
gsize cmd_len = 1 + 2 + 2 + 2 + 2 + data_len;
guint8 *cmd = g_new0 (guint8, cmd_len);
cmd[0] = VCSFW_CMD_NEW_RECORD;
FP_WRITE_UINT16_LE (&cmd[1], parent);
FP_WRITE_UINT16_LE (&cmd[3], type);
FP_WRITE_UINT16_LE (&cmd[5], storage);
FP_WRITE_UINT16_LE (&cmd[7], data_len);
if (data_len > 0)
memcpy (&cmd[9], data, data_len);
*out_len = cmd_len;
return cmd;
}
/* cmd 0x48: Delete record
* Format: 0x48 | dbid(2LE) */
guint8 *
validity_db_build_cmd_del_record (guint16 dbid,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 3);
cmd[0] = VCSFW_CMD_DEL_RECORD;
FP_WRITE_UINT16_LE (&cmd[1], dbid);
*out_len = 3;
return cmd;
}
/* cmd 0x1a: Call cleanups (commit pending writes) */
guint8 *
validity_db_build_cmd_call_cleanups (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 1);
cmd[0] = 0x1a;
*out_len = 1;
return cmd;
}
/* cmd 0x69: Create enrollment / End enrollment
* Format: 0x69 | flag(4LE)
* start=TRUE: flag=1, start=FALSE: flag=0 */
guint8 *
validity_db_build_cmd_create_enrollment (gboolean start,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 5);
cmd[0] = VCSFW_CMD_CREATE_ENROLLMENT;
FP_WRITE_UINT32_LE (&cmd[1], start ? 1 : 0);
*out_len = 5;
return cmd;
}
/* cmd 0x68: Enrollment update start
* Format: 0x68 | key(4LE) | 0(4LE) */
guint8 *
validity_db_build_cmd_enrollment_update_start (guint32 key,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 9);
cmd[0] = VCSFW_CMD_ENROLLMENT_UPDATE_START;
FP_WRITE_UINT32_LE (&cmd[1], key);
FP_WRITE_UINT32_LE (&cmd[5], 0);
*out_len = 9;
return cmd;
}
/* cmd 0x6B: Enrollment update (with template data)
* Format: 0x6B | prev_data */
guint8 *
validity_db_build_cmd_enrollment_update (const guint8 *prev_data,
gsize prev_len,
gsize *out_len)
{
gsize cmd_len = 1 + prev_len;
guint8 *cmd = g_new0 (guint8, cmd_len);
cmd[0] = VCSFW_CMD_ENROLLMENT_UPDATE;
if (prev_len > 0)
memcpy (&cmd[1], prev_data, prev_len);
*out_len = cmd_len;
return cmd;
}
/* cmd 0x51: Get program status
* Format: 0x51 | flags(4bytes)
* extended=FALSE: 00000000, extended=TRUE: 00200000 */
guint8 *
validity_db_build_cmd_get_prg_status (gboolean extended,
gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 5);
cmd[0] = VCSFW_CMD_GET_PRG_STATUS;
if (extended)
cmd[2] = 0x20; /* 0x00200000 LE = 00 00 20 00 */
*out_len = 5;
return cmd;
}
/* cmd 0x04: Capture stop */
guint8 *
validity_db_build_cmd_capture_stop (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 1);
cmd[0] = VCSFW_CMD_CAPTURE_STOP;
*out_len = 1;
return cmd;
}
/* cmd 0x5E: Match finger
* Format: 0x5E | type(1) | 0xFF | stg_id(2LE) | usr_id(2LE) | 1(2LE) | 0(2LE) | 0(2LE)
* type=2 matches against any storage/user */
guint8 *
validity_db_build_cmd_match_finger (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 12);
cmd[0] = VCSFW_CMD_MATCH_FINGER;
cmd[1] = 0x02; /* match type */
cmd[2] = 0xFF; /* match against all subtypes */
FP_WRITE_UINT16_LE (&cmd[3], 0); /* stg_id = any */
FP_WRITE_UINT16_LE (&cmd[5], 0); /* usr_id = any */
FP_WRITE_UINT16_LE (&cmd[7], 1); /* unknown, always 1 */
FP_WRITE_UINT16_LE (&cmd[9], 0);
/* cmd[11] already 0 from g_new0, total = 12 bytes
* but python-validity uses 11 bytes: pack('<BBBHHHHH', ...) = 1+1+1+2+2+2+2+2 = 13
* Actually: B(1)+B(1)+B(1)+H(2)+H(2)+H(2)+H(2)+H(2) = 13 */
/* Correct: recalculate per python-validity format */
g_free (cmd);
cmd = g_new0 (guint8, 13);
cmd[0] = VCSFW_CMD_MATCH_FINGER;
cmd[1] = 0x02;
cmd[2] = 0xFF;
FP_WRITE_UINT16_LE (&cmd[3], 0); /* stg_id */
FP_WRITE_UINT16_LE (&cmd[5], 0); /* usr_id */
FP_WRITE_UINT16_LE (&cmd[7], 1);
FP_WRITE_UINT16_LE (&cmd[9], 0);
FP_WRITE_UINT16_LE (&cmd[11], 0);
*out_len = 13;
return cmd;
}
/* cmd 0x60: Get match result
* Format: 0x60 | 00000000 */
guint8 *
validity_db_build_cmd_get_match_result (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 5);
cmd[0] = VCSFW_CMD_GET_MATCH_RESULT;
/* remaining 4 bytes are 0 */
*out_len = 5;
return cmd;
}
/* cmd 0x62: Match cleanup
* Format: 0x62 | 00000000 */
guint8 *
validity_db_build_cmd_match_cleanup (gsize *out_len)
{
guint8 *cmd = g_new0 (guint8, 5);
cmd[0] = VCSFW_CMD_MATCH_CLEANUP;
/* remaining 4 bytes are 0 */
*out_len = 5;
return cmd;
}
/* ================================================================
* Response parsers
*
* All data parameters point PAST the 2-byte status.
* ================================================================ */
/* Parse DB info response (cmd 0x45) */
gboolean
validity_db_parse_info (const guint8 *data,
gsize data_len,
ValidityDbInfo *out)
{
FpiByteReader reader;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: unknown1(4LE) unknown0(4LE) total(4LE) used(4LE) free(4LE) records(2LE) nroots(2LE) = 0x18 bytes */
if (data_len < 0x18)
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->unknown1))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->unknown0))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->total))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->used))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->free_space))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->records))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->n_roots))
return FALSE;
/* Root record IDs */
if (out->n_roots > 0)
{
out->roots = g_new0 (guint16, out->n_roots);
for (guint16 i = 0; i < out->n_roots; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->roots[i]))
{
validity_db_info_clear (out);
return FALSE;
}
}
}
return TRUE;
}
/* Parse user storage response (cmd 0x4B)
* Format: recid(2LE) usercnt(2LE) namesz(2LE) unknown(2LE)
* usrtab[usercnt * 4 bytes] name[namesz bytes] */
gboolean
validity_db_parse_user_storage (const guint8 *data,
gsize data_len,
ValidityUserStorage *out)
{
FpiByteReader reader;
guint16 name_sz, unknown;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 8 bytes */
if (data_len < 8)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->user_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &name_sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &unknown))
return FALSE;
/* User table: 4 bytes per entry (dbid(2LE) + value_size(2LE)) */
if (out->user_count > 0)
{
out->user_dbids = g_new0 (guint16, out->user_count);
out->user_val_sizes = g_new0 (guint16, out->user_count);
for (guint16 i = 0; i < out->user_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->user_dbids[i]) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->user_val_sizes[i]))
{
validity_user_storage_clear (out);
return FALSE;
}
}
}
/* Name */
if (name_sz > 0)
{
const guint8 *name_data;
if (!fpi_byte_reader_get_data (&reader, name_sz, &name_data))
{
validity_user_storage_clear (out);
return FALSE;
}
out->name = g_strndup ((const gchar *) name_data, name_sz);
}
return TRUE;
}
/* Parse user response (cmd 0x4A)
* Format: recid(2LE) fingercnt(2LE) unknown(2LE) identitysz(2LE)
* fingertab[8 * fingercnt] identity[identitysz] */
gboolean
validity_db_parse_user (const guint8 *data,
gsize data_len,
ValidityUser *out)
{
FpiByteReader reader;
guint16 unknown, identity_sz;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 8 bytes */
if (data_len < 8)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->finger_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &unknown))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &identity_sz))
return FALSE;
/* Finger table: 8 bytes per entry */
if (out->finger_count > 0)
{
out->fingers = g_new0 (ValidityFingerEntry, out->finger_count);
for (guint16 i = 0; i < out->finger_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].dbid) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].subtype) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].storage) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].value_size))
{
validity_user_clear (out);
return FALSE;
}
}
}
/* Identity bytes */
if (identity_sz > 0)
{
const guint8 *id_data;
if (!fpi_byte_reader_get_data (&reader, identity_sz, &id_data))
{
validity_user_clear (out);
return FALSE;
}
out->identity = g_memdup2 (id_data, identity_sz);
out->identity_len = identity_sz;
}
return TRUE;
}
/* Parse record value response (cmd 0x49)
* Format: dbid(2LE) type(2LE) storage(2LE) sz(2LE) pad(2bytes) value[sz] */
gboolean
validity_db_parse_record_value (const guint8 *data,
gsize data_len,
ValidityDbRecord *out)
{
FpiByteReader reader;
guint16 sz;
guint16 pad;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 10 bytes (python-validity: unpack('<xxHHHHxx', rsp[:12])
* but our data is already past status, so: HHHHxx = 10 bytes) */
if (data_len < 10)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->type))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->storage))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &pad))
return FALSE;
/* Value data */
if (sz > 0)
{
const guint8 *val_data;
if (!fpi_byte_reader_get_data (&reader, sz, &val_data))
{
validity_db_record_clear (out);
return FALSE;
}
out->value = g_memdup2 (val_data, sz);
out->value_len = sz;
}
return TRUE;
}
/* Parse record children response (cmd 0x46)
* Format: dbid(2LE) type(2LE) storage(2LE) sz(2LE) cnt(2LE) pad(2bytes)
* children[cnt * 4 bytes: dbid(2LE) type(2LE)] */
gboolean
validity_db_parse_record_children (const guint8 *data,
gsize data_len,
ValidityRecordChildren *out)
{
FpiByteReader reader;
guint16 sz, pad;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 12 bytes (python-validity: unpack('<xxHHHHHxx', rsp[:14])
* past status: HHHH H xx = 12 bytes) */
if (data_len < 12)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->type))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->storage))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->child_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &pad))
return FALSE;
/* Child entries */
if (out->child_count > 0)
{
out->children = g_new0 (ValidityRecordChild, out->child_count);
for (guint16 i = 0; i < out->child_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->children[i].dbid) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->children[i].type))
{
validity_record_children_clear (out);
return FALSE;
}
}
}
return TRUE;
}
/* Parse new record response (cmd 0x47)
* Format: record_id(2LE) */
gboolean
validity_db_parse_new_record_id (const guint8 *data,
gsize data_len,
guint16 *out_record_id)
{
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out_record_id != NULL, FALSE);
if (data_len < 2)
return FALSE;
*out_record_id = FP_READ_UINT16_LE (data);
return TRUE;
}
/* ================================================================
* Identity helpers
* ================================================================ */
/* Build a UUID-based identity for the sensor DB.
*
* python-validity uses Windows SIDs. For libfprint we use a UUID
* encoded as a type-3 (SID-type) identity with zero-padding to
* the Windows union minimum size of 0x4C bytes.
*
* Format: type(4LE) | len(4LE) | uuid_bytes(len) | padding */
guint8 *
validity_db_build_identity (const gchar *uuid_str,
gsize *out_len)
{
gsize uuid_len;
gsize payload_len;
gsize total_len;
guint8 *buf;
g_return_val_if_fail (uuid_str != NULL, NULL);
uuid_len = strlen (uuid_str);
/* type(4) + len(4) + uuid bytes */
payload_len = 4 + 4 + uuid_len;
/* Pad to minimum identity size as python-validity does */
total_len = MAX (payload_len, VALIDITY_IDENTITY_MIN_SIZE);
buf = g_new0 (guint8, total_len);
FP_WRITE_UINT32_LE (&buf[0], VALIDITY_IDENTITY_TYPE_SID);
FP_WRITE_UINT32_LE (&buf[4], uuid_len);
memcpy (&buf[8], uuid_str, uuid_len);
*out_len = total_len;
return buf;
}
/* Build finger data for new_finger (from python-validity make_finger_data)
*
* Format:
* subtype(2LE) | flags=3(2LE) | tinfo_len(2LE) | 0x20(2LE)
* tag1=1(2LE) | len(2LE) | template_data
* tag2=2(2LE) | len(2LE) | tid
* padding[0x20 bytes] */
guint8 *
validity_db_build_finger_data (guint16 subtype,
const guint8 *template_data,
gsize template_len,
const guint8 *tid,
gsize tid_len,
gsize *out_len)
{
gsize template_block = 4 + template_len; /* tag(2) + len(2) + data */
gsize tid_block = 4 + tid_len;
gsize tinfo_len = template_block + tid_block;
gsize header_len = 8; /* subtype(2) + flags(2) + tinfo_len(2) + 0x20(2) */
gsize total = header_len + tinfo_len + 0x20; /* + padding */
guint8 *buf = g_new0 (guint8, total);
gsize pos = 0;
/* Header */
FP_WRITE_UINT16_LE (&buf[pos], subtype);
pos += 2;
FP_WRITE_UINT16_LE (&buf[pos], 3); /* flags */
pos += 2;
FP_WRITE_UINT16_LE (&buf[pos], tinfo_len);
pos += 2;
FP_WRITE_UINT16_LE (&buf[pos], 0x20);
pos += 2;
/* Template block */
FP_WRITE_UINT16_LE (&buf[pos], 1); /* tag */
pos += 2;
FP_WRITE_UINT16_LE (&buf[pos], template_len);
pos += 2;
if (template_len > 0)
memcpy (&buf[pos], template_data, template_len);
pos += template_len;
/* TID block */
FP_WRITE_UINT16_LE (&buf[pos], 2); /* tag */
pos += 2;
FP_WRITE_UINT16_LE (&buf[pos], tid_len);
pos += 2;
if (tid_len > 0)
memcpy (&buf[pos], tid, tid_len);
pos += tid_len;
/* Remaining 0x20 bytes are zero-filled from g_new0 */
*out_len = total;
return buf;
}
/* ================================================================
* db_write_enable blob access
* ================================================================ */
const guint8 *
validity_db_get_write_enable_blob (gsize *out_len)
{
*out_len = sizeof (db_write_enable_009a);
return db_write_enable_009a;
}

View file

@ -0,0 +1,285 @@
/*
* Database operations for Validity/Synaptics VCSFW sensors
*
* Implements on-chip template database management: listing users,
* storing/deleting fingerprint records, and db_write_enable.
*
* The sensor has a hierarchical record DB stored on flash partition 4:
* Storage User Finger Data
*
* Reference: python-validity db.py, flash.py
*
* Copyright (C) 2024 libfprint 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
#include <glib.h>
/* ================================================================
* Database record types (from python-validity observation)
* ================================================================ */
#define VALIDITY_DB_RECORD_TYPE_STORAGE 4
#define VALIDITY_DB_RECORD_TYPE_USER 5
#define VALIDITY_DB_RECORD_TYPE_FINGER 6
#define VALIDITY_DB_RECORD_TYPE_DATA 8
/* Identity type used in record payloads */
#define VALIDITY_IDENTITY_TYPE_SID 3
/* Minimum size for encoded identity (Windows union minimum = 0x4c) */
#define VALIDITY_IDENTITY_MIN_SIZE 0x4C
/* Storage name used by the sensor's DB */
#define VALIDITY_STORAGE_NAME "StgWindsor"
/* Flash partition for calibration data */
#define VALIDITY_FLASH_CALIBRATION_PARTITION 6
/* Clean slate flash header magic */
#define VALIDITY_CLEAN_SLATE_MAGIC 0x5002
/* ================================================================
* DB Info returned by cmd 0x45
* ================================================================ */
typedef struct
{
guint32 unknown1; /* Always 1 */
guint32 unknown0; /* Always 0 */
guint32 total; /* Partition size */
guint32 used; /* Used (not deleted) */
guint32 free_space; /* Unallocated space */
guint16 records; /* Total number, including deleted */
guint16 n_roots; /* Number of root records */
guint16 *roots; /* Root record IDs (owned, g_free) */
} ValidityDbInfo;
/* ================================================================
* User Storage returned by cmd 0x4B
* Represents a named storage container that holds users.
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 user_count;
gchar *name; /* owned, g_free */
/* Per-user entries: array of {dbid, value_size} pairs */
guint16 *user_dbids;
guint16 *user_val_sizes;
} ValidityUserStorage;
/* ================================================================
* Finger record entry part of a User record
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 subtype; /* WINBIO finger constant (1-10) */
guint16 storage;
guint16 value_size;
} ValidityFingerEntry;
/* ================================================================
* User returned by cmd 0x4A
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 finger_count;
guint8 *identity; /* Raw identity bytes, owned */
gsize identity_len;
ValidityFingerEntry *fingers; /* owned array of finger_count entries */
} ValidityUser;
/* ================================================================
* DB Record returned by cmd 0x49 (get_record_value)
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 type;
guint16 storage;
guint8 *value; /* owned, g_free */
gsize value_len;
} ValidityDbRecord;
/* ================================================================
* Record child entry from cmd 0x46 (get_record_children)
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 type;
} ValidityRecordChild;
typedef struct
{
guint16 dbid;
guint16 type;
guint16 storage;
guint16 child_count;
ValidityRecordChild *children; /* owned array */
} ValidityRecordChildren;
/* ================================================================
* Command builders produce binary TLS command payloads
*
* All returned buffers are g_malloc'd and must be g_free'd by caller.
* ================================================================ */
/* cmd 0x45: DB info */
guint8 *validity_db_build_cmd_info (gsize *out_len);
/* cmd 0x4B: Get user storage by name */
guint8 *validity_db_build_cmd_get_user_storage (const gchar *name,
gsize *out_len);
/* cmd 0x4A: Get user by dbid */
guint8 *validity_db_build_cmd_get_user (guint16 dbid,
gsize *out_len);
/* cmd 0x4A: Lookup user by identity within a storage */
guint8 *validity_db_build_cmd_lookup_user (guint16 storage_dbid,
const guint8 *identity,
gsize identity_len,
gsize *out_len);
/* cmd 0x49: Get record value */
guint8 *validity_db_build_cmd_get_record_value (guint16 dbid,
gsize *out_len);
/* cmd 0x46: Get record children */
guint8 *validity_db_build_cmd_get_record_children (guint16 dbid,
gsize *out_len);
/* cmd 0x47: New record */
guint8 *validity_db_build_cmd_new_record (guint16 parent,
guint16 type,
guint16 storage,
const guint8 *data,
gsize data_len,
gsize *out_len);
/* cmd 0x48: Delete record */
guint8 *validity_db_build_cmd_del_record (guint16 dbid,
gsize *out_len);
/* cmd 0x1a: Call cleanups (commit pending writes) */
guint8 *validity_db_build_cmd_call_cleanups (gsize *out_len);
/* ================================================================
* Response parsers parse binary TLS response payloads
*
* All parse functions take data AFTER the 2-byte status has been stripped.
* Return TRUE on success, FALSE on parse error.
* ================================================================ */
gboolean validity_db_parse_info (const guint8 *data,
gsize data_len,
ValidityDbInfo *out);
gboolean validity_db_parse_user_storage (const guint8 *data,
gsize data_len,
ValidityUserStorage *out);
gboolean validity_db_parse_user (const guint8 *data,
gsize data_len,
ValidityUser *out);
gboolean validity_db_parse_record_value (const guint8 *data,
gsize data_len,
ValidityDbRecord *out);
gboolean validity_db_parse_record_children (const guint8 *data,
gsize data_len,
ValidityRecordChildren *out);
/* cmd 0x47 response: parse new record ID */
gboolean validity_db_parse_new_record_id (const guint8 *data,
gsize data_len,
guint16 *out_record_id);
/* ================================================================
* Identity helpers
* ================================================================ */
/* Build a UUID-based identity suitable for the sensor DB.
* Returns a buffer of at least VALIDITY_IDENTITY_MIN_SIZE bytes. */
guint8 *validity_db_build_identity (const gchar *uuid_str,
gsize *out_len);
/* Build finger data payload for new_finger (format from make_finger_data) */
guint8 *validity_db_build_finger_data (guint16 subtype,
const guint8 *template_data,
gsize template_len,
const guint8 *tid,
gsize tid_len,
gsize *out_len);
/* ================================================================
* Structure cleanup helpers
* ================================================================ */
void validity_db_info_clear (ValidityDbInfo *info);
void validity_user_storage_clear (ValidityUserStorage *storage);
void validity_user_clear (ValidityUser *user);
void validity_db_record_clear (ValidityDbRecord *record);
void validity_record_children_clear (ValidityRecordChildren *children);
/* ================================================================
* Enrollment commands
* ================================================================ */
/* cmd 0x69: Create enrollment (start) / End enrollment */
guint8 *validity_db_build_cmd_create_enrollment (gboolean start,
gsize *out_len);
/* cmd 0x68: Enrollment update start */
guint8 *validity_db_build_cmd_enrollment_update_start (guint32 key,
gsize *out_len);
/* cmd 0x6B: Enrollment update (with template data) */
guint8 *validity_db_build_cmd_enrollment_update (const guint8 *prev_data,
gsize prev_len,
gsize *out_len);
/* cmd 0x51: Get program status */
guint8 *validity_db_build_cmd_get_prg_status (gboolean extended,
gsize *out_len);
/* cmd 0x04: Capture stop */
guint8 *validity_db_build_cmd_capture_stop (gsize *out_len);
/* ================================================================
* Match commands
* ================================================================ */
/* cmd 0x5E: Match finger */
guint8 *validity_db_build_cmd_match_finger (gsize *out_len);
/* cmd 0x60: Get match result */
guint8 *validity_db_build_cmd_get_match_result (gsize *out_len);
/* cmd 0x62: Match cleanup */
guint8 *validity_db_build_cmd_match_cleanup (gsize *out_len);
/* ================================================================
* db_write_enable blob access
* ================================================================ */
const guint8 *validity_db_get_write_enable_blob (gsize *out_len);

View file

@ -0,0 +1,741 @@
/*
* Enrollment state machine for Validity/Synaptics VCSFW sensors
*
* Implements the FpDevice::enroll virtual method. The enrollment flow:
* 1. Create enrollment session on sensor (cmd 0x69)
* 2. Loop VALIDITY_ENROLL_STAGES times:
* a. LED on, build capture cmd, send via TLS
* b. Wait for finger-down interrupt (EP 0x83)
* c. Wait for scan-complete interrupt
* d. Run enrollment_update_start (cmd 0x68)
* e. Send db_write_enable, enrollment_update (cmd 0x6B), cleanups
* f. Parse response for template/header/tid
* g. Report progress, enrollment_update_end (cmd 0x69 flag=0)
* 3. When tid is returned: store user + finger in DB
* 4. LED off, report FpPrint to libfprint
*
* Reference: python-validity sensor.py Sensor.enroll()
*
* Copyright (C) 2024 libfprint 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
*/
#define FP_COMPONENT "validity"
#include "drivers_api.h"
#include "fpi-byte-reader.h"
#include "fpi-byte-utils.h"
#include "fpi-print.h"
#include "validity.h"
#include "vcsfw_protocol.h"
/* Magic length for enrollment response parsing (hardcoded in DLL) */
#define ENROLLMENT_MAGIC_LEN 0x38
/* ================================================================
* Interrupt helpers read from EP 0x83
* ================================================================ */
static void
interrupt_cb (FpiUsbTransfer *transfer,
FpDevice *device,
gpointer user_data,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (device);
FpiSsm *ssm = user_data;
guint8 int_type;
int target_state = GPOINTER_TO_INT (fpi_ssm_get_data (ssm));
if (error)
{
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_REMOVED));
g_error_free (error);
return;
}
fpi_ssm_mark_failed (ssm, error);
return;
}
if (transfer->actual_length < 1)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
int_type = transfer->buffer[0];
fp_dbg ("Interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
/* Check if this is the interrupt we're waiting for */
if (int_type == (guint8) target_state)
{
/* Check scan-complete bit if waiting for type 3 */
if (int_type == 3 && transfer->actual_length >= 3)
{
if (!(transfer->buffer[2] & VALIDITY_INT_SCAN_COMPLETE))
{
/* Not scan complete yet, keep waiting */
goto read_again;
}
}
fpi_ssm_next_state (ssm);
return;
}
/* Type 0 = capture started, type 3 without scan_complete = in progress */
if (int_type == 0 || int_type == 3)
goto read_again;
/* Unexpected interrupt type, keep listening */
fp_dbg ("Ignoring unexpected interrupt type 0x%02x", int_type);
read_again:
{
FpiUsbTransfer *new_transfer = fpi_usb_transfer_new (device);
fpi_usb_transfer_fill_interrupt (new_transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (new_transfer, VALIDITY_USB_TIMEOUT,
self->interrupt_cancellable,
interrupt_cb, ssm);
}
}
static void
start_interrupt_wait (FpiDeviceValidity *self,
FpiSsm *ssm,
guint8 target_type)
{
FpiUsbTransfer *transfer;
/* Store the target interrupt type in ssm data temporarily.
* We'll restore it after the wait. */
fpi_ssm_set_data (ssm, GINT_TO_POINTER ((int) target_type), NULL);
transfer = fpi_usb_transfer_new (FP_DEVICE (self));
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT,
self->interrupt_cancellable,
interrupt_cb, ssm);
}
/* ================================================================
* Enrollment response parsing
*
* The enrollment_update (cmd 0x6B) response contains tagged fields:
* While data remains:
* tag(2LE) | len(2LE) | data[MAGIC_LEN + len]
* tag 0 template, tag 1 header, tag 3 tid (enrollment complete)
* ================================================================ */
typedef struct
{
guint8 *header;
gsize header_len;
guint8 *template_data;
gsize template_len;
guint8 *tid;
gsize tid_len;
} EnrollmentUpdateResult;
static void
enrollment_update_result_clear (EnrollmentUpdateResult *r)
{
g_clear_pointer (&r->header, g_free);
g_clear_pointer (&r->template_data, g_free);
g_clear_pointer (&r->tid, g_free);
memset (r, 0, sizeof (*r));
}
static gboolean
parse_enrollment_update_response (const guint8 *data,
gsize data_len,
EnrollmentUpdateResult *result)
{
gsize pos = 0;
memset (result, 0, sizeof (*result));
while (pos + 4 <= data_len)
{
guint16 tag = FP_READ_UINT16_LE (&data[pos]);
guint16 len = FP_READ_UINT16_LE (&data[pos + 2]);
gsize block_size = ENROLLMENT_MAGIC_LEN + len;
if (pos + block_size > data_len)
break;
if (tag == 0)
{
/* Template: first MAGIC_LEN + len bytes */
result->template_data = g_memdup2 (&data[pos], block_size);
result->template_len = block_size;
}
else if (tag == 1)
{
/* Header: data after MAGIC_LEN */
if (len > 0)
{
result->header = g_memdup2 (&data[pos + ENROLLMENT_MAGIC_LEN], len);
result->header_len = len;
}
}
else if (tag == 3)
{
/* TID: data after MAGIC_LEN — enrollment is complete */
if (len > 0)
{
result->tid = g_memdup2 (&data[pos + ENROLLMENT_MAGIC_LEN], len);
result->tid_len = len;
}
}
pos += block_size;
}
return TRUE;
}
/* ================================================================
* Enrollment SSM
* ================================================================ */
static void
enroll_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
switch (fpi_ssm_get_cur_state (ssm))
{
case ENROLL_START:
{
/* cmd 0x69 flag=1: create enrollment session */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (TRUE, &cmd_len);
self->enroll_key = 0;
self->enroll_stage = 0;
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_START_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("create_enrollment failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_LED_ON:
{
gsize cmd_len;
const guint8 *cmd = validity_capture_glow_start_cmd (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case ENROLL_LED_ON_RECV:
/* Glow start doesn't need status check (best effort) */
fpi_ssm_next_state (ssm);
break;
case ENROLL_BUILD_CAPTURE:
{
gsize cmd_len;
guint8 *cmd = validity_capture_build_cmd_02 (&self->capture,
self->sensor.type_info,
VALIDITY_CAPTURE_ENROLL,
&cmd_len);
if (!cmd)
{
fp_warn ("Failed to build enroll capture command");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CAPTURE_SEND:
/* Capture command sent, now handled in RECV */
fpi_ssm_next_state (ssm);
break;
case ENROLL_CAPTURE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("Capture command failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Now wait for finger-down interrupt */
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_WAIT_FINGER:
/* Wait for interrupt type 2 (finger down) */
start_interrupt_wait (self, ssm, VALIDITY_INT_FINGER_DOWN);
break;
case ENROLL_WAIT_SCAN_COMPLETE:
/* Wait for interrupt type 3 with scan_complete bit */
start_interrupt_wait (self, ssm, 3);
break;
case ENROLL_UPDATE_START:
{
/* cmd 0x68: enrollment_update_start(key) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_enrollment_update_start (
self->enroll_key, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_UPDATE_START_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("enrollment_update_start failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Response: new_key(4LE) */
if (self->cmd_response_data && self->cmd_response_len >= 4)
self->enroll_key = FP_READ_UINT32_LE (self->cmd_response_data);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_DB_WRITE_ENABLE:
{
/* Send db_write_enable blob before enrollment_update */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (&blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
}
break;
case ENROLL_DB_WRITE_ENABLE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("db_write_enable failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_APPEND_IMAGE:
{
/* cmd 0x6B: enrollment_update with current template */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_enrollment_update (
self->enroll_template, self->enroll_template_len, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_APPEND_IMAGE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("enrollment_update failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Parse the enrollment update response */
if (self->cmd_response_data && self->cmd_response_len > 0)
{
EnrollmentUpdateResult result;
if (parse_enrollment_update_response (self->cmd_response_data,
self->cmd_response_len,
&result))
{
/* Update template for next iteration */
g_clear_pointer (&self->enroll_template, g_free);
if (result.template_data)
{
self->enroll_template = g_steal_pointer (&result.template_data);
self->enroll_template_len = result.template_len;
}
/* If tid is present, enrollment is complete */
if (result.tid)
{
/* Store tid for finger creation */
/* tid stays in enroll_template context — we'll
* build finger data in the commit phase */
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data = g_steal_pointer (&result.tid);
self->bulk_data_len = result.tid_len;
}
enrollment_update_result_clear (&result);
}
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CLEANUPS:
{
/* cmd 0x1a: call_cleanups after db_write_enable + enrollment_update */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CLEANUPS_RECV:
{
/* Status 0x0491 = nothing to commit, which is OK */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
{
fp_warn ("call_cleanups failed: status=0x%04x",
self->cmd_response_status);
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_UPDATE_END:
{
/* cmd 0x69 flag=0: enrollment_update_end */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (FALSE, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_UPDATE_END_RECV:
fpi_ssm_next_state (ssm);
break;
case ENROLL_LOOP_CHECK:
{
self->enroll_stage++;
/* Report progress */
fpi_device_enroll_progress (dev, self->enroll_stage, NULL, NULL);
fp_info ("Enrollment stage %u/%u", self->enroll_stage,
VALIDITY_ENROLL_STAGES);
/* If we have a TID, enrollment is complete — go to DB commit */
if (self->bulk_data && self->bulk_data_len > 0)
{
fpi_ssm_jump_to_state (ssm, ENROLL_DB_WRITE_ENABLE2);
return;
}
/* If we reached max stages without TID, that's an error */
if (self->enroll_stage >= VALIDITY_ENROLL_STAGES)
{
fp_warn ("Enrollment did not complete within %u stages",
VALIDITY_ENROLL_STAGES);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
/* Loop back for next stage */
fpi_ssm_jump_to_state (ssm, ENROLL_LED_ON);
}
break;
case ENROLL_DB_WRITE_ENABLE2:
{
/* Enable DB writes for storing the finger record */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (&blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
}
break;
case ENROLL_DB_WRITE_ENABLE2_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("db_write_enable for finger creation failed: 0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* TODO: In a full implementation, we'd look up/create user here.
* For now, we create a new user record with a UUID identity. */
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CREATE_USER:
{
/* Create user with UUID identity via cmd 0x47 (new_record).
* First need to get the storage to use as parent. This requires
* a command exchange. For simplicity, we use the storage dbid
* that was part of the open-time DB init. */
/* Build identity from the print's driver data (UUID) */
FpPrint *print = NULL;
g_autofree gchar *user_id = NULL;
g_autofree guint8 *identity = NULL;
gsize identity_len;
fpi_device_get_enroll_data (dev, &print);
identity = validity_db_build_identity (user_id, &identity_len);
/* Store user_id in print for later retrieval */
GVariant *data = g_variant_new_string (user_id);
g_object_set_data_full (G_OBJECT (print), "validity-user-id",
g_variant_ref_sink (data),
(GDestroyNotify) g_variant_unref);
/* cmd 0x47: new_record(parent=storage_dbid, type=5=user, storage=storage_dbid, data=identity) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_new_record (
3, /* root storage dbid (standard for StgWindsor) */
VALIDITY_DB_RECORD_TYPE_USER,
3, /* storage */
identity, identity_len,
&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CREATE_USER_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("create user failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Parse the new user record ID — stash for finger creation */
guint16 user_dbid;
if (self->cmd_response_data &&
validity_db_parse_new_record_id (self->cmd_response_data,
self->cmd_response_len,
&user_dbid))
{
fp_info ("Created user record: dbid=%u", user_dbid);
/* Store user_dbid for finger creation.
* Reuse cmd_response_status field temporarily. */
self->delete_storage_dbid = user_dbid;
}
else
{
fp_warn ("Failed to parse new user record ID");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CREATE_FINGER:
{
FpPrint *print = NULL;
FpFinger finger;
fpi_device_get_enroll_data (dev, &print);
finger = fp_print_get_finger (print);
guint16 subtype = validity_finger_to_subtype (finger);
guint16 user_dbid = self->delete_storage_dbid;
/* Build finger data from template + tid */
gsize finger_data_len;
guint8 *finger_data = validity_db_build_finger_data (
subtype,
self->enroll_template, self->enroll_template_len,
self->bulk_data, self->bulk_data_len,
&finger_data_len);
/* cmd 0x47: new_record(parent=user, type=0x0b, storage=3, data=finger_data)
* python-validity: type 0xb becomes 0x6 due to db_write_enable magic */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_new_record (
user_dbid,
0x0b, /* finger type: becomes 0x06 after db_write_enable */
3, /* storage */
finger_data, finger_data_len,
&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
g_free (finger_data);
}
break;
case ENROLL_CREATE_FINGER_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("create finger failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
guint16 finger_dbid;
if (self->cmd_response_data &&
validity_db_parse_new_record_id (self->cmd_response_data,
self->cmd_response_len,
&finger_dbid))
fp_info ("Created finger record: dbid=%u", finger_dbid);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_FINAL_CLEANUPS:
{
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_FINAL_CLEANUPS_RECV:
fpi_ssm_next_state (ssm);
break;
case ENROLL_LED_OFF:
{
gsize cmd_len;
const guint8 *cmd = validity_capture_glow_end_cmd (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case ENROLL_LED_OFF_RECV:
fpi_ssm_next_state (ssm);
break;
case ENROLL_DONE:
fpi_ssm_mark_completed (ssm);
break;
}
}
static void
enroll_ssm_done (FpiSsm *ssm,
FpDevice *dev,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
if (error)
{
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
fpi_device_enroll_complete (dev, NULL, error);
return;
}
/* Build FpPrint for the enrolled finger */
FpPrint *print = NULL;
fpi_device_get_enroll_data (dev, &print);
/* Set the print metadata */
fpi_print_set_type (print, FPI_PRINT_RAW);
fpi_print_set_device_stored (print, TRUE);
/* Store the user ID as driver data for later verify/identify */
GVariant *user_id_var = g_object_get_data (G_OBJECT (print),
"validity-user-id");
if (user_id_var)
{
GDate *date = g_date_new ();
g_date_set_time_t (date, time (NULL));
fp_print_set_enroll_date (print, date);
g_date_free (date);
}
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
fpi_device_enroll_complete (dev, g_object_ref (print), NULL);
}
void
validity_enroll (FpDevice *device)
{
FpiSsm *ssm;
G_DEBUG_HERE ();
ssm = fpi_ssm_new (device, enroll_run_state, ENROLL_NUM_STATES);
fpi_ssm_start (ssm, enroll_ssm_done);
}

View file

@ -0,0 +1,791 @@
/*
* Verify and Identify state machines for Validity/Synaptics VCSFW sensors
*
* Implements FpDevice::verify and FpDevice::identify virtual methods.
*
* Both operations share the same state machine since the sensor-side
* flow is nearly identical:
* 1. LED on
* 2. Send capture command (IDENTIFY mode)
* 3. Wait for finger-down interrupt (EP 0x83)
* 4. Wait for scan-complete interrupt
* 5. Start match (cmd 0x5E)
* 6. Wait for match interrupt
* 7. Get match result (cmd 0x60)
* 8. Match cleanup (cmd 0x62)
* 9. LED off
* 10. Report result
*
* The difference between verify and identify is only in how the
* result is reported to libfprint.
*
* Also implements list and delete operations.
*
* Reference: python-validity sensor.py Sensor.identify(), Sensor.match_finger()
*
* Copyright (C) 2024 libfprint 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
*/
#define FP_COMPONENT "validity"
#include "drivers_api.h"
#include "fpi-byte-reader.h"
#include "fpi-byte-utils.h"
#include "fpi-print.h"
#include "validity.h"
#include "vcsfw_protocol.h"
/* ================================================================
* Interrupt helpers (shared with enrollment)
* ================================================================ */
static void
verify_interrupt_cb (FpiUsbTransfer *transfer,
FpDevice *device,
gpointer user_data,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (device);
FpiSsm *ssm = user_data;
guint8 int_type;
if (error)
{
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_REMOVED));
g_error_free (error);
return;
}
fpi_ssm_mark_failed (ssm, error);
return;
}
if (transfer->actual_length < 1)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
int_type = transfer->buffer[0];
fp_dbg ("Verify interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
/* During match wait, type 3 = result available */
if (fpi_ssm_get_cur_state (ssm) == VERIFY_WAIT_MATCH_INT)
{
if (int_type == 3)
{
fpi_ssm_next_state (ssm);
return;
}
/* Not ready yet, keep waiting */
goto read_again;
}
/* During finger wait: type 2 = finger down */
if (fpi_ssm_get_cur_state (ssm) == VERIFY_WAIT_FINGER)
{
if (int_type == VALIDITY_INT_FINGER_DOWN)
{
fpi_ssm_next_state (ssm);
return;
}
/* type 0 = capture started, expected */
if (int_type == 0)
goto read_again;
}
/* During scan wait: type 3 with scan_complete bit */
if (fpi_ssm_get_cur_state (ssm) == VERIFY_WAIT_SCAN_COMPLETE)
{
if (int_type == 3 && transfer->actual_length >= 3 &&
(transfer->buffer[2] & VALIDITY_INT_SCAN_COMPLETE))
{
fpi_ssm_next_state (ssm);
return;
}
if (int_type == 3 || int_type == 0)
goto read_again;
}
/* Unexpected, but keep listening */
fp_dbg ("Ignoring verify interrupt type 0x%02x in state %d",
int_type, fpi_ssm_get_cur_state (ssm));
read_again:
{
FpiUsbTransfer *new_transfer = fpi_usb_transfer_new (device);
fpi_usb_transfer_fill_interrupt (new_transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (new_transfer, VALIDITY_USB_TIMEOUT,
self->interrupt_cancellable,
verify_interrupt_cb, ssm);
}
}
static void
verify_start_interrupt_wait (FpiDeviceValidity *self,
FpiSsm *ssm)
{
FpiUsbTransfer *transfer;
transfer = fpi_usb_transfer_new (FP_DEVICE (self));
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT,
self->interrupt_cancellable,
verify_interrupt_cb, ssm);
}
/* ================================================================
* Match result parsing
*
* cmd 0x60 response: len(2LE) | dict_data[len]
* dict_data is a sequence of tagged TLV entries:
* tag(1) | data
* tag 1 user_id(4LE), tag 3 subtype(2LE), tag 4 hash
* ================================================================ */
typedef struct
{
gboolean matched;
guint32 user_dbid;
guint16 subtype;
guint8 *hash;
gsize hash_len;
} MatchResult;
static void
match_result_clear (MatchResult *r)
{
g_clear_pointer (&r->hash, g_free);
memset (r, 0, sizeof (*r));
}
static gboolean
parse_match_result (const guint8 *data,
gsize data_len,
MatchResult *result)
{
FpiByteReader reader;
guint16 dict_len;
memset (result, 0, sizeof (*result));
fpi_byte_reader_init (&reader, data, data_len);
if (data_len < 2)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &dict_len))
return FALSE;
/* Parse the dict entries — python-validity parse_dict() */
const guint8 *dict_data;
gsize remaining = MIN (dict_len, data_len - 2);
if (!fpi_byte_reader_get_data (&reader, remaining, &dict_data))
return FALSE;
gsize pos = 0;
while (pos < remaining)
{
/* Each entry has a variable format.
* Based on python-validity parse_dict():
* rsp[1] = user_id(4LE)
* rsp[3] = subtype(2LE)
* rsp[4] = hash
* These are indexed by occurrence order, not by tag byte.
*
* The response format from cmd 0x60 is actually:
* len(2LE) | entries
* where entries are variable-length tagged data.
*
* For simplicity, parse the known fixed structure below. */
break;
}
/* For the initial implementation, we try to extract the match result
* from the raw response. The python-validity code extracts fields
* via indexed dict parsing. For now, mark as matched if we got data. */
if (remaining >= 6)
{
result->matched = TRUE;
result->user_dbid = FP_READ_UINT32_LE (dict_data);
if (remaining >= 8)
result->subtype = FP_READ_UINT16_LE (&dict_data[4]);
if (remaining > 8)
{
result->hash = g_memdup2 (&dict_data[6], remaining - 6);
result->hash_len = remaining - 6;
}
}
return TRUE;
}
/* ================================================================
* Verify/Identify SSM
* ================================================================ */
static void
verify_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
switch (fpi_ssm_get_cur_state (ssm))
{
case VERIFY_LED_ON:
{
gsize cmd_len;
const guint8 *cmd = validity_capture_glow_start_cmd (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case VERIFY_LED_ON_RECV:
fpi_ssm_next_state (ssm);
break;
case VERIFY_BUILD_CAPTURE:
{
gsize cmd_len;
guint8 *cmd = validity_capture_build_cmd_02 (&self->capture,
self->sensor.type_info,
VALIDITY_CAPTURE_IDENTIFY,
&cmd_len);
if (!cmd)
{
fp_warn ("Failed to build identify capture command");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case VERIFY_CAPTURE_SEND:
fpi_ssm_next_state (ssm);
break;
case VERIFY_CAPTURE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("Capture (identify) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case VERIFY_WAIT_FINGER:
verify_start_interrupt_wait (self, ssm);
break;
case VERIFY_WAIT_SCAN_COMPLETE:
verify_start_interrupt_wait (self, ssm);
break;
case VERIFY_MATCH_START:
{
/* cmd 0x5E: match_finger */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_match_finger (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case VERIFY_MATCH_START_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("match_finger failed: status=0x%04x",
self->cmd_response_status);
/* No match — continue to cleanup */
fpi_ssm_jump_to_state (ssm, VERIFY_CLEANUP);
return;
}
fpi_ssm_next_state (ssm);
}
break;
case VERIFY_WAIT_MATCH_INT:
/* Wait for interrupt type 3 indicating match result ready */
verify_start_interrupt_wait (self, ssm);
break;
case VERIFY_GET_RESULT:
{
/* cmd 0x60: get_match_result */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_match_result (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case VERIFY_GET_RESULT_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_info ("No match found (status=0x%04x)",
self->cmd_response_status);
/* Store no-match indicator */
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
}
else if (self->cmd_response_data && self->cmd_response_len > 0)
{
/* Store match result for later reporting */
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data = g_memdup2 (self->cmd_response_data,
self->cmd_response_len);
self->bulk_data_len = self->cmd_response_len;
}
fpi_ssm_next_state (ssm);
}
break;
case VERIFY_CLEANUP:
{
/* cmd 0x62: match_cleanup */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_match_cleanup (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case VERIFY_CLEANUP_RECV:
/* Cleanup status doesn't matter */
fpi_ssm_next_state (ssm);
break;
case VERIFY_LED_OFF:
{
gsize cmd_len;
const guint8 *cmd = validity_capture_glow_end_cmd (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case VERIFY_LED_OFF_RECV:
fpi_ssm_next_state (ssm);
break;
case VERIFY_DONE:
fpi_ssm_mark_completed (ssm);
break;
}
}
static void
verify_ssm_done (FpiSsm *ssm,
FpDevice *dev,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
if (error)
{
if (self->identify_mode)
fpi_device_identify_complete (dev, error);
else
fpi_device_verify_complete (dev, error);
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
return;
}
/* Parse stored match result */
MatchResult match = { 0 };
gboolean have_match = FALSE;
if (self->bulk_data && self->bulk_data_len > 0)
{
if (parse_match_result (self->bulk_data, self->bulk_data_len, &match))
have_match = match.matched;
}
if (self->identify_mode)
{
/* Identify mode: report which print matched */
if (have_match)
{
fp_info ("Identify matched: user_dbid=%u subtype=%u",
match.user_dbid, match.subtype);
/* For identify, we'd need to match against the gallery.
* Since the sensor does the matching internally,
* we report FPI_MATCH_SUCCESS with the first gallery print
* for now. In a full implementation, we'd look up the
* user_dbid against the gallery. */
FpPrint *gallery_match = NULL;
GPtrArray *gallery = NULL;
fpi_device_get_identify_data (dev, &gallery);
if (gallery && gallery->len > 0)
gallery_match = g_ptr_array_index (gallery, 0);
fpi_device_identify_report (dev, gallery_match, NULL, NULL);
}
else
{
fpi_device_identify_report (dev, NULL, NULL, NULL);
}
fpi_device_identify_complete (dev, NULL);
}
else
{
/* Verify mode */
if (have_match)
{
fp_info ("Verify matched: user_dbid=%u", match.user_dbid);
fpi_device_verify_report (dev, FPI_MATCH_SUCCESS, NULL, NULL);
}
else
{
fp_info ("Verify: no match");
fpi_device_verify_report (dev, FPI_MATCH_FAIL, NULL, NULL);
}
fpi_device_verify_complete (dev, NULL);
}
match_result_clear (&match);
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
}
void
validity_verify (FpDevice *device)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (device);
FpiSsm *ssm;
G_DEBUG_HERE ();
self->identify_mode = FALSE;
ssm = fpi_ssm_new (device, verify_run_state, VERIFY_NUM_STATES);
fpi_ssm_start (ssm, verify_ssm_done);
}
void
validity_identify (FpDevice *device)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (device);
FpiSsm *ssm;
G_DEBUG_HERE ();
self->identify_mode = TRUE;
ssm = fpi_ssm_new (device, verify_run_state, VERIFY_NUM_STATES);
fpi_ssm_start (ssm, verify_ssm_done);
}
/* ================================================================
* List prints enumerate enrolled fingerprints from sensor DB
* ================================================================ */
static void
list_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
GPtrArray *prints_array = fpi_ssm_get_data (ssm);
switch (fpi_ssm_get_cur_state (ssm))
{
case LIST_GET_STORAGE:
{
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_user_storage (
VALIDITY_STORAGE_NAME, &cmd_len);
self->list_user_idx = 0;
memset (&self->list_storage, 0, sizeof (self->list_storage));
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case LIST_GET_STORAGE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_info ("No user storage found (status=0x%04x)",
self->cmd_response_status);
fpi_ssm_jump_to_state (ssm, LIST_DONE);
return;
}
if (!self->cmd_response_data ||
!validity_db_parse_user_storage (self->cmd_response_data,
self->cmd_response_len,
&self->list_storage))
{
fp_info ("Failed to parse user storage — no enrolled prints");
fpi_ssm_jump_to_state (ssm, LIST_DONE);
return;
}
fp_info ("Storage '%s': %u users",
self->list_storage.name ? self->list_storage.name : "",
self->list_storage.user_count);
if (self->list_storage.user_count == 0)
{
fpi_ssm_jump_to_state (ssm, LIST_DONE);
return;
}
self->list_user_idx = 0;
fpi_ssm_next_state (ssm);
}
break;
case LIST_GET_USER:
{
if (self->list_user_idx >= self->list_storage.user_count)
{
fpi_ssm_jump_to_state (ssm, LIST_DONE);
return;
}
guint16 user_dbid = self->list_storage.user_dbids[self->list_user_idx];
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_user (user_dbid, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case LIST_GET_USER_RECV:
{
if (self->cmd_response_status == VCSFW_STATUS_OK &&
self->cmd_response_data)
{
ValidityUser user = { 0 };
if (validity_db_parse_user (self->cmd_response_data,
self->cmd_response_len,
&user))
{
for (guint16 i = 0; i < user.finger_count; i++)
{
FpPrint *print = fp_print_new (dev);
gint finger = validity_subtype_to_finger (
user.fingers[i].subtype);
fpi_print_set_type (print, FPI_PRINT_RAW);
fpi_print_set_device_stored (print, TRUE);
if (finger >= 0)
fp_print_set_finger (print, (FpFinger) finger);
g_ptr_array_add (prints_array, print);
}
validity_user_clear (&user);
}
}
self->list_user_idx++;
if (self->list_user_idx < self->list_storage.user_count)
fpi_ssm_jump_to_state (ssm, LIST_GET_USER);
else
fpi_ssm_next_state (ssm);
}
break;
case LIST_DONE:
fpi_ssm_mark_completed (ssm);
break;
}
}
static void
list_ssm_done (FpiSsm *ssm,
FpDevice *dev,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
GPtrArray *prints_array = fpi_ssm_get_data (ssm);
validity_user_storage_clear (&self->list_storage);
if (error)
{
fpi_device_list_complete (dev, NULL, error);
return;
}
fpi_device_list_complete (dev, g_steal_pointer (&prints_array), NULL);
}
void
validity_list (FpDevice *device)
{
FpiSsm *ssm;
GPtrArray *prints_array;
G_DEBUG_HERE ();
prints_array = g_ptr_array_new_with_free_func (g_object_unref);
ssm = fpi_ssm_new (device, list_run_state, LIST_NUM_STATES);
fpi_ssm_set_data (ssm, prints_array, (GDestroyNotify) g_ptr_array_unref);
fpi_ssm_start (ssm, list_ssm_done);
}
/* ================================================================
* Delete print remove a fingerprint record from the sensor DB
* ================================================================ */
static void
delete_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
switch (fpi_ssm_get_cur_state (ssm))
{
case DELETE_GET_STORAGE:
{
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_user_storage (
VALIDITY_STORAGE_NAME, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case DELETE_GET_STORAGE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_DATA_NOT_FOUND));
return;
}
ValidityUserStorage storage = { 0 };
if (!self->cmd_response_data ||
!validity_db_parse_user_storage (self->cmd_response_data,
self->cmd_response_len,
&storage))
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_DATA_NOT_FOUND));
return;
}
self->delete_storage_dbid = storage.dbid;
validity_user_storage_clear (&storage);
fpi_ssm_next_state (ssm);
}
break;
case DELETE_LOOKUP_USER:
{
/* For delete, we need to find the user matching the print.
* Since we use device-stored prints, we can use the print's
* driver-specific data to identify the record. For now,
* we delete the first user's matching finger. */
FpPrint *print = NULL;
fpi_device_get_delete_data (dev, &print);
/* TODO: Use print's stored user ID to look up the specific
* record. For now, skip lookup and go to delete. */
fpi_ssm_next_state (ssm);
}
break;
case DELETE_LOOKUP_USER_RECV:
fpi_ssm_next_state (ssm);
break;
case DELETE_DEL_RECORD:
{
/* Without proper print-to-dbid mapping, we can't delete
* a specific record. Report success for now a full
* implementation needs the print's dbid stored as driver data. */
fp_info ("Delete: record deletion requires print-to-dbid mapping "
"(not yet implemented)");
fpi_ssm_jump_to_state (ssm, DELETE_DONE);
}
break;
case DELETE_DEL_RECORD_RECV:
fpi_ssm_next_state (ssm);
break;
case DELETE_DONE:
fpi_ssm_mark_completed (ssm);
break;
}
}
static void
delete_ssm_done (FpiSsm *ssm,
FpDevice *dev,
GError *error)
{
fpi_device_delete_complete (dev, error);
}
void
validity_delete (FpDevice *device)
{
FpiSsm *ssm;
G_DEBUG_HERE ();
ssm = fpi_ssm_new (device, delete_run_state, DELETE_NUM_STATES);
fpi_ssm_start (ssm, delete_ssm_done);
}
void
validity_clear_storage (FpDevice *device)
{
/* Clear storage would need to enumerate all records and delete each.
* For now, report not supported a full implementation would:
* 1. Get user storage
* 2. For each user: del_record(user.dbid)
* 3. Report complete */
fpi_device_clear_storage_complete (device,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
}

View file

@ -159,7 +159,10 @@ driver_sources = {
'drivers/validity/validity_tls.c',
'drivers/validity/validity_fwext.c',
'drivers/validity/validity_sensor.c',
'drivers/validity/validity_capture.c' ],
'drivers/validity/validity_capture.c',
'drivers/validity/validity_db.c',
'drivers/validity/validity_enroll.c',
'drivers/validity/validity_verify.c' ],
}
helper_sources = {

View file

@ -383,6 +383,20 @@ if 'validity' in supported_drivers
env: envs,
)
endif
# Validity DB operations unit tests
validity_db_test = executable('test-validity-db',
sources: 'test-validity-db.c',
dependencies: [ libfprint_private_dep ],
c_args: common_cflags,
link_with: libfprint_drivers,
install: false,
)
test('validity-db',
validity_db_test,
suite: ['unit-tests'],
env: envs,
)
endif
# Run udev rule generator with fatal warnings

749
tests/test-validity-db.c Normal file
View file

@ -0,0 +1,749 @@
/*
* Unit tests for validity database operations
*
* Copyright (C) 2024 libfprint 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.
*/
#include <glib.h>
#include <string.h>
#include "fpi-byte-utils.h"
#include "drivers/validity/validity_db.h"
#include "drivers/validity/vcsfw_protocol.h"
/* ================================================================
* T6.1: test_cmd_db_info
*
* Verify cmd 0x45 (DB info) is a single-byte command.
* ================================================================ */
static void
test_cmd_db_info (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_info (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 1);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DB_INFO);
}
/* ================================================================
* T6.2: test_cmd_get_user_storage
*
* Verify cmd 0x4B format with a known storage name.
* ================================================================ */
static void
test_cmd_get_user_storage (void)
{
gsize len;
const gchar *name = "StgWindsor";
gsize name_len = strlen (name) + 1; /* includes NUL */
g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (name, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 1 + 2 + 2 + name_len); /* cmd + dbid + name_len + name */
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 → lookup by name */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, name_len);
g_assert_cmpmem (&cmd[5], name_len, name, name_len);
}
/* ================================================================
* T6.3: test_cmd_get_user_storage_null_name
*
* When name is NULL, should produce a command with zero name_len.
* ================================================================ */
static void
test_cmd_get_user_storage_null_name (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_get_user_storage (NULL, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 5); /* cmd(1) + dbid(2) + name_len(2) */
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER_STORAGE);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0); /* name_len = 0 */
}
/* ================================================================
* T6.4: test_cmd_get_user
*
* Verify cmd 0x4A format for get-by-dbid.
* ================================================================ */
static void
test_cmd_get_user (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_get_user (0x1234, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 7);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0);
}
/* ================================================================
* T6.5: test_cmd_lookup_user
*
* Verify cmd 0x4A format for lookup-by-identity.
* ================================================================ */
static void
test_cmd_lookup_user (void)
{
gsize len;
guint8 identity[] = { 0x01, 0x02, 0x03, 0x04 };
g_autofree guint8 *cmd = validity_db_build_cmd_lookup_user (
0x0003, identity, sizeof (identity), &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 7 + sizeof (identity));
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_USER);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0); /* dbid = 0 */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0003); /* storage */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, sizeof (identity));
g_assert_cmpmem (&cmd[7], sizeof (identity), identity, sizeof (identity));
}
/* ================================================================
* T6.6: test_cmd_new_record
*
* Verify cmd 0x47 format.
* ================================================================ */
static void
test_cmd_new_record (void)
{
gsize len;
guint8 data[] = { 0xAA, 0xBB };
g_autofree guint8 *cmd = validity_db_build_cmd_new_record (
0x0003, 0x0005, 0x0003, data, sizeof (data), &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 9 + sizeof (data));
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_NEW_RECORD);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x0003); /* parent */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0x0005); /* type */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0x0003); /* storage */
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, sizeof (data));
g_assert_cmpmem (&cmd[9], sizeof (data), data, sizeof (data));
}
/* ================================================================
* T6.7: test_cmd_del_record
*
* Verify cmd 0x48 format.
* ================================================================ */
static void
test_cmd_del_record (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_del_record (0xABCD, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 3);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_DEL_RECORD);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0xABCD);
}
/* ================================================================
* T6.8: test_cmd_create_enrollment
*
* Verify cmd 0x69 start and end variants.
* ================================================================ */
static void
test_cmd_create_enrollment (void)
{
gsize len;
/* Start enrollment */
g_autofree guint8 *cmd_start = validity_db_build_cmd_create_enrollment (TRUE, &len);
g_assert_nonnull (cmd_start);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (cmd_start[0], ==, VCSFW_CMD_CREATE_ENROLLMENT);
g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_start[1]), ==, 1);
/* End enrollment */
g_autofree guint8 *cmd_end = validity_db_build_cmd_create_enrollment (FALSE, &len);
g_assert_nonnull (cmd_end);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (cmd_end[0], ==, VCSFW_CMD_CREATE_ENROLLMENT);
g_assert_cmpuint (FP_READ_UINT32_LE (&cmd_end[1]), ==, 0);
}
/* ================================================================
* T6.9: test_cmd_enrollment_update_start
*
* Verify cmd 0x68 format.
* ================================================================ */
static void
test_cmd_enrollment_update_start (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update_start (0x12345678, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 9);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE_START);
g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[1]), ==, 0x12345678);
g_assert_cmpuint (FP_READ_UINT32_LE (&cmd[5]), ==, 0);
}
/* ================================================================
* T6.10: test_cmd_match_finger
*
* Verify cmd 0x5E format (13 bytes per python-validity).
* ================================================================ */
static void
test_cmd_match_finger (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_match_finger (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 13);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_FINGER);
g_assert_cmpuint (cmd[1], ==, 0x02);
g_assert_cmpuint (cmd[2], ==, 0xFF);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[3]), ==, 0);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[5]), ==, 0);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[7]), ==, 1);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[9]), ==, 0);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[11]), ==, 0);
}
/* ================================================================
* T6.11: test_cmd_get_match_result
*
* Verify cmd 0x60 format.
* ================================================================ */
static void
test_cmd_get_match_result (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_get_match_result (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_MATCH_RESULT);
/* remaining bytes should be 0 */
g_assert_cmpuint (cmd[1], ==, 0);
g_assert_cmpuint (cmd[2], ==, 0);
g_assert_cmpuint (cmd[3], ==, 0);
g_assert_cmpuint (cmd[4], ==, 0);
}
/* ================================================================
* T6.12: test_cmd_match_cleanup
*
* Verify cmd 0x62 format.
* ================================================================ */
static void
test_cmd_match_cleanup (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_match_cleanup (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_MATCH_CLEANUP);
}
/* ================================================================
* T6.13: test_cmd_get_prg_status
*
* Verify cmd 0x51 both normal and extended variant.
* ================================================================ */
static void
test_cmd_get_prg_status (void)
{
gsize len;
g_autofree guint8 *normal = validity_db_build_cmd_get_prg_status (FALSE, &len);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (normal[0], ==, VCSFW_CMD_GET_PRG_STATUS);
/* Normal: 00000000 */
g_assert_cmpuint (normal[1], ==, 0);
g_assert_cmpuint (normal[2], ==, 0);
g_assert_cmpuint (normal[3], ==, 0);
g_assert_cmpuint (normal[4], ==, 0);
g_autofree guint8 *ext = validity_db_build_cmd_get_prg_status (TRUE, &len);
g_assert_cmpuint (len, ==, 5);
g_assert_cmpuint (ext[0], ==, VCSFW_CMD_GET_PRG_STATUS);
/* Extended: 00200000 LE */
g_assert_cmpuint (ext[1], ==, 0x00);
g_assert_cmpuint (ext[2], ==, 0x20);
g_assert_cmpuint (ext[3], ==, 0x00);
g_assert_cmpuint (ext[4], ==, 0x00);
}
/* ================================================================
* T6.14: test_cmd_capture_stop
*
* Verify cmd 0x04 format.
* ================================================================ */
static void
test_cmd_capture_stop (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_capture_stop (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 1);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_CAPTURE_STOP);
}
/* ================================================================
* T6.15: test_cmd_call_cleanups
*
* Verify cmd 0x1a format.
* ================================================================ */
static void
test_cmd_call_cleanups (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_call_cleanups (&len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 1);
g_assert_cmpuint (cmd[0], ==, 0x1a);
}
/* ================================================================
* T6.16: test_parse_db_info
*
* Construct a known db_info binary response and verify parsing.
* ================================================================ */
static void
test_parse_db_info (void)
{
guint8 data[0x1C]; /* 24 bytes header + 4 bytes for 2 roots */
ValidityDbInfo info;
memset (data, 0, sizeof (data));
/* Header: unknown1=1, unknown0=0, total=65536, used=1024, free=64512, records=10, n_roots=2 */
FP_WRITE_UINT32_LE (&data[0], 1); /* unknown1 */
FP_WRITE_UINT32_LE (&data[4], 0); /* unknown0 */
FP_WRITE_UINT32_LE (&data[8], 65536); /* total */
FP_WRITE_UINT32_LE (&data[12], 1024); /* used */
FP_WRITE_UINT32_LE (&data[16], 64512); /* free */
FP_WRITE_UINT16_LE (&data[20], 10); /* records */
FP_WRITE_UINT16_LE (&data[22], 2); /* n_roots */
FP_WRITE_UINT16_LE (&data[24], 0x0001); /* root[0] */
FP_WRITE_UINT16_LE (&data[26], 0x0003); /* root[1] */
g_assert_true (validity_db_parse_info (data, sizeof (data), &info));
g_assert_cmpuint (info.unknown1, ==, 1);
g_assert_cmpuint (info.unknown0, ==, 0);
g_assert_cmpuint (info.total, ==, 65536);
g_assert_cmpuint (info.used, ==, 1024);
g_assert_cmpuint (info.free_space, ==, 64512);
g_assert_cmpuint (info.records, ==, 10);
g_assert_cmpuint (info.n_roots, ==, 2);
g_assert_nonnull (info.roots);
g_assert_cmpuint (info.roots[0], ==, 1);
g_assert_cmpuint (info.roots[1], ==, 3);
validity_db_info_clear (&info);
}
/* ================================================================
* T6.17: test_parse_db_info_too_short
*
* A response shorter than 24 bytes should fail.
* ================================================================ */
static void
test_parse_db_info_too_short (void)
{
guint8 data[20] = { 0 };
ValidityDbInfo info;
g_assert_false (validity_db_parse_info (data, sizeof (data), &info));
}
/* ================================================================
* T6.18: test_parse_user_storage
*
* Construct a user storage response with 2 users and verify.
* ================================================================ */
static void
test_parse_user_storage (void)
{
/* Header: dbid=3, user_count=2, name_sz=11, unknown=0
* User table: {dbid=10, val_sz=100}, {dbid=11, val_sz=200}
* Name: "StgWindsor\0" */
gsize name_len = strlen ("StgWindsor") + 1;
gsize total = 8 + 2 * 4 + name_len;
g_autofree guint8 *data = g_new0 (guint8, total);
FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */
FP_WRITE_UINT16_LE (&data[2], 2); /* user_count */
FP_WRITE_UINT16_LE (&data[4], name_len); /* name_sz */
FP_WRITE_UINT16_LE (&data[6], 0); /* unknown */
FP_WRITE_UINT16_LE (&data[8], 10); /* user[0].dbid */
FP_WRITE_UINT16_LE (&data[10], 100); /* user[0].val_sz */
FP_WRITE_UINT16_LE (&data[12], 11); /* user[1].dbid */
FP_WRITE_UINT16_LE (&data[14], 200); /* user[1].val_sz */
memcpy (&data[16], "StgWindsor", name_len);
ValidityUserStorage storage;
g_assert_true (validity_db_parse_user_storage (data, total, &storage));
g_assert_cmpuint (storage.dbid, ==, 3);
g_assert_cmpuint (storage.user_count, ==, 2);
g_assert_cmpstr (storage.name, ==, "StgWindsor");
g_assert_nonnull (storage.user_dbids);
g_assert_cmpuint (storage.user_dbids[0], ==, 10);
g_assert_cmpuint (storage.user_dbids[1], ==, 11);
g_assert_cmpuint (storage.user_val_sizes[0], ==, 100);
g_assert_cmpuint (storage.user_val_sizes[1], ==, 200);
validity_user_storage_clear (&storage);
}
/* ================================================================
* T6.19: test_parse_user
*
* Construct a user response with one finger and verify.
* ================================================================ */
static void
test_parse_user (void)
{
guint8 identity_bytes[] = { 0xDE, 0xAD, 0xBE, 0xEF };
/* Header: dbid=10, finger_count=1, unknown=0, identity_sz=4
* Finger: dbid=20, subtype=2, storage=3, value_size=500
* Identity: 4 bytes */
gsize total = 8 + 8 + sizeof (identity_bytes);
g_autofree guint8 *data = g_new0 (guint8, total);
FP_WRITE_UINT16_LE (&data[0], 10); /* dbid */
FP_WRITE_UINT16_LE (&data[2], 1); /* finger_count */
FP_WRITE_UINT16_LE (&data[4], 0); /* unknown */
FP_WRITE_UINT16_LE (&data[6], sizeof (identity_bytes)); /* identity_sz */
/* Finger entry */
FP_WRITE_UINT16_LE (&data[8], 20); /* finger.dbid */
FP_WRITE_UINT16_LE (&data[10], 2); /* finger.subtype = right index */
FP_WRITE_UINT16_LE (&data[12], 3); /* finger.storage */
FP_WRITE_UINT16_LE (&data[14], 500); /* finger.value_size */
memcpy (&data[16], identity_bytes, sizeof (identity_bytes));
ValidityUser user;
g_assert_true (validity_db_parse_user (data, total, &user));
g_assert_cmpuint (user.dbid, ==, 10);
g_assert_cmpuint (user.finger_count, ==, 1);
g_assert_nonnull (user.fingers);
g_assert_cmpuint (user.fingers[0].dbid, ==, 20);
g_assert_cmpuint (user.fingers[0].subtype, ==, 2);
g_assert_cmpuint (user.fingers[0].storage, ==, 3);
g_assert_cmpuint (user.fingers[0].value_size, ==, 500);
g_assert_nonnull (user.identity);
g_assert_cmpuint (user.identity_len, ==, sizeof (identity_bytes));
g_assert_cmpmem (user.identity, user.identity_len,
identity_bytes, sizeof (identity_bytes));
validity_user_clear (&user);
}
/* ================================================================
* T6.20: test_parse_new_record_id
*
* Verify parsing of new_record response (cmd 0x47).
* ================================================================ */
static void
test_parse_new_record_id (void)
{
guint16 record_id;
guint8 data[] = { 0x42, 0x00 };
g_assert_true (validity_db_parse_new_record_id (data, sizeof (data), &record_id));
g_assert_cmpuint (record_id, ==, 0x0042);
}
/* ================================================================
* T6.21: test_parse_new_record_id_too_short
*
* A 1-byte response should fail.
* ================================================================ */
static void
test_parse_new_record_id_too_short (void)
{
guint16 record_id;
guint8 data[] = { 0x42 };
g_assert_false (validity_db_parse_new_record_id (data, sizeof (data), &record_id));
}
/* ================================================================
* T6.22: test_build_identity
*
* Build a UUID identity and verify structure.
* ================================================================ */
static void
test_build_identity (void)
{
gsize len;
const gchar *uuid = "550e8400-e29b-41d4-a716-446655440000";
g_autofree guint8 *id = validity_db_build_identity (uuid, &len);
g_assert_nonnull (id);
/* Minimum size enforced */
g_assert_cmpuint (len, >=, VALIDITY_IDENTITY_MIN_SIZE);
/* type = SID (3) */
g_assert_cmpuint (FP_READ_UINT32_LE (&id[0]), ==, VALIDITY_IDENTITY_TYPE_SID);
/* len field = UUID string length */
gsize uuid_len = strlen (uuid);
g_assert_cmpuint (FP_READ_UINT32_LE (&id[4]), ==, uuid_len);
/* UUID payload */
g_assert_cmpmem (&id[8], uuid_len, uuid, uuid_len);
/* Remaining bytes should be zero-padded */
for (gsize i = 8 + uuid_len; i < len; i++)
g_assert_cmpuint (id[i], ==, 0);
}
/* ================================================================
* T6.23: test_build_finger_data
*
* Build finger data and verify the tagged format.
* ================================================================ */
static void
test_build_finger_data (void)
{
gsize len;
guint8 template[] = { 0x11, 0x22, 0x33 };
guint8 tid[] = { 0xAA, 0xBB };
g_autofree guint8 *fd = validity_db_build_finger_data (
2, template, sizeof (template), tid, sizeof (tid), &len);
g_assert_nonnull (fd);
/* Check header: subtype(2) | flags=3(2) | tinfo_len(2) | 0x20(2) */
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[0]), ==, 2); /* subtype */
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[2]), ==, 3); /* flags */
gsize expected_tinfo_len = 4 + sizeof (template) + 4 + sizeof (tid);
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[4]), ==, expected_tinfo_len);
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[6]), ==, 0x20);
/* Tag 1 (template) at offset 8 */
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[8]), ==, 1);
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[10]), ==, sizeof (template));
g_assert_cmpmem (&fd[12], sizeof (template), template, sizeof (template));
/* Tag 2 (tid) at offset 12+3 = 15 */
gsize tid_offset = 12 + sizeof (template);
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset]), ==, 2);
g_assert_cmpuint (FP_READ_UINT16_LE (&fd[tid_offset + 2]), ==, sizeof (tid));
g_assert_cmpmem (&fd[tid_offset + 4], sizeof (tid), tid, sizeof (tid));
/* Total should be header(8) + tinfo + 0x20 padding */
gsize expected_total = 8 + expected_tinfo_len + 0x20;
g_assert_cmpuint (len, ==, expected_total);
}
/* ================================================================
* T6.24: test_db_write_enable_blob
*
* Verify db_write_enable blob accessor returns a valid blob.
* ================================================================ */
static void
test_db_write_enable_blob (void)
{
gsize len;
const guint8 *blob = validity_db_get_write_enable_blob (&len);
g_assert_nonnull (blob);
g_assert_cmpuint (len, >, 0);
/* The 009a blob is 3621 bytes */
g_assert_cmpuint (len, ==, 3621);
}
/* ================================================================
* T6.25: test_parse_record_value
*
* Construct a record value response and verify parsing.
* ================================================================ */
static void
test_parse_record_value (void)
{
guint8 value[] = { 0x01, 0x02, 0x03 };
/* Format: dbid(2) type(2) storage(2) sz(2) pad(2) value */
gsize total = 10 + sizeof (value);
g_autofree guint8 *data = g_new0 (guint8, total);
FP_WRITE_UINT16_LE (&data[0], 42); /* dbid */
FP_WRITE_UINT16_LE (&data[2], 8); /* type = DATA */
FP_WRITE_UINT16_LE (&data[4], 3); /* storage */
FP_WRITE_UINT16_LE (&data[6], sizeof (value)); /* sz */
FP_WRITE_UINT16_LE (&data[8], 0); /* pad */
memcpy (&data[10], value, sizeof (value));
ValidityDbRecord record;
g_assert_true (validity_db_parse_record_value (data, total, &record));
g_assert_cmpuint (record.dbid, ==, 42);
g_assert_cmpuint (record.type, ==, 8);
g_assert_cmpuint (record.storage, ==, 3);
g_assert_nonnull (record.value);
g_assert_cmpuint (record.value_len, ==, sizeof (value));
g_assert_cmpmem (record.value, record.value_len, value, sizeof (value));
validity_db_record_clear (&record);
}
/* ================================================================
* T6.26: test_parse_record_children
*
* Construct a record children response and verify parsing.
* ================================================================ */
static void
test_parse_record_children (void)
{
/* Format: dbid(2) type(2) storage(2) sz(2) cnt(2) pad(2)
* children[cnt * 4: dbid(2) type(2)] */
gsize total = 12 + 2 * 4;
g_autofree guint8 *data = g_new0 (guint8, total);
FP_WRITE_UINT16_LE (&data[0], 3); /* dbid */
FP_WRITE_UINT16_LE (&data[2], 4); /* type = STORAGE */
FP_WRITE_UINT16_LE (&data[4], 3); /* storage */
FP_WRITE_UINT16_LE (&data[6], 0); /* sz */
FP_WRITE_UINT16_LE (&data[8], 2); /* child_count */
FP_WRITE_UINT16_LE (&data[10], 0); /* pad */
/* Children */
FP_WRITE_UINT16_LE (&data[12], 10); /* child[0].dbid */
FP_WRITE_UINT16_LE (&data[14], 5); /* child[0].type = USER */
FP_WRITE_UINT16_LE (&data[16], 11); /* child[1].dbid */
FP_WRITE_UINT16_LE (&data[18], 5); /* child[1].type = USER */
ValidityRecordChildren children;
g_assert_true (validity_db_parse_record_children (data, total, &children));
g_assert_cmpuint (children.dbid, ==, 3);
g_assert_cmpuint (children.type, ==, 4);
g_assert_cmpuint (children.storage, ==, 3);
g_assert_cmpuint (children.child_count, ==, 2);
g_assert_nonnull (children.children);
g_assert_cmpuint (children.children[0].dbid, ==, 10);
g_assert_cmpuint (children.children[0].type, ==, 5);
g_assert_cmpuint (children.children[1].dbid, ==, 11);
g_assert_cmpuint (children.children[1].type, ==, 5);
validity_record_children_clear (&children);
}
/* ================================================================
* T6.27: test_cmd_enrollment_update
*
* Verify cmd 0x6B format with template data.
* ================================================================ */
static void
test_cmd_enrollment_update (void)
{
gsize len;
guint8 prev[] = { 0xDE, 0xAD, 0xBE, 0xEF };
g_autofree guint8 *cmd = validity_db_build_cmd_enrollment_update (
prev, sizeof (prev), &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 1 + sizeof (prev));
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_ENROLLMENT_UPDATE);
g_assert_cmpmem (&cmd[1], sizeof (prev), prev, sizeof (prev));
}
/* ================================================================
* T6.28: test_cmd_get_record_value
*
* Verify cmd 0x49 format.
* ================================================================ */
static void
test_cmd_get_record_value (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_get_record_value (0x5678, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 3);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_VALUE);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x5678);
}
/* ================================================================
* T6.29: test_cmd_get_record_children
*
* Verify cmd 0x46 format.
* ================================================================ */
static void
test_cmd_get_record_children (void)
{
gsize len;
g_autofree guint8 *cmd = validity_db_build_cmd_get_record_children (0x1234, &len);
g_assert_nonnull (cmd);
g_assert_cmpuint (len, ==, 3);
g_assert_cmpuint (cmd[0], ==, VCSFW_CMD_GET_RECORD_CHILDREN);
g_assert_cmpuint (FP_READ_UINT16_LE (&cmd[1]), ==, 0x1234);
}
int
main (int argc, char *argv[])
{
g_test_init (&argc, &argv, NULL);
/* Command builder tests */
g_test_add_func ("/validity/db/cmd_db_info", test_cmd_db_info);
g_test_add_func ("/validity/db/cmd_get_user_storage", test_cmd_get_user_storage);
g_test_add_func ("/validity/db/cmd_get_user_storage_null_name", test_cmd_get_user_storage_null_name);
g_test_add_func ("/validity/db/cmd_get_user", test_cmd_get_user);
g_test_add_func ("/validity/db/cmd_lookup_user", test_cmd_lookup_user);
g_test_add_func ("/validity/db/cmd_new_record", test_cmd_new_record);
g_test_add_func ("/validity/db/cmd_del_record", test_cmd_del_record);
g_test_add_func ("/validity/db/cmd_create_enrollment", test_cmd_create_enrollment);
g_test_add_func ("/validity/db/cmd_enrollment_update_start", test_cmd_enrollment_update_start);
g_test_add_func ("/validity/db/cmd_enrollment_update", test_cmd_enrollment_update);
g_test_add_func ("/validity/db/cmd_match_finger", test_cmd_match_finger);
g_test_add_func ("/validity/db/cmd_get_match_result", test_cmd_get_match_result);
g_test_add_func ("/validity/db/cmd_match_cleanup", test_cmd_match_cleanup);
g_test_add_func ("/validity/db/cmd_get_prg_status", test_cmd_get_prg_status);
g_test_add_func ("/validity/db/cmd_capture_stop", test_cmd_capture_stop);
g_test_add_func ("/validity/db/cmd_call_cleanups", test_cmd_call_cleanups);
g_test_add_func ("/validity/db/cmd_get_record_value", test_cmd_get_record_value);
g_test_add_func ("/validity/db/cmd_get_record_children", test_cmd_get_record_children);
/* Response parser tests */
g_test_add_func ("/validity/db/parse_info", test_parse_db_info);
g_test_add_func ("/validity/db/parse_info_too_short", test_parse_db_info_too_short);
g_test_add_func ("/validity/db/parse_user_storage", test_parse_user_storage);
g_test_add_func ("/validity/db/parse_user", test_parse_user);
g_test_add_func ("/validity/db/parse_new_record_id", test_parse_new_record_id);
g_test_add_func ("/validity/db/parse_new_record_id_too_short", test_parse_new_record_id_too_short);
g_test_add_func ("/validity/db/parse_record_value", test_parse_record_value);
g_test_add_func ("/validity/db/parse_record_children", test_parse_record_children);
/* Identity and finger data tests */
g_test_add_func ("/validity/db/build_identity", test_build_identity);
g_test_add_func ("/validity/db/build_finger_data", test_build_finger_data);
/* Blob accessor test */
g_test_add_func ("/validity/db/write_enable_blob", test_db_write_enable_blob);
return g_test_run ();
}

View file

@ -23,16 +23,15 @@ del devices
assert d.get_driver() == "validity", f"Expected 'validity', got '{d.get_driver()}'"
# Verify features detected by auto_initialize_features
# Since iteration 1 stubs provide verify/identify/delete function pointers,
# those features are reported even though they return NOT_SUPPORTED.
# Iteration 6 added enroll, verify, identify, list, delete, clear_storage.
assert not d.has_feature(FPrint.DeviceFeature.CAPTURE)
assert d.has_feature(FPrint.DeviceFeature.VERIFY)
assert d.has_feature(FPrint.DeviceFeature.IDENTIFY)
assert not d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK)
assert not d.has_feature(FPrint.DeviceFeature.STORAGE)
assert not d.has_feature(FPrint.DeviceFeature.STORAGE_LIST)
assert d.has_feature(FPrint.DeviceFeature.STORAGE)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE)
assert not d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
assert d.has_feature(FPrint.DeviceFeature.ALWAYS_ON)
# Test open (sends GET_VERSION, cmd 0x19, GET_FW_INFO) and close