From 9cfad7c6214e187624e165a18b64fe266ec68340 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Thu, 15 Aug 2019 16:28:58 +0200 Subject: [PATCH 1/6] egg-testing: Sync with gnome-keyring --- egg/egg-testing.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++ egg/egg-testing.h | 10 ++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/egg/egg-testing.c b/egg/egg-testing.c index 9e7cba5..f1e0b68 100644 --- a/egg/egg-testing.c +++ b/egg/egg-testing.c @@ -171,3 +171,71 @@ egg_tests_run_with_loop (void) return ret; } + +void +egg_tests_copy_scratch_file (const gchar *directory, + const gchar *filename) +{ + GError *error = NULL; + gchar *basename; + gchar *contents; + gchar *destination; + gsize length; + + g_assert (directory); + + g_file_get_contents (filename, &contents, &length, &error); + g_assert_no_error (error); + + basename = g_path_get_basename (filename); + destination = g_build_filename (directory, basename, NULL); + g_free (basename); + + g_file_set_contents (destination, contents, length, &error); + g_assert_no_error (error); + g_free (destination); + g_free (contents); +} + +gchar * +egg_tests_create_scratch_directory (const gchar *file_to_copy, + ...) +{ + gchar *basename; + gchar *directory; + va_list va; + + basename = g_path_get_basename (g_get_prgname ()); + directory = g_strdup_printf ("/tmp/scratch-%s.XXXXXX", basename); + g_free (basename); + + if (!g_mkdtemp (directory)) + g_assert_not_reached (); + + va_start (va, file_to_copy); + + while (file_to_copy != NULL) { + egg_tests_copy_scratch_file (directory, file_to_copy); + file_to_copy = va_arg (va, const gchar *); + } + + va_end (va); + + return directory; +} + +void +egg_tests_remove_scratch_directory (const gchar *directory) +{ + gchar *argv[] = { "rm", "-rf", (gchar *)directory, NULL }; + GError *error = NULL; + gint rm_status; + + g_assert_cmpstr (directory, !=, ""); + g_assert_cmpstr (directory, !=, "/"); + + g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, + NULL, NULL, NULL, &rm_status, &error); + g_assert_no_error (error); + g_assert_cmpint (rm_status, ==, 0); +} diff --git a/egg/egg-testing.h b/egg/egg-testing.h index 1f07f0c..1a240b2 100644 --- a/egg/egg-testing.h +++ b/egg/egg-testing.h @@ -56,4 +56,12 @@ void egg_test_wait_idle (void); gint egg_tests_run_with_loop (void); -#endif /* EGG_DH_H_ */ +void egg_tests_copy_scratch_file (const gchar *directory, + const gchar *file_to_copy); + +gchar * egg_tests_create_scratch_directory (const gchar *file_to_copy, + ...) G_GNUC_NULL_TERMINATED; + +void egg_tests_remove_scratch_directory (const gchar *directory); + +#endif /* EGG_TESTING_H_ */ From 2d642b5b7d0e70d8d8c9038d2306173f9cf1a7fd Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Tue, 13 Aug 2019 18:12:35 +0200 Subject: [PATCH 2/6] secret-file-backend: New backend for storing secrets in file This adds a new backend based on locally stored file. --- libsecret/Makefile.am | 21 + libsecret/fixtures/default.keyring | Bin 0 -> 538 bytes libsecret/meson.build | 20 +- libsecret/secret-backend.c | 8 + libsecret/secret-file-backend.c | 498 +++++++++++++++++ libsecret/secret-file-backend.h | 31 ++ libsecret/secret-file-collection.c | 842 +++++++++++++++++++++++++++++ libsecret/secret-file-collection.h | 56 ++ libsecret/secret-file-item.c | 252 +++++++++ libsecret/secret-file-item.h | 34 ++ libsecret/secret-types.h | 1 + libsecret/test-file-collection.c | 364 +++++++++++++ 12 files changed, 2124 insertions(+), 3 deletions(-) create mode 100644 libsecret/fixtures/default.keyring create mode 100644 libsecret/secret-file-backend.c create mode 100644 libsecret/secret-file-backend.h create mode 100644 libsecret/secret-file-collection.c create mode 100644 libsecret/secret-file-collection.h create mode 100644 libsecret/secret-file-item.c create mode 100644 libsecret/secret-file-item.h create mode 100644 libsecret/test-file-collection.c diff --git a/libsecret/Makefile.am b/libsecret/Makefile.am index 0e34ea3..f760e6c 100644 --- a/libsecret/Makefile.am +++ b/libsecret/Makefile.am @@ -60,6 +60,17 @@ libsecret_PRIVATE = \ libsecret/secret-util.c \ $(NULL) +if WITH_GCRYPT +libsecret_PRIVATE += \ + libsecret/secret-file-backend.h \ + libsecret/secret-file-backend.c \ + libsecret/secret-file-collection.h \ + libsecret/secret-file-collection.c \ + libsecret/secret-file-item.h \ + libsecret/secret-file-item.c \ + $(NULL) +endif + libsecret_@SECRET_MAJOR@_la_SOURCES = \ $(libsecret_PUBLIC) \ $(libsecret_PRIVATE) \ @@ -247,6 +258,15 @@ test_session_LDADD = $(libsecret_LIBS) test_value_SOURCES = libsecret/test-value.c test_value_LDADD = $(libsecret_LIBS) +if WITH_GCRYPT +C_TESTS += \ + test-file-collection \ + $(NULL) + +test_file_collection_SOURCES = libsecret/test-file-collection.c +test_file_collection_LDADD = $(libsecret_LIBS) +endif + JS_TESTS = \ libsecret/test-js-lookup.js \ libsecret/test-js-clear.js \ @@ -377,4 +397,5 @@ EXTRA_DIST += \ libsecret/mock-service-prompt.py \ $(JS_TESTS) \ $(PY_TESTS) \ + libsecret/fixtures \ $(NULL) diff --git a/libsecret/fixtures/default.keyring b/libsecret/fixtures/default.keyring new file mode 100644 index 0000000000000000000000000000000000000000..cf049bf1ebb3e4f1efaa953b1f7ac96eb196837c GIT binary patch literal 538 zcmZ?I%g;^qPOU7;%uDCuW#D3DP+(wSu-d=PvUFUdM^~=N}(hJn7B{z9grx4~B_{SX908eIzg3nx3ilpt(r&TI3s< z#S!e{{i@3v`xO7lZL;qvw^`gK>33q!2QGQ@dzT(cot?rj7P-}7hAc;%bNtJsrIsaU z&e}!)JDx7?+^uNgAlIj`@m#`t_df#Jo(~&aUOnwx|L($z{5v~qroQ1f{p5b$`9e`* zK|xL`L8q}LCFTJkL4UI67bWJUrxFY_c6I;q@X5b#*;$=W<(Zbzt0=P}Uvu)Dz1QD| z&KFy9`Dop{fQ7SbKg4tunpz%o3Ka^EcU-`6-gc>9EnkU_ou`mgZt<_gkmN%`&oBJC z$Y!lr@TAeqK;hp6>61(AcTaIKy#BzaM?&on|IWDRc`^bVKa@Idid8*kILD~M006HW B_init_flags = g_value_get_flags (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_backend_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (object); + + switch (prop_id) { + case PROP_FLAGS: + g_value_set_flags (value, self->init_flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_backend_finalize (GObject *object) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (object); + + g_clear_object (&self->collection); + + G_OBJECT_CLASS (secret_file_backend_parent_class)->finalize (object); +} + +static void +secret_file_backend_class_init (SecretFileBackendClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = secret_file_backend_set_property; + object_class->get_property = secret_file_backend_get_property; + object_class->finalize = secret_file_backend_finalize; + + /** + * SecretFileBackend:flags: + * + * A set of flags describing which parts of the secret file have + * been initialized. + */ + g_object_class_override_property (object_class, PROP_FLAGS, "flags"); +} + +static void +on_collection_new_async (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + SecretFileBackend *self = g_task_get_source_object (task); + GObject *object; + GError *error = NULL; + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), + result, + &error); + if (object == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + self->collection = SECRET_FILE_COLLECTION (object); + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +secret_file_backend_real_init_async (GAsyncInitable *initable, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + gchar *path; + GFile *file; + GFile *dir; + SecretValue *password; + const gchar *envvar; + GTask *task; + GError *error = NULL; + gboolean ret; + + task = g_task_new (initable, cancellable, callback, user_data); + + envvar = g_getenv ("SECRET_FILE_TEST_PATH"); + if (envvar != NULL && *envvar != '\0') + path = g_strdup (envvar); + else { + path = g_build_filename (g_get_user_data_dir (), + "keyrings", + SECRET_COLLECTION_DEFAULT ".keyring", + NULL); + } + + file = g_file_new_for_path (path); + g_free (path); + + dir = g_file_get_parent (file); + if (dir == NULL) { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "not a valid path"); + g_object_unref (file); + g_object_unref (task); + return; + } + + ret = g_file_make_directory_with_parents (dir, cancellable, &error); + g_object_unref (dir); + if (!ret) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + g_clear_error (&error); + else { + g_task_return_error (task, error); + g_object_unref (file); + g_object_unref (task); + return; + } + } + + envvar = g_getenv ("SECRET_FILE_TEST_PASSWORD"); + if (envvar != NULL && *envvar != '\0') + password = secret_value_new (envvar, -1, "text/plain"); + else { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "master password is not retrievable"); + g_object_unref (task); + return; + } + + g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION, + io_priority, + cancellable, + on_collection_new_async, + task, + "file", file, + "password", password, + NULL); + g_object_unref (file); + secret_value_unref (password); +} + +static gboolean +secret_file_backend_real_init_finish (GAsyncInitable *initable, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, initable), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +secret_file_backend_async_initable_iface (GAsyncInitableIface *iface) +{ + iface->init_async = secret_file_backend_real_init_async; + iface->init_finish = secret_file_backend_real_init_finish; +} + +static void +on_collection_write (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SecretFileCollection *collection = + SECRET_FILE_COLLECTION (source_object); + GTask *task = G_TASK (user_data); + GError *error = NULL; + + if (!secret_file_collection_write_finish (collection, result, &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +secret_file_backend_real_store (SecretBackend *backend, + const SecretSchema *schema, + GHashTable *attributes, + const gchar *collection, + const gchar *label, + SecretValue *value, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (backend); + GTask *task; + GError *error = NULL; + + /* Warnings raised already */ + if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE)) + return; + + task = g_task_new (self, cancellable, callback, user_data); + + if (!secret_file_collection_replace (self->collection, + attributes, + label, + value, + &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + secret_file_collection_write (self->collection, + cancellable, + on_collection_write, + task); +} + +static gboolean +secret_file_backend_real_store_finish (SecretBackend *backend, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, backend), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +on_retrieve_secret (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SecretRetrievable *retrievable = SECRET_RETRIEVABLE (source_object); + GTask *task = G_TASK (user_data); + SecretValue *value; + GError *error; + + value = secret_retrievable_retrieve_secret_finish (retrievable, + result, + &error); + g_object_unref (retrievable); + if (value == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + } + g_task_return_pointer (task, value, secret_value_unref); + g_object_unref (task); +} + +static void +secret_file_backend_real_lookup (SecretBackend *backend, + const SecretSchema *schema, + GHashTable *attributes, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (backend); + GTask *task; + GList *matches; + GVariant *variant; + SecretFileItem *item; + GError *error = NULL; + + /* Warnings raised already */ + if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE)) + return; + + task = g_task_new (self, cancellable, callback, user_data); + + matches = secret_file_collection_search (self->collection, attributes); + + if (matches == NULL) { + g_task_return_pointer (task, NULL, NULL); + g_object_unref (task); + return; + } + + variant = g_variant_ref (matches->data); + g_list_free_full (matches, (GDestroyNotify)g_variant_unref); + + item = _secret_file_item_decrypt (variant, self->collection, &error); + g_variant_unref (variant); + if (item == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + secret_retrievable_retrieve_secret (SECRET_RETRIEVABLE (item), + cancellable, + on_retrieve_secret, + task); +} + +static SecretValue * +secret_file_backend_real_lookup_finish (SecretBackend *backend, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, backend), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +secret_file_backend_real_clear (SecretBackend *backend, + const SecretSchema *schema, + GHashTable *attributes, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (backend); + GTask *task; + GError *error = NULL; + gboolean ret; + + /* Warnings raised already */ + if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE)) + return; + + task = g_task_new (self, cancellable, callback, user_data); + + ret = secret_file_collection_clear (self->collection, attributes, &error); + if (error != NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + /* No need to write as nothing has been removed. */ + if (!ret) { + g_task_return_boolean (task, FALSE); + g_object_unref (task); + return; + } + + secret_file_collection_write (self->collection, + cancellable, + on_collection_write, + task); +} + +static gboolean +secret_file_backend_real_clear_finish (SecretBackend *backend, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, backend), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +unref_objects (gpointer data) +{ + GList *list = data; + + g_list_free_full (list, g_object_unref); +} + +static void +secret_file_backend_real_search (SecretBackend *backend, + const SecretSchema *schema, + GHashTable *attributes, + SecretSearchFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (backend); + GTask *task; + GList *matches; + GList *results = NULL; + GList *l; + GError *error = NULL; + + /* Warnings raised already */ + if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE)) + return; + + task = g_task_new (self, cancellable, callback, user_data); + + matches = secret_file_collection_search (self->collection, attributes); + for (l = matches; l; l = g_list_next (l)) { + SecretFileItem *item = _secret_file_item_decrypt (l->data, self->collection, &error); + if (item == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + results = g_list_append (results, item); + } + g_list_free_full (matches, (GDestroyNotify)g_variant_unref); + + g_task_return_pointer (task, results, unref_objects); + g_object_unref (task); +} + +static GList * +secret_file_backend_real_search_finish (SecretBackend *backend, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, backend), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +secret_file_backend_backend_iface (SecretBackendInterface *iface) +{ + iface->store = secret_file_backend_real_store; + iface->store_finish = secret_file_backend_real_store_finish; + iface->lookup = secret_file_backend_real_lookup; + iface->lookup_finish = secret_file_backend_real_lookup_finish; + iface->clear = secret_file_backend_real_clear; + iface->clear_finish = secret_file_backend_real_clear_finish; + iface->search = secret_file_backend_real_search; + iface->search_finish = secret_file_backend_real_search_finish; +} diff --git a/libsecret/secret-file-backend.h b/libsecret/secret-file-backend.h new file mode 100644 index 0000000..27d896c --- /dev/null +++ b/libsecret/secret-file-backend.h @@ -0,0 +1,31 @@ +/* libsecret - GLib wrapper for Secret Service + * + * Copyright 2019 Red Hat, Inc. + * + * This program 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 licence or (at + * your option) any later version. + * + * See the included COPYING file for more information. + * + * Author: Daiki Ueno + */ + +#if !defined (__SECRET_INSIDE_HEADER__) && !defined (SECRET_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __SECRET_FILE_BACKEND_H__ +#define __SECRET_FILE_BACKEND_H__ + +#include + +G_BEGIN_DECLS + +#define SECRET_TYPE_FILE_BACKEND (secret_file_backend_get_type ()) +G_DECLARE_FINAL_TYPE (SecretFileBackend, secret_file_backend, SECRET, FILE_BACKEND, GObject) + +G_END_DECLS + +#endif /* __SECRET_FILE_BACKEND_H__ */ diff --git a/libsecret/secret-file-collection.c b/libsecret/secret-file-collection.c new file mode 100644 index 0000000..79863ea --- /dev/null +++ b/libsecret/secret-file-collection.c @@ -0,0 +1,842 @@ +/* libsecret - GLib wrapper for Secret Service + * + * Copyright 2019 Red Hat, Inc. + * + * This program 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 licence or (at + * your option) any later version. + * + * See the included COPYING file for more information. + * + * Author: Daiki Ueno + */ + +#include "config.h" + +#include "secret-file-collection.h" + +#include "egg/egg-secure-memory.h" + +EGG_SECURE_DECLARE (secret_file_collection); + +#ifdef WITH_GCRYPT +#include +#endif + +#define PBKDF2_HASH_ALGO GCRY_MD_SHA256 +#define SALT_SIZE 32 +#define ITERATION_COUNT 100000 + +#define MAC_ALGO GCRY_MAC_HMAC_SHA256 +#define MAC_SIZE 32 + +#define CIPHER_ALGO GCRY_CIPHER_AES256 +#define CIPHER_BLOCK_SIZE 16 +#define IV_SIZE CIPHER_BLOCK_SIZE + +#define KEYRING_FILE_HEADER "GnomeKeyring\n\r\0\n" +#define KEYRING_FILE_HEADER_LEN 16 + +#define MAJOR_VERSION 1 +#define MINOR_VERSION 0 + +struct _SecretFileCollection +{ + GObject parent; + GFile *file; + gchar *etag; + SecretValue *password; + GBytes *salt; + guint32 iteration_count; + GDateTime *modified; + guint64 usage_count; + GBytes *key; + GVariant *items; +}; + +static void secret_file_collection_async_initable_iface (GAsyncInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (SecretFileCollection, secret_file_collection, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, secret_file_collection_async_initable_iface); +); + +enum { + PROP_0, + PROP_FILE, + PROP_PASSWORD +}; + +static gboolean +derive (SecretFileCollection *self) +{ + const gchar *password; + gsize n_password; + gchar *key; + gsize n_salt; + gcry_error_t gcry; + + password = secret_value_get (self->password, &n_password); + + key = egg_secure_alloc (CIPHER_BLOCK_SIZE); + self->key = g_bytes_new_with_free_func (key, + CIPHER_BLOCK_SIZE, + egg_secure_free, + key); + + n_salt = g_bytes_get_size (self->salt); + gcry = gcry_kdf_derive (password, n_password, + GCRY_KDF_PBKDF2, PBKDF2_HASH_ALGO, + g_bytes_get_data (self->salt, NULL), n_salt, + self->iteration_count, CIPHER_BLOCK_SIZE, key); + return (gcry != 0) ? FALSE : TRUE; +} + +static gboolean +calculate_mac (SecretFileCollection *self, + const guint8 *value, gsize n_value, + guint8 *buffer) +{ + gcry_mac_hd_t hd; + gcry_error_t gcry; + gconstpointer secret; + gsize n_secret; + gboolean ret = FALSE; + + gcry = gcry_mac_open (&hd, MAC_ALGO, 0, NULL); + g_return_val_if_fail (gcry == 0, FALSE); + + secret = g_bytes_get_data (self->key, &n_secret); + gcry = gcry_mac_setkey (hd, secret, n_secret); + if (gcry != 0) + goto out; + + gcry = gcry_mac_write (hd, value, n_value); + if (gcry != 0) + goto out; + + n_value = MAC_SIZE; + gcry = gcry_mac_read (hd, buffer, &n_value); + if (gcry != 0) + goto out; + + if (n_value != MAC_SIZE) + goto out; + + ret = TRUE; + out: + gcry_mac_close (hd); + return ret; +} + +static gboolean +decrypt (SecretFileCollection *self, + guint8 *data, + gsize n_data) +{ + gcry_cipher_hd_t hd; + gcry_error_t gcry; + gconstpointer secret; + gsize n_secret; + gboolean ret = FALSE; + + gcry = gcry_cipher_open (&hd, CIPHER_ALGO, GCRY_CIPHER_MODE_CBC, 0); + if (gcry != 0) + goto out; + + secret = g_bytes_get_data (self->key, &n_secret); + gcry = gcry_cipher_setkey (hd, secret, n_secret); + if (gcry != 0) + goto out; + + gcry = gcry_cipher_setiv (hd, data + n_data, IV_SIZE); + if (gcry != 0) + goto out; + + gcry = gcry_cipher_decrypt (hd, data, n_data, NULL, 0); + if (gcry != 0) + goto out; + + ret = TRUE; + out: + (void) gcry_cipher_close (hd); + return ret; +} + +static gboolean +encrypt (SecretFileCollection *self, + guint8 *data, + gsize n_data) +{ + gcry_cipher_hd_t hd; + gcry_error_t gcry; + gconstpointer secret; + gsize n_secret; + gboolean ret = FALSE; + + gcry = gcry_cipher_open (&hd, CIPHER_ALGO, GCRY_CIPHER_MODE_CBC, 0); + if (gcry != 0) + goto out; + + secret = g_bytes_get_data (self->key, &n_secret); + gcry = gcry_cipher_setkey (hd, secret, n_secret); + if (gcry != 0) + goto out; + + gcry_create_nonce (data + n_data, IV_SIZE); + + gcry = gcry_cipher_setiv (hd, data + n_data, IV_SIZE); + if (gcry != 0) + goto out; + + gcry = gcry_cipher_encrypt (hd, data, n_data, NULL, 0); + if (gcry != 0) + goto out; + + ret = TRUE; + out: + (void) gcry_cipher_close (hd); + return ret; +} + +static void +secret_file_collection_init (SecretFileCollection *self) +{ +} + +static void +secret_file_collection_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SecretFileCollection *self = SECRET_FILE_COLLECTION (object); + + switch (prop_id) { + case PROP_FILE: + self->file = g_value_dup_object (value); + break; + case PROP_PASSWORD: + self->password = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_collection_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_collection_finalize (GObject *object) +{ + SecretFileCollection *self = SECRET_FILE_COLLECTION (object); + + g_object_unref (self->file); + g_free (self->etag); + + secret_value_unref (self->password); + + g_clear_pointer (&self->salt, g_bytes_unref); + g_clear_pointer (&self->key, g_bytes_unref); + g_clear_pointer (&self->items, g_variant_unref); + g_clear_pointer (&self->modified, g_date_time_unref); + + G_OBJECT_CLASS (secret_file_collection_parent_class)->finalize (object); +} + +static void +secret_file_collection_class_init (SecretFileCollectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->set_property = secret_file_collection_set_property; + object_class->get_property = secret_file_collection_get_property; + object_class->finalize = secret_file_collection_finalize; + + g_object_class_install_property (object_class, PROP_FILE, + g_param_spec_object ("file", "File", "File", + G_TYPE_FILE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (object_class, PROP_PASSWORD, + g_param_spec_boxed ("password", "password", "Password", + SECRET_TYPE_VALUE, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +on_load_contents (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GFile *file = G_FILE (source_object); + GTask *task = G_TASK (user_data); + SecretFileCollection *self = g_task_get_source_object (task); + gchar *contents; + gchar *p; + gsize length; + GVariant *variant; + GVariant *salt_array; + guint32 salt_size; + guint64 modified_time; + gconstpointer data; + gsize n_data; + GError *error = NULL; + gboolean ret; + + ret = g_file_load_contents_finish (file, result, + &contents, &length, + &self->etag, + &error); + + if (!ret) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + GVariantBuilder builder; + guint8 salt[SALT_SIZE]; + + g_clear_error (&error); + + gcry_create_nonce (salt, sizeof(salt)); + self->salt = g_bytes_new (salt, sizeof(salt)); + self->iteration_count = ITERATION_COUNT; + self->modified = g_date_time_new_now_utc (); + self->usage_count = 0; + + if (!derive (self)) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't derive key"); + g_object_unref (task); + return; + } + + g_variant_builder_init (&builder, + G_VARIANT_TYPE ("a(a{say}ay)")); + self->items = g_variant_builder_end (&builder); + g_variant_ref_sink (self->items); + g_task_return_boolean (task, TRUE); + g_object_unref (task); + return; + } + + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + p = contents; + if (length < KEYRING_FILE_HEADER_LEN || + memcmp (p, KEYRING_FILE_HEADER, KEYRING_FILE_HEADER_LEN) != 0) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_INVALID_FILE_FORMAT, + "file header mismatch"); + g_object_unref (task); + return; + } + p += KEYRING_FILE_HEADER_LEN; + length -= KEYRING_FILE_HEADER_LEN; + + if (length < 2 || *p != MAJOR_VERSION || *(p + 1) != MINOR_VERSION) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_INVALID_FILE_FORMAT, + "version mismatch"); + g_object_unref (task); + return; + } + p += 2; + length -= 2; + + variant = g_variant_new_from_data (G_VARIANT_TYPE ("(uayutua(a{say}ay))"), + p, + length, + TRUE, + g_free, + contents); + g_variant_get (variant, "(u@ayutu@a(a{say}ay))", + &salt_size, &salt_array, &self->iteration_count, + &modified_time, &self->usage_count, + &self->items); + + self->modified = g_date_time_new_from_unix_utc (modified_time); + + data = g_variant_get_fixed_array (salt_array, &n_data, sizeof(guint8)); + g_assert (n_data == salt_size); + + self->salt = g_bytes_new (data, n_data); + if (!derive (self)) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't derive key"); + goto out; + } + + g_task_return_boolean (task, TRUE); + + out: + g_variant_unref (salt_array); + g_variant_unref (variant); + g_object_unref (task); +} + +static void +secret_file_collection_real_init_async (GAsyncInitable *initable, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileCollection *self = SECRET_FILE_COLLECTION (initable); + GTask *task; + + task = g_task_new (initable, cancellable, callback, user_data); + + g_file_load_contents_async (self->file, cancellable, on_load_contents, task); +} + +static gboolean +secret_file_collection_real_init_finish (GAsyncInitable *initable, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, initable), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +secret_file_collection_async_initable_iface (GAsyncInitableIface *iface) +{ + iface->init_async = secret_file_collection_real_init_async; + iface->init_finish = secret_file_collection_real_init_finish; +} + +static GVariant * +hash_attributes (SecretFileCollection *self, + GHashTable *attributes) +{ + GVariantBuilder builder; + guint8 buffer[MAC_SIZE]; + GList *keys; + GList *l; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{say}")); + + keys = g_hash_table_get_keys (attributes); + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); + + for (l = keys; l; l = g_list_next (l)) { + const gchar *value; + GVariant *variant; + + value = g_hash_table_lookup (attributes, l->data); + if (!calculate_mac (self, (guint8 *)value, strlen (value), buffer)) { + g_list_free (keys); + return NULL; + } + + variant = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + buffer, + MAC_SIZE, + sizeof(guint8)); + g_variant_builder_add (&builder, "{s@ay}", l->data, variant); + } + g_list_free (keys); + + return g_variant_builder_end (&builder); +} + +static gboolean +hashed_attributes_match (SecretFileCollection *self, + GVariant *hashed_attributes, + GHashTable *attributes) +{ + GHashTableIter iter; + GVariant *hashed_attribute = NULL; + gpointer key; + gpointer value; + guint8 buffer[MAC_SIZE]; + + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const guint8 *data; + gsize n_data; + + if (!g_variant_lookup (hashed_attributes, key, + "@ay", &hashed_attribute)) + return FALSE; + + data = g_variant_get_fixed_array (hashed_attribute, + &n_data, sizeof(guint8)); + if (n_data != MAC_SIZE) { + g_variant_unref (hashed_attribute); + return FALSE; + } + + if (!calculate_mac (self, value, strlen ((char *)value), buffer)) { + g_variant_unref (hashed_attribute); + return FALSE; + } + + if (memcmp (data, buffer, MAC_SIZE) != 0) { + g_variant_unref (hashed_attribute); + return FALSE; + } + g_variant_unref (hashed_attribute); + } + + return TRUE; +} + +gboolean +secret_file_collection_replace (SecretFileCollection *self, + GHashTable *attributes, + const gchar *label, + SecretValue *value, + GError **error) +{ + GVariantBuilder builder; + GVariant *hashed_attributes; + GVariantIter iter; + GVariant *child; + SecretFileItem *item; + GVariant *serialized_item; + guint8 *data = NULL; + gsize n_data; + gsize n_padded; + GVariant *variant; + GDateTime *created = NULL; + GDateTime *modified; + + hashed_attributes = hash_attributes (self, attributes); + if (!hashed_attributes) { + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + + /* Filter out the existing item */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(a{say}ay)")); + g_variant_iter_init (&iter, self->items); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + GVariant *_hashed_attributes; + g_variant_get (child, "(@a{say}ay)", &_hashed_attributes, NULL); + if (g_variant_equal (hashed_attributes, _hashed_attributes)) { + SecretFileItem *existing = + _secret_file_item_decrypt (child, self, error); + guint64 created_time; + + if (existing == NULL) { + g_variant_builder_clear (&builder); + g_variant_unref (child); + g_variant_unref (_hashed_attributes); + return FALSE; + } + g_object_get (existing, "created", &created_time, NULL); + g_object_unref (existing); + + created = g_date_time_new_from_unix_utc (created_time); + } else { + g_variant_builder_add_value (&builder, child); + } + g_variant_unref (child); + g_variant_unref (_hashed_attributes); + } + + modified = g_date_time_new_now_utc (); + if (created == NULL) + created = g_date_time_ref (modified); + + /* Create a new item and append it */ + item = g_object_new (SECRET_TYPE_FILE_ITEM, + "attributes", attributes, + "label", label, + "value", value, + "created", g_date_time_to_unix (created), + "modified", g_date_time_to_unix (modified), + NULL); + + g_date_time_unref (created); + g_date_time_unref (modified); + + serialized_item = secret_file_item_serialize (item); + g_object_unref (item); + + /* Encrypt the item with PKCS #7 padding */ + n_data = g_variant_get_size (serialized_item); + n_padded = ((n_data + CIPHER_BLOCK_SIZE) / CIPHER_BLOCK_SIZE) * + CIPHER_BLOCK_SIZE; + data = egg_secure_alloc (n_padded + IV_SIZE + MAC_SIZE); + g_variant_store (serialized_item, data); + g_variant_unref (serialized_item); + memset (data + n_data, n_padded - n_data, n_padded - n_data); + if (!encrypt (self, data, n_padded)) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't encrypt item"); + return FALSE; + } + + if (!calculate_mac (self, data, n_padded + IV_SIZE, + data + n_padded + IV_SIZE)) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + + self->usage_count++; + g_date_time_unref (self->modified); + self->modified = g_date_time_new_now_utc (); + + variant = g_variant_new_from_data (G_VARIANT_TYPE ("ay"), + data, + n_padded + IV_SIZE + MAC_SIZE, + TRUE, + egg_secure_free, + data); + variant = g_variant_new ("(@a{say}@ay)", hashed_attributes, variant); + g_variant_builder_add_value (&builder, variant); + + g_variant_unref (self->items); + self->items = g_variant_builder_end (&builder); + g_variant_ref_sink (self->items); + + return TRUE; +} + +GList * +secret_file_collection_search (SecretFileCollection *self, + GHashTable *attributes) +{ + GVariantIter iter; + GVariant *child; + GList *result = NULL; + + g_variant_iter_init (&iter, self->items); + while ((child = g_variant_iter_next_value (&iter)) != NULL) { + GVariant *hashed_attributes; + gboolean matched; + + g_variant_get (child, "(@a{say}ay)", &hashed_attributes, NULL); + matched = hashed_attributes_match (self, + hashed_attributes, + attributes); + g_variant_unref (hashed_attributes); + if (matched) + result = g_list_append (result, g_variant_ref (child)); + g_variant_unref (child); + } + + return result; +} + +SecretFileItem * +_secret_file_item_decrypt (GVariant *encrypted, + SecretFileCollection *collection, + GError **error) +{ + GVariant *blob; + gconstpointer padded; + gsize n_data; + gsize n_padded; + guint8 *data; + SecretFileItem *item; + GVariant *serialized_item; + guint8 mac[MAC_SIZE]; + + g_variant_get (encrypted, "(a{say}@ay)", NULL, &blob); + + /* Decrypt the item */ + padded = g_variant_get_fixed_array (blob, &n_padded, sizeof(guint8)); + data = egg_secure_alloc (n_padded); + memcpy (data, padded, n_padded); + g_variant_unref (blob); + + if (n_padded < IV_SIZE + MAC_SIZE) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + n_padded -= IV_SIZE + MAC_SIZE; + + if (!calculate_mac (collection, data, n_padded + IV_SIZE, mac)) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + + if (memcmp (data + n_padded + IV_SIZE, mac, MAC_SIZE) != 0) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "mac doesn't match"); + return FALSE; + } + + if (!decrypt (collection, data, n_padded)) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't decrypt item"); + return NULL; + } + + /* Remove PKCS #7 padding */ + n_data = n_padded - data[n_padded - 1]; + + serialized_item = + g_variant_new_from_data (G_VARIANT_TYPE ("(a{ss}sttay)"), + data, + n_data, + TRUE, + egg_secure_free, + data); + item = secret_file_item_deserialize (serialized_item); + g_variant_unref (serialized_item); + return item; +} + +gboolean +secret_file_collection_clear (SecretFileCollection *self, + GHashTable *attributes, + GError **error) +{ + GVariantBuilder builder; + GVariantIter items; + GVariant *child; + gboolean removed = FALSE; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(a{say}ay)")); + g_variant_iter_init (&items, self->items); + while ((child = g_variant_iter_next_value (&items)) != NULL) { + GVariant *hashed_attributes; + gboolean matched; + + g_variant_get (child, "(@a{say}ay)", &hashed_attributes, NULL); + matched = hashed_attributes_match (self, + hashed_attributes, + attributes); + g_variant_unref (hashed_attributes); + if (matched) + removed = TRUE; + else + g_variant_builder_add_value (&builder, child); + g_variant_unref (child); + } + + g_variant_unref (self->items); + self->items = g_variant_builder_end (&builder); + g_variant_ref_sink (self->items); + + return removed; +} + +static void +on_replace_contents (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GFile *file = G_FILE (source_object); + GTask *task = G_TASK (user_data); + SecretFileCollection *self = g_task_get_source_object (task); + GError *error = NULL; + + if (!g_file_replace_contents_finish (file, result, &self->etag, &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +void +secret_file_collection_write (SecretFileCollection *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + guint8 *contents; + gsize n_contents; + guint8 *p; + GVariant *salt_array; + GVariant *variant; + + salt_array = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (self->salt, NULL), + g_bytes_get_size (self->salt), + sizeof(guint8)); + variant = g_variant_new ("(u@ayutu@a(a{say}ay))", + g_bytes_get_size (self->salt), + salt_array, + self->iteration_count, + g_date_time_to_unix (self->modified), + self->usage_count, + self->items); + + g_variant_get_data (variant); /* force serialize */ + n_contents = KEYRING_FILE_HEADER_LEN + 2 + g_variant_get_size (variant); + contents = g_new (guint8, n_contents); + + p = contents; + memcpy (p, KEYRING_FILE_HEADER, KEYRING_FILE_HEADER_LEN); + p += KEYRING_FILE_HEADER_LEN; + + *p++ = MAJOR_VERSION; + *p++ = MINOR_VERSION; + + g_variant_store (variant, p); + g_variant_unref (variant); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, contents, g_free); + g_file_replace_contents_async (self->file, + (gchar *) contents, + n_contents, + self->etag, + TRUE, + G_FILE_CREATE_PRIVATE | + G_FILE_CREATE_REPLACE_DESTINATION, + cancellable, + on_replace_contents, + task); +} + +gboolean +secret_file_collection_write_finish (SecretFileCollection *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/libsecret/secret-file-collection.h b/libsecret/secret-file-collection.h new file mode 100644 index 0000000..e9f2eef --- /dev/null +++ b/libsecret/secret-file-collection.h @@ -0,0 +1,56 @@ +/* libsecret - GLib wrapper for Secret Service + * + * Copyright 2019 Red Hat, Inc. + * + * This program 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 licence or (at + * your option) any later version. + * + * See the included COPYING file for more information. + * + * Author: Daiki Ueno + */ + +#if !defined (__SECRET_INSIDE_HEADER__) && !defined (SECRET_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __SECRET_FILE_COLLECTION_H__ +#define __SECRET_FILE_COLLECTION_H__ + +#include "secret-file-item.h" +#include "secret-value.h" + +G_BEGIN_DECLS + +#define SECRET_TYPE_FILE_COLLECTION (secret_file_collection_get_type ()) +G_DECLARE_FINAL_TYPE (SecretFileCollection, secret_file_collection, SECRET, FILE_COLLECTION, GObject) + +gboolean secret_file_collection_replace (SecretFileCollection *self, + GHashTable *attributes, + const gchar *label, + SecretValue *value, + GError **error); +GList *secret_file_collection_search (SecretFileCollection *self, + GHashTable *attributes); +gboolean secret_file_collection_clear (SecretFileCollection *self, + GHashTable *attributes, + GError **error); +void secret_file_collection_write (SecretFileCollection *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean secret_file_collection_write_finish + (SecretFileCollection *self, + GAsyncResult *result, + GError **error); + +SecretFileItem *_secret_file_item_decrypt + (GVariant *encrypted, + SecretFileCollection *collection, + GError **error); + +G_END_DECLS + +#endif /* __SECRET_FILE_COLLECTION_H__ */ diff --git a/libsecret/secret-file-item.c b/libsecret/secret-file-item.c new file mode 100644 index 0000000..52a18aa --- /dev/null +++ b/libsecret/secret-file-item.c @@ -0,0 +1,252 @@ +/* libsecret - GLib wrapper for Secret Service + * + * Copyright 2019 Red Hat, Inc. + * + * This program 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 licence or (at + * your option) any later version. + * + * See the included COPYING file for more information. + * + * Author: Daiki Ueno + */ + +#include "config.h" + +#include "secret-file-item.h" +#include "secret-retrievable.h" +#include "secret-value.h" + +struct _SecretFileItem +{ + GObject parent; + GHashTable *attributes; + gchar *label; + guint64 created; + guint64 modified; + SecretValue *value; + GVariant *encrypted; +}; + +static void secret_file_item_retrievable_iface (SecretRetrievableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (SecretFileItem, secret_file_item, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SECRET_TYPE_RETRIEVABLE, secret_file_item_retrievable_iface); +); + +enum { + PROP_0, + PROP_ATTRIBUTES, + PROP_LABEL, + PROP_CREATED, + PROP_MODIFIED, + PROP_VALUE +}; + +static void +secret_file_item_init (SecretFileItem *self) +{ +} + +static void +secret_file_item_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SecretFileItem *self = SECRET_FILE_ITEM (object); + + switch (prop_id) { + case PROP_ATTRIBUTES: + self->attributes = g_value_dup_boxed (value); + break; + case PROP_LABEL: + self->label = g_value_dup_string (value); + break; + case PROP_CREATED: + self->created = g_value_get_uint64 (value); + break; + case PROP_MODIFIED: + self->modified = g_value_get_uint64 (value); + break; + case PROP_VALUE: + self->value = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_item_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SecretFileItem *self = SECRET_FILE_ITEM (object); + + switch (prop_id) { + case PROP_ATTRIBUTES: + g_value_set_boxed (value, self->attributes); + break; + case PROP_LABEL: + g_value_set_string (value, self->label); + break; + case PROP_CREATED: + g_value_set_uint64 (value, self->created); + break; + case PROP_MODIFIED: + g_value_set_uint64 (value, self->modified); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +secret_file_item_finalize (GObject *object) +{ + SecretFileItem *self = SECRET_FILE_ITEM (object); + + g_hash_table_unref (self->attributes); + g_free (self->label); + secret_value_unref (self->value); + G_OBJECT_CLASS (secret_file_item_parent_class)->finalize (object); +} + +static void +secret_file_item_class_init (SecretFileItemClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + gobject_class->set_property = secret_file_item_set_property; + gobject_class->get_property = secret_file_item_get_property; + gobject_class->finalize = secret_file_item_finalize; + + g_object_class_override_property (gobject_class, PROP_ATTRIBUTES, "attributes"); + g_object_class_override_property (gobject_class, PROP_LABEL, "label"); + g_object_class_override_property (gobject_class, PROP_CREATED, "created"); + g_object_class_override_property (gobject_class, PROP_MODIFIED, "modified"); + g_object_class_install_property (gobject_class, PROP_VALUE, + g_param_spec_boxed ("value", "Value", "Value", + SECRET_TYPE_VALUE, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +secret_file_item_retrieve_secret (SecretRetrievable *retrievable, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SecretFileItem *self = SECRET_FILE_ITEM (retrievable); + GTask *task = g_task_new (retrievable, cancellable, callback, user_data); + + g_task_return_pointer (task, + secret_value_ref (self->value), + secret_value_unref); + g_object_unref (task); +} + +static SecretValue * +secret_file_item_retrieve_secret_finish (SecretRetrievable *retrievable, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, retrievable), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +secret_file_item_retrievable_iface (SecretRetrievableInterface *iface) +{ + iface->retrieve_secret = secret_file_item_retrieve_secret; + iface->retrieve_secret_finish = secret_file_item_retrieve_secret_finish; +} + +static GHashTable * +variant_to_attributes (GVariant *variant) +{ + GVariantIter iter; + gchar *key; + gchar *value; + GHashTable *attributes; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + + g_variant_iter_init (&iter, variant); + while (g_variant_iter_next (&iter, "{ss}", &key, &value)) + g_hash_table_insert (attributes, key, value); + + return attributes; +} + +SecretFileItem * +secret_file_item_deserialize (GVariant *serialized) +{ + GVariant *attributes_variant; + GHashTable *attributes; + const gchar *label; + guint64 created; + guint64 modified; + GVariant *array; + const gchar *secret; + gsize n_secret; + SecretValue *value; + SecretFileItem *result; + + g_variant_get (serialized, "(@a{ss}&stt@ay)", + &attributes_variant, &label, &created, &modified, &array); + + secret = g_variant_get_fixed_array (array, &n_secret, sizeof(gchar)); + value = secret_value_new (secret, n_secret, "text/plain"); + + attributes = variant_to_attributes (attributes_variant); + g_variant_unref (attributes_variant); + + result = g_object_new (SECRET_TYPE_FILE_ITEM, + "attributes", attributes, + "label", label, + "created", created, + "modified", modified, + "value", value, + NULL); + g_hash_table_unref (attributes); + g_variant_unref (array); + secret_value_unref (value); + + return result; +} + +GVariant * +secret_file_item_serialize (SecretFileItem *self) +{ + GVariantBuilder builder; + GHashTableIter iter; + gpointer key; + gpointer value; + GVariant *variant; + const gchar *secret; + gsize n_secret; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}")); + g_hash_table_iter_init (&iter, self->attributes); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_variant_builder_add (&builder, "{ss}", key, value); + + secret = secret_value_get (self->value, &n_secret); + variant = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + secret, n_secret, sizeof(guint8)); + + variant = g_variant_new ("(@a{ss}stt@ay)", + g_variant_builder_end (&builder), + self->label, + self->created, + self->modified, + variant); + g_variant_get_data (variant); /* force serialize */ + return g_variant_ref_sink (variant); +} diff --git a/libsecret/secret-file-item.h b/libsecret/secret-file-item.h new file mode 100644 index 0000000..5a88f72 --- /dev/null +++ b/libsecret/secret-file-item.h @@ -0,0 +1,34 @@ +/* libsecret - GLib wrapper for Secret Service + * + * Copyright 2019 Red Hat, Inc. + * + * This program 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 licence or (at + * your option) any later version. + * + * See the included COPYING file for more information. + * + * Author: Daiki Ueno + */ + +#if !defined (__SECRET_INSIDE_HEADER__) && !defined (SECRET_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __SECRET_FILE_ITEM_H__ +#define __SECRET_FILE_ITEM_H__ + +#include + +G_BEGIN_DECLS + +#define SECRET_TYPE_FILE_ITEM (secret_file_item_get_type ()) +G_DECLARE_FINAL_TYPE (SecretFileItem, secret_file_item, SECRET, FILE_ITEM, GObject) + +SecretFileItem *secret_file_item_deserialize (GVariant *serialized); +GVariant *secret_file_item_serialize (SecretFileItem *self); + +G_END_DECLS + +#endif /* __SECRET_FILE_ITEM_H__ */ diff --git a/libsecret/secret-types.h b/libsecret/secret-types.h index cbbd3b1..2dc09a5 100644 --- a/libsecret/secret-types.h +++ b/libsecret/secret-types.h @@ -32,6 +32,7 @@ typedef enum { SECRET_ERROR_IS_LOCKED = 2, SECRET_ERROR_NO_SUCH_OBJECT = 3, SECRET_ERROR_ALREADY_EXISTS = 4, + SECRET_ERROR_INVALID_FILE_FORMAT = 5, } SecretError; #define SECRET_COLLECTION_DEFAULT "default" diff --git a/libsecret/test-file-collection.c b/libsecret/test-file-collection.c new file mode 100644 index 0000000..e016d45 --- /dev/null +++ b/libsecret/test-file-collection.c @@ -0,0 +1,364 @@ + +#include "config.h" + +#include "egg/egg-testing.h" +#include "secret-file-collection.h" +#include "secret-retrievable.h" +#include "secret-schema.h" + +typedef struct { + gchar *directory; + GMainLoop *loop; + SecretFileCollection *collection; +} Test; + +static void +on_new_async (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + Test *test = user_data; + GObject *object; + GError *error = NULL; + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), + result, + &error); + test->collection = SECRET_FILE_COLLECTION (object); + g_main_loop_quit (test->loop); + g_assert_no_error (error); +} + +static void +setup (Test *test, + gconstpointer data) +{ + GFile *file; + gchar *path; + SecretValue *password; + gchar *fixture = NULL; + + if (data != NULL) + fixture = g_build_filename (SRCDIR, "libsecret", "fixtures", data, NULL); + test->directory = egg_tests_create_scratch_directory (fixture, NULL); + g_free (fixture); + + test->loop = g_main_loop_new (NULL, TRUE); + + path = g_build_filename (test->directory, "default.keyring", NULL); + file = g_file_new_for_path (path); + g_free (path); + + password = secret_value_new ("password", -1, "text/plain"); + + g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION, + G_PRIORITY_DEFAULT, + NULL, + on_new_async, + test, + "file", file, + "password", password, + NULL); + + g_object_unref (file); + secret_value_unref (password); + + g_main_loop_run (test->loop); +} + +static void +teardown (Test *test, + gconstpointer unused) +{ + egg_tests_remove_scratch_directory (test->directory); + g_free (test->directory); + + g_clear_object (&test->collection); + g_main_loop_unref (test->loop); +} + +static void +test_init (Test *test, + gconstpointer unused) +{ +} + +static void +test_replace (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + gboolean ret; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("bar"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("baz"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + + value = secret_value_new ("test2", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + g_hash_table_unref (attributes); +} + +static void +test_clear (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + gboolean ret; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("bar"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("baz"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + + ret = secret_file_collection_clear (test->collection, + attributes, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_hash_table_unref (attributes); +} + +static void +test_search (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + GList *matches; + gboolean ret; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("bar"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("baz"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + + g_hash_table_remove (attributes, "foo"); + + value = secret_value_new ("test2", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + + matches = secret_file_collection_search (test->collection, attributes); + g_assert_cmpint (g_list_length (matches), ==, 2); + g_list_free_full (matches, (GDestroyNotify)g_variant_unref); + + g_hash_table_unref (attributes); +} + +static void +test_decrypt (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + GList *matches; + SecretFileItem *item; + const gchar *secret; + gsize n_secret; + gchar *label; + gboolean ret; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("bar"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("baz"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + + matches = secret_file_collection_search (test->collection, attributes); + g_assert_cmpint (g_list_length (matches), ==, 1); + + item = _secret_file_item_decrypt ((GVariant *)matches->data, + test->collection, + &error); + g_list_free_full (matches, (GDestroyNotify)g_variant_unref); + g_assert_no_error (error); + g_assert_nonnull (item); + + g_object_get (item, "label", &label, NULL); + g_assert_cmpstr (label, ==, "label"); + g_free (label); + + value = secret_retrievable_retrieve_secret_sync (SECRET_RETRIEVABLE (item), + NULL, + &error); + g_assert_no_error (error); + + secret = secret_value_get (value, &n_secret); + g_assert_cmpstr (secret, ==, "test1"); + + secret_value_unref (value); + g_object_unref (item); + g_hash_table_unref (attributes); +} + +static void +on_write (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + SecretFileCollection *collection = + SECRET_FILE_COLLECTION (source_object); + Test *test = user_data; + GError *error = NULL; + gboolean ret; + + ret = secret_file_collection_write_finish (collection, + result, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + g_main_loop_quit (test->loop); +} + +static void +test_write (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + gboolean ret; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("bar"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("baz"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label1", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + g_hash_table_unref (attributes); + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("apple"), g_strdup ("a")); + g_hash_table_insert (attributes, g_strdup ("orange"), g_strdup ("b")); + g_hash_table_insert (attributes, g_strdup ("banana"), g_strdup ("c")); + + value = secret_value_new ("test1", -1, "text/plain"); + ret = secret_file_collection_replace (test->collection, + attributes, "label2", value, + &error); + g_assert_no_error (error); + g_assert_true (ret); + secret_value_unref (value); + g_hash_table_unref (attributes); + + secret_file_collection_write (test->collection, + NULL, + on_write, + test); + + g_main_loop_run (test->loop); +} + +static void +test_read (Test *test, + gconstpointer unused) +{ + GHashTable *attributes; + SecretValue *value; + GError *error = NULL; + GList *matches; + SecretFileItem *item; + const gchar *secret; + gsize n_secret; + gchar *label; + + attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (attributes, g_strdup ("foo"), g_strdup ("a")); + + matches = secret_file_collection_search (test->collection, attributes); + g_assert_cmpint (g_list_length (matches), ==, 1); + + item = _secret_file_item_decrypt ((GVariant *)matches->data, + test->collection, + &error); + g_list_free_full (matches, (GDestroyNotify)g_variant_unref); + g_assert_no_error (error); + g_assert_nonnull (item); + + g_object_get (item, "label", &label, NULL); + g_assert_cmpstr (label, ==, "label1"); + g_free (label); + + value = secret_retrievable_retrieve_secret_sync (SECRET_RETRIEVABLE (item), + NULL, + &error); + g_assert_no_error (error); + + secret = secret_value_get (value, &n_secret); + g_assert_cmpstr (secret, ==, "test1"); + + secret_value_unref (value); + g_object_unref (item); + g_hash_table_unref (attributes); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + g_set_prgname ("test-file-collection"); + g_test_add ("/file-collection/init", Test, NULL, setup, test_init, teardown); + g_test_add ("/file-collection/replace", Test, NULL, setup, test_replace, teardown); + g_test_add ("/file-collection/clear", Test, NULL, setup, test_clear, teardown); + g_test_add ("/file-collection/search", Test, NULL, setup, test_search, teardown); + g_test_add ("/file-collection/decrypt", Test, NULL, setup, test_decrypt, teardown); + g_test_add ("/file-collection/write", Test, NULL, setup, test_write, teardown); + g_test_add ("/file-collection/read", Test, "default.keyring", setup, test_read, teardown); + + return egg_tests_run_with_loop (); +} From 8e4317235e43f4dc03049f1ea91ab90becebf820 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Tue, 20 Aug 2019 10:15:14 +0200 Subject: [PATCH 3/6] autotools: Generate secret-tool executable in tool/ This makes it consistent with meson build. --- tool/Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tool/Makefile.am b/tool/Makefile.am index 422e0d3..2361311 100644 --- a/tool/Makefile.am +++ b/tool/Makefile.am @@ -1,7 +1,7 @@ -bin_PROGRAMS += secret-tool +bin_PROGRAMS += tool/secret-tool -secret_tool_SOURCES = \ +tool_secret_tool_SOURCES = \ tool/secret-tool.c -secret_tool_LDADD = \ +tool_secret_tool_LDADD = \ libsecret-@SECRET_MAJOR@.la From f2b7f6d505488a6bc2a04e48e89bf5511d2949a9 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Mon, 19 Aug 2019 17:56:17 +0200 Subject: [PATCH 4/6] secret-tool: Add tests using file backend --- Makefile.am | 5 +- meson.build | 4 ++ tool/Makefile.am | 4 ++ tool/meson.build | 6 +++ tool/test-secret-tool.sh | 104 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100755 tool/test-secret-tool.sh diff --git a/Makefile.am b/Makefile.am index 9804b42..f7066b4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -52,7 +52,10 @@ dist-hook: dist-check-valac distcleancheck_listfiles = \ find . -name '*.gc[dn][oa]' -prune -o -type f -print -TESTS_ENVIRONMENT = LD_LIBRARY_PATH=$(builddir)/.libs GI_TYPELIB_PATH=$(builddir) +TESTS_ENVIRONMENT = \ + LD_LIBRARY_PATH=$(builddir)/.libs \ + GI_TYPELIB_PATH=$(builddir) \ + abs_top_builddir=$(abs_top_builddir) TEST_EXTENSIONS = .py .js # Default executable tests diff --git a/meson.build b/meson.build index f20a66e..bb58f68 100644 --- a/meson.build +++ b/meson.build @@ -70,6 +70,10 @@ conf.set('_DEBUG', enable_debug) conf.set('HAVE_MLOCK', meson.get_compiler('c').has_function('mlock')) configure_file(output: 'config.h', configuration: conf) +# Test environment +test_env = environment() +test_env.set('abs_top_builddir', meson.build_root()) + # Subfolders subdir('po') subdir('egg') diff --git a/tool/Makefile.am b/tool/Makefile.am index 2361311..9d25ea1 100644 --- a/tool/Makefile.am +++ b/tool/Makefile.am @@ -5,3 +5,7 @@ tool_secret_tool_SOURCES = \ tool_secret_tool_LDADD = \ libsecret-@SECRET_MAJOR@.la + +if WITH_GCRYPT +TESTS += tool/test-secret-tool.sh +endif diff --git a/tool/meson.build b/tool/meson.build index 686cf24..1bf9a84 100644 --- a/tool/meson.build +++ b/tool/meson.build @@ -9,3 +9,9 @@ secret_tool = executable('secret-tool', c_args: libsecret_cflags, install: true, ) + +if with_gcrypt and host_machine.system() != 'windows' + test('test-secret-tool.sh', + find_program('test-secret-tool.sh'), + env: test_env) +endif diff --git a/tool/test-secret-tool.sh b/tool/test-secret-tool.sh new file mode 100755 index 0000000..9bd4fbd --- /dev/null +++ b/tool/test-secret-tool.sh @@ -0,0 +1,104 @@ +#!/bin/sh + +set -e + +testdir=$PWD/test-secret-tool-$$ +test -d "$testdir" || mkdir "$testdir" + +cleanup () { + rm -rf "$testdir" +} +trap cleanup 0 + +cd "$testdir" + +SECRET_BACKEND=file +export SECRET_BACKEND + +SECRET_FILE_TEST_PATH=$testdir/keyring +export SECRET_FILE_TEST_PATH + +SECRET_FILE_TEST_PASSWORD=test +export SECRET_FILE_TEST_PASSWORD + +: ${SECRET_TOOL="$abs_top_builddir"/tool/secret-tool} + +: ${DIFF=diff} + +echo 1..4 + +echo test1 | ${SECRET_TOOL} store --label label1 foo bar +if test $? -eq 0; then + echo "ok 1 /secret-tool/store" +else + echo "not ok 1 /secret-tool/store" +fi + +echo test2 | ${SECRET_TOOL} store --label label2 foo bar apple orange +if test $? -eq 0; then + echo "ok 1 /secret-tool/store" +else + echo "not ok 1 /secret-tool/store" +fi + +echo test1 > lookup.exp +${SECRET_TOOL} lookup foo bar > lookup.out +if ${DIFF} lookup.exp lookup.out > lookup.diff; then + echo "ok 2 /secret-tool/lookup" +else + echo "not ok 2 /secret-tool/lookup" + sed 's/^/# /' lookup.diff + exit 1 +fi + +cat > search.exp < search.out +if test $? -ne 0; then + echo "not ok 3 /secret-tool/search" + exit 1 +fi +if ${DIFF} search.exp search.out > search.diff; then + echo "ok 3 /secret-tool/search" +else + echo "not ok 3 /secret-tool/search" + sed 's/^/# /' search.diff + exit 1 +fi + +${SECRET_TOOL} clear apple orange +if test $? -eq 0; then + echo "ok 4 /secret-tool/clear" +else + echo "not ok 4 /secret-tool/clear" + exit 1 +fi + +cat > search-after-clear.exp < search-after-clear.out +if test $? -ne 0; then + echo "not ok 5 /secret-tool/search-after-clear" + exit 1 +fi +if ${DIFF} search-after-clear.exp search-after-clear.out > search-after-clear.diff; then + echo "ok 5 /secret-tool/search-after-clear" +else + echo "not ok 5 /secret-tool/search-after-clear" + sed 's/^/# /' search-after-clear.diff + exit 1 +fi From 8f886f0797638b2d321aed49dd3481ad2ad5ca20 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Mon, 19 Aug 2019 17:59:22 +0200 Subject: [PATCH 5/6] secret-file-backend: Retrieve master password from flatpak portal --- libsecret/secret-backend.c | 19 +- libsecret/secret-file-backend.c | 306 ++++++++++++++++++++++++++++++-- 2 files changed, 306 insertions(+), 19 deletions(-) diff --git a/libsecret/secret-backend.c b/libsecret/secret-backend.c index baa2e9a..30e3abb 100644 --- a/libsecret/secret-backend.c +++ b/libsecret/secret-backend.c @@ -149,17 +149,24 @@ backend_get_impl_type (void) GIOExtension *e; GIOExtensionPoint *ep; - envvar = g_getenv ("SECRET_BACKEND"); - if (envvar == NULL || *envvar == '\0') - extension_name = "service"; - else - extension_name = envvar; - g_type_ensure (secret_service_get_type ()); #ifdef WITH_GCRYPT g_type_ensure (secret_file_backend_get_type ()); #endif +#ifdef WITH_GCRYPT + if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) + extension_name = "file"; + else +#endif + { + envvar = g_getenv ("SECRET_BACKEND"); + if (envvar == NULL || *envvar == '\0') + extension_name = "service"; + else + extension_name = envvar; + } + ep = g_io_extension_point_lookup (SECRET_BACKEND_EXTENSION_POINT_NAME); e = g_io_extension_point_get_extension_by_name (ep, extension_name); if (e == NULL) { diff --git a/libsecret/secret-file-backend.c b/libsecret/secret-file-backend.c index 3e6d0e2..c557754 100644 --- a/libsecret/secret-file-backend.c +++ b/libsecret/secret-file-backend.c @@ -21,6 +21,19 @@ #include "secret-private.h" #include "secret-retrievable.h" +#include "egg/egg-secure-memory.h" + +EGG_SECURE_DECLARE (secret_file_backend); + +#include +#include +#include + +#define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" +#define PORTAL_REQUEST_INTERFACE "org.freedesktop.portal.Request" +#define PORTAL_SECRET_INTERFACE "org.freedesktop.portal.Secret" + static void secret_file_backend_async_initable_iface (GAsyncInitableIface *iface); static void secret_file_backend_backend_iface (SecretBackendInterface *iface); @@ -138,6 +151,267 @@ on_collection_new_async (GObject *source_object, g_object_unref (task); } +typedef struct { + gint io_priority; + GFile *file; + GInputStream *stream; + gchar *buffer; + GDBusConnection *connection; + gchar *request_path; + guint portal_signal_id; + gulong cancellable_signal_id; +} InitClosure; + +static void +init_closure_free (gpointer data) +{ + InitClosure *init = data; + g_object_unref (init->file); + g_clear_object (&init->stream); + g_clear_pointer (&init->buffer, egg_secure_free); + g_clear_object (&init->connection); + g_clear_pointer (&init->request_path, g_free); + g_slice_free (InitClosure, init); +} + +#define PASSWORD_SIZE 64 + +static void +on_read_all (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GInputStream *stream = G_INPUT_STREAM (source_object); + GTask *task = G_TASK (user_data); + InitClosure *init = g_task_get_task_data (task); + gsize bytes_read; + SecretValue *password; + GError *error = NULL; + + if (!g_input_stream_read_all_finish (stream, result, &bytes_read, + &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + if (bytes_read != PASSWORD_SIZE) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "invalid password returned from portal"); + g_object_unref (task); + return; + } + + password = secret_value_new (init->buffer, bytes_read, "text/plain"); + g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION, + init->io_priority, + g_task_get_cancellable (task), + on_collection_new_async, + task, + "file", g_object_ref (init->file), + "password", password, + NULL); + g_object_unref (init->file); + secret_value_unref (password); +} + +static void +on_portal_response (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + InitClosure *init = g_task_get_task_data (task); + guint32 response; + + g_dbus_connection_signal_unsubscribe (connection, + init->portal_signal_id); + + g_variant_get (parameters, "(ua{sv})", &response, NULL); + + switch (response) { + case 0: + init->buffer = egg_secure_alloc (PASSWORD_SIZE); + g_input_stream_read_all_async (init->stream, + init->buffer, PASSWORD_SIZE, + G_PRIORITY_DEFAULT, + g_task_get_cancellable (task), + on_read_all, + task); + break; + case 1: + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "user interaction cancelled"); + g_object_unref (task); + break; + case 2: + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "user interaction failed"); + g_object_unref (task); + break; + } +} + +static void +on_portal_request_close (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = G_DBUS_CONNECTION (source_object); + GTask *task = G_TASK (user_data); + GError *error = NULL; + + if (!g_dbus_connection_call_finish (connection, result, &error)) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +static void +on_portal_cancel (GCancellable *cancellable, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + InitClosure *init = g_task_get_task_data (task); + + g_dbus_connection_call (init->connection, + PORTAL_BUS_NAME, + init->request_path, + PORTAL_REQUEST_INTERFACE, + "Close", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + on_portal_request_close, + task); + + g_cancellable_disconnect (cancellable, init->cancellable_signal_id); +} + +static void +on_portal_retrieve_secret (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = G_DBUS_CONNECTION (source_object); + GTask *task = G_TASK (user_data); + InitClosure *init = g_task_get_task_data (task); + GCancellable *cancellable = g_task_get_cancellable (task); + GVariant *reply; + GError *error = NULL; + + reply = g_dbus_connection_call_with_unix_fd_list_finish (connection, + NULL, + result, + &error); + if (reply == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + g_variant_get (reply, "(o)", &init->request_path); + g_variant_unref (reply); + + init->portal_signal_id = + g_dbus_connection_signal_subscribe (connection, + PORTAL_BUS_NAME, + PORTAL_REQUEST_INTERFACE, + "Response", + init->request_path, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + on_portal_response, + task, + NULL); + + if (cancellable != NULL) + init->cancellable_signal_id = + g_cancellable_connect (cancellable, + G_CALLBACK (on_portal_cancel), + task, + NULL); +} + +static void +on_bus_get (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection; + GTask *task = G_TASK (user_data); + InitClosure *init = g_task_get_task_data (task); + GUnixFDList *fd_list; + gint fds[2]; + gint fd_index; + GVariantBuilder options; + GError *error = NULL; + + connection = g_bus_get_finish (result, &error); + if (connection == NULL) { + g_task_return_error (task, error); + g_object_unref (task); + return; + } + init->connection = connection; + + if (!g_unix_open_pipe (fds, FD_CLOEXEC, &error)) { + g_object_unref (connection); + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + fd_list = g_unix_fd_list_new (); + fd_index = g_unix_fd_list_append (fd_list, fds[1], &error); + close (fds[1]); + if (fd_index < 0) { + close (fds[0]); + g_object_unref (fd_list); + g_object_unref (connection); + g_task_return_error (task, error); + g_object_unref (task); + return; + } + + close (fds[1]); + init->stream = g_unix_input_stream_new (fds[0], TRUE); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + g_dbus_connection_call_with_unix_fd_list (connection, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + PORTAL_SECRET_INTERFACE, + "RetrieveSecret", + g_variant_new ("(h@a{sv})", + fd_index, + g_variant_builder_end (&options)), + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + g_task_get_cancellable (task), + on_portal_retrieve_secret, + task); + g_object_unref (fd_list); +} + static void secret_file_backend_real_init_async (GAsyncInitable *initable, int io_priority, @@ -152,6 +426,7 @@ secret_file_backend_real_init_async (GAsyncInitable *initable, const gchar *envvar; GTask *task; GError *error = NULL; + InitClosure *init; gboolean ret; task = g_task_new (initable, cancellable, callback, user_data); @@ -194,9 +469,25 @@ secret_file_backend_real_init_async (GAsyncInitable *initable, } envvar = g_getenv ("SECRET_FILE_TEST_PASSWORD"); - if (envvar != NULL && *envvar != '\0') + if (envvar != NULL && *envvar != '\0') { password = secret_value_new (envvar, -1, "text/plain"); - else { + g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION, + io_priority, + cancellable, + on_collection_new_async, + task, + "file", file, + "password", password, + NULL); + g_object_unref (file); + secret_value_unref (password); + } else if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) { + init = g_slice_new0 (InitClosure); + init->io_priority = io_priority; + init->file = file; + g_task_set_task_data (task, init, init_closure_free); + g_bus_get (G_BUS_TYPE_SESSION, cancellable, on_bus_get, task); + } else { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, @@ -204,17 +495,6 @@ secret_file_backend_real_init_async (GAsyncInitable *initable, g_object_unref (task); return; } - - g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION, - io_priority, - cancellable, - on_collection_new_async, - task, - "file", file, - "password", password, - NULL); - g_object_unref (file); - secret_value_unref (password); } static gboolean From a278adc208a03207e5b3d90f33d33a909f5ca746 Mon Sep 17 00:00:00 2001 From: Daiki Ueno Date: Mon, 7 Oct 2019 17:20:21 +0200 Subject: [PATCH 6/6] secret-backend: Check if portal is available Before decising to use the file backend, check if the necessary portal interface is available on the D-Bus. Suggested by Patrick Griffis. --- libsecret/secret-backend.c | 3 ++- libsecret/secret-file-backend.c | 48 +++++++++++++++++++++++++++++++++ libsecret/secret-file-backend.h | 2 ++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/libsecret/secret-backend.c b/libsecret/secret-backend.c index 30e3abb..6ce2645 100644 --- a/libsecret/secret-backend.c +++ b/libsecret/secret-backend.c @@ -155,7 +155,8 @@ backend_get_impl_type (void) #endif #ifdef WITH_GCRYPT - if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) + if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS) && + _secret_file_backend_check_portal_version ()) extension_name = "file"; else #endif diff --git a/libsecret/secret-file-backend.c b/libsecret/secret-file-backend.c index c557754..d618352 100644 --- a/libsecret/secret-file-backend.c +++ b/libsecret/secret-file-backend.c @@ -33,6 +33,7 @@ EGG_SECURE_DECLARE (secret_file_backend); #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" #define PORTAL_REQUEST_INTERFACE "org.freedesktop.portal.Request" #define PORTAL_SECRET_INTERFACE "org.freedesktop.portal.Secret" +#define PORTAL_SECRET_VERSION 1 static void secret_file_backend_async_initable_iface (GAsyncInitableIface *iface); static void secret_file_backend_backend_iface (SecretBackendInterface *iface); @@ -776,3 +777,50 @@ secret_file_backend_backend_iface (SecretBackendInterface *iface) iface->search = secret_file_backend_real_search; iface->search_finish = secret_file_backend_real_search_finish; } + +gboolean +_secret_file_backend_check_portal_version (void) +{ + GDBusConnection *connection; + GVariant *ret; + GVariant *value; + guint32 version; + GError *error = NULL; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (!connection) { + g_warning ("couldn't get session bus: %s", error->message); + g_error_free (error); + return FALSE; + } + + ret = g_dbus_connection_call_sync (connection, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", + PORTAL_SECRET_INTERFACE, + "version"), + G_VARIANT_TYPE ("(v)"), + 0, -1, NULL, &error); + g_object_unref (connection); + if (!ret) { + g_message ("secret portal is not available: %s", + error->message); + g_error_free (error); + return FALSE; + } + + g_variant_get (ret, "(v)", &value); + g_variant_unref (ret); + version = g_variant_get_uint32 (value); + g_variant_unref (value); + if (version != PORTAL_SECRET_VERSION) { + g_message ("secret portal version mismatch: %u != %u", + version, PORTAL_SECRET_VERSION); + return FALSE; + } + + return TRUE; +} diff --git a/libsecret/secret-file-backend.h b/libsecret/secret-file-backend.h index 27d896c..655bed3 100644 --- a/libsecret/secret-file-backend.h +++ b/libsecret/secret-file-backend.h @@ -26,6 +26,8 @@ G_BEGIN_DECLS #define SECRET_TYPE_FILE_BACKEND (secret_file_backend_get_type ()) G_DECLARE_FINAL_TYPE (SecretFileBackend, secret_file_backend, SECRET, FILE_BACKEND, GObject) +gboolean _secret_file_backend_check_portal_version (void); + G_END_DECLS #endif /* __SECRET_FILE_BACKEND_H__ */