Jelajahi Sumber

Read and decrypt notes from standard file server

Matthias Vogelgesang 7 tahun lalu
induk
melakukan
13a1b0e0fc
7 mengubah file dengan 914 tambahan dan 71 penghapusan
  1. 42 2
      src/iridium-note.c
  2. 4 1
      src/iridium-note.h
  3. 616 0
      src/iridium-standard-file.c
  4. 50 0
      src/iridium-standard-file.h
  5. 79 16
      src/iridium-window.c
  6. 118 52
      src/iridium-window.ui
  7. 5 0
      src/meson.build

+ 42 - 2
src/iridium-note.c

@@ -26,6 +26,7 @@ typedef struct _IridiumNotePrivate
 {
   gchar *title;
   gchar *content;
+  GDateTime *last_modified;
   GList *tags;
 } IridiumNotePrivate;
 
@@ -33,6 +34,7 @@ enum {
   PROP_0,
   PROP_TITLE,
   PROP_CONTENT,
+  PROP_LAST_MODIFIED,
   NUM_PROPERTIES,
 };
 
@@ -49,9 +51,14 @@ G_DEFINE_TYPE_WITH_PRIVATE (IridiumNote, iridium_note, G_TYPE_OBJECT);
 
 IridiumNote *
 iridium_note_new (const gchar *title,
-                  const gchar *content)
+                  const gchar *content,
+                  GDateTime *last_modified)
 {
-  return g_object_new (IRIDIUM_TYPE_NOTE, "title", title, NULL);
+  return g_object_new (IRIDIUM_TYPE_NOTE,
+      "title", title,
+      "content", content,
+      "last-modified", last_modified,
+      NULL);
 }
 
 void
@@ -85,6 +92,12 @@ iridium_note_get_title (IridiumNote *note)
   return IRIDIUM_NOTE_GET_PRIVATE (note)->title;
 }
 
+GDateTime *
+iridium_note_get_last_modified (IridiumNote *note)
+{
+  return IRIDIUM_NOTE_GET_PRIVATE (note)->last_modified;
+}
+
 gboolean
 iridium_note_matches_fuzzy (IridiumNote *note,
                             const gchar *needle)
@@ -121,6 +134,14 @@ iridium_note_matches_fuzzy (IridiumNote *note,
   return result;
 }
 
+static void
+iridium_note_update_last_modified (IridiumNotePrivate *priv)
+{
+  if (priv->last_modified)
+    g_date_time_unref (priv->last_modified);
+  priv->last_modified = g_date_time_new_now_local ();
+}
+
 static void
 iridium_note_set_property (GObject *object,
                            guint property_id,
@@ -135,10 +156,17 @@ iridium_note_set_property (GObject *object,
     case PROP_TITLE:
       g_free (priv->title);
       priv->title = g_value_dup_string (value);
+      iridium_note_update_last_modified (priv);
       break;
     case PROP_CONTENT:
       g_free (priv->content);
       priv->content = g_value_dup_string (value);
+      iridium_note_update_last_modified (priv);
+      break;
+    case PROP_LAST_MODIFIED:
+      if (priv->last_modified)
+        g_date_time_unref (priv->last_modified);
+      priv->last_modified = g_date_time_ref (g_value_get_boxed (value));
       break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -162,6 +190,9 @@ iridium_note_get_property (GObject *object,
     case PROP_CONTENT:
       g_value_set_string (value, priv->content);
       break;
+    case PROP_LAST_MODIFIED:
+      g_value_set_boxed (value, priv->last_modified);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
   }
@@ -175,6 +206,7 @@ iridium_note_dispose (GObject *object)
   priv = IRIDIUM_NOTE_GET_PRIVATE (object);
   g_list_free_full (priv->tags, (GDestroyNotify) g_object_unref);
   priv->tags = NULL;
+  g_date_time_unref (priv->last_modified);
 }
 
 static void
@@ -207,6 +239,13 @@ iridium_note_class_init (IridiumNoteClass *klass)
       "Content of the note",
       "", G_PARAM_READWRITE);
 
+  properties[PROP_LAST_MODIFIED] =
+    g_param_spec_boxed ("last-modified",
+      "Last modified",
+      "Last modified",
+      G_TYPE_DATE_TIME,
+      G_PARAM_READWRITE);
+
   g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
 
   signals[TAGS_CHANGED] = g_signal_new ("tags-changed",
@@ -226,5 +265,6 @@ iridium_note_init (IridiumNote *self)
   priv = IRIDIUM_NOTE_GET_PRIVATE (self);
   priv->title = g_strdup ("");
   priv->content = g_strdup ("");
+  priv->last_modified = NULL;
   priv->tags = NULL;
 }

+ 4 - 1
src/iridium-note.h

@@ -18,11 +18,14 @@ struct _IridiumNoteClass {
 };
 
 IridiumNote *iridium_note_new           (const gchar    *title,
-                                         const gchar    *content);
+                                         const gchar    *content,
+                                         GDateTime      *last_modified);
 void         iridium_note_add_tag       (IridiumNote    *note,
                                          IridiumTag     *tag);
 GList       *iridium_note_get_tags      (IridiumNote    *note);
 const gchar *iridium_note_get_title     (IridiumNote    *note);
