/* 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 ();
}