[libsecret/wip/dueno/local-file] storage: Add local-file storage backend
- From: Daiki Ueno <dueno src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libsecret/wip/dueno/local-file] storage: Add local-file storage backend
- Date: Fri, 21 Sep 2018 15:33:33 +0000 (UTC)
commit 19fa27e12eb2e9800f48501b7e1b5820562c7f0b
Author: Daiki Ueno <dueno src gnome org>
Date: Fri Sep 21 15:14:52 2018 +0200
storage: Add local-file storage backend
libsecret/Makefile.am | 13 +-
libsecret/secret-storage.c | 465 +++++++++++++++++++++++++++++++++++++++++++++
libsecret/secret-storage.h | 56 ++++++
libsecret/test-storage.c | 155 +++++++++++++++
libsecret/test-store1.json | 8 +
5 files changed, 696 insertions(+), 1 deletion(-)
---
diff --git a/libsecret/Makefile.am b/libsecret/Makefile.am
index 1d1c489..ce0911c 100644
--- a/libsecret/Makefile.am
+++ b/libsecret/Makefile.am
@@ -50,6 +50,10 @@ libsecret_PRIVATE = \
libsecret/secret-util.c \
$(NULL)
+if WITH_GCRYPT
+libsecret_PRIVATE += libsecret/secret-storage.c libsecret/secret-storage.h
+endif
+
libsecret_@SECRET_MAJOR@_la_SOURCES = \
$(libsecret_PUBLIC) \
$(libsecret_PRIVATE) \
@@ -138,7 +142,7 @@ libsecret-$(SECRET_MAJOR).deps: Makefile.am
vapi_DATA += \
libsecret-@SECRET_MAJOR@.vapi \
libsecret-@SECRET_MAJOR@.deps
-
+
endif # ENABLE_VAPIGEN
endif # HAVE_INTROSPECTION
@@ -230,6 +234,12 @@ test_service_LDADD = $(libsecret_LIBS)
test_session_SOURCES = libsecret/test-session.c
test_session_LDADD = $(libsecret_LIBS)
+if WITH_GCRYPT
+C_TESTS += test-storage
+test_storage_SOURCES = libsecret/test-storage.c
+test_storage_LDADD = $(libsecret_LIBS)
+endif
+
test_value_SOURCES = libsecret/test-value.c
test_value_LDADD = $(libsecret_LIBS)
@@ -363,4 +373,5 @@ EXTRA_DIST += \
libsecret/mock-service-prompt.py \
$(JS_TESTS) \
$(PY_TESTS) \
+ libsecret/test-store1.json \
$(NULL)
diff --git a/libsecret/secret-storage.c b/libsecret/secret-storage.c
new file mode 100644
index 0000000..0017e36
--- /dev/null
+++ b/libsecret/secret-storage.c
@@ -0,0 +1,465 @@
+/* libsecret - GLib wrapper for Secret Service
+ *
+ * Copyright 2011 Collabora Ltd.
+ * Copyright 2018 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-storage.h"
+#include "secret-private.h"
+
+#include "egg/egg-base64.h"
+#include "egg/egg-hkdf.h"
+#include "egg/egg-jwe.h"
+#include "egg/egg-secure-memory.h"
+
+#define CONTEXT "secret storage key"
+
+EGG_SECURE_DECLARE (secret_storage);
+
+enum {
+ PROP_0,
+ PROP_FILE,
+ PROP_PASSWORD
+};
+
+struct _SecretStorage
+{
+ GObject parent;
+ GFile *file;
+ gchar *password;
+ guchar *key;
+ gsize n_key;
+ JsonNode *session_collection;
+ JsonNode *default_collection;
+ gchar *etag;
+};
+
+struct _SecretStorageClass
+{
+ GObjectClass parent_class;
+};
+
+static void secret_storage_async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (SecretStorage, secret_storage, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
secret_storage_async_initable_iface_init));
+
+static void
+secret_storage_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SecretStorage *self = SECRET_STORAGE (object);
+
+ switch (prop_id) {
+ case PROP_FILE:
+ self->file = g_value_dup_object (value);
+ break;
+ case PROP_PASSWORD:
+ self->password = g_value_dup_string (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+secret_storage_finalize (GObject *object)
+{
+ SecretStorage *self = SECRET_STORAGE (object);
+
+ g_clear_object (&self->file);
+ g_free (self->password);
+
+ if (self->key) {
+ egg_secure_clear (self->key, self->n_key);
+ egg_secure_free (self->key);
+ }
+
+ json_node_unref (self->session_collection);
+ json_node_unref (self->default_collection);
+
+ G_OBJECT_CLASS (secret_storage_parent_class)->finalize (object);
+}
+
+static void
+secret_storage_class_init (SecretStorageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = secret_storage_set_property;
+ object_class->finalize = secret_storage_finalize;
+
+ g_object_class_install_property (object_class,
+ PROP_FILE,
+ g_param_spec_object ("file", "File", "Storage file",
+ G_TYPE_FILE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_PASSWORD,
+ g_param_spec_string ("password", "Password", "Master password of
storage",
+ "",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+secret_storage_init (SecretStorage *self)
+{
+ self->session_collection = json_node_new (JSON_NODE_ARRAY);
+ json_node_take_array (self->session_collection, json_array_new ());
+}
+
+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);
+ SecretStorage *self = g_task_get_source_object (task);
+ GError *error;
+ gchar *contents;
+ gsize length;
+ JsonParser *parser;
+ JsonNode *root;
+ guchar *plaintext;
+ gsize n_plaintext;
+ gchar *etag;
+
+ error = NULL;
+ if (!g_file_load_contents_finish (file, result, &contents, &length,
+ &etag, &error)) {
+ if (error->code == G_IO_ERROR_NOT_FOUND) {
+ g_error_free (error);
+ self->default_collection =
+ json_node_new (JSON_NODE_ARRAY);
+ json_node_take_array (self->default_collection,
+ json_array_new ());
+ g_task_return_boolean (task, TRUE);
+ } else {
+ g_task_return_error (task, error);
+ }
+ g_object_unref (task);
+ return;
+ }
+
+ g_free (self->etag);
+ self->etag = etag;
+
+ parser = json_parser_new ();
+ error = NULL;
+ if (!json_parser_load_from_data (parser, contents, length, &error)) {
+ g_object_unref (parser);
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+ root = json_parser_steal_root (parser);
+ g_object_unref (parser);
+
+ error = NULL;
+ plaintext = egg_jwe_symmetric_decrypt (root, self->key, self->n_key,
+ &n_plaintext, &error);
+ json_node_unref (root);
+ if (!plaintext) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ parser = json_parser_new ();
+ error = NULL;
+ if (!json_parser_load_from_data (parser, (gchar *)plaintext, n_plaintext,
+ &error)) {
+ g_object_unref (parser);
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+ self->default_collection = json_parser_steal_root (parser);
+ g_object_unref (parser);
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static void
+secret_storage_init_async (GAsyncInitable *initable,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretStorage *self = SECRET_STORAGE (initable);
+ GTask *task;
+
+ task = g_task_new (initable, cancellable, callback, user_data);
+
+ self->n_key = 16;
+ self->key = egg_secure_alloc (self->n_key);
+
+ if (!egg_hkdf_perform ("sha256",
+ self->password,
+ strlen (self->password),
+ NULL,
+ 0,
+ CONTEXT,
+ sizeof (CONTEXT)-1,
+ self->key,
+ self->n_key)) {
+ egg_secure_free (self->key);
+ self->key = NULL;
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_FAILED,
+ "couldn't derive encryption key");
+ g_object_unref (task);
+ return;
+ }
+
+ g_file_load_contents_async (self->file, cancellable, on_load_contents, task);
+}
+
+static gboolean
+secret_storage_init_finish (GAsyncInitable *initable,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (res, initable), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+secret_storage_async_initable_iface_init (GAsyncInitableIface *iface)
+{
+ iface->init_async = secret_storage_init_async;
+ iface->init_finish = secret_storage_init_finish;
+}
+
+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);
+ SecretStorage *self = g_task_get_source_object (task);
+ GError *error = NULL;
+ gchar *etag;
+
+ if (!g_file_replace_contents_finish (file, result, &etag, &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ g_free (self->etag);
+ self->etag = etag;
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static gboolean
+node_matches_attributes (JsonNode *node,
+ GHashTable *attributes)
+{
+ JsonObject *object;
+ GHashTableIter iter;
+ gpointer key, val;
+ const gchar *attribute_value;
+
+ /* Check if all the attributes are set on node */
+ object = json_node_get_object (node);
+ if (!object)
+ return FALSE;
+
+ g_hash_table_iter_init (&iter, attributes);
+ while (g_hash_table_iter_next (&iter, &key, &val)) {
+ attribute_value = json_object_get_string_member (object, key);
+ if (!attribute_value || !g_str_equal (attribute_value, val))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static JsonNode *
+lookup_from_collection (JsonNode *collection,
+ GHashTable *attributes)
+{
+ JsonArray *array;
+ guint length, i;
+
+ array = json_node_get_array (collection);
+ if (!array)
+ return NULL;
+
+ length = json_array_get_length (array);
+ for (i = 0; i < length; i++) {
+ JsonNode *node = json_array_get_element (array, i);
+ if (node_matches_attributes (node, attributes))
+ return node;
+ }
+
+ return NULL;
+}
+
+static JsonNode *
+hash_table_to_json (GHashTable *hash_table)
+{
+ GHashTableIter iter;
+ gpointer key, val;
+ JsonBuilder *builder;
+ JsonNode *result;
+
+ builder = json_builder_new ();
+ json_builder_begin_object (builder);
+ g_hash_table_iter_init (&iter, hash_table);
+ while (g_hash_table_iter_next (&iter, &key, &val)) {
+ json_builder_set_member_name (builder, key);
+ json_builder_add_string_value (builder, val);
+ }
+ json_builder_end_object (builder);
+ result = json_builder_get_root (builder);
+ g_object_unref (builder);
+ return result;
+}
+
+void
+secret_storage_store (SecretStorage *self,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ const gchar *collection,
+ const gchar *label,
+ SecretValue *value,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ JsonNode *collection_, *item;
+ JsonNode *root;
+ JsonObject *object;
+ JsonGenerator *generator;
+ const gchar *data;
+ gsize length;
+ gchar *encoded;
+ guchar *plaintext;
+ gsize n_plaintext;
+ gchar *ciphertext;
+ gsize n_ciphertext;
+ GTask *task;
+ GError *error;
+
+ g_return_if_fail (SECRET_IS_STORAGE (self));
+ g_return_if_fail (attributes != NULL);
+ g_return_if_fail (label != NULL);
+ g_return_if_fail (value != NULL);
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+
+ /* Warnings raised already */
+ if (schema != NULL &&
+ !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE)) {
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_ARGUMENT,
+ "couldn't validate attributes");
+ g_object_unref (task);
+ return;
+ }
+
+ /* We only support default and session collections */
+ if (g_str_equal (collection, SECRET_COLLECTION_DEFAULT)) {
+ collection_ = self->default_collection;
+ } else if (g_str_equal (collection, SECRET_COLLECTION_SESSION)) {
+ collection_ = self->session_collection;
+ } else {
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+ return;
+ }
+
+ item = lookup_from_collection (collection_, attributes);
+ if (!item) {
+ item = json_node_new (JSON_NODE_OBJECT);
+ object = json_object_new ();
+ json_node_take_object (item, object);
+ json_object_set_member (object, "attributes",
+ hash_table_to_json (attributes));
+ json_array_add_element (json_node_get_array (collection_), item);
+ }
+
+ object = json_node_get_object (item);
+ json_object_set_string_member (object, "content-type",
+ secret_value_get_content_type (value));
+
+ json_object_set_string_member (object, "label", label);
+
+ data = secret_value_get (value, &length);
+ encoded = egg_base64_encode ((guchar *) data, length);
+ json_object_set_string_member (object, "value", encoded);
+ g_free (encoded);
+
+ generator = json_generator_new ();
+ json_generator_set_root (generator, collection_);
+ plaintext = (guchar *) json_generator_to_data (generator, &n_plaintext);
+ g_object_unref (generator);
+
+ if (collection_ == self->session_collection) {
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+ return;
+ }
+
+ error = NULL;
+ root = egg_jwe_symmetric_encrypt (plaintext, n_plaintext, "A128GCM",
+ self->key, self->n_key, NULL, 0,
+ &error);
+ g_free (plaintext);
+ if (!root) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ return;
+ }
+
+ generator = json_generator_new ();
+ json_generator_set_root (generator, root);
+ json_node_unref (root);
+ ciphertext = json_generator_to_data (generator, &n_ciphertext);
+ g_object_unref (generator);
+
+ g_file_replace_contents_async (self->file,
+ ciphertext, n_ciphertext,
+ self->etag, TRUE,
+ G_FILE_CREATE_PRIVATE,
+ cancellable,
+ on_replace_contents,
+ task);
+ g_task_set_task_data (task, ciphertext, g_free);
+}
+
+gboolean
+secret_storage_store_finish (SecretStorage *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-storage.h b/libsecret/secret-storage.h
new file mode 100644
index 0000000..d6d192e
--- /dev/null
+++ b/libsecret/secret-storage.h
@@ -0,0 +1,56 @@
+/* libsecret - GLib wrapper for Secret Service
+ *
+ * Copyright 2011 Collabora Ltd.
+ * Copyright 2018 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
+ */
+
+
+#ifndef SECRET_STORAGE_H_
+#define SECRET_STORAGE_H_
+
+#include <glib-object.h>
+
+#include "secret-schema.h"
+#include "secret-value.h"
+
+G_BEGIN_DECLS
+
+#define SECRET_TYPE_STORAGE (secret_storage_get_type ())
+#define SECRET_STORAGE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), SECRET_TYPE_STORAGE,
SecretStorage))
+#define SECRET_STORAGE_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), SECRET_TYPE_STORAGE,
SecretStorageClass))
+#define SECRET_IS_STORAGE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), SECRET_TYPE_STORAGE))
+#define SECRET_IS_STORAGE_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), SECRET_TYPE_STORAGE))
+#define SECRET_STORAGE_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), SECRET_TYPE_STORAGE,
SecretStorageClass))
+
+typedef struct _SecretStorage SecretStorage;
+typedef struct _SecretStorageClass SecretStorageClass;
+
+GType secret_storage_get_type (void) G_GNUC_CONST;
+
+void secret_storage_store (SecretStorage *self,
+ const SecretSchema *schema,
+ GHashTable *attributes,
+ const gchar *collection,
+ const gchar *label,
+ SecretValue *value,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean secret_storage_store_finish
+ (SecretStorage *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* SECRET_STORAGE_H_ */
diff --git a/libsecret/test-storage.c b/libsecret/test-storage.c
new file mode 100644
index 0000000..f924a16
--- /dev/null
+++ b/libsecret/test-storage.c
@@ -0,0 +1,155 @@
+/* libsecret - GLib wrapper for Secret Service
+ *
+ * Copyright 2018 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 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ */
+
+
+#include "config.h"
+
+#include "secret-storage.h"
+#include "egg/egg-testing.h"
+
+typedef struct {
+ gchar *directory;
+ GFile *file;
+ GMainLoop *loop;
+ SecretStorage *storage;
+} Test;
+
+static void
+setup (Test *test,
+ gconstpointer data)
+{
+ test->directory = egg_tests_create_scratch_directory (NULL, NULL);
+ test->loop = g_main_loop_new (NULL, FALSE);
+}
+
+static void
+teardown (Test *test,
+ gconstpointer data)
+{
+ egg_tests_remove_scratch_directory (test->directory);
+ g_free (test->directory);
+ g_clear_object (&test->file);
+ g_main_loop_unref (test->loop);
+ g_clear_object (&test->storage);
+}
+
+static void
+on_new_async (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncInitable *initable = G_ASYNC_INITABLE (source_object);
+ Test *test = user_data;
+ GError *error = NULL;
+
+ test->storage = SECRET_STORAGE (g_async_initable_new_finish (initable, result, &error));
+ g_main_loop_quit (test->loop);
+}
+
+static void
+test_load_nonexistent (Test *test,
+ gconstpointer data)
+{
+ gchar *path = g_build_filename (test->directory, "nonexistent", NULL);
+ test->file = g_file_new_for_path (path);
+ g_free (path);
+ g_async_initable_new_async (SECRET_TYPE_STORAGE, G_PRIORITY_DEFAULT,
+ NULL, on_new_async, test,
+ "file", test->file,
+ "password", "password",
+ NULL);
+ g_main_loop_run (test->loop);
+ g_assert_nonnull (test->storage);
+}
+
+static void
+test_load (Test *test,
+ gconstpointer data)
+{
+ gchar *path = g_build_filename (test->directory, "store1", NULL);
+ egg_tests_copy_scratch_file (test->directory,
+ SRCDIR "/libsecret/test-store1.json");
+ test->file = g_file_new_for_path (path);
+ g_free (path);
+ g_async_initable_new_async (SECRET_TYPE_STORAGE, G_PRIORITY_DEFAULT,
+ NULL, on_new_async, test,
+ "file", test->file,
+ "password", "password",
+ NULL);
+ g_main_loop_run (test->loop);
+ g_assert_nonnull (test->storage);
+}
+
+static void
+on_store (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretStorage *storage = SECRET_STORAGE (source_object);
+ Test *test = user_data;
+ gboolean ret;
+ GError *error = NULL;
+
+ ret = secret_storage_store_finish (storage, result, &error);
+ g_assert_true (ret);
+ g_assert_no_error (error);
+
+ g_main_loop_quit (test->loop);
+}
+
+static void
+test_store (Test *test,
+ gconstpointer data)
+{
+ gchar *path = g_build_filename (test->directory, "store1", NULL);
+ GHashTable *attributes;
+ SecretValue *value;
+
+ test->file = g_file_new_for_path (path);
+ g_free (path);
+ g_async_initable_new_async (SECRET_TYPE_STORAGE, G_PRIORITY_DEFAULT,
+ NULL, on_new_async, test,
+ "file", test->file,
+ "password", "password",
+ NULL);
+ g_main_loop_run (test->loop);
+ g_assert_nonnull (test->storage);
+
+ attributes = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_insert (attributes, "attr1", "value1");
+ g_hash_table_insert (attributes, "attr2", "value2");
+ g_hash_table_insert (attributes, "attr3", "value3");
+
+ value = secret_value_new ("secret", 6, "text/plain");
+ secret_storage_store (test->storage, NULL, attributes,
+ SECRET_COLLECTION_DEFAULT,
+ "label",
+ value,
+ NULL,
+ on_store,
+ test);
+ g_hash_table_unref (attributes);
+ secret_value_unref (value);
+ g_main_loop_run (test->loop);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add ("/storage/load/nonexistent", Test, NULL, setup, test_load_nonexistent, teardown);
+ g_test_add ("/storage/load", Test, NULL, setup, test_load, teardown);
+ g_test_add ("/storage/store", Test, NULL, setup, test_store, teardown);
+
+ return egg_tests_run_with_loop ();
+}
diff --git a/libsecret/test-store1.json b/libsecret/test-store1.json
new file mode 100644
index 0000000..635cfd2
--- /dev/null
+++ b/libsecret/test-store1.json
@@ -0,0 +1,8 @@
+{
+
"ciphertext":"PYVvmLQt4Y_gYxyYJk8XcEXBtDrw0yCj6W8eCD4UHgA8kaSdtZabtXPpLR-1A8-flzk9Uh5i3kbibjbgnnzD8BXm1KxXtbRUjL38c9Zz6awH7PR5zaP4M1rueP-EgJ4lOsZ07V3hCCR4ZUll3UyKtzoB5BKZr1ENPZV58HuCjhazR1G1",
+ "encrypted_key":"",
+ "iv":"z1kaerFLRwq3o5Gj",
+ "tag":"S4sqRqaV1dbzJ2PRr_E-8Q",
+ "protected":"eyJlbmMiOiJBMTI4R0NNIn0",
+ "header":{"alg":"dir"}
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]