+GDateTime   *iridium_note_get_last_modified
+                                        (IridiumNote    *note);
 gboolean     iridium_note_has_tag       (IridiumNote    *note,
                                          IridiumTag     *tag);
 gboolean     iridium_note_matches_fuzzy (IridiumNote    *note,

+ 616 - 0
src/iridium-standard-file.c

@@ -0,0 +1,616 @@
+/* iridium-standard-file.c
+ *
+ * Copyright 2018 Matthias Vogelgesang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+#include <nettle/aes.h>
+#include <nettle/cbc.h>
+#include <nettle/sha2.h>
+#include <nettle/hmac.h>
+#include <nettle/pbkdf2.h>
+#include "iridium-standard-file.h"
+#include "iridium-note.h"
+
+typedef enum {
+  SF_VERSION_001,
+  SF_VERSION_002,
+} StandardFileVersion;
+
+typedef enum {
+  SF_FUNC_PBKDF2,
+} StandardFileFunc;
+
+typedef enum {
+  SF_HASH_SHA512,
+} StandardFileHash;
+
+typedef struct {
+  guint cost;
+  guint key_size;
+  gchar *salt;
+  StandardFileFunc func;
+  StandardFileFunc hash;
+  StandardFileVersion version;
+  struct {
+    guint8 password[32];
+    guint8 master[32];
+    guint8 auth[32];
+  } keys;
+} StandardFileAuthParams;
+
+typedef struct {
+  IridiumStandardFile *client;
+  JsonParser *parser;
+  gchar *password;
+  GCancellable *cancellable;
+} ReadAuthParams;
+
+struct _IridiumStandardFile
+{
+  GObject parent_instance;
+
+  gchar *email;
+  gchar *token;
+  SoupSession *session;
+  SoupURI *base_uri;
+  StandardFileAuthParams auth_params;
+};
+
+G_DEFINE_TYPE (IridiumStandardFile, iridium_standard_file, G_TYPE_OBJECT)
+
+const SecretSchema *
+standard_file_get_schema (void)
+{
+  static const SecretSchema schema = {
+    "net.bloerg.Iridium", SECRET_SCHEMA_NONE,
+    {
+      { "email", SECRET_SCHEMA_ATTRIBUTE_STRING },
+      { "server", SECRET_SCHEMA_ATTRIBUTE_STRING },
+      { NULL, 0 },
+    }
+  };
+  return &schema;
+}
+
+IridiumStandardFile *
+iridium_standard_file_new (void)
+{
+  return g_object_new (IRIDIUM_TYPE_STANDARD_FILE, NULL);
+}
+
+static guint8 *
+unhexlify (const gchar *s, gsize length)
+{
+  guint8 *result;
+
+  result = g_malloc0 (length / 2);
+
+  for (gsize i = 0; i < length / 2; i++)
+    sscanf (&s[i * 2], "%2hhx", &result[i]);
+
+  return result;
+}
+
+static gchar *
+hexlify (const guint8 *s, gsize length)
+{
+  gchar *result;
+
+  result = g_malloc0 (2 * length + 1);
+
+  for (gsize i = 0; i < length; i++)
+    g_snprintf (&result[2 * i], 3, "%02x", s[i]);
+
+  return result;
+}
+
+static void
+derive_keys (const guint8 *password,
+             StandardFileAuthParams *params)
+{
+  struct hmac_sha512_ctx context;
+  guint8 dst[96];
+
+  g_assert_nonnull (params->salt);
+
+  hmac_sha512_set_key (&context, strlen ((gchar *) password), password);
+
+  PBKDF2 (&context, hmac_sha512_update, hmac_sha512_digest, SHA512_DIGEST_SIZE,
+          params->cost, strlen (params->salt), (guint8 *) params->salt, 96, dst);
+
+  memcpy (&params->keys, dst, sizeof (dst));
+}
+
+static gchar *
+decrypt (const gchar *s,
+         const gchar *check_uuid,
+         const guint8 *enc_key,
+         const guint8 *auth_key,
+         gsize key_length)
+{
+  gchar **v;
+  gchar *to_auth;
+  gchar *hash;
+  gsize cipher_length;
+  gsize raw_length;
+  const gchar *version;
+  const gchar *auth_hash;
+  const gchar *uuid;
+  const gchar *iv;
+  const gchar *cipher_text;
+  guint8 *iv_bytes;
+  guint8 *dst;
+  guchar *cipher_raw_text;
+  struct hmac_sha256_ctx hmac_contextt;
+  guint8 digest[SHA256_DIGEST_SIZE];
+  struct aes_ctx aes_context;
+
+  v = g_strsplit (s, ":", 0);
+  g_assert_nonnull (v[0]); version = v[0];
+  g_assert_nonnull (v[1]); auth_hash = v[1];
+  g_assert_nonnull (v[2]); uuid = v[2];
+  g_assert_nonnull (v[3]); iv = v[3];
+  g_assert_nonnull (v[4]); cipher_text = v[4];
+
+  g_assert_cmpstr (uuid, ==, check_uuid);
+
+  to_auth = g_strjoin (":", version, uuid, iv, cipher_text, NULL);
+  hmac_sha256_set_key (&hmac_contextt, key_length, auth_key);
+  hmac_sha256_update (&hmac_contextt, strlen (to_auth), (guint8 *) to_auth);
+  hmac_sha256_digest (&hmac_contextt, SHA256_DIGEST_SIZE, digest);
+
+  hash = hexlify (digest, sizeof (digest));
+  g_assert_cmpstr (hash, ==, (gchar *) auth_hash);
+  g_free (hash);
+
+  cipher_length = strlen (cipher_text);
+  cipher_length = AES_BLOCK_SIZE * (cipher_length / AES_BLOCK_SIZE + (cipher_length % AES_BLOCK_SIZE ? 1 : 0));
+  dst = g_malloc0 (cipher_length);
+
+  aes_set_decrypt_key (&aes_context, key_length, enc_key);
+  iv_bytes = unhexlify (iv, strlen (iv));
+  cipher_raw_text = g_base64_decode (cipher_text, &raw_length);
+
+  cbc_decrypt (&aes_context, (nettle_cipher_func *) &aes_decrypt, AES_BLOCK_SIZE, iv_bytes,
+               raw_length, dst, (guint8 *) cipher_raw_text);
+
+  dst[raw_length] = '\0';
+  g_free (cipher_raw_text);
+  g_free (iv_bytes);
+  g_free (to_auth);
+  g_strfreev (v);
+  return (gchar *) dst;
+}
+
+static gchar *
+decrypt_item (JsonObject *item, StandardFileAuthParams *params)
+{
+  const gchar *enc_item_key;
+  const gchar *uuid;
+  gchar *enc_auth_key;
+  const gchar *enc_key;
+  const gchar *auth_key;
+  guint8 *enc_key_bytes;
+  guint8 *auth_key_bytes;
+  gsize enc_key_size;
+  const gchar *enc_content;
+  gchar *content;
+
+  uuid = json_object_get_string_member (item, "uuid");
+  enc_item_key = json_object_get_string_member (item, "enc_item_key");
+  enc_auth_key = decrypt (enc_item_key, uuid, params->keys.master, params->keys.auth, sizeof (params->keys.master));
+
+  enc_key = (gchar *) enc_auth_key;
+  enc_key_size = strlen (enc_auth_key) / 2 - 8;
+  auth_key = &enc_auth_key[enc_key_size];
+
+  enc_key_bytes = unhexlify (enc_key, enc_key_size);
+  auth_key_bytes = unhexlify (auth_key, enc_key_size);
+
+  enc_content = json_object_get_string_member (item, "content");
+  content = decrypt (enc_content, uuid, enc_key_bytes, auth_key_bytes, enc_key_size / 2);
+
+  g_free (enc_key_bytes);
+  g_free (auth_key_bytes);
+  g_free (enc_auth_key);
+  return content;
+}
+
+static IridiumNote *
+deserialize_note (JsonObject *meta, JsonObject *data)
+{
+  IridiumNote *note;
+  GTimeVal time;
+  GDateTime *last_modified;
+
+  if (!g_time_val_from_iso8601 (json_object_get_string_member (meta, "created_at"), &time)) {
+    g_print ("Problem parsing\n");
+  }
+
+  last_modified = g_date_time_new_from_timeval_local (&time);
+
+  note = iridium_note_new (json_object_get_string_member (data, "title"),
+                           json_object_get_string_member (data, "text"),
+                           last_modified);
+
+  return note;
+}
+
+static GObject *
+deserialize_item (JsonObject *meta, const gchar *data)
+{
+  JsonObject *root;
+  g_autoptr(JsonParser) parser;
+  const gchar *type;
+  GError *error = NULL;
+
+  parser = json_parser_new_immutable ();
+  json_parser_load_from_data (parser, data, -1, &error);
+  type = json_object_get_string_member (meta, "content_type");
+  root = json_node_get_object (json_parser_get_root (parser));
+
+  if (!g_strcmp0 (type, "Note"))
+    return G_OBJECT (deserialize_note (meta, root));
+
+  return NULL;
+}
+
+static gboolean
+get_auth_params (ReadAuthParams *params,
+                 GError **error)
+{
+  JsonObject *object;
+  const gchar *s;
+
+  object = json_node_get_object (json_parser_get_root (params->parser));
+
+  params->client->auth_params.func = SF_FUNC_PBKDF2;
+  params->client->auth_params.hash = SF_HASH_SHA512;
+  params->client->auth_params.cost = json_object_get_int_member (object, "pw_cost");
+  params->client->auth_params.key_size = json_object_get_int_member (object, "pw_key_size");
+  params->client->auth_params.salt = g_strdup (json_object_get_string_member (object, "pw_salt"));
+
+  if (g_strcmp0 (json_object_get_string_member (object, "pw_alg"), "sha512")) {
+    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                         "Hash algorithm other than sha512 is not supported");
+    return FALSE;
+  }
+
+  if (g_strcmp0 (json_object_get_string_member (object, "pw_func"), "pbkdf2")) {
+    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                         "Password derivative function other than PBKDF2 is not supported");
+    return FALSE;
+  }
+
+  s = json_object_get_string_member (object, "version");
+
+  if (!g_strcmp0 (s, "001"))
+    params->client->auth_params.version = SF_VERSION_001;
+  else if (!g_strcmp0 (s, "002"))
+    params->client->auth_params.version = SF_VERSION_002;
+  else {
+    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                         "StandardFile protocols other than 001 and 002 are not supported");
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+GList *
+iridium_standard_file_load (IridiumStandardFile *client,
+                            const gchar *filename)
+{
+  JsonObject *root;
+  JsonArray *array;
+  g_autoptr(JsonParser) parser;
+  GList *result = NULL;
+  GError *error = NULL;
+
+  parser = json_parser_new_immutable ();
+  json_parser_load_from_file (parser, filename, &error);
+
+  if (error != NULL) {
+    g_error ("Error: %s\n", error->message);
+    return NULL;
+  }
+
+  root = json_node_get_object (json_parser_get_root (parser));
+  array = json_object_get_array_member (root, "items");
+
+  for (guint i = 0; i < json_array_get_length (array); i++) {
+    JsonObject *data;
+    GObject *item;
+    gchar *content;
+
+    data = json_array_get_object_element (array, i);
+    content = decrypt_item (data, &client->auth_params);
+    item = deserialize_item (data, content);
+
+    if (item)
+      result = g_list_append (result, item);
+
+    g_free (content);
+  }
+
+  return result;
+}
+
+static void
+read_auth_params_data_free (ReadAuthParams *data)
+{
+  secret_password_free (data->password);
+  g_object_unref (data->parser);
+}
+
+static void
+on_signin_response_parsed (GObject *object,
+                           GAsyncResult *result,
+                           gpointer user_data)
+{
+  GTask *task;
+  ReadAuthParams *data;
+  JsonObject *root_object;
+  GError *error = NULL;
+
+  task = user_data;
+  data = g_task_get_task_data (task);
+
+  if (!json_parser_load_from_stream_finish (data->parser, result, &error)) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  root_object = json_node_get_object (json_parser_get_root (data->parser));
+
+  g_free (data->client->token);
+  data->client->token = g_strdup (json_object_get_string_member (root_object, "token"));
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+on_send_signin_message (GObject *object,
+                        GAsyncResult *result,
+                        gpointer user_data)
+{
+  GInputStream *stream;
+  GTask *task;
+  ReadAuthParams *data;
+  GError *error = NULL;
+
+  task = user_data;
+  data = g_task_get_task_data (task);
+  stream = soup_session_send_finish (SOUP_SESSION (object), result, &error);
+
+  if (stream == NULL) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  /* we re-use the old parser */
+  json_parser_load_from_stream_async (data->parser, stream, data->cancellable, on_signin_response_parsed, task);
+}
+
+static void
+on_auth_params_response_parsed (GObject *object,
+                                GAsyncResult *result,
+                                gpointer user_data)
+{
+  GTask *task;
+  ReadAuthParams *data;
+  SoupURI *uri;
+  SoupMessage *msg;
+  gchar *password;
+  gchar *body;
+  GError *error = NULL;
+
+  task = user_data;
+  data = g_task_get_task_data (task);
+
+  if (!json_parser_load_from_stream_finish (data->parser, result, &error)) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  if (!get_auth_params (data, &error)) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  derive_keys ((guint8 *) data->password, &data->client->auth_params);
+
+  g_debug ("StandardFile parameters: version=%i func=%i hash=%i key_size=%u iterations=%u",
+           data->client->auth_params.version,
+           data->client->auth_params.func,
+           data->client->auth_params.hash,
+           data->client->auth_params.key_size,
+           data->client->auth_params.cost);
+
+  uri = soup_uri_new_with_base (data->client->base_uri, "api/auth/sign_in");
+  msg = soup_message_new_from_uri ("POST", uri);
+  password = hexlify (data->client->auth_params.keys.password, sizeof (data->client->auth_params.keys.password));
+  body = g_strdup_printf ("{\"email\": \"%s\", \"password\": \"%s\"}", data->client->email, password);
+  soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, body, strlen (body));
+  soup_session_send_async (data->client->session, msg, NULL, on_send_signin_message, task);
+
+  g_free (password);
+  soup_uri_free (uri);
+  g_object_unref (msg);
+}
+
+static void
+on_send_auth_params_message (GObject *object,
+                             GAsyncResult *result,
+                             gpointer user_data)
+{
+  GInputStream *stream;
+  GTask *task;
+  ReadAuthParams *data;
+  GError *error = NULL;
+
+  task = user_data;
+  data = g_task_get_task_data (task);
+  stream = soup_session_send_finish (SOUP_SESSION (object), result, &error);
+
+  if (stream == NULL) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  data->parser = json_parser_new ();
+  json_parser_load_from_stream_async (data->parser, stream, data->cancellable, on_auth_params_response_parsed, task);
+}
+
+static void
+on_send_sync_request (GObject *object,
+                      GAsyncResult *result,
+                      gpointer user_data)
+{
+  GInputStream *stream;
+  GTask *task;
+  GError *error = NULL;
+
+  task = user_data;
+  stream = soup_request_send_finish (SOUP_REQUEST (object), result, &error);
+
+  if (stream == NULL) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+gboolean
+iridium_standard_file_load_async (IridiumStandardFile *client,
+                                  GCancellable *cancellable,
+                                  GAsyncReadyCallback callback,
+                                  gpointer user_data)
+{
+  SoupURI *uri;
+  SoupMessage *msg;
+  SoupRequestHTTP *request;
+  SoupMessageHeaders *headers;
+  gchar *value;
+  GTask *task;
+  GError *error = NULL;
+
+  task = g_task_new (client, cancellable, callback, user_data);
+
+  uri = soup_uri_new_with_base (client->base_uri, "api/items/sync");
+  request = soup_session_request_http_uri (client->session, "POST", uri, &error);
+
+  if (request == NULL) {
+    g_task_return_error (task, error);
+    return FALSE;
+  }
+
+  msg = soup_request_http_get_message (request);
+  g_object_get (msg, "request-headers", &headers, NULL);
+  value = g_strdup_printf ("Bearer %s", client->token);
+  soup_message_headers_append (headers, "Authorization", value);
+
+  soup_request_send_async (SOUP_REQUEST (request), cancellable, on_send_sync_request, task);
+
+  g_object_unref (msg);
+  soup_uri_free (uri);
+
+  return TRUE;
+}
+
+gboolean
+iridium_standard_file_connect_async (IridiumStandardFile *client,
+                                     const gchar *server,
+                                     const gchar *email,
+                                     const gchar *password,
+                                     GCancellable *cancellable,
+                                     GAsyncReadyCallback callback,
+                                     gpointer user_data)
+{
+  SoupURI *uri;
+  SoupMessage *msg;
+  ReadAuthParams *data;
+  GTask *task;
+
+  if (client->base_uri)
+    soup_uri_free (client->base_uri);
+
+  client->base_uri = soup_uri_new (server);
+  client->email = g_strdup (email);
+
+  uri = soup_uri_new_with_base (client->base_uri, "api/auth/params");
+  soup_uri_set_query_from_fields (uri, "email", email, NULL);
+  msg = soup_message_new_from_uri ("GET", uri);
+
+  data = g_new0 (ReadAuthParams, 1);
+  data->client = client;
+  data->password = g_strdup (password);
+  data->cancellable = cancellable;
+
+  task = g_task_new (client, cancellable, callback, user_data);
+  g_task_set_task_data (task, data, (GDestroyNotify) read_auth_params_data_free);
+  soup_session_send_async (client->session, msg, NULL, on_send_auth_params_message, task);
+
+  g_object_unref (msg);
+  soup_uri_free (uri);
+  return TRUE;
+}
+
+static void
+iridium_standard_file_dispose (GObject *object)
+{
+  IridiumStandardFile *self;
+
+  self = IRIDIUM_STANDARD_FILE (object);
+  g_object_unref (self->session);
+  soup_uri_free (self->base_uri);
+  G_OBJECT_CLASS (iridium_standard_file_parent_class)->dispose (object);
+}
+
+static void
+iridium_standard_file_finalize (GObject *object)
+{
+  IridiumStandardFile *self;
+
+  self = IRIDIUM_STANDARD_FILE (object);
+  g_free (self->email);
+  g_free (self->token);
+  G_OBJECT_CLASS (iridium_standard_file_parent_class)->finalize (object);
+}
+
+static void
+iridium_standard_file_class_init (IridiumStandardFileClass *klass)
+{
+  GObjectClass *oclass;
+
+  oclass = G_OBJECT_CLASS (klass);
+  oclass->dispose = iridium_standard_file_dispose;
+  oclass->finalize = iridium_standard_file_finalize;
+}
+
+static void
+iridium_standard_file_init (IridiumStandardFile *self)
+{
+  self->session = soup_session_new ();
+  self->base_uri = NULL;
+  self->email = NULL;
+  self->token = NULL;
+}

+ 50 - 0
src/iridium-standard-file.h

@@ -0,0 +1,50 @@
+/* iridium-standard-file.h
+ *
+ * Copyright 2018 Matthias Vogelgesang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <libsecret/secret.h>
+#include "iridium-note.h"
+
+G_BEGIN_DECLS
+
+
+const SecretSchema *standard_file_get_schema (void) G_GNUC_CONST;
+
+#define STANDARD_FILE_SCHEMA standard_file_get_schema ()
+#define IRIDIUM_TYPE_STANDARD_FILE (iridium_standard_file_get_type())
+
+G_DECLARE_FINAL_TYPE (IridiumStandardFile, iridium_standard_file, IRIDIUM, STANDARD_FILE, GObject);
+
+IridiumStandardFile *iridium_standard_file_new              (void);
+gboolean             iridium_standard_file_connect_async    (IridiumStandardFile    *client,
+                                                             const gchar            *server,
+                                                             const gchar            *email,
+                                                             const gchar            *password,
+                                                             GCancellable           *cancellable,
+                                                             GAsyncReadyCallback     callback,
+                                                             gpointer                user_data);
+gboolean             iridium_standard_file_load_async       (IridiumStandardFile    *client,
+                                                             GCancellable           *cancellable,
+                                                             GAsyncReadyCallback     callback,
+                                                             gpointer                user_data);
+GList               *iridium_standard_file_load             (IridiumStandardFile    *client,
+                                                             const gchar            *filename);
+
+G_END_DECLS

+ 79 - 16
src/iridium-window.c

@@ -23,6 +23,7 @@
 #include "iridium-markdown.h"
 #include "iridium-note.h"
 #include "iridium-note-row.h"
+#include "iridium-standard-file.h"
 #include "iridium-tag-row.h"
 #include "iridium-window.h"
 
@@ -39,6 +40,7 @@ struct _IridiumWindow
   GtkSearchBar      *search_bar;
   GtkSearchEntry    *search_entry;
   GtkToggleButton   *toggle_html_view;
+  GtkRevealer       *notification_revealer;
   GtkRevealer       *html_view_revealer;
   WebKitWebView     *html_view;
 
@@ -46,6 +48,7 @@ struct _IridiumWindow
   GBinding          *content_binding;
 
   IridiumMarkdown   *markdown;
+  IridiumStandardFile *client;
 };
 
 G_DEFINE_TYPE (IridiumWindow, iridium_window, GTK_TYPE_APPLICATION_WINDOW)
@@ -141,6 +144,60 @@ note_visible (IridiumNoteRow *row, IridiumWindow *window)
   return TRUE;
 }
 
+static gint
+note_date_cmp (IridiumNoteRow *row1,
+               IridiumNoteRow *row2,
+               gpointer user_data)
+{
+  GDateTime *dt1;
+  GDateTime *dt2;
+
+  dt1 = iridium_note_get_last_modified (iridium_note_row_get_note (row1));
+  dt2 = iridium_note_get_last_modified (iridium_note_row_get_note (row2));
+
+  /* sort later before earlier notes */
+  return g_date_time_compare (dt2, dt1);
+}
+
+static void
+on_standard_file_connected (GObject *object,
+                            GAsyncResult *result,
+                            gpointer user_data)
+{
+  IridiumWindow *self;
+  GList *notes;
+  IridiumTag *tag_all;
+
+  self = IRIDIUM_WINDOW (user_data);
+  tag_all = iridium_tag_new ("All");
+
+  /* TODO: implement iridium_standard_file_connect_finish */
+  notes = iridium_standard_file_load (self->client, "sn.json");
+
+  for (GList *it = g_list_first (notes); it != NULL; it = g_list_next (it)) {
+    IridiumNote *note;
+
+    note = IRIDIUM_NOTE (it->data);
+    iridium_note_add_tag (note, tag_all);
+    gtk_list_box_insert (self->note_list, iridium_note_row_new (note), -1);
+  }
+
+  gtk_list_box_insert (self->tag_list, iridium_tag_row_new (tag_all), -1);
+  g_list_free (notes);
+
+  gtk_widget_show_all (GTK_WIDGET (self->note_list));
+  gtk_widget_show_all (GTK_WIDGET (self->tag_list));
+
+  gtk_revealer_set_reveal_child (self->notification_revealer, FALSE);
+}
+
+static void
+on_notification_close_clicked (IridiumWindow *self,
+                               GtkButton *button)
+{
+  gtk_revealer_set_reveal_child (self->notification_revealer, FALSE);
+}
+
 static void
 iridium_window_dispose (GObject *object)
 {
@@ -150,6 +207,7 @@ iridium_window_dispose (GObject *object)
   g_clear_object (&self->title_binding);
   g_clear_object (&self->content_binding);
   g_clear_object (&self->markdown);
+  g_clear_object (&self->client);
   G_OBJECT_CLASS (iridium_window_parent_class)->dispose (object);
 }
 
