summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaiki Ueno <ueno@gnu.org>2019-10-13 06:31:41 +0000
committerDaiki Ueno <ueno@gnu.org>2019-10-13 06:31:41 +0000
commita6530135ebf2137b238f1b1bc03042f22a9153ac (patch)
tree6b5df4693419b9594e9f5215c8632b67bc8c430a
parent29cc14153df36cfd917c3743c4ecd1625cd9b399 (diff)
parenta278adc208a03207e5b3d90f33d33a909f5ca746 (diff)
downloadlibsecret-a6530135ebf2137b238f1b1bc03042f22a9153ac.tar.gz
Merge branch 'wip/dueno/local-file' into 'master'
secret-backend: Add local-storage backend See merge request GNOME/libsecret!6
-rw-r--r--Makefile.am5
-rw-r--r--egg/egg-testing.c68
-rw-r--r--egg/egg-testing.h10
-rw-r--r--libsecret/Makefile.am21
-rw-r--r--libsecret/fixtures/default.keyringbin0 -> 538 bytes
-rw-r--r--libsecret/meson.build20
-rw-r--r--libsecret/secret-backend.c28
-rw-r--r--libsecret/secret-file-backend.c826
-rw-r--r--libsecret/secret-file-backend.h33
-rw-r--r--libsecret/secret-file-collection.c842
-rw-r--r--libsecret/secret-file-collection.h56
-rw-r--r--libsecret/secret-file-item.c252
-rw-r--r--libsecret/secret-file-item.h34
-rw-r--r--libsecret/secret-types.h1
-rw-r--r--libsecret/test-file-collection.c364
-rw-r--r--meson.build4
-rw-r--r--tool/Makefile.am10
-rw-r--r--tool/meson.build6
-rwxr-xr-xtool/test-secret-tool.sh104
19 files changed, 2670 insertions, 14 deletions
diff --git a/Makefile.am b/Makefile.am
index 9804b42..f7066b4 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -52,7 +52,10 @@ dist-hook: dist-check-valac
distcleancheck_listfiles = \
find . -name '*.gc[dn][oa]' -prune -o -type f -print
-TESTS_ENVIRONMENT = LD_LIBRARY_PATH=$(builddir)/.libs GI_TYPELIB_PATH=$(builddir)
+TESTS_ENVIRONMENT = \
+ LD_LIBRARY_PATH=$(builddir)/.libs \
+ GI_TYPELIB_PATH=$(builddir) \
+ abs_top_builddir=$(abs_top_builddir)
TEST_EXTENSIONS = .py .js
# Default executable tests
diff --git a/egg/egg-testing.c b/egg/egg-testing.c
index 9e7cba5..f1e0b68 100644
--- a/egg/egg-testing.c
+++ b/egg/egg-testing.c
@@ -171,3 +171,71 @@ egg_tests_run_with_loop (void)
return ret;
}
+
+void
+egg_tests_copy_scratch_file (const gchar *directory,
+ const gchar *filename)
+{
+ GError *error = NULL;
+ gchar *basename;
+ gchar *contents;
+ gchar *destination;
+ gsize length;
+
+ g_assert (directory);
+
+ g_file_get_contents (filename, &contents, &length, &error);
+ g_assert_no_error (error);
+
+ basename = g_path_get_basename (filename);
+ destination = g_build_filename (directory, basename, NULL);
+ g_free (basename);
+
+ g_file_set_contents (destination, contents, length, &error);
+ g_assert_no_error (error);
+ g_free (destination);
+ g_free (contents);
+}
+
+gchar *
+egg_tests_create_scratch_directory (const gchar *file_to_copy,
+ ...)
+{
+ gchar *basename;
+ gchar *directory;
+ va_list va;
+
+ basename = g_path_get_basename (g_get_prgname ());
+ directory = g_strdup_printf ("/tmp/scratch-%s.XXXXXX", basename);
+ g_free (basename);
+
+ if (!g_mkdtemp (directory))
+ g_assert_not_reached ();
+
+ va_start (va, file_to_copy);
+
+ while (file_to_copy != NULL) {
+ egg_tests_copy_scratch_file (directory, file_to_copy);
+ file_to_copy = va_arg (va, const gchar *);
+ }
+
+ va_end (va);
+
+ return directory;
+}
+
+void
+egg_tests_remove_scratch_directory (const gchar *directory)
+{
+ gchar *argv[] = { "rm", "-rf", (gchar *)directory, NULL };
+ GError *error = NULL;
+ gint rm_status;
+
+ g_assert_cmpstr (directory, !=, "");
+ g_assert_cmpstr (directory, !=, "/");
+
+ g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL,
+ NULL, NULL, NULL, &rm_status, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (rm_status, ==, 0);
+}
diff --git a/egg/egg-testing.h b/egg/egg-testing.h
index 1f07f0c..1a240b2 100644
--- a/egg/egg-testing.h
+++ b/egg/egg-testing.h
@@ -56,4 +56,12 @@ void egg_test_wait_idle (void);
gint egg_tests_run_with_loop (void);
-#endif /* EGG_DH_H_ */
+void egg_tests_copy_scratch_file (const gchar *directory,
+ const gchar *file_to_copy);
+
+gchar * egg_tests_create_scratch_directory (const gchar *file_to_copy,
+ ...) G_GNUC_NULL_TERMINATED;
+
+void egg_tests_remove_scratch_directory (const gchar *directory);
+
+#endif /* EGG_TESTING_H_ */
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
--- /dev/null
+++ b/libsecret/fixtures/default.keyring
Binary files differ
diff --git a/libsecret/meson.build b/libsecret/meson.build
index 6cbd88e..031951f 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()
@@ -168,7 +176,7 @@ pkg.generate(description: 'GObject bindings for Secret Service API (Unstable)',
requires: libsecret)
# Tests
-mock_cflags = [
+test_cflags = [
libsecret_cflags,
'-DSRCDIR="@0@"'.format(meson.source_root()),
]
@@ -176,7 +184,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,
)
@@ -193,6 +201,12 @@ test_names = [
'test-collection',
]
+if with_gcrypt
+ test_names += [
+ 'test-file-collection',
+ ]
+endif
+
foreach _test : test_names
test_bin = executable(_test,
@@ -200,7 +214,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..6ce2645 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"
@@ -144,13 +149,24 @@ backend_get_impl_type (void)
GIOExtension *e;
GIOExtensionPoint *ep;
- envvar = g_getenv ("SECRET_BACKEND");
- if (envvar == NULL || *envvar == '\0')
- extension_name = "service";
- else
- extension_name = envvar;
-
g_type_ensure (secret_service_get_type ());
+#ifdef WITH_GCRYPT
+ g_type_ensure (secret_file_backend_get_type ());
+#endif
+
+#ifdef WITH_GCRYPT
+ if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS) &&
+ _secret_file_backend_check_portal_version ())
+ extension_name = "file";
+ else
+#endif
+ {
+ envvar = g_getenv ("SECRET_BACKEND");
+ if (envvar == NULL || *envvar == '\0')
+ extension_name = "service";
+ else
+ extension_name = envvar;
+ }
ep = g_io_extension_point_lookup (SECRET_BACKEND_EXTENSION_POINT_NAME);
e = g_io_extension_point_get_extension_by_name (ep, extension_name);
diff --git a/libsecret/secret-file-backend.c b/libsecret/secret-file-backend.c
new file mode 100644
index 0000000..d618352
--- /dev/null
+++ b/libsecret/secret-file-backend.c
@@ -0,0 +1,826 @@
+/* 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"
+
+#include "egg/egg-secure-memory.h"
+
+EGG_SECURE_DECLARE (secret_file_backend);
+
+#include <gio/gunixfdlist.h>
+#include <gio/gunixinputstream.h>
+#include <glib-unix.h>
+
+#define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop"
+#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
+#define PORTAL_REQUEST_INTERFACE "org.freedesktop.portal.Request"
+#define PORTAL_SECRET_INTERFACE "org.freedesktop.portal.Secret"
+#define PORTAL_SECRET_VERSION 1
+
+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);
+}
+
+typedef struct {
+ gint io_priority;
+ GFile *file;
+ GInputStream *stream;
+ gchar *buffer;
+ GDBusConnection *connection;
+ gchar *request_path;
+ guint portal_signal_id;
+ gulong cancellable_signal_id;
+} InitClosure;
+
+static void
+init_closure_free (gpointer data)
+{
+ InitClosure *init = data;
+ g_object_unref (init->file);
+ g_clear_object (&init->stream);
+ g_clear_pointer (&init->buffer, egg_secure_free);
+ g_clear_object (&init->connection);
+ g_clear_pointer (&init->request_path, g_free);
+ g_slice_free (InitClosure, init);
+}
+
+#define PASSWORD_SIZE 64
+
+static void
+on_read_all (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GInputStream *stream = G_INPUT_STREAM (source_object);
+ GTask *task = G_TASK (user_data);
+ InitClosure *init = g_task_get_task_data (task);
+ gsize bytes_read;
+ SecretValue *password;
+ GError *error = NULL;
+
+ if (!g_input_stream_read_all_finish (stream, result, &bytes_read,
+ &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ if (bytes_read != PASSWORD_SIZE) {
+ g_task_return_new_error (task,
+ SECRET_ERROR,
+ SECRET_ERROR_PROTOCOL,
+ "invalid password returned from portal");
+ g_object_unref (task);
+ return;
+ }
+
+ password = secret_value_new (init->buffer, bytes_read, "text/plain");
+ g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION,
+ init->io_priority,
+ g_task_get_cancellable (task),
+ on_collection_new_async,
+ task,
+ "file", g_object_ref (init->file),
+ "password", password,
+ NULL);
+ g_object_unref (init->file);
+ secret_value_unref (password);
+}
+
+static void
+on_portal_response (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ InitClosure *init = g_task_get_task_data (task);
+ guint32 response;
+
+ g_dbus_connection_signal_unsubscribe (connection,
+ init->portal_signal_id);
+
+ g_variant_get (parameters, "(ua{sv})", &response, NULL);
+
+ switch (response) {
+ case 0:
+ init->buffer = egg_secure_alloc (PASSWORD_SIZE);
+ g_input_stream_read_all_async (init->stream,
+ init->buffer, PASSWORD_SIZE,
+ G_PRIORITY_DEFAULT,
+ g_task_get_cancellable (task),
+ on_read_all,
+ task);
+ break;
+ case 1:
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_CANCELLED,
+ "user interaction cancelled");
+ g_object_unref (task);
+ break;
+ case 2:
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "user interaction failed");
+ g_object_unref (task);
+ break;
+ }
+}
+
+static void
+on_portal_request_close (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection = G_DBUS_CONNECTION (source_object);
+ GTask *task = G_TASK (user_data);
+ GError *error = NULL;
+
+ if (!g_dbus_connection_call_finish (connection, result, &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static void
+on_portal_cancel (GCancellable *cancellable,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ InitClosure *init = g_task_get_task_data (task);
+
+ g_dbus_connection_call (init->connection,
+ PORTAL_BUS_NAME,
+ init->request_path,
+ PORTAL_REQUEST_INTERFACE,
+ "Close",
+ NULL,
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cancellable,
+ on_portal_request_close,
+ task);
+
+ g_cancellable_disconnect (cancellable, init->cancellable_signal_id);
+}
+
+static void
+on_portal_retrieve_secret (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection = G_DBUS_CONNECTION (source_object);
+ GTask *task = G_TASK (user_data);
+ InitClosure *init = g_task_get_task_data (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ GVariant *reply;
+ GError *error = NULL;
+
+ reply = g_dbus_connection_call_with_unix_fd_list_finish (connection,
+ NULL,
+ result,
+ &error);
+ if (reply == NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ g_variant_get (reply, "(o)", &init->request_path);
+ g_variant_unref (reply);
+
+ init->portal_signal_id =
+ g_dbus_connection_signal_subscribe (connection,
+ PORTAL_BUS_NAME,
+ PORTAL_REQUEST_INTERFACE,
+ "Response",
+ init->request_path,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+ on_portal_response,
+ task,
+ NULL);
+
+ if (cancellable != NULL)
+ init->cancellable_signal_id =
+ g_cancellable_connect (cancellable,
+ G_CALLBACK (on_portal_cancel),
+ task,
+ NULL);
+}
+
+static void
+on_bus_get (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection;
+ GTask *task = G_TASK (user_data);
+ InitClosure *init = g_task_get_task_data (task);
+ GUnixFDList *fd_list;
+ gint fds[2];
+ gint fd_index;
+ GVariantBuilder options;
+ GError *error = NULL;
+
+ connection = g_bus_get_finish (result, &error);
+ if (connection == NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+ init->connection = connection;
+
+ if (!g_unix_open_pipe (fds, FD_CLOEXEC, &error)) {
+ g_object_unref (connection);
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ fd_list = g_unix_fd_list_new ();
+ fd_index = g_unix_fd_list_append (fd_list, fds[1], &error);
+ close (fds[1]);
+ if (fd_index < 0) {
+ close (fds[0]);
+ g_object_unref (fd_list);
+ g_object_unref (connection);
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ close (fds[1]);
+ init->stream = g_unix_input_stream_new (fds[0], TRUE);
+
+ g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
+ g_dbus_connection_call_with_unix_fd_list (connection,
+ PORTAL_BUS_NAME,
+ PORTAL_OBJECT_PATH,
+ PORTAL_SECRET_INTERFACE,
+ "RetrieveSecret",
+ g_variant_new ("(h@a{sv})",
+ fd_index,
+ g_variant_builder_end (&options)),
+ G_VARIANT_TYPE ("(o)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ fd_list,
+ g_task_get_cancellable (task),
+ on_portal_retrieve_secret,
+ task);
+ g_object_unref (fd_list);
+}
+
+static void
+secret_file_backend_real_init_async (GAsyncInitable *initable,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ gchar *path;
+ GFile *file;
+ GFile *dir;
+ SecretValue *password;
+ const gchar *envvar;
+ GTask *task;
+ GError *error = NULL;
+ InitClosure *init;
+ 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");
+ g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION,
+ io_priority,
+ cancellable,
+ on_collection_new_async,
+ task,
+ "file", file,
+ "password", password,
+ NULL);
+ g_object_unref (file);
+ secret_value_unref (password);
+ } else if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) {
+ init = g_slice_new0 (InitClosure);
+ init->io_priority = io_priority;
+ init->file = file;
+ g_task_set_task_data (task, init, init_closure_free);
+ g_bus_get (G_BUS_TYPE_SESSION, cancellable, on_bus_get, task);
+ } else {
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_ARGUMENT,
+ "master password is not retrievable");
+ g_object_unref (task);
+ return;
+ }
+}
+
+static gboolean
+secret_file_backend_real_init_finish (GAsyncInitable *initable,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, initable), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+secret_file_backend_async_initable_iface (GAsyncInitableIface *iface)
+{
+ iface->init_async = secret_file_backend_real_init_async;
+ iface->init_finish = secret_file_backend_real_init_finish;
+}
+
+static void
+on_collection_write (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretFileCollection *collection =
+ SECRET_FILE_COLLECTION (source_object);
+ GTask *task = G_TASK (user_data);
+ GError *error = NULL;
+
+ if (!secret_file_collection_write_finish (collection, result, &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static void
+secret_file_backend_real_store (SecretBackend *backend,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ const gchar *collection,
+ const gchar *label,
+ SecretValue *value,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
+ GTask *task;
+ GError *error = NULL;
+
+ /* Warnings raised already */
+ if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE))
+ return;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+
+ if (!secret_file_collection_replace (self->collection,
+ attributes,
+ label,
+ value,
+ &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ secret_file_collection_write (self->collection,
+ cancellable,
+ on_collection_write,
+ task);
+}
+
+static gboolean
+secret_file_backend_real_store_finish (SecretBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, backend), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+on_retrieve_secret (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretRetrievable *retrievable = SECRET_RETRIEVABLE (source_object);
+ GTask *task = G_TASK (user_data);
+ SecretValue *value;
+ GError *error;
+
+ value = secret_retrievable_retrieve_secret_finish (retrievable,
+ result,
+ &error);
+ g_object_unref (retrievable);
+ if (value == NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ }
+ g_task_return_pointer (task, value, secret_value_unref);
+ g_object_unref (task);
+}
+
+static void
+secret_file_backend_real_lookup (SecretBackend *backend,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
+ GTask *task;
+ GList *matches;
+ GVariant *variant;
+ SecretFileItem *item;
+ GError *error = NULL;
+
+ /* Warnings raised already */
+ if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE))
+ return;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+
+ matches = secret_file_collection_search (self->collection, attributes);
+
+ if (matches == NULL) {
+ g_task_return_pointer (task, NULL, NULL);
+ g_object_unref (task);
+ return;
+ }
+
+ variant = g_variant_ref (matches->data);
+ g_list_free_full (matches, (GDestroyNotify)g_variant_unref);
+
+ item = _secret_file_item_decrypt (variant, self->collection, &error);
+ g_variant_unref (variant);
+ if (item == NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ secret_retrievable_retrieve_secret (SECRET_RETRIEVABLE (item),
+ cancellable,
+ on_retrieve_secret,
+ task);
+}
+
+static SecretValue *
+secret_file_backend_real_lookup_finish (SecretBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, backend), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+secret_file_backend_real_clear (SecretBackend *backend,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
+ GTask *task;
+ GError *error = NULL;
+ gboolean ret;
+
+ /* Warnings raised already */
+ if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE))
+ return;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+
+ ret = secret_file_collection_clear (self->collection, attributes, &error);
+ if (error != NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ /* No need to write as nothing has been removed. */
+ if (!ret) {
+ g_task_return_boolean (task, FALSE);
+ g_object_unref (task);
+ return;
+ }
+
+ secret_file_collection_write (self->collection,
+ cancellable,
+ on_collection_write,
+ task);
+}
+
+static gboolean
+secret_file_backend_real_clear_finish (SecretBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, backend), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+unref_objects (gpointer data)
+{
+ GList *list = data;
+
+ g_list_free_full (list, g_object_unref);
+}
+
+static void
+secret_file_backend_real_search (SecretBackend *backend,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ SecretSearchFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
+ GTask *task;
+ GList *matches;
+ GList *results = NULL;
+ GList *l;
+ GError *error = NULL;
+
+ /* Warnings raised already */
+ if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE))
+ return;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+
+ matches = secret_file_collection_search (self->collection, attributes);
+ for (l = matches; l; l = g_list_next (l)) {
+ SecretFileItem *item = _secret_file_item_decrypt (l->data, self->collection, &error);
+ if (item == NULL) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+ results = g_list_append (results, item);
+ }
+ g_list_free_full (matches, (GDestroyNotify)g_variant_unref);
+
+ g_task_return_pointer (task, results, unref_objects);
+ g_object_unref (task);
+}
+
+static GList *
+secret_file_backend_real_search_finish (SecretBackend *backend,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, backend), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+secret_file_backend_backend_iface (SecretBackendInterface *iface)
+{
+ iface->store = secret_file_backend_real_store;
+ iface->store_finish = secret_file_backend_real_store_finish;
+ iface->lookup = secret_file_backend_real_lookup;
+ iface->lookup_finish = secret_file_backend_real_lookup_finish;
+ iface->clear = secret_file_backend_real_clear;
+ iface->clear_finish = secret_file_backend_real_clear_finish;
+ iface->search = secret_file_backend_real_search;
+ iface->search_finish = secret_file_backend_real_search_finish;
+}
+
+gboolean
+_secret_file_backend_check_portal_version (void)
+{
+ GDBusConnection *connection;
+ GVariant *ret;
+ GVariant *value;
+ guint32 version;
+ GError *error = NULL;
+
+ connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (!connection) {
+ g_warning ("couldn't get session bus: %s", error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ ret = g_dbus_connection_call_sync (connection,
+ PORTAL_BUS_NAME,
+ PORTAL_OBJECT_PATH,
+ "org.freedesktop.DBus.Properties",
+ "Get",
+ g_variant_new ("(ss)",
+ PORTAL_SECRET_INTERFACE,
+ "version"),
+ G_VARIANT_TYPE ("(v)"),
+ 0, -1, NULL, &error);
+ g_object_unref (connection);
+ if (!ret) {
+ g_message ("secret portal is not available: %s",
+ error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ g_variant_get (ret, "(v)", &value);
+ g_variant_unref (ret);
+ version = g_variant_get_uint32 (value);
+ g_variant_unref (value);
+ if (version != PORTAL_SECRET_VERSION) {
+ g_message ("secret portal version mismatch: %u != %u",
+ version, PORTAL_SECRET_VERSION);
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/libsecret/secret-file-backend.h b/libsecret/secret-file-backend.h
new file mode 100644
index 0000000..655bed3
--- /dev/null
+++ b/libsecret/secret-file-backend.h
@@ -0,0 +1,33 @@
+/* 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)
+
+gboolean _secret_file_backend_check_portal_version (void);
+
+G_END_DECLS
+
+#endif /* __SECRET_FILE_BACKEND_H__ */
diff --git a/libsecret/secret-file-collection.c b/libsecret/secret-file-collection.c
new file mode 100644
index 0000000..79863ea
--- /dev/null
+++ b/libsecret/secret-file-collection.c
@@ -0,0 +1,842 @@
+/* libsecret - GLib wrapper for Secret Service
+ *
+ * Copyright 2019 Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2.1 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ *
+ * Author: Daiki Ueno
+ */
+
+#include "config.h"
+
+#include "secret-file-collection.h"
+
+#include "egg/egg-secure-memory.h"
+
+EGG_SECURE_DECLARE (secret_file_collection);
+
+#ifdef WITH_GCRYPT
+#include <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);
+}
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 ();
+}
diff --git a/meson.build b/meson.build
index f20a66e..bb58f68 100644
--- a/meson.build
+++ b/meson.build
@@ -70,6 +70,10 @@ conf.set('_DEBUG', enable_debug)
conf.set('HAVE_MLOCK', meson.get_compiler('c').has_function('mlock'))
configure_file(output: 'config.h', configuration: conf)
+# Test environment
+test_env = environment()
+test_env.set('abs_top_builddir', meson.build_root())
+
# Subfolders
subdir('po')
subdir('egg')
diff --git a/tool/Makefile.am b/tool/Makefile.am
index 422e0d3..9d25ea1 100644
--- a/tool/Makefile.am
+++ b/tool/Makefile.am
@@ -1,7 +1,11 @@
-bin_PROGRAMS += secret-tool
+bin_PROGRAMS += tool/secret-tool
-secret_tool_SOURCES = \
+tool_secret_tool_SOURCES = \
tool/secret-tool.c
-secret_tool_LDADD = \
+tool_secret_tool_LDADD = \
libsecret-@SECRET_MAJOR@.la
+
+if WITH_GCRYPT
+TESTS += tool/test-secret-tool.sh
+endif
diff --git a/tool/meson.build b/tool/meson.build
index 686cf24..1bf9a84 100644
--- a/tool/meson.build
+++ b/tool/meson.build
@@ -9,3 +9,9 @@ secret_tool = executable('secret-tool',
c_args: libsecret_cflags,
install: true,
)
+
+if with_gcrypt and host_machine.system() != 'windows'
+ test('test-secret-tool.sh',
+ find_program('test-secret-tool.sh'),
+ env: test_env)
+endif
diff --git a/tool/test-secret-tool.sh b/tool/test-secret-tool.sh
new file mode 100755
index 0000000..9bd4fbd
--- /dev/null
+++ b/tool/test-secret-tool.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+set -e
+
+testdir=$PWD/test-secret-tool-$$
+test -d "$testdir" || mkdir "$testdir"
+
+cleanup () {
+ rm -rf "$testdir"
+}
+trap cleanup 0
+
+cd "$testdir"
+
+SECRET_BACKEND=file
+export SECRET_BACKEND
+
+SECRET_FILE_TEST_PATH=$testdir/keyring
+export SECRET_FILE_TEST_PATH
+
+SECRET_FILE_TEST_PASSWORD=test
+export SECRET_FILE_TEST_PASSWORD
+
+: ${SECRET_TOOL="$abs_top_builddir"/tool/secret-tool}
+
+: ${DIFF=diff}
+
+echo 1..4
+
+echo test1 | ${SECRET_TOOL} store --label label1 foo bar
+if test $? -eq 0; then
+ echo "ok 1 /secret-tool/store"
+else
+ echo "not ok 1 /secret-tool/store"
+fi
+
+echo test2 | ${SECRET_TOOL} store --label label2 foo bar apple orange
+if test $? -eq 0; then
+ echo "ok 1 /secret-tool/store"
+else
+ echo "not ok 1 /secret-tool/store"
+fi
+
+echo test1 > lookup.exp
+${SECRET_TOOL} lookup foo bar > lookup.out
+if ${DIFF} lookup.exp lookup.out > lookup.diff; then
+ echo "ok 2 /secret-tool/lookup"
+else
+ echo "not ok 2 /secret-tool/lookup"
+ sed 's/^/# /' lookup.diff
+ exit 1
+fi
+
+cat > search.exp <<EOF
+[no path]
+label = label1
+secret = test1
+
+[no path]
+label = label2
+secret = test2
+
+EOF
+
+${SECRET_TOOL} search foo bar | sed '/^created\|^modified/d' > search.out
+if test $? -ne 0; then
+ echo "not ok 3 /secret-tool/search"
+ exit 1
+fi
+if ${DIFF} search.exp search.out > search.diff; then
+ echo "ok 3 /secret-tool/search"
+else
+ echo "not ok 3 /secret-tool/search"
+ sed 's/^/# /' search.diff
+ exit 1
+fi
+
+${SECRET_TOOL} clear apple orange
+if test $? -eq 0; then
+ echo "ok 4 /secret-tool/clear"
+else
+ echo "not ok 4 /secret-tool/clear"
+ exit 1
+fi
+
+cat > search-after-clear.exp <<EOF
+[no path]
+label = label1
+secret = test1
+
+EOF
+
+${SECRET_TOOL} search foo bar | sed '/^created\|^modified/d' > search-after-clear.out
+if test $? -ne 0; then
+ echo "not ok 5 /secret-tool/search-after-clear"
+ exit 1
+fi
+if ${DIFF} search-after-clear.exp search-after-clear.out > search-after-clear.diff; then
+ echo "ok 5 /secret-tool/search-after-clear"
+else
+ echo "not ok 5 /secret-tool/search-after-clear"
+ sed 's/^/# /' search-after-clear.diff
+ exit 1
+fi