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 0000000..db635a3 Binary files /dev/null and b/libsecret/fixtures/default.keyring differ diff --git a/libsecret/meson.build b/libsecret/meson.build index 7e3dcd3..cfb7c0a 100644 --- a/libsecret/meson.build +++ b/libsecret/meson.build @@ -35,6 +35,14 @@ libsecret_headers = [ 'secret-value.h', ] +if with_gcrypt + libsecret_sources += [ + 'secret-file-backend.c', + 'secret-file-collection.c', + 'secret-file-item.c', + ] +endif + version_numbers = meson.project_version().split('.') version_major = version_numbers[0].to_int() version_minor = version_numbers[1].to_int() @@ -174,7 +182,7 @@ configure_file( ) # Tests -mock_cflags = [ +test_cflags = [ libsecret_cflags, '-DSRCDIR="@0@"'.format(meson.source_root()), ] @@ -182,7 +190,7 @@ mock_cflags = [ mock_service_lib = static_library('mock-service', 'mock-service.c', dependencies: glib_deps, - c_args: mock_cflags, + c_args: test_cflags, include_directories: config_h_dir, ) @@ -199,6 +207,12 @@ test_names = [ 'test-collection', ] +if with_gcrypt + test_names += [ + 'test-file-collection', + ] +endif + foreach _test : test_names test_bin = executable(_test, @@ -206,7 +220,7 @@ foreach _test : test_names dependencies: libsecret_dep, link_with: mock_service_lib, include_directories: config_h_dir, - c_args: libsecret_cflags, + c_args: test_cflags, ) test(_test, test_bin) diff --git a/libsecret/secret-backend.c b/libsecret/secret-backend.c index a63b75c..baa2e9a 100644 --- a/libsecret/secret-backend.c +++ b/libsecret/secret-backend.c @@ -15,6 +15,11 @@ #include "config.h" #include "secret-backend.h" + +#ifdef WITH_GCRYPT +#include "secret-file-backend.h" +#endif + #include "secret-private.h" #include "libsecret/secret-enum-types.h" @@ -151,6 +156,9 @@ backend_get_impl_type (void) extension_name = envvar; g_type_ensure (secret_service_get_type ()); +#ifdef WITH_GCRYPT + g_type_ensure (secret_file_backend_get_type ()); +#endif ep = g_io_extension_point_lookup (SECRET_BACKEND_EXTENSION_POINT_NAME); e = g_io_extension_point_get_extension_by_name (ep, extension_name); diff --git a/libsecret/secret-file-backend.c b/libsecret/secret-file-backend.c new file mode 100644 index 0000000..1152b9a --- /dev/null +++ b/libsecret/secret-file-backend.c @@ -0,0 +1,478 @@ +/* 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-backend.h" +#include "secret-file-backend.h" +#include "secret-file-collection.h" +#include "secret-file-item.h" +#include "secret-private.h" +#include "secret-retrievable.h" + +static void secret_file_backend_async_initable_iface (GAsyncInitableIface *iface); +static void secret_file_backend_backend_iface (SecretBackendInterface *iface); + +struct _SecretFileBackend { + GObject parent; + SecretFileCollection *collection; + SecretServiceFlags init_flags; +}; + +G_DEFINE_TYPE_WITH_CODE (SecretFileBackend, secret_file_backend, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, secret_file_backend_async_initable_iface); + G_IMPLEMENT_INTERFACE (SECRET_TYPE_BACKEND, secret_file_backend_backend_iface); + _secret_backend_ensure_extension_point (); + g_io_extension_point_implement (SECRET_BACKEND_EXTENSION_POINT_NAME, + g_define_type_id, + "file", + 0) +); + +enum { + PROP_0, + PROP_FLAGS +}; + +static void +secret_file_backend_init (SecretFileBackend *self) +{ +} + +static void +secret_file_backend_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SecretFileBackend *self = SECRET_FILE_BACKEND (object); + + switch (prop_id) { + case PROP_FLAGS: + self->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 && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { + 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_error ("master password is not retrievable"); + 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_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_backend_async_initable_iface (GAsyncInitableIface *iface) +{ + iface->init_async = secret_file_backend_real_init_async; + iface->init_finish = secret_file_collection_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); + 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); + 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; + + /* Warnings raised already */ + if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE)) + return; + + task = g_task_new (self, cancellable, callback, user_data); + + if (!secret_file_collection_clear (self->collection, attributes, &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_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_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..c716500 --- /dev/null +++ b/libsecret/secret-file-collection.c @@ -0,0 +1,851 @@ +/* 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; + GBytes *key; + GBytes *iv; + 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; + gconstpointer iv; + gsize n_iv; + 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; + + iv = g_bytes_get_data (self->iv, &n_iv); + gcry = gcry_cipher_setiv (hd, iv, n_iv); + 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; + gconstpointer iv; + gsize n_iv; + 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; + + iv = g_bytes_get_data (self->iv, &n_iv); + gcry = gcry_cipher_setiv (hd, iv, n_iv); + 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->iv, g_bytes_unref); + g_clear_pointer (&self->items, g_variant_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; + GVariant *iv_array; + guint32 iv_size; + 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 iv[IV_SIZE]; + 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; + + 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)")); + gcry_create_nonce (iv, sizeof(iv)); + self->iv = g_bytes_new (iv, sizeof(iv)); + 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 ("(uayuuaya(a{say}ay))"), + p, + length, + TRUE, + g_free, + contents); + g_variant_get (variant, "(u@ayuu@ay@a(a{say}ay))", + &salt_size, &salt_array, &self->iteration_count, + &iv_size, &iv_array, &self->items); + + 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; + } + + if (iv_size < IV_SIZE) { + g_task_return_new_error (task, + SECRET_ERROR, + SECRET_ERROR_INVALID_FILE_FORMAT, + "invalid iv size"); + goto out; + } + + data = g_variant_get_fixed_array (iv_array, &n_data, sizeof(guint8)); + g_assert (n_data == iv_size); + + self->iv = g_bytes_new (g_variant_get_data (iv_array), iv_size); + + g_task_return_boolean (task, TRUE); + + out: + g_variant_unref (iv_array); + 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); + } + g_clear_pointer (&self->items, g_variant_unref); + + 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 + 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, data + n_padded)) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + + variant = g_variant_new_from_data (G_VARIANT_TYPE ("ay"), + data, + n_padded + MAC_SIZE, + TRUE, + egg_secure_free, + data); + variant = g_variant_new ("(@a{say}@ay)", hashed_attributes, variant); + g_variant_builder_add_value (&builder, variant); + + 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 < MAC_SIZE) { + egg_secure_free (data); + g_set_error (error, + SECRET_ERROR, + SECRET_ERROR_PROTOCOL, + "couldn't calculate mac"); + return FALSE; + } + n_padded -= MAC_SIZE; + + if (!calculate_mac (collection, data, n_padded, 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, 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; + + 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) + g_variant_builder_add_value (&builder, child); + g_variant_unref (child); + } + g_variant_unref (self->items); + self->items = g_variant_builder_end (&builder); + return TRUE; +} + +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 *iv_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)); + iv_array = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (self->iv, NULL), + g_bytes_get_size (self->iv), + sizeof(guint8)); + variant = g_variant_new ("(u@ayuu@ay@a(a{say}ay))", + g_bytes_get_size (self->salt), + salt_array, + self->iteration_count, + g_bytes_get_size (self->iv), + iv_array, + 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 (); +}