@@ -176,10 +234,12 @@ iridium_window_class_init (IridiumWindowClass *klass)
   gtk_widget_class_bind_template_child (widget_class, IridiumWindow, html_view);
   gtk_widget_class_bind_template_child (widget_class, IridiumWindow, html_view_revealer);
   gtk_widget_class_bind_template_child (widget_class, IridiumWindow, toggle_html_view);
+  gtk_widget_class_bind_template_child (widget_class, IridiumWindow, notification_revealer);
 
   gtk_widget_class_bind_template_callback (widget_class, on_tag_selected);
   gtk_widget_class_bind_template_callback (widget_class, on_note_selected);
   gtk_widget_class_bind_template_callback (widget_class, on_search_changed);
+  gtk_widget_class_bind_template_callback (widget_class, on_notification_close_clicked);
 }
 
 static void
@@ -188,9 +248,11 @@ iridium_window_init (IridiumWindow *self)
   GtkSourceLanguageManager *manager;
   GtkStyleProvider *provider;
   GtkSourceBuffer *buffer;
-  IridiumNote *notes[2];
-  IridiumTag *tags[2];
   GSimpleAction *action;
+  const gchar *server;
+  const gchar *email;
+  gchar *password;
+  GError *error = NULL;
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
@@ -207,16 +269,6 @@ iridium_window_init (IridiumWindow *self)
                           self->html_view_revealer, "reveal-child", G_BINDING_DEFAULT);
   gtk_search_bar_connect_entry (self->search_bar, GTK_ENTRY (self->search_entry));
 
