/* 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 . */ #include #include #include #include #include #include #include #include #include #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; gchar *password; } 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 (¶ms->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"); if (!g_strcmp0 (enc_item_key, "")) return NULL; 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 (JsonParser *parser, ReadAuthParams *params, GError **error) { JsonObject *object; const gchar *s; object = json_node_get_object (json_parser_get_root (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; } static void read_auth_params_data_free (ReadAuthParams *data) { secret_password_free (data->password); } static void on_signin_response_parsed (GObject *object, GAsyncResult *result, gpointer user_data) { GTask *task; ReadAuthParams *data; JsonParser *parser; JsonObject *root_object; GError *error = NULL; task = user_data; data = g_task_get_task_data (task); parser = JSON_PARSER (object); if (!json_parser_load_from_stream_finish (parser, result, &error)) { g_task_return_error (task, error); g_object_unref (task); return; } root_object = json_node_get_object (json_parser_get_root (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); g_object_unref (task); g_object_unref (parser); } static void on_send_signin_message (GObject *object, GAsyncResult *result, gpointer user_data) { GInputStream *stream; GTask *task; JsonParser *parser; 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; } parser = json_parser_new (); json_parser_load_from_stream_async (parser, stream, g_task_get_cancellable (task), on_signin_response_parsed, task); g_object_unref (stream); } static void on_auth_params_response_parsed (GObject *object, GAsyncResult *result, gpointer user_data) { GTask *task; JsonParser *parser; ReadAuthParams *data; SoupURI *uri; SoupRequestHTTP *request; SoupMessage *msg; gchar *password; gchar *body; GError *error = NULL; task = user_data; data = g_task_get_task_data (task); parser = JSON_PARSER (object); if (!json_parser_load_from_stream_finish (parser, result, &error)) { g_task_return_error (task, error); return; } if (!get_auth_params (parser, 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"); request = soup_session_request_http_uri (data->client->session, "POST", uri, &error); if (request == NULL) { g_task_return_error (task, error); return; } 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); msg = soup_request_http_get_message (request); soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, body, strlen (body)); soup_request_send_async (SOUP_REQUEST (request), g_task_get_cancellable (task), on_send_signin_message, task); g_free (password); soup_uri_free (uri); g_object_unref (msg); g_object_unref (parser); } static void on_send_auth_params_message (GObject *object, GAsyncResult *result, gpointer user_data) { GInputStream *stream; GTask *task; JsonParser *parser; 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; } parser = json_parser_new (); json_parser_load_from_stream_async (parser, stream, g_task_get_cancellable (task), on_auth_params_response_parsed, task); g_object_unref (object); g_object_unref (stream); } static void on_sync_response_parsed (GObject *object, GAsyncResult *result, gpointer user_data) { GTask *task; StandardFileAuthParams *auth_params; JsonParser *parser; JsonObject *root; JsonArray *array; GList *items = NULL; GError *error = NULL; task = G_TASK (user_data); auth_params = g_task_get_task_data (task); parser = JSON_PARSER (object); if (!json_parser_load_from_stream_finish (parser, result, &error)) { g_task_return_error (task, error); return; } root = json_node_get_object (json_parser_get_root (parser)); array = json_object_get_array_member (root, "retrieved_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, auth_params); if (content) { item = deserialize_item (data, content); if (item) items = g_list_append (items, item); } g_free (content); } g_task_return_pointer (task, items, NULL); g_object_unref (parser); } static void on_send_sync_request (GObject *object, GAsyncResult *result, gpointer user_data) { GInputStream *stream; GTask *task; JsonParser *parser; 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; } parser = json_parser_new (); json_parser_load_from_stream_async (parser, stream, g_task_get_cancellable (task), on_sync_response_parsed, task); g_object_unref (stream); } GList * iridium_standard_file_load_finish (IridiumStandardFile *client, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, client), FALSE); return g_task_propagate_pointer (G_TASK (result), error); } void iridium_standard_file_load_async (IridiumStandardFile *client, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { SoupURI *uri; SoupRequestHTTP *request; SoupMessage *msg; SoupMessageHeaders *headers; gchar *value; const gchar *body; GTask *task; GError *error = NULL; task = g_task_new (client, cancellable, callback, user_data); g_task_set_task_data (task, &client->auth_params, NULL); 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; } 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); body = "{\"items\": []}"; soup_message_set_request (msg, "application/json", SOUP_MEMORY_STATIC, body, strlen (body)); soup_request_send_async (SOUP_REQUEST (request), cancellable, on_send_sync_request, task); g_object_unref (msg); soup_uri_free (uri); } gboolean iridium_standard_file_connect_finish (IridiumStandardFile *client, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, client), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } void 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; SoupRequestHTTP *request; ReadAuthParams *data; GTask *task; GError *error = NULL; 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); task = g_task_new (client, cancellable, callback, user_data); request = soup_session_request_http_uri (client->session, "GET", uri, &error); if (request == NULL) { g_task_return_error (task, error); return; } data = g_new0 (ReadAuthParams, 1); data->client = client; data->password = g_strdup (password); g_task_set_task_data (task, data, (GDestroyNotify) read_auth_params_data_free); soup_request_send_async (SOUP_REQUEST (request), cancellable, on_send_auth_params_message, task); soup_uri_free (uri); } 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->base_uri = NULL; self->email = NULL; self->token = NULL; self->session = soup_session_new (); }