diff options
Diffstat (limited to 'libsecret/secret-file-collection.c')
-rw-r--r-- | libsecret/secret-file-collection.c | 842 |
1 files changed, 842 insertions, 0 deletions
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 <gcrypt.h> +#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); +} |