-  notes[0] = iridium_note_new ("Hello", NULL);
-  notes[1] = iridium_note_new ("Todo", NULL);
-
-  tags[0] = iridium_tag_new ("todo");
-  tags[1] = iridium_tag_new ("home");
-
-  iridium_note_add_tag (notes[0], tags[1]);
-  iridium_note_add_tag (notes[1], tags[0]);
-  iridium_note_add_tag (notes[1], tags[1]);
-
   manager = gtk_source_language_manager_get_default ();
   buffer = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view)));
   gtk_source_buffer_set_language (buffer, gtk_source_language_manager_get_language (manager, "markdown"));
@@ -226,14 +278,25 @@ iridium_window_init (IridiumWindow *self)
   gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider), "/net/bloerg/Iridium/iridium.css");
   gtk_style_context_add_provider_for_screen (gtk_window_get_screen (GTK_WINDOW (self)), provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
 
-  gtk_list_box_insert (self->note_list, iridium_note_row_new (notes[0]), -1);
-  gtk_list_box_insert (self->note_list, iridium_note_row_new (notes[1]), -1);
+  self->client = iridium_standard_file_new ();
+  server = "https://sf.bloerg.net";
+  email = "matthias.vogelgesang@gmail.com";
 
-  gtk_list_box_insert (self->tag_list, iridium_tag_row_new (tags[0]), -1);
-  gtk_list_box_insert (self->tag_list, iridium_tag_row_new (tags[1]), -1);
+  /* TODO: add dialog */
+  password = secret_password_lookup_sync (STANDARD_FILE_SCHEMA, NULL, &error,
+      "email", email, "server", server, NULL);
+
+  gtk_revealer_set_reveal_child (self->notification_revealer, TRUE);
+
+  iridium_standard_file_connect_async (self->client, server, email, password, NULL,
+      on_standard_file_connected, self);
+
+  secret_password_free (password);
 
   gtk_list_box_set_filter_func (self->note_list, (GtkListBoxFilterFunc) note_visible, self, NULL);
 
+  gtk_list_box_set_sort_func (self->note_list, (GtkListBoxSortFunc) note_date_cmp, NULL, NULL);
+
   gtk_widget_show_all (GTK_WIDGET (self->tag_list));
   gtk_widget_show_all (GTK_WIDGET (self->note_list));
 }

