diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 77d78b7..f65d139 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,7 +55,10 @@ stages: before_script: # Make sure we don't build or link against the system libfprint - dnf remove -y libfprint-devel - - git clone https://gitlab.freedesktop.org/libfprint/libfprint.git + - git clone https://gitlab.freedesktop.org/joshuagrisham/libfprint.git # TODO: Changed temporarily for testing + - cd libfprint # TODO: Changed temporarily for testing + - git switch feature/sdcp-v2 # TODO: Changed temporarily for testing + - cd .. # TODO: Changed temporarily for testing - meson setup libfprint/_build libfprint --prefix=/usr -Ddrivers=virtual_image,virtual_device,virtual_device_storage -Ddoc=false - meson compile -C libfprint/_build - meson install -C libfprint/_build diff --git a/src/device.c b/src/device.c index 4de4a10..5746d44 100644 --- a/src/device.c +++ b/src/device.c @@ -1024,6 +1024,21 @@ _fprint_device_add_client (FprintDevice *rdev, const char *sender) } } +static void +reset_sdcp_if_device_untrusted (FpDevice *dev, + GError *error) +{ + if (g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_UNTRUSTED)) + { + g_debug ("resetting SDCP connection"); + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (dev), "sdcp-data")) + g_object_set (G_OBJECT (dev), "sdcp-data", NULL, NULL); + + store.sdcp_data_delete (dev); + } +} + static void dev_open_cb (FpDevice *dev, GAsyncResult *res, void *user_data) { @@ -1048,11 +1063,14 @@ dev_open_cb (FpDevice *dev, GAsyncResult *res, void *user_data) "Open failed with error: %s", error->message); g_dbus_method_invocation_return_gerror (invocation, dbus_error); session_data_set_new (priv, NULL, NULL); + reset_sdcp_if_device_untrusted (dev, error); return; } g_debug ("claimed device %d", priv->id); + store.sdcp_data_save (priv->dev); + fprint_dbus_device_complete_claim (FPRINT_DBUS_DEVICE (rdev), invocation); } @@ -1120,6 +1138,8 @@ fprint_device_claim (FprintDBusDevice *dbus_dev, g_debug ("user '%s' claiming the device: %d", session->username, priv->id); + store.sdcp_data_load (priv->dev); + priv->current_action = ACTION_OPEN; fp_device_open (priv->dev, NULL, (GAsyncReadyCallback) dev_open_cb, rdev); @@ -1571,6 +1591,8 @@ verify_cb (FpDevice *dev, GAsyncResult *res, void *user_data) g_debug ("verify_cb: result %s", name); + reset_sdcp_if_device_untrusted (dev, error); + /* Automatically restart the operation for retry failures */ if (error && error->domain == FP_DEVICE_RETRY) { @@ -1616,6 +1638,8 @@ identify_cb (FpDevice *dev, GAsyncResult *res, void *user_data) g_debug ("identify_cb: result %s", name); + reset_sdcp_if_device_untrusted (dev, error); + /* Automatically restart the operation for retry failures */ if (error && error->domain == FP_DEVICE_RETRY) { @@ -2052,6 +2076,8 @@ enroll_cb (FpDevice *dev, GAsyncResult *res, void *user_data) g_signal_emit (rdev, signals[SIGNAL_ENROLL_STATUS], 0, name, TRUE); + reset_sdcp_if_device_untrusted (dev, error); + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Device reported an error during enroll: %s", error->message); @@ -2091,6 +2117,8 @@ enroll_identify_cb (FpDevice *dev, GAsyncResult *res, void *user_data) g_clear_error (&error); } + reset_sdcp_if_device_untrusted (dev, error); + /* We may need to retry or error out. */ if (error) { diff --git a/src/file_storage.c b/src/file_storage.c index e12fe58..05b7972 100644 --- a/src/file_storage.c +++ b/src/file_storage.c @@ -366,3 +366,191 @@ file_storage_deinit (void) g_clear_pointer (&storage_path, g_free); return 0; } + +static char *current_boot_id = NULL; + +static const char * +get_boot_id (void) +{ + g_autofree gchar *kernel_boot_id = NULL; + + /* read the boot_id exposed by the kernel */ + if (current_boot_id == NULL) + { + if (g_file_get_contents ("/proc/sys/kernel/random/boot_id", &kernel_boot_id, NULL, NULL)) + { + current_boot_id = g_strdup (kernel_boot_id); + g_strchomp (current_boot_id); + } + else + { + g_warning ("get_boot_id(): could not read '/proc/sys/kernel/random/boot_id'"); + } + } + + /* + * TODO: is it better to read this boot_id sysfs file or to get BootID from + * org.freedesktop.hostname1 using D-Bus ? Or maybe we should try one and use + * the other as a fallback ? + */ + + return current_boot_id; +} + +static char * +get_sdcp_data_path (FpDevice *dev) +{ + char *path = g_build_filename (get_storage_path (), + ".sdcp", + fp_device_get_driver (dev), + fp_device_get_device_id (dev), + NULL); + + return g_steal_pointer (&path); +} + +static int +delete_sdcp_data (FpDevice *dev, + const char *boot_id) +{ + g_autofree gchar *dirpath = NULL; + g_autofree gchar *path = NULL; + int r; + + dirpath = get_sdcp_data_path (dev); + path = g_build_filename (dirpath, boot_id, NULL); + + if (!g_file_test (path, G_FILE_TEST_EXISTS)) + return 0; + + r = g_unlink (path); + g_debug ("delete_sdcp_data(): unlink(\"%s\") %s", + path, g_strerror (r)); + + return r; +} + +int +file_storage_sdcp_data_save (FpDevice *dev) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GBytes) sdcp_data = NULL; + g_autofree gchar *dirpath = NULL; + g_autofree gchar *path = NULL; + int r; + + dirpath = get_sdcp_data_path (dev); + path = g_build_filename (dirpath, get_boot_id (), NULL); + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (dev), "sdcp-data") == NULL) + { + g_debug ("file_storage_sdcp_data_save(): device does not have 'scdp-data'"); + return -ENOENT; + } + + g_object_get (G_OBJECT (dev), "sdcp-data", &sdcp_data, NULL); + + if (!sdcp_data) + { + g_debug ("file_storage_sdcp_data_save(): device does not have 'scdp-data'"); + return -ENOENT; + } + + r = g_mkdir_with_parents (dirpath, DIR_PERMS); + if (r < 0) + { + g_debug ("file_storage_sdcp_data_save(): could not mkdir(\"%s\"): %s", + dirpath, g_strerror (r)); + return r; + } + + g_file_set_contents (path, + g_bytes_get_data (sdcp_data, NULL), + g_bytes_get_size (sdcp_data), + &err); + if (err) + { + g_debug ("file_storage_sdcp_data_save(): could not save '%s': %s", + path, err->message); + /* FIXME interpret error codes */ + return err->code; + } + + g_debug ("file_storage_sdcp_data_save(): SDCP data saved to %s", path); + + return 0; +} + +int +file_storage_sdcp_data_load (FpDevice *dev) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar *dirpath = NULL; + GDir *dir = NULL; + const gchar *entry; + g_autofree gchar *path = NULL; + gboolean found = FALSE; + gchar *buf = NULL; + gsize len; + + dirpath = get_sdcp_data_path (dev); + + if (!g_file_test (dirpath, G_FILE_TEST_EXISTS)) + return 0; + + dir = g_dir_open (dirpath, 0, &err); + if (!dir) + { + g_debug ("file_storage_sdcp_data_load(): failed to open directory '%s': %s", + dirpath, err->message); + g_clear_error (&err); + return -ENOENT; + } + + while ((entry = g_dir_read_name (dir)) != NULL) + { + /* + * Each dir entry should be a boot_id including the "sdcp-data". If + * an entry does not match the current boot_id, we should delete it. + * Otherwise, we load the sdcp-data from that file. + */ + + if (g_strcmp0 (entry, get_boot_id ()) != 0) + { + g_debug ("file_storage_sdcp_data_load(): deleting SDCP data from prior system boot '%s'", + entry); + delete_sdcp_data (dev, entry); + continue; + } + else + { + path = g_build_filename (dirpath, entry, NULL); + g_file_get_contents (path, &buf, &len, &err); + if (err) + { + g_debug ("file_storage_sdcp_data_load(): could not read SDCP data file '%s': %s", + path, err->message); + continue; + } + + g_object_set (G_OBJECT (dev), "sdcp-data", g_bytes_new_take (buf, len), NULL); + + g_debug ("file_storage_sdcp_data_load(): loaded SDCP data from file '%s'", path); + + found = TRUE; + } + } + + g_dir_close (dir); + + if (found) + return 0; + else + return -ENOENT; +} + +int +file_storage_sdcp_data_delete (FpDevice *dev) +{ + return delete_sdcp_data (dev, get_boot_id ()); +} diff --git a/src/file_storage.h b/src/file_storage.h index 29e70df..86fcf24 100644 --- a/src/file_storage.h +++ b/src/file_storage.h @@ -38,3 +38,9 @@ int file_storage_deinit (void); GSList *file_storage_discover_prints (FpDevice *dev, const char *username); GSList *file_storage_discover_users (void); + +int file_storage_sdcp_data_save (FpDevice *dev); + +int file_storage_sdcp_data_load (FpDevice *dev); + +int file_storage_sdcp_data_delete (FpDevice *dev); diff --git a/src/main.c b/src/main.c index eba2f4b..96842c3 100644 --- a/src/main.c +++ b/src/main.c @@ -51,6 +51,9 @@ set_storage_file (void) store.print_data_delete = &file_storage_print_data_delete; store.discover_prints = &file_storage_discover_prints; store.discover_users = &file_storage_discover_users; + store.sdcp_data_save = &file_storage_sdcp_data_save; + store.sdcp_data_load = &file_storage_sdcp_data_load; + store.sdcp_data_delete = &file_storage_sdcp_data_delete; } static gboolean @@ -75,7 +78,10 @@ load_storage_module (const char *module_name) !g_module_symbol (module, "print_data_load", (gpointer *) &store.print_data_load) || !g_module_symbol (module, "print_data_delete", (gpointer *) &store.print_data_delete) || !g_module_symbol (module, "discover_prints", (gpointer *) &store.discover_prints) || - !g_module_symbol (module, "discover_users", (gpointer *) &store.discover_users)) + !g_module_symbol (module, "discover_users", (gpointer *) &store.discover_users) || + !g_module_symbol (module, "sdcp_data_save", (gpointer *) &store.sdcp_data_save) || + !g_module_symbol (module, "sdcp_data_load", (gpointer *) &store.sdcp_data_load) || + !g_module_symbol (module, "sdcp_data_delete", (gpointer *) &store.sdcp_data_delete)) { g_module_close (module); g_debug ("Failed to load module. Please update your code."); diff --git a/src/storage.h b/src/storage.h index ef25a84..053f5e5 100644 --- a/src/storage.h +++ b/src/storage.h @@ -31,6 +31,9 @@ typedef int (*storage_print_data_delete)(FpDevice *dev, typedef GSList *(*storage_discover_prints)(FpDevice *dev, const char *username); typedef GSList *(*storage_discover_users)(void); +typedef int (*storage_sdcp_data_load)(FpDevice *dev); +typedef int (*storage_sdcp_data_save)(FpDevice *dev); +typedef int (*storage_sdcp_data_delete)(FpDevice *dev); typedef int (*storage_init)(void); typedef int (*storage_deinit)(void); @@ -43,6 +46,9 @@ struct storage storage_print_data_delete print_data_delete; storage_discover_prints discover_prints; storage_discover_users discover_users; + storage_sdcp_data_save sdcp_data_save; + storage_sdcp_data_load sdcp_data_load; + storage_sdcp_data_delete sdcp_data_delete; }; typedef struct storage fp_storage; diff --git a/tests/fprintd.py b/tests/fprintd.py index 8d8fe6e..80045f8 100644 --- a/tests/fprintd.py +++ b/tests/fprintd.py @@ -3448,6 +3448,136 @@ class FPrintdUtilsTest(FPrintdVirtualStorageDeviceBaseTest): self.run_verify(finger=FPrint.Finger.UNKNOWN, match=False, error=FPrint.DeviceError.PROTO) +class FPrintdVirtualSdcpDeviceBaseTests(FPrintdVirtualDeviceBaseTest): + socket_env = 'FP_VIRTUAL_SDCP' + device_driver = 'virtual_sdcp' + driver_name = 'Virtual SDCP device for debugging' + has_identification = True + + def get_sdcp_data_path(self): + with open("/proc/sys/kernel/random/boot_id", "r") as f: + boot_id = f.read().strip() + return os.path.join(self.state_dir, ".sdcp", self.device_driver, str(self.device_id), + boot_id) + + def test_connect(self): + # To test a Connect, we just need to open and close the device + self.device.Claim('(s)', 'testuser') + self.device.Release() + + # Check that the SDCP data file was created + self.assertTrue(os.path.isfile(self.get_sdcp_data_path())) + + def test_reconnect(self): + # To test a Reconnect, we should connect, disconnect, and connect again + self.device.Claim('(s)', 'testuser') + self.device.Release() + + self.device.Claim('(s)', 'testuser') + self.daemon_log.check_line('SDCP Reconnect succeeded', timeout=3) + self.device.Release() + + def test_reconnect_from_sdcp_data(self): + # Connecting will create the SDCP data file + self.device.Claim('(s)', 'testuser') + self.device.Release() + + # Now stop and restart the daemon so that sdcp-data will be unloaded from memory and force + # reading data from disk instead + self.daemon_stop() + self.daemon_start(self.driver_name) + + self.device.Claim('(s)', 'testuser') + self.daemon_log.check_line('loaded SDCP data from file', timeout=3) + self.daemon_log.check_line('SDCP Reconnect succeeded', timeout=3) + self.device.Release() + + def test_reconnect_with_invalid_data(self): + self.device.Claim('(s)', 'testuser') + self.device.Release() + self.daemon_stop() + + # virtual_sdcp uses a hard-coded application secret; if we just write something else then it should stop working + bad_sdcp_data = bytes.fromhex("0123456789abcdef1032547698badcfeaabbccddeeff00112233445566778899") + with open(self.get_sdcp_data_path(), "wb") as f: + f.write(bad_sdcp_data) + + # Now when we try to connect, the Reconnect will fail and it should reset SDCP data and perform a normal Connect again + self.daemon_start(self.driver_name) + self.device.Claim('(s)', 'testuser') + self.daemon_log.check_line('SDCP Reconnect failed', timeout=3) + self.daemon_log.check_line('SDCP ConnectResponse claim validated successfully', timeout=3) + self.device.Release() + + # And check that the SDCP data file has been changed from the above bogus value + with open(self.get_sdcp_data_path(), "rb") as f: + new_sdcp_data = f.read() + + self.assertNotEqual(bad_sdcp_data, new_sdcp_data) + + def test_list(self): + self.device.Claim('(s)', 'testuser') + with self.assertFprintError('NoEnrolledPrints'): + self.device.ListEnrolledFingers('(s)', 'testuser') + self.device.Release() + + def test_enroll_list_verify(self): + self.device.Claim('(s)', 'testuser') + + self.device.EnrollStart('(s)', 'right-thumb') + self.device.EnrollStop() + + enrolled = self.device.ListEnrolledFingers('(s)', 'testuser') + self.assertEqual(enrolled, ['right-thumb']) + + self.device.VerifyStart('(s)', 'any') + self.wait_for_result('verify-match') + self.device.VerifyStop() + + self.device.Release() + + def test_enroll_with_invalid_data(self): + # To test this we need to ensure that a Reconnect will not be attempted + # (fprintd will just use the sdcp-data file as-is instead of correcting is as part of Reconnect) + os.environ['FP_VIRTUAL_SDCP_NO_RECONNECT'] = '1' + + self.device.Claim('(s)', 'testuser') + self.device.Release() + self.daemon_stop() + + # Now we set the bad sdcp-data which will be used with the next operation + bad_sdcp_data = bytes.fromhex("0123456789abcdef1032547698badcfeaabbccddeeff00112233445566778899") + with open(self.get_sdcp_data_path(), "wb") as f: + f.write(bad_sdcp_data) + + self.daemon_start(self.driver_name) + self.device.Claim('(s)', 'testuser') + + self.device.EnrollStart('(s)', 'right-thumb') + self.device.EnrollStop() + + self.wait_for_result('enroll-unknown-error') + self.daemon_log.check_line('resetting SDCP connection', timeout=3) + + # When exception occurs fprintd should release the device and then + # claim again for the next operation, so we will mimic that here + self.device.Release() + self.device.Claim('(s)', 'testuser') + + # Check that a new Connect was performed + self.daemon_log.check_line('SDCP ConnectResponse claim validated successfully', timeout=3) + + with self.assertFprintError('NoEnrolledPrints'): + self.device.ListEnrolledFingers('(s)', 'testuser') + + self.device.EnrollStart('(s)', 'right-thumb') + self.device.EnrollStop() + + enrolled = self.device.ListEnrolledFingers('(s)', 'testuser') + self.assertEqual(enrolled, ['right-thumb']) + + self.device.Release() + def list_tests(): import unittest_inspector