diff --git a/examples/enroll.c b/examples/enroll.c index f133c8b8..10dfa5da 100644 --- a/examples/enroll.c +++ b/examples/enroll.c @@ -35,6 +35,7 @@ typedef struct _EnrollData unsigned int sigint_handler; FpFinger finger; int ret_value; + gboolean update_fingerprint; } EnrollData; static void @@ -84,7 +85,8 @@ on_enroll_completed (FpDevice *dev, GAsyncResult *res, void *user_data) /* Even if the device has storage, it may not be able to save all the * metadata that the print contains, so we can always save a local copy * containing the handle to the device print */ - int r = print_data_save (print, enroll_data->finger); + int r = print_data_save (print, enroll_data->finger, + enroll_data->update_fingerprint); if (r < 0) { g_warning ("Data save failed, code %d", r); @@ -124,6 +126,40 @@ on_enroll_progress (FpDevice *device, fp_device_get_nr_enroll_stages (device)); } +static gboolean +should_update_fingerprint (void) +{ + int update_choice; + gboolean update_fingerprint = FALSE; + + printf ("Should an existing fingerprint be updated instead of being replaced (if present)? " + "Enter Y/y or N/n to make a choice.\n"); + update_choice = getchar (); + if (update_choice == EOF) + { + g_warning ("EOF encountered while reading a character"); + return EXIT_FAILURE; + } + + switch (update_choice) + { + case 'y': + case 'Y': + update_fingerprint = TRUE; + break; + + case 'n': + case 'N': + update_fingerprint = FALSE; + break; + + default: + g_warning ("Invalid choice %c, should be Y/y or N/n.", update_choice); + return EXIT_FAILURE; + } + return update_fingerprint; +} + static void on_device_opened (FpDevice *dev, GAsyncResult *res, void *user_data) { @@ -139,13 +175,26 @@ on_device_opened (FpDevice *dev, GAsyncResult *res, void *user_data) return; } - printf ("Opened device. It's now time to enroll your finger.\n\n"); + printf ("Opened device.\n"); + + if (fp_device_has_feature (dev, FP_DEVICE_FEATURE_UPDATE_PRINT)) + { + printf ("The device supports fingerprint updates.\n"); + enroll_data->update_fingerprint = should_update_fingerprint (); + } + else + { + printf ("The device doesn't support fingerprint updates. Old prints will be erased.\n"); + enroll_data->update_fingerprint = FALSE; + } + + printf ("It's now time to enroll your finger.\n\n"); printf ("You will need to successfully scan your %s finger %d times to " "complete the process.\n\n", finger_to_string (enroll_data->finger), fp_device_get_nr_enroll_stages (dev)); printf ("Scan your finger now.\n"); - print_template = print_create_template (dev, enroll_data->finger); + print_template = print_create_template (dev, enroll_data->finger, enroll_data->update_fingerprint); fp_device_enroll (dev, print_template, enroll_data->cancellable, on_enroll_progress, NULL, NULL, (GAsyncReadyCallback) on_enroll_completed, @@ -171,11 +220,9 @@ main (void) FpDevice *dev; FpFinger finger; - g_print ("This program will enroll the selected finger, unconditionally " - "overwriting any print for the same finger that was enrolled " - "previously. If you want to continue, press enter, otherwise hit " - "Ctrl+C\n"); - getchar (); + g_print ("This program will enroll the selected finger overwriting any print for the same" + " finger that was enrolled previously. Fingerprint updates without erasing old data" + " are possible on devices supporting that. Ctrl+C interrupts program execution.\n"); g_print ("Choose the finger to enroll:\n"); finger = finger_chooser (); diff --git a/examples/storage.c b/examples/storage.c index 6c92dba6..b84c72d5 100644 --- a/examples/storage.c +++ b/examples/storage.c @@ -102,8 +102,23 @@ save_data (GVariant *data) return 0; } +static FpPrint * +load_print_from_data (GVariant *data) +{ + const guchar *stored_data = NULL; + gsize stored_len; + FpPrint *print; + + g_autoptr(GError) error = NULL; + stored_data = (const guchar *) g_variant_get_fixed_array (data, &stored_len, 1); + print = fp_print_deserialize (stored_data, stored_len, &error); + if (error) + g_warning ("Error deserializing data: %s", error->message); + return print; +} + int -print_data_save (FpPrint *print, FpFinger finger) +print_data_save (FpPrint *print, FpFinger finger, gboolean update_fingerprint) { g_autofree gchar *descr = get_print_data_descriptor (print, NULL, finger); @@ -137,25 +152,12 @@ print_data_load (FpDevice *dev, FpFinger finger) g_autoptr(GVariant) val = NULL; g_autoptr(GVariantDict) dict = NULL; - const guchar *stored_data = NULL; - gsize stored_len; dict = load_data (); val = g_variant_dict_lookup_value (dict, descr, G_VARIANT_TYPE ("ay")); if (val) - { - FpPrint *print; - g_autoptr(GError) error = NULL; - - stored_data = (const guchar *) g_variant_get_fixed_array (val, &stored_len, 1); - print = fp_print_deserialize (stored_data, stored_len, &error); - - if (error) - g_warning ("Error deserializing data: %s", error->message); - - return print; - } + return load_print_from_data (val); return NULL; } @@ -207,16 +209,30 @@ gallery_data_load (FpDevice *dev) } FpPrint * -print_create_template (FpDevice *dev, FpFinger finger) +print_create_template (FpDevice *dev, FpFinger finger, gboolean load_existing) { + g_autoptr(GVariantDict) dict = NULL; g_autoptr(GDateTime) datetime = NULL; g_autoptr(GDate) date = NULL; + g_autoptr(GVariant) existing_val = NULL; + g_autofree gchar *descr = get_print_data_descriptor (NULL, dev, finger); FpPrint *template = NULL; gint year, month, day; - template = fp_print_new (dev); - fp_print_set_finger (template, finger); - fp_print_set_username (template, g_get_user_name ()); + if (load_existing) + { + dict = load_data (); + existing_val = g_variant_dict_lookup_value (dict, descr, G_VARIANT_TYPE ("ay")); + if (existing_val != NULL) + template = load_print_from_data (existing_val); + } + if (template == NULL) + { + template = fp_print_new (dev); + fp_print_set_finger (template, finger); + fp_print_set_username (template, g_get_user_name ()); + } + datetime = g_date_time_new_now_local (); g_date_time_get_ymd (datetime, &year, &month, &day); date = g_date_new_dmy (day, month, year); diff --git a/examples/storage.h b/examples/storage.h index 81390bd0..0922b902 100644 --- a/examples/storage.h +++ b/examples/storage.h @@ -21,12 +21,14 @@ #pragma once int print_data_save (FpPrint *print, - FpFinger finger); + FpFinger finger, + gboolean update_fingerprint); FpPrint * print_data_load (FpDevice *dev, FpFinger finger); GPtrArray * gallery_data_load (FpDevice *dev); -FpPrint * print_create_template (FpDevice *dev, - FpFinger finger); +FpPrint * print_create_template (FpDevice *dev, + FpFinger finger, + const gboolean load_existing); gboolean print_image_save (FpPrint *print, const char *path); gboolean save_image_to_pgm (FpImage *img, diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c index 35e2f2b8..82f309e8 100644 --- a/libfprint/fp-device.c +++ b/libfprint/fp-device.c @@ -1189,10 +1189,11 @@ fp_device_resume_finish (FpDevice *device, * fp_device_enroll_finish(). * * The @template_print parameter is a #FpPrint with available metadata filled - * in. The driver may make use of this metadata, when e.g. storing the print on - * device memory. It is undefined whether this print is filled in by the driver - * and returned, or whether the driver will return a newly created print after - * enrollment succeeded. + * in and, optionally, with existing fingerprint data to be updated with newly + * enrolled fingerprints if a device driver supports it. The driver may make use + * of the metadata, when e.g. storing the print on device memory. It is undefined + * whether this print is filled in by the driver and returned, or whether the + * driver will return a newly created print after enrollment succeeded. */ void fp_device_enroll (FpDevice *device, @@ -1229,19 +1230,30 @@ fp_device_enroll (FpDevice *device, if (!FP_IS_PRINT (template_print)) { - g_warning ("User did not pass a print template!"); g_task_return_error (task, - fpi_device_error_new (FP_DEVICE_ERROR_DATA_INVALID)); + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "User did not pass a print template!")); return; } g_object_get (template_print, "fpi-type", &print_type, NULL); if (print_type != FPI_PRINT_UNDEFINED) { - g_warning ("Passed print template must be newly created and blank!"); - g_task_return_error (task, - fpi_device_error_new (FP_DEVICE_ERROR_DATA_INVALID)); - return; + if (!fp_device_has_feature (device, FP_DEVICE_FEATURE_UPDATE_PRINT)) + { + g_task_return_error (task, + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "A device does not support print updates!")); + return; + } + if (!fp_print_compatible (template_print, device)) + { + g_task_return_error (task, + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "The print and device must have a matching driver and device id" + " for a fingerprint update to succeed")); + return; + } } priv->current_action = FPI_DEVICE_ACTION_ENROLL; diff --git a/libfprint/fp-device.h b/libfprint/fp-device.h index 85be34c4..9dda747c 100644 --- a/libfprint/fp-device.h +++ b/libfprint/fp-device.h @@ -59,6 +59,7 @@ typedef enum { * @FP_DEVICE_FEATURE_STORAGE_CLEAR: Supports clearing the whole storage * @FP_DEVICE_FEATURE_DUPLICATES_CHECK: Natively supports duplicates detection * @FP_DEVICE_FEATURE_ALWAYS_ON: Whether the device can run continuously + * @FP_DEVICE_FEATURE_UPDATE_PRINT: Supports updating an existing print record using new scans */ typedef enum /*< flags >*/ { FP_DEVICE_FEATURE_NONE = 0, @@ -71,6 +72,7 @@ typedef enum /*< flags >*/ { FP_DEVICE_FEATURE_STORAGE_CLEAR = 1 << 6, FP_DEVICE_FEATURE_DUPLICATES_CHECK = 1 << 7, FP_DEVICE_FEATURE_ALWAYS_ON = 1 << 8, + FP_DEVICE_FEATURE_UPDATE_PRINT = 1 << 9, } FpDeviceFeature; /** diff --git a/libfprint/fp-image-device.c b/libfprint/fp-image-device.c index 82e69f99..519ad754 100644 --- a/libfprint/fp-image-device.c +++ b/libfprint/fp-image-device.c @@ -101,6 +101,7 @@ fp_image_device_start_capture_action (FpDevice *device) FpImageDevice *self = FP_IMAGE_DEVICE (device); FpImageDevicePrivate *priv = fp_image_device_get_instance_private (self); FpiDeviceAction action; + FpiPrintType print_type; /* There is just one action that we cannot support out * of the box, which is a capture without first waiting @@ -124,7 +125,9 @@ fp_image_device_start_capture_action (FpDevice *device) FpPrint *enroll_print = NULL; fpi_device_get_enroll_data (device, &enroll_print); - fpi_print_set_type (enroll_print, FPI_PRINT_NBIS); + g_object_get (enroll_print, "fpi-type", &print_type, NULL); + if (print_type != FPI_PRINT_NBIS) + fpi_print_set_type (enroll_print, FPI_PRINT_NBIS); } priv->enroll_stage = 0; @@ -221,6 +224,7 @@ fp_image_device_class_init (FpImageDeviceClass *klass) fp_device_class->cancel = fp_image_device_cancel_action; fpi_device_class_auto_initialize_features (fp_device_class); + fp_device_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT; /* Default implementations */ klass->activate = fp_image_device_default_activate; diff --git a/libfprint/fp-print.c b/libfprint/fp-print.c index c8a1b071..8532b6cc 100644 --- a/libfprint/fp-print.c +++ b/libfprint/fp-print.c @@ -61,6 +61,7 @@ enum { /* Private property*/ PROP_FPI_TYPE, PROP_FPI_DATA, + PROP_FPI_PRINTS, N_PROPS }; @@ -133,6 +134,10 @@ fp_print_get_property (GObject *object, g_value_set_variant (value, self->data); break; + case PROP_FPI_PRINTS: + g_value_set_pointer (value, self->prints); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -188,6 +193,11 @@ fp_print_set_property (GObject *object, self->data = g_value_dup_variant (value); break; + case PROP_FPI_PRINTS: + g_clear_pointer (&self->prints, g_ptr_array_unref); + self->prints = g_value_get_pointer (value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -299,6 +309,19 @@ fp_print_class_init (FpPrintClass *klass) NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE); + /** + * FpPrint::fpi-prints: (skip) + * + * This property is only for internal purposes. + * + * Stability: private + */ + properties[PROP_FPI_PRINTS] = + g_param_spec_pointer ("fpi-prints", + "Prints", + "Prints for internal use only", + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE); + g_object_class_install_properties (object_class, N_PROPS, properties); } diff --git a/tests/test-fpi-device.c b/tests/test-fpi-device.c index 408e2f92..ff658e97 100644 --- a/tests/test-fpi-device.c +++ b/tests/test-fpi-device.c @@ -27,6 +27,7 @@ #include "fpi-compat.h" #include "fpi-log.h" #include "test-device-fake.h" +#include "fp-print-private.h" /* Utility functions */ @@ -142,6 +143,16 @@ make_fake_print (FpDevice *device, return enrolled_print; } +static FpPrint * +make_fake_nbis_print (FpDevice *device) +{ + FpPrint *enrolled_print = fp_print_new (device); + + fpi_print_set_type (enrolled_print, FPI_PRINT_NBIS); + + return enrolled_print; +} + static FpPrint * make_fake_print_reffed (FpDevice *device, GVariant *print_data) @@ -1045,7 +1056,6 @@ test_driver_enroll_error_no_print (void) out_print = fp_device_enroll_sync (device, fp_print_new (device), NULL, NULL, NULL, &error); - g_test_assert_expected_messages (); g_assert (fake_dev->last_called_function == dev_class->enroll); g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_GENERAL); g_assert_null (out_print); @@ -1053,6 +1063,111 @@ test_driver_enroll_error_no_print (void) g_clear_error (&error); } +static void +test_driver_enroll_update_nbis (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(FpAutoResetClass) dev_class = auto_reset_device_class (); + g_autoptr(FpAutoCloseDevice) device = NULL; + g_autoptr(FpPrint) template_print = NULL; + FpiDeviceFake *fake_dev = NULL; + FpPrint *out_print = NULL; + + dev_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT; + device = auto_close_fake_device_new (); + fake_dev = FPI_DEVICE_FAKE (device); + + template_print = make_fake_nbis_print (device); + fake_dev->ret_print = template_print; + + out_print = + fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error); + + g_assert (fake_dev->last_called_function == dev_class->enroll); + g_assert (fake_dev->action_data == template_print); + + g_assert_no_error (error); + g_assert (out_print == template_print); +} + +static void +test_driver_enroll_update_nbis_wrong_device (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(FpAutoResetClass) dev_class = auto_reset_device_class (); + g_autoptr(FpAutoCloseDevice) device = NULL; + g_autoptr(FpPrint) template_print = NULL; + FpiDeviceFake *fake_dev = NULL; + FpPrint *out_print = NULL; + + dev_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT; + + device = auto_close_fake_device_new (); + fake_dev = FPI_DEVICE_FAKE (device); + + template_print = make_fake_nbis_print (device); + template_print->device_id = g_strdup ("wrong_device"); + fake_dev->ret_print = template_print; + + out_print = + fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error); + + g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID); + g_assert (out_print == NULL); +} + +static void +test_driver_enroll_update_nbis_wrong_driver (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(FpAutoResetClass) dev_class = auto_reset_device_class (); + g_autoptr(FpAutoCloseDevice) device = NULL; + g_autoptr(FpPrint) template_print = NULL; + FpiDeviceFake *fake_dev = NULL; + FpPrint *out_print = NULL; + + dev_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT; + + device = auto_close_fake_device_new (); + fake_dev = FPI_DEVICE_FAKE (device); + + template_print = make_fake_nbis_print (device); + template_print->driver = g_strdup ("wrong_driver"); + fake_dev->ret_print = template_print; + + out_print = + fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error); + + g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID); + g_assert (out_print == NULL); +} + +static void +test_driver_enroll_update_nbis_missing_feature (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(FpAutoResetClass) dev_class = auto_reset_device_class (); + g_autoptr(FpAutoCloseDevice) device = NULL; + g_autoptr(FpPrint) template_print = NULL; + FpiDeviceFake *fake_dev = NULL; + FpPrint *out_print = NULL; + + device = auto_close_fake_device_new (); + fake_dev = FPI_DEVICE_FAKE (device); + + template_print = make_fake_nbis_print (device); + fake_dev->ret_print = template_print; + + out_print = + fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error); + + g_assert (fake_dev->last_called_function == dev_class->open); + g_assert (fake_dev->action_data == NULL); + + g_assert_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID); + g_assert (out_print == NULL); +} + typedef struct { gint completed_stages; @@ -3306,6 +3421,13 @@ main (int argc, char *argv[]) g_test_add_func ("/driver/enroll/error", test_driver_enroll_error); g_test_add_func ("/driver/enroll/error/no_print", test_driver_enroll_error_no_print); g_test_add_func ("/driver/enroll/progress", test_driver_enroll_progress); + g_test_add_func ("/driver/enroll/update_nbis", test_driver_enroll_update_nbis); + g_test_add_func ("/driver/enroll/update_nbis_wrong_device", + test_driver_enroll_update_nbis_wrong_device); + g_test_add_func ("/driver/enroll/update_nbis_wrong_driver", + test_driver_enroll_update_nbis_wrong_driver); + g_test_add_func ("/driver/enroll/update_nbis_missing_feature", + test_driver_enroll_update_nbis_missing_feature); g_test_add_func ("/driver/verify", test_driver_verify); g_test_add_func ("/driver/verify/fail", test_driver_verify_fail); g_test_add_func ("/driver/verify/retry", test_driver_verify_retry); diff --git a/tests/virtual-image.py b/tests/virtual-image.py index 7605b583..e4a464eb 100755 --- a/tests/virtual-image.py +++ b/tests/virtual-image.py @@ -136,6 +136,7 @@ class VirtualImage(unittest.TestCase): self.assertTrue(self.dev.has_feature(FPrint.DeviceFeature.CAPTURE)) self.assertTrue(self.dev.has_feature(FPrint.DeviceFeature.IDENTIFY)) self.assertTrue(self.dev.has_feature(FPrint.DeviceFeature.VERIFY)) + self.assertTrue(self.dev.has_feature(FPrint.DeviceFeature.UPDATE_PRINT)) self.assertFalse(self.dev.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK)) self.assertFalse(self.dev.has_feature(FPrint.DeviceFeature.STORAGE)) self.assertFalse(self.dev.has_feature(FPrint.DeviceFeature.STORAGE_LIST)) @@ -144,7 +145,8 @@ class VirtualImage(unittest.TestCase): self.assertEqual(self.dev.get_features(), FPrint.DeviceFeature.CAPTURE | FPrint.DeviceFeature.IDENTIFY | - FPrint.DeviceFeature.VERIFY) + FPrint.DeviceFeature.VERIFY | + FPrint.DeviceFeature.UPDATE_PRINT) def test_capture_prevents_close(self): cancel = Gio.Cancellable() @@ -167,7 +169,7 @@ class VirtualImage(unittest.TestCase): while not self._cancelled: ctx.iteration(True) - def enroll_print(self, image): + def enroll_print(self, image, template=None): self._step = 0 self._enrolled = None @@ -181,14 +183,15 @@ class VirtualImage(unittest.TestCase): self.assertEqual(self.dev.get_finger_status(), FPrint.FingerStatusFlags.NONE) self._enrolled = fp - template = FPrint.Print.new(self.dev) - template.props.finger = FPrint.Finger.LEFT_THUMB - template.props.username = "testuser" - template.props.description = "test print" - datetime = GLib.DateTime.new_now_local() - date = GLib.Date() - date.set_dmy(*datetime.get_ymd()[::-1]) - template.props.enroll_date = date + if template is None: + template = FPrint.Print.new(self.dev) + template.props.finger = FPrint.Finger.LEFT_THUMB + template.props.username = "testuser" + template.props.description = "test print" + datetime = GLib.DateTime.new_now_local() + date = GLib.Date() + date.set_dmy(*datetime.get_ymd()[::-1]) + template.props.enroll_date = date self.assertEqual(self.dev.get_finger_status(), FPrint.FingerStatusFlags.NONE) self.dev.enroll(template, None, progress_cb, tuple(), done_cb) @@ -264,6 +267,28 @@ class VirtualImage(unittest.TestCase): ctx.iteration(True) assert(not self._verify_match) + # Test fingerprint updates + # Enroll a second print + fp_whorl_tended_arch = self.enroll_print('tented_arch', fp_whorl) + + # Make sure the first print verifies successfully after the update + self._verify_match = None + self._verify_fp = None + self.dev.verify(fp_whorl_tended_arch, callback=verify_cb) + self.send_image('whorl') + while self._verify_match is None: + ctx.iteration(True) + assert(self._verify_match) + + # Make sure the second print verifies successfully after the update + self._verify_match = None + self._verify_fp = None + self.dev.verify(fp_whorl_tended_arch, callback=verify_cb) + self.send_image('tented_arch') + while self._verify_match is None: + ctx.iteration(True) + assert(self._verify_match) + # Test verify error cases self._verify_fp = None self._verify_error = None