+ 118 - 52
src/iridium-window.ui

@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <template class="IridiumWindow" parent="GtkApplicationWindow">
-    <property name="default-width">600</property>
-    <property name="default-height">300</property>
+    <property name="default-width">800</property>
+    <property name="default-height">600</property>
     <child type="titlebar">
       <object class="GtkHeaderBar" id="header_bar">
         <property name="visible">True</property>
@@ -26,21 +26,56 @@
       </object>
     </child>
     <child>
-      <object class="GtkBox">
+      <object class="GtkOverlay">
         <property name="visible">True</property>
-        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
-        <child>
-          <object class="GtkSearchBar" id="search_bar">
+        <child type="overlay">
+          <object class="GtkRevealer" id="notification_revealer">
             <property name="visible">True</property>
+            <property name="valign">start</property>
+            <property name="halign">center</property>
             <child>
-              <object class="GtkBox">
+              <object class="GtkFrame">
                 <property name="visible">True</property>
-                <property name="margin">6</property>
+                <style>
+                  <class name="app-notification"/>
+                </style>
                 <child>
-                  <object class="GtkSearchEntry" id="search_entry">
+                  <object class="GtkBox">
                     <property name="visible">True</property>
-                    <property name="hexpand">True</property>
-                    <signal name="search-changed" handler="on_search_changed" object="IridiumWindow"/>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="margin_end">30</property>
+                        <property name="label">Connecting to standard file server …</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="visible">True</property>
+                        <property name="relief">none</property>
+                        <property name="margin_end">6</property>
+                        <signal name="clicked" handler="on_notification_close_clicked" object="IridiumWindow"/>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">True</property>
+                            <property name="icon_name">window-close-symbolic</property>
+                            <style>
+                              <class name="image-button"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
                   </object>
                 </child>
               </object>
