[libsecret/wip/dueno/local-file] storage: Add local-file storage backend



commit 0a73a5524e92198fb8de92cc1b618c00d40c761b
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      |  14 +-
 libsecret/secret-storage.c | 448 +++++++++++++++++++++++++++++++++++++++++++++
 libsecret/secret-storage.h |  56 ++++++
 libsecret/test-storage.c   | 155 ++++++++++++++++
 libsecret/test-store1.json |   8 +
 5 files changed, 680 insertions(+), 1 deletion(-)
---
diff --git a/libsecret/Makefile.am b/libsecret/Makefile.am
index 1d1c489..21d371b 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,13 @@ 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_CFLAGS = -DSRCDIR=\"$(abs_top_srcdir)/libsecret\"
+test_storage_LDADD = $(libsecret_LIBS)
+endif
+
 test_value_SOURCES = libsecret/test-value.c
 test_value_LDADD = $(libsecret_LIBS)
 
@@ -363,4 +374,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..77b81e0
--- /dev/null
+++ b/libsecret/secret-storage.c
@@ -0,0 +1,448 @@
+/* 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 *root;
+       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->root);
+
+       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)
+{
+}
+
+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->root = json_node_new (JSON_NODE_ARRAY);
+                       json_node_take_array (self->root, 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->root = 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 *
+storage_find_item (SecretStorage *storage,
+                   GHashTable    *attributes)
+{
+       JsonArray *array;
+       guint length, i;
+
+       array = json_node_get_array (storage->root);
+       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 *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;
+       }
+
+       if (!g_str_equal (collection, SECRET_COLLECTION_DEFAULT)) {
+               g_task_return_boolean (task, TRUE);
+               g_object_unref (task);
+               return;
+       }
+
+       item = storage_find_item (self, 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 (self->root), 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, self->root);
+       plaintext = (guchar *) json_generator_to_data (generator, &n_plaintext);
+       g_object_unref (generator);
+
+       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..9af3165
--- /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 "/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]