[libsecret/wip/dueno/local-file: 11/14] secret-file-backend: New backend for storing secrets in file



commit f6775294d69d4ff37fd7049f18a3fcbfd9852423
Author: Daiki Ueno <dueno src gnome org>
Date:   Tue Aug 13 18:12:35 2019 +0200

    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    | 489 +++++++++++++++++++++
 libsecret/secret-file-backend.h    |  31 ++
 libsecret/secret-file-collection.c | 839 +++++++++++++++++++++++++++++++++++++
 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, 2112 insertions(+), 3 deletions(-)
---
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..cf049bf
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..6383e54
--- /dev/null
+++ b/libsecret/secret-file-backend.c
@@ -0,0 +1,489 @@
+/* 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) {
+               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_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);
+       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;
+
+       /* 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_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 <libsecret/secret.h> can be included directly."
+#endif
+
+#ifndef __SECRET_FILE_BACKEND_H__
+#define __SECRET_FILE_BACKEND_H__
+
+#include <glib-object.h>
+
+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..2c6455c
--- /dev/null
+++ b/libsecret/secret-file-collection.c
@@ -0,0 +1,839 @@
+/* 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;
+
+       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);
+       g_variant_ref_sink (self->items);
+
+       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 *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 <libsecret/secret.h> 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 <libsecret/secret.h> can be included directly."
+#endif
+
+#ifndef __SECRET_FILE_ITEM_H__
+#define __SECRET_FILE_ITEM_H__
+
+#include <glib-object.h>
+
+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 ();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]