@@ -48,75 +83,106 @@
           </object>
         </child>
         <child>
-          <object class="GtkPaned" id="main_pane">
-            <property name="name">main-pane</property>
+          <object class="GtkBox">
             <property name="visible">True</property>
+            <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
             <child>
-              <object class="GtkBox">
+              <object class="GtkSearchBar" id="search_bar">
                 <property name="visible">True</property>
-                <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
-                <child>
-                  <object class="GtkListBox" id="tag_list">
-                    <property name="visible">True</property>
-                    <property name="expand">True</property>
-                    <signal name="row-selected" handler="on_tag_selected" object="IridiumWindow"/>
-                    <style>
-                      <class name="list-box"/>
-                    </style>
-                  </object>
-                </child>
                 <child>
-                  <object class="GtkListBox" id="note_list">
+                  <object class="GtkBox">
                     <property name="visible">True</property>
-                    <property name="expand">True</property>
-                    <signal name="row-selected" handler="on_note_selected" object="IridiumWindow"/>
-                    <style>
-                      <class name="list-box"/>
-                    </style>
+                    <property name="margin">6</property>
+                    <child>
+                      <object class="GtkSearchEntry" id="search_entry">
+                        <property name="visible">True</property>
+                        <property name="hexpand">True</property>
+                        <signal name="search-changed" handler="on_search_changed" object="IridiumWindow"/>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
             </child>
             <child>
-              <object class="GtkBox">
+              <object class="GtkPaned" id="main_pane">
+                <property name="name">main-pane</property>
                 <property name="visible">True</property>
-                <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
-                <child>
-                  <object class="GtkEntry" id="title_entry">
-                    <property name="name">title-entry</property>
-                    <property name="visible">True</property>
-                    <property name="placeholder-text">Add title …</property>
-                    <property name="margin-top">6</property>
-                    <property name="margin-left">6</property>
-                    <property name="margin-right">6</property>
-                    <property name="halign">GTK_ALIGN_FILL</property>
-                    <property name="hexpand">TRUE</property>
-                  </object>
-                </child>
                 <child>
                   <object class="GtkBox">
                     <property name="visible">True</property>
+                    <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+                    <child>
+                      <object class="GtkListBox" id="tag_list">
+                        <property name="visible">True</property>
+                        <property name="expand">True</property>
+                        <signal name="row-selected" handler="on_tag_selected" object="IridiumWindow"/>
+                        <style>
+                          <class name="list-box"/>
+                        </style>
+                      </object>
+                    </child>
                     <child>
                       <object class="GtkScrolledWindow">
                         <property name="visible">True</property>
-                        <property name="margin">12</property>
+                        <property name="hexpand">True</property>
+                        <property name="min-content-width">200</property>
                         <child>
-                          <object class="GtkSourceView" id="source_view">
-                            <property name="name">source-view</property>
+                          <object class="GtkListBox" id="note_list">
                             <property name="visible">True</property>
                             <property name="expand">True</property>
+                            <signal name="row-selected" handler="on_note_selected" object="IridiumWindow"/>
+                            <style>
+                              <class name="list-box"/>
+                            </style>
                           </object>
                         </child>
                       </object>
                     </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+                    <child>
+                      <object class="GtkEntry" id="title_entry">
+                        <property name="name">title-entry</property>
+                        <property name="visible">True</property>
+                        <property name="placeholder-text">Add title …</property>
+                        <property name="margin-top">6</property>
+                        <property name="margin-left">6</property>
+                        <property name="margin-right">6</property>
+                        <property name="halign">GTK_ALIGN_FILL</property>
+                        <property name="hexpand">TRUE</property>
+                      </object>
+                    </child>
                     <child>
-                      <object class="GtkRevealer" id="html_view_revealer">
+                      <object class="GtkBox">
                         <property name="visible">True</property>
-                        <property name="transition-type">GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT</property>
                         <child>
-                          <object class="WebKitWebView" id="html_view">
+                          <object class="GtkScrolledWindow">
                             <property name="visible">True</property>
-                            <property name="expand">True</property>
+                            <property name="margin">12</property>
+                            <child>
+                              <object class="GtkSourceView" id="source_view">
+                                <property name="name">source-view</property>
+                                <property name="visible">True</property>
+                                <property name="expand">True</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkRevealer" id="html_view_revealer">
+                            <property name="visible">True</property>
+                            <property name="transition-type">GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT</property>
+                            <child>
+                              <object class="WebKitWebView" id="html_view">
+                                <property name="visible">True</property>
+                                <property name="expand">True</property>
+                              </object>
+                            </child>
                           </object>
                         </child>
                       </object>

+ 5 - 0
src/meson.build

@@ -7,6 +7,7 @@ iridium_sources = [
   'iridium-note-row.c',
   'iridium-tag.c',
   'iridium-tag-row.c',
+  'iridium-standard-file.c',
   'iridium-storage.c',
   'iridium-markdown.c',
   'iridium-window.c',
@@ -16,6 +17,10 @@ iridium_deps = [
   dependency('gio-2.0', version: '>= 2.48'),
   dependency('gtk+-3.0', version: '>= 3.18'),
   dependency('gtksourceview-3.0', version: '>= 3.18'),
+  dependency('json-glib-1.0', version: '>= 1.1.2'),
+  dependency('libsecret-1', version: '>= 0.18.4'),
+  dependency('libsoup-2.4', version: '>= 2.52.2'),
+  dependency('nettle', version: '>= 3.2'),
   dependency('webkit2gtk-4.0', version: '>= 2.20.2'),
   declare_dependency(
     dependencies: cc.find_library('markdown'),