/**
 * Matrix end-to-end encryption support
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
 */

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <sqlite3.h>
#include "libmatrix.h"
#include "matrix-api.h"
#include "matrix-e2e.h"
#include "matrix-json.h"
#include "debug.h"

/* json-glib */
#include <json-glib/json-glib.h>

#include "connection.h"
#ifndef MATRIX_NO_E2E
#include "olm/olm.h"
#include <gcrypt.h>

struct _MatrixOlmSession;

struct _MatrixE2EData {
    OlmAccount *oa;
    gchar *device_id;
    gchar *curve25519_pubkey;
    gchar *ed25519_pubkey;
    sqlite3 *db;
    /* Mapping from MatrixHashKeyOlm to MatrixOlmSession */
    GHashTable *olm_session_hash;
};

#define PURPLE_CONV_E2E_STATE "e2e"

/* Hung off the Purple conversation with the PURPLE_CONV_E2E_STATE */
typedef struct _MatrixE2ERoomData {
    /* Mapping from _MatrixHashKeyInBoundMegOlm to OlmInboundGroupSession */
    GHashTable *megolm_sessions_inbound;
} MatrixE2ERoomData;

typedef struct _MatrixHashKeyOlm {
    gchar *sender_key;
    gchar *sender_id;
} MatrixHashKeyOlm;

typedef struct _MatrixOlmSession {
    gchar *sender_key;
    gchar *sender_id;
    OlmSession *session;
    sqlite3_int64 unique;
    struct _MatrixOlmSession *next;
} MatrixOlmSession;

typedef struct _MatrixHashKeyInBoundMegOlm {
    gchar *sender_key;
    gchar *sender_id;
    gchar *session_id;
    gchar *device_id;
} MatrixHashKeyInBoundMegOlm;

struct _MatrixMediaCryptInfo {
    guchar sha256[32];
    guchar aes_k[32];
    guchar aes_iv[16];
};

static void key_upload_callback(MatrixConnectionData *conn,
                                gpointer user_data,
                                struct _JsonNode *json_root,
                                const char *body,
                                size_t body_len, const char *content_type);

/* Really clear an area of memory */
static void clear_mem(volatile char *data, size_t len)
{
#ifdef __STDC_LIB_EXT1__
    /* Untested! */
    memset_s(data, len, '\0', len);
#else
    size_t index;
    for(index = 0;index < len; index ++)
    {
        data[index] = '\0';
    }
#endif
}

/* Returns a pointer to a freshly allocated buffer of 'n' bytes of random data.
 * If it fails it returns NULL.
 * TODO: There must be some portable function we can call to do this.
 */
static void *get_random(size_t n)
{
    FILE *urandom = fopen("/dev/urandom", "rb");
    if (!urandom) {
        return NULL;
    }
    void *buffer = g_malloc(n);
    if (fread(buffer, 1, n, urandom) != n) {
        g_free(buffer);
        buffer = NULL;
    }
    fclose(urandom);
    return buffer;
}

/* GHashFunc for two MatrixHasKeyOlm */
static gboolean olm_inbound_equality(gconstpointer a, gconstpointer b)
{
    const MatrixHashKeyOlm *hk_a;
    const MatrixHashKeyOlm *hk_b;
    hk_a = (const MatrixHashKeyOlm *)a;
    hk_b = (const MatrixHashKeyOlm *)b;

    return !strcmp(hk_a->sender_key, hk_b->sender_key) &&
           !strcmp(hk_a->sender_id, hk_b->sender_id);
}

/* GHashFunc for a _MatrixHashKeyOlm */
static guint olm_inbound_hash(gconstpointer a)
{
    const MatrixHashKeyOlm *hk;
    hk = (const MatrixHashKeyOlm *)a;
    return g_str_hash(hk->sender_key) +
           g_str_hash(hk->sender_id);
}

static void olm_hash_key_destroy(gpointer k)
{
    MatrixHashKeyOlm *ok = k;

    clear_mem(ok->sender_key, strlen(ok->sender_key));
    g_free(ok->sender_key);
    g_free(ok->sender_id);
    g_free(ok);
}

static void free_matrix_olm_session(MatrixOlmSession *msession,
                                    gboolean free_session)
{
    g_free(msession->sender_id);
    g_free(msession->sender_key);
    if (free_session) {
        olm_clear_session(msession->session);
        g_free(msession->session);
    }
}

static void olm_hash_value_destroy(gpointer v)
{
    MatrixOlmSession *os = v;

    while (os) {
        MatrixOlmSession *next = os->next;
        free_matrix_olm_session(os, TRUE);
        os = next;
    }
}

/* GEqualFunc for two MatrixHashKeyInBoundMegOlm */
static gboolean megolm_inbound_equality(gconstpointer a, gconstpointer b)
{
    const MatrixHashKeyInBoundMegOlm *hk_a;
    const MatrixHashKeyInBoundMegOlm *hk_b;
    hk_a = (const MatrixHashKeyInBoundMegOlm *)a;
    hk_b = (const MatrixHashKeyInBoundMegOlm *)b;

    return !strcmp(hk_a->sender_key, hk_b->sender_key) &&
           !strcmp(hk_a->sender_id, hk_b->sender_id) &&
           !strcmp(hk_a->session_id, hk_b->session_id) &&
           !strcmp(hk_a->device_id, hk_b->device_id);
}

/* GHashFunc for a _MatrixHashKeyInBoundMegOlm */
static guint megolm_inbound_hash(gconstpointer a)
{
    const MatrixHashKeyInBoundMegOlm *hk;
    hk = (const MatrixHashKeyInBoundMegOlm *)a;

    return g_str_hash(hk->sender_key) +
           g_str_hash(hk->session_id) +
           g_str_hash(hk->sender_id) +
           g_str_hash(hk->device_id);
}

static void megolm_inbound_key_destroy(gpointer v)
{
    MatrixHashKeyInBoundMegOlm *key = v;
    g_free(key->sender_key);
    g_free(key->session_id);
    g_free(key->sender_id);
    g_free(key->device_id);
}

static void megolm_inbound_value_destroy(gpointer v)
{
    OlmInboundGroupSession *oigs = v;

    olm_clear_inbound_group_session(oigs);
    g_free(oigs);
}

static MatrixE2ERoomData *get_e2e_room_data(PurpleConversation *conv)
{
    MatrixE2ERoomData *result;

    result = purple_conversation_get_data(conv, PURPLE_CONV_E2E_STATE);
    if (!result) {
        result = g_new0(MatrixE2ERoomData, 1);
        purple_conversation_set_data(conv, PURPLE_CONV_E2E_STATE, result);
    }

    return result;
}

static GHashTable *get_e2e_inbound_megolm_hash(PurpleConversation *conv)
{
    MatrixE2ERoomData *rd = get_e2e_room_data(conv);

    if (!rd->megolm_sessions_inbound) {
        rd->megolm_sessions_inbound = g_hash_table_new_full(megolm_inbound_hash,
                                                   megolm_inbound_equality,
                                                   megolm_inbound_key_destroy,
                                                   megolm_inbound_value_destroy);
    }

    return rd->megolm_sessions_inbound;
}

static OlmInboundGroupSession *get_inbound_megolm_session(
       PurpleConversation *conv,
        const gchar *sender_key, const gchar *sender_id,
        const gchar *session_id, const gchar *device_id)
{
    MatrixHashKeyInBoundMegOlm match;
    match.sender_key = (gchar *)sender_key;
    match.sender_id = (gchar *)sender_id;
    match.session_id = (gchar *)session_id;
    match.device_id = (gchar *)device_id;

    OlmInboundGroupSession *result =
       (OlmInboundGroupSession *)g_hash_table_lookup(
               get_e2e_inbound_megolm_hash(conv), &match);
    purple_debug_info("matrixprpl",  "%s: %s/%s/%s/%s: %p\n",
                      __func__, device_id, sender_id, sender_key, session_id,
                      result);
    return result;
}

static void store_inbound_megolm_session(PurpleConversation *conv,
        const gchar *sender_key, const gchar *sender_id,
        const gchar *session_id, const gchar *device_id,
        OlmInboundGroupSession *igs) {
    MatrixHashKeyInBoundMegOlm *key = g_new0(MatrixHashKeyInBoundMegOlm, 1);
    key->sender_key = g_strdup(sender_key);
    key->sender_id = g_strdup(sender_id);
    key->session_id = g_strdup(session_id);
    key->device_id = g_strdup(device_id);
    purple_debug_info("matrixprpl", "%s: %s/%s/%s/%s\n",
               __func__, device_id, sender_id, sender_key, session_id);
    g_hash_table_insert(get_e2e_inbound_megolm_hash(conv), key, igs);
}

/* Find if we already have an OlmSession for this sender/sender_key somewhere
 * that this body matches.
 */
static MatrixOlmSession *find_olm_session(MatrixConnectionData *conn,
                                    const char *sender_id, const char *sender_key,
                                    const char *body)
{
    gboolean have_sender = FALSE;
    MatrixOlmSession *cur_entry, *list_head, *hash_result;
    MatrixOlmSession *result = NULL;
    MatrixHashKeyOlm match;

    purple_debug_info("matrixprpl", "find_olm_session for %s/%s\n",
                       sender_id, sender_key);
    match.sender_key = (gchar *)sender_key;
    match.sender_id = (gchar *)sender_id;
    hash_result = (MatrixOlmSession *)g_hash_table_lookup(
                       conn->e2e->olm_session_hash, &match);
    cur_entry = hash_result;
    list_head = hash_result;

    while (cur_entry) {
        if (!strcmp(sender_id, cur_entry->sender_id) &&
            !strcmp(sender_key, cur_entry->sender_key)) {
            size_t ret;
            char *body_double = g_strdup(body);
            have_sender = TRUE;
            ret = olm_matches_inbound_session(cur_entry->session, body_double,
                                              strlen(body));
            g_free(body_double);
            if (ret == 1) {
                purple_debug_info("matrixprpl",
                                  "%s: Found matching session for %s/%s\n",
                                  __func__, sender_id, sender_key);
                return cur_entry;
            }
            if (ret == olm_error()) {
                purple_debug_warning("matrixprpl",
                        "%s: Error while checking session %p for "
                        "match with %s/%s: %s\n", __func__, cur_entry->session,
                        sender_id, sender_key,
                        olm_session_last_error(cur_entry->session));
            }
        }
        cur_entry = cur_entry->next;
    }

    if (!have_sender) {
        MatrixOlmSession **chain = &list_head;
        int ret;
        /* We have no entries at all for the sender+key, lets load all the
         * data from the db
         */
        const char *query = "SELECT session_pickle, rowid "
                            "FROM olmsessions "
                            "WHERE sender_name = ? AND "
                            "sender_key = ?";

        sqlite3_stmt *dbstmt = NULL;
        ret = sqlite3_prepare_v2(conn->e2e->db, query, -1, &dbstmt, NULL);
        if (ret != SQLITE_OK || !dbstmt) {
             purple_debug_warning("matrixprpl",
                       "%s: Failed to prep select %d '%s'\n",
                       __func__, ret, query);
            goto bad_sql;
        }
        ret = sqlite3_bind_text(dbstmt, 1, sender_id, -1, NULL);
        if (ret == SQLITE_OK) {
            ret = sqlite3_bind_text(dbstmt, 2, sender_key, -1, NULL);
        }
        if (ret != SQLITE_OK) {
            purple_debug_warning("matrixprpl", "%s: Failed to bind %d\n",
                               __func__, ret);
            goto bad_sql;
        }

        /* Find the end of the chain to get the pointer to update */
        for (cur_entry = list_head; cur_entry; cur_entry=cur_entry->next) {
            chain = &(cur_entry->next);
        }

        while (ret = sqlite3_step(dbstmt), ret == SQLITE_ROW) {
            const gchar *pickle = (gchar *)sqlite3_column_text(dbstmt, 0);
            gchar *dupe_pickle;
            if (!pickle) {
                purple_debug_warning("matrixprpl",
                        "%s: Empty pickle for %s/%s\n", __func__,
                        sender_id, sender_key);
                continue;
            };
            dupe_pickle = g_strdup(pickle);
            OlmSession *session = olm_session(g_malloc0(olm_session_size()));
            if (olm_unpickle_session(session, "!", 1, dupe_pickle,
                                     strlen(dupe_pickle)) == olm_error()) {
                purple_debug_warning("matrixprpl",
                                     "%s: Failed to unpickle %s for %s/%s\n",
                                     __func__, pickle, sender_id, sender_key);
                g_free(dupe_pickle);
                g_free(session);
                continue;
            }
            g_free(dupe_pickle);
            cur_entry = g_new0(MatrixOlmSession, 1);
            cur_entry->sender_id = g_strdup(sender_id);
            cur_entry->sender_key = g_strdup(sender_key);
            cur_entry->session = session;
            cur_entry->unique = sqlite3_column_int64(dbstmt, 1);
            *chain = cur_entry;

            if (!result) {
                char *body_double = g_strdup(body);
                /* But is this the session we're after ? */
                ret = olm_matches_inbound_session(session,
                                                  body_double, strlen(body));
                g_free(body_double);
                if (ret == 1) {
                    purple_debug_info("matrixprpl",
                               "%s: Found (loaded) session for %s/%s\n",
                               __func__, sender_id, sender_key);
                    result = cur_entry;
                    /* Carry on loading any other sessions */
                }
                if (ret == olm_error()) {
                    purple_debug_warning("matrixprpl",
                        "%s: Error while checking loaded session %p for "
                        "match with %s/%s: %s\n", __func__, session,
                        sender_id, sender_key,
                        olm_session_last_error(session));
                }
                if (!ret) {
                    purple_debug_warning("matrixprpl",
                            "%s: Loaded session (%" PRIx64
                            ") is not a match for %s/%s\n",
                            __func__, (int64_t)cur_entry->unique, sender_id,
                            sender_key);
                }
            }
        }
        if (ret != SQLITE_DONE) {
            purple_debug_warning("matrixprpl", "%s: db step failed %d\n",
                                 __func__, ret);
            goto bad_sql;
        }
bad_sql:
        sqlite3_finalize(dbstmt);
    }
    if (list_head && !hash_result) {
        MatrixHashKeyOlm *key = g_new0(MatrixHashKeyOlm, 1);
        key->sender_key = g_strdup(sender_key);
        key->sender_id = g_strdup(sender_id);
        /* We loaded entries where there were none before, set the hash */
        g_hash_table_insert(conn->e2e->olm_session_hash, key, list_head);
    }
    return result;
}

/* Save a new olm session into the database */
static MatrixOlmSession *store_olm_session(MatrixConnectionData *conn,
                                           OlmSession *session,
                                           const char *sender_id,
                                           const char *sender_key)
{
    MatrixOlmSession *cur_entry = g_new0(MatrixOlmSession, 1);
    size_t pickle_len = olm_pickle_session_length(session);
    gchar *pickle = g_malloc(pickle_len+1);
    sqlite3_stmt *dbstmt = NULL;

    pickle_len = olm_pickle_session(session, "!", 1, pickle, pickle_len);
    if (pickle_len == olm_error()) {
        purple_debug_warning("matrixprpl",
                             "%s: Failed to pickle session for %s/%s: %s\n",
                             __func__, sender_id, sender_key,
                             olm_session_last_error(session));
        goto err;
    }
    pickle[pickle_len] = '\0';

    cur_entry->sender_id = g_strdup(sender_id);
    cur_entry->sender_key = g_strdup(sender_key);
    cur_entry->session = session;

    const char *query = "INSERT into olmsessions "
                        "(sender_name, sender_key, session_pickle) "
                        "VALUES (?, ?, ?)";

    int ret = sqlite3_prepare_v2(conn->e2e->db, query, -1, &dbstmt, NULL);
    if (ret != SQLITE_OK || !dbstmt) {
        purple_debug_warning("matrixprpl",
                             "%s: Failed to prep insert %d '%s'\n",
                             __func__, ret, query);
        goto err;
    }
    ret = sqlite3_bind_text(dbstmt, 1, sender_id, -1, NULL);
    if (ret == SQLITE_OK) {
        ret = sqlite3_bind_text(dbstmt, 2, sender_key, -1, NULL);
    }
    if (ret == SQLITE_OK) {
        ret = sqlite3_bind_text(dbstmt, 3, pickle, -1, NULL);
    }
    if (ret != SQLITE_OK) {
        purple_debug_warning("matrixprpl",
                             "%s: Failed to bind %d\n", __func__, ret);
        goto err;
    }

    ret = sqlite3_step(dbstmt);
    if (ret != SQLITE_DONE) {
        purple_debug_warning("matrixprpl",
                             "%s: Insert failed %d (%s)\n", __func__,
                             ret, query);
        goto err;
    }
    sqlite3_finalize(dbstmt);
    cur_entry->unique = sqlite3_last_insert_rowid(conn->e2e->db);

    MatrixHashKeyOlm match;
    match.sender_key = (gchar *)sender_key;
    match.sender_id = (gchar *)sender_id;
    MatrixOlmSession *hash_result = (MatrixOlmSession *)g_hash_table_lookup(
                       conn->e2e->olm_session_hash, &match);
    if (hash_result) {
        /* If there's already an entry, insert it after the head */
        cur_entry->next = hash_result->next;
        hash_result->next = cur_entry;
    } else {
        /* No entry, we need to stuff it into the hash table */
        MatrixHashKeyOlm *key = g_new0(MatrixHashKeyOlm, 1);
        key->sender_key = g_strdup(sender_key);
        key->sender_id = g_strdup(sender_id);
        /* We loaded entries where there were none before, set the hash */
        g_hash_table_insert(conn->e2e->olm_session_hash, key, cur_entry);
    }

    return cur_entry;

err:
    g_free(pickle);
    free_matrix_olm_session(cur_entry, FALSE);
    return NULL;
}

/* Update an existing OLM session in the database */
static int update_olm_session(MatrixConnectionData *conn,
                              MatrixOlmSession *mos)
{
    size_t pickle_len = olm_pickle_session_length(mos->session);
    gchar *pickle = g_malloc(pickle_len+1);
    sqlite3_stmt *dbstmt = NULL;
    int ret = -1;

    pickle_len = olm_pickle_session(mos->session, "!", 1, pickle, pickle_len);
    if (pickle_len == olm_error()) {
        purple_debug_warning("matrixprpl",
                "%s: Failed to pickle session for %s/%s: %s\n",
                __func__, mos->sender_id, mos->sender_key,
                olm_session_last_error(mos->session));
        goto err;
    }
    pickle[pickle_len] = '\0';

    const char *query ="UPDATE olmsessions SET session_pickle=? "
                       "WHERE sender_name = ? AND sender_key = ? AND "
                       "ROWID = ?";
    ret = sqlite3_prepare_v2(conn->e2e->db, query, -1, &dbstmt, NULL);
    if (ret != SQLITE_OK || !dbstmt) {
        purple_debug_warning("matrixprpl",
                "%s: Failed to prep update %d '%s'\n",
                __func__, ret, query);
        ret = -1;
        goto err;
    }
    ret = sqlite3_bind_text(dbstmt, 1, pickle, -1, NULL);
    if (ret == SQLITE_OK) {
        ret = sqlite3_bind_text(dbstmt, 2, mos->sender_id, -1, NULL);
    }
    if (ret == SQLITE_OK) {
        ret = sqlite3_bind_text(dbstmt, 3, mos->sender_key, -1, NULL);
    }
    if (ret == SQLITE_OK) {
        ret = sqlite3_bind_int64(dbstmt, 4, mos->unique);
    }

    if (ret != SQLITE_OK) {
        purple_debug_warning("matrixprpl",
                             "%s: Failed to bind %d\n", __func__, ret);
        ret = -1;
        goto err;
    }

    ret = sqlite3_step(dbstmt);
    if (ret != SQLITE_DONE) {
        purple_debug_warning("matrixprpl",
                             "%s: Update failed %d (%s)\n", __func__,
                ret, query);
        ret = -1;
        goto err;

    }
    ret = 0;

err:
    sqlite3_finalize(dbstmt);
    g_free(pickle);

    return ret;
}

/* Sign the JsonObject with olm_account_sign and add it to the object
 * as a 'signatures' member of the top level object.
 * 0 on success
 */
int matrix_sign_json(MatrixConnectionData *conn, JsonObject *tosign)
{
    int ret = -1;
    OlmAccount *account = conn->e2e->oa;
    const gchar *device_id = conn->e2e->device_id;
    PurpleConnection *pc = conn->pc;
    GString *can_json = matrix_canonical_json(tosign);
    gchar *can_json_c = g_string_free(can_json, FALSE);
    size_t sig_length = olm_account_signature_length(account);
    gchar *sig = g_malloc0(sig_length+1);
    if (olm_account_sign(account, can_json_c, strlen(can_json_c),
            sig, sig_length)==olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(account));
        goto out;
    }

    /* We need to add a "signatures" member which is an object, with
     * a "user_id" member that is itself an object which has an "ed25519:$DEVICEID" member
     * that is the signature.
     */
    GString *alg_dev = g_string_new(NULL);
    g_string_printf(alg_dev, "ed25519:%s", device_id);
    gchar *alg_dev_c = g_string_free(alg_dev, FALSE);
    JsonObject *sig_dev = json_object_new();
    json_object_set_string_member(sig_dev, alg_dev_c, sig);
    JsonObject *sig_obj = json_object_new();
    json_object_set_object_member(sig_obj, conn->user_id, sig_dev);
    json_object_set_object_member(tosign, "signatures", sig_obj);
    
    g_free(alg_dev_c);
    ret = 0;
out:
    g_free(can_json_c);
    g_free(sig);

    return ret;
}

/* Store the current Olm account data into the Purple account data
 */
static int matrix_store_e2e_account(MatrixConnectionData *conn)
{
    PurpleConnection *pc = conn->pc;

    size_t pickle_len = olm_pickle_account_length(conn->e2e->oa);
    char *pickled_account = g_malloc0(pickle_len+1);

    /* TODO: Wth to use as the key? We've not got anything in purple to protect
     * it with? We could do with stuffing something into the system key ring
     */
    if (olm_pickle_account(conn->e2e->oa, "!", 1, pickled_account, pickle_len) ==
        olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(conn->e2e->oa));
        g_free(pickled_account);
        return -1;
    }

    /* Create a JSON string to store in our account data, we include
     * our device and server as sanity checks.
     * TODO: Should we defer this until we've sent it to the server?
     */
    JsonObject *settings_body = json_object_new();
    json_object_set_string_member(settings_body, "device_id", conn->e2e->device_id);
    json_object_set_string_member(settings_body, "server", conn->homeserver);
    json_object_set_string_member(settings_body, "pickle", pickled_account);
    g_free(pickled_account);

    JsonNode *settings_node = json_node_new(JSON_NODE_OBJECT);
    json_node_set_object(settings_node, settings_body);
    json_object_unref(settings_body);

    JsonGenerator *settings_generator = json_generator_new();
    json_generator_set_root(settings_generator, settings_node);
    gchar *settings_string = json_generator_to_data(settings_generator, NULL);
    g_object_unref(G_OBJECT(settings_generator));
    json_node_free(settings_node);
    purple_account_set_string(pc->account,
                    PRPL_ACCOUNT_OPT_OLM_ACCOUNT_KEYS, settings_string);
    g_free(settings_string);

    return 0;
}

/* Retrieve an Olm account from the Purple account data
 * Returns: 1 on success
 *          0 on no stored account
 *          -1 on error
 */
static int matrix_restore_e2e_account(MatrixConnectionData *conn)
{
    PurpleConnection *pc = conn->pc;
    gchar *pickled_account = NULL;
    const char *account_string =  purple_account_get_string(pc->account,
                    PRPL_ACCOUNT_OPT_OLM_ACCOUNT_KEYS, NULL);
    int ret = -1;
    if (!account_string || !*account_string) {
        return 0;
    }
    /* Deal with existing account string */
    JsonParser *json_parser = json_parser_new();
    const gchar *retrieved_device_id, *retrieved_hs, *retrieved_pickle;
    GError *err = NULL;
    if (!json_parser_load_from_data(json_parser,
            account_string, strlen(account_string),
            &err)) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Failed to parse stored account key");
        purple_debug_info("matrixprpl",
                "unable to parse account JSON: %s",
                err->message);
        g_error_free(err);
        g_object_unref(json_parser);
        ret = -1;
        goto out;

    }
    JsonNode *settings_node = json_parser_get_root(json_parser);
    JsonObject *settings_body = matrix_json_node_get_object(settings_node);
    retrieved_device_id = matrix_json_object_get_string_member(settings_body, "device_id");
    retrieved_hs = matrix_json_object_get_string_member(settings_body, "server");
    retrieved_pickle = matrix_json_object_get_string_member(settings_body, "pickle");
    if (!retrieved_device_id || !retrieved_hs || !retrieved_pickle) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Unable to retrieve part of the stored account key");
        g_object_unref(json_parser);

        ret = -1;
        goto out;
    }
    if (strcmp(retrieved_device_id, conn->e2e->device_id) ||
        strcmp(retrieved_hs, conn->homeserver)) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Device ID/HS doesn't matched for stored account key");
        g_object_unref(json_parser);

        ret = -1;
        goto out;
    }
    pickled_account = g_strdup(retrieved_pickle);
    if (olm_unpickle_account(conn->e2e->oa, "!", 1, pickled_account, strlen(retrieved_pickle)) ==
        olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(conn->e2e->oa));
        g_object_unref(json_parser);
        ret = -1;
        goto out;
    }
    g_object_unref(json_parser);
    purple_debug_info("matrixprpl", "Succesfully unpickled account\n");
    ret = 1;

out:
    g_free(pickled_account);
    return ret;
}

/* Returns the list of algorithms and our keys for those algorithms on the current account */
static int get_id_keys(PurpleConnection *pc, OlmAccount *account, gchar ***algorithms, gchar ***keys)
{
    /* There has to be an easier way than this.... */
    size_t id_key_len = olm_account_identity_keys_length(account);
    gchar *id_keys = g_malloc0(id_key_len+1);
    if (olm_account_identity_keys(account, id_keys, id_key_len) ==
        olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(account));
        g_free(id_keys);
        return -1;
    }

    /* We get back a json string, something like:
     * {"curve25519":"encodedkey...","ed25519":"encodedkey...."}'
     */
    JsonParser *json_parser = json_parser_new();
    GError *err = NULL;
    if (!json_parser_load_from_data(json_parser,
            id_keys, strlen(id_keys), &err)) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Failed to parse olm account ID keys");
        purple_debug_info("matrixprpl",
                "unable to parse olm account ID keys: %s",
                err->message);
        g_error_free(err);
        g_free(id_keys);
        g_object_unref(json_parser);
        return -1;
    }
    /* We have one object with a series of string members where
     * each member is named after the algorithm.
     */
    JsonNode *id_node = json_parser_get_root(json_parser);
    JsonObject *id_body = matrix_json_node_get_object(id_node);
    guint n_keys = json_object_get_size(id_body);
    *algorithms = g_new(gchar *, n_keys);
    *keys = g_new(gchar *, n_keys);
    JsonObjectIter iter;
    const gchar *key_algo;
    JsonNode *key_node;
    guint i = 0;
    json_object_iter_init(&iter, id_body);
    while (json_object_iter_next(&iter, &key_algo, &key_node)) {
        (*algorithms)[i] = g_strdup(key_algo);
        (*keys)[i] = g_strdup(matrix_json_object_get_string_member(id_body, key_algo));
        i++;
    }

    g_free(id_keys);
    g_object_unref(json_parser);

    return n_keys;
}

/* See: https://matrix.org/docs/guides/e2e_implementation.html#creating-and-registering-one-time-keys */
static int send_one_time_keys(MatrixConnectionData *conn, size_t n_keys)
{
    PurpleConnection *pc = conn->pc;
    int ret;
    size_t random_needed;
    void *random_buffer;
    void *olm_1t_keys_json = NULL;
    JsonParser *json_parser = NULL;
    size_t olm_keys_buffer_size;
    JsonObject *otk_json = NULL;
    random_needed = olm_account_generate_one_time_keys_random_length(
                       conn->e2e->oa, n_keys);
    random_buffer = get_random(random_needed);
    if (!random_buffer) {
        return -1;
    }

    if (olm_account_generate_one_time_keys(conn->e2e->oa, n_keys, random_buffer,
                                               random_needed) == olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(conn->e2e->oa));
        ret = -1;
        goto out;
    }

    olm_keys_buffer_size = olm_account_one_time_keys_length(conn->e2e->oa);
    olm_1t_keys_json = g_malloc0(olm_keys_buffer_size+1);
    if (olm_account_one_time_keys(conn->e2e->oa, olm_1t_keys_json,
                                       olm_keys_buffer_size) == olm_error()) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                olm_account_last_error(conn->e2e->oa));
        ret = -1;
        goto out;
    }

    /* olm_1t_keys_json has json like:
     *   {
     *     curve25519: {
     *       "keyid1": "base64encodedcurve25519key1",
     *       "keyid2": "base64encodedcurve25519key2"
     *     }
     *   }
     *   I think in practice this is just curve25519 but I'll avoid hard coding
     *   We need to produce an object with a set of signed objects each having
     *   one key
     */
    json_parser = json_parser_new();
    GError *err = NULL;
    if (!json_parser_load_from_data(json_parser,
          olm_1t_keys_json, strlen(olm_1t_keys_json), &err)) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
            "Failed to parse generated 1-time json");
        g_error_free(err);
        ret = -1;
        goto out;
    }

    /* The output JSON we're generating */
    otk_json = json_object_new();

    JsonNode *olm_1tk_root = json_parser_get_root(json_parser);
    JsonObject *olm_1tk_obj = matrix_json_node_get_object(olm_1tk_root);
    JsonObjectIter algo_iter;
    json_object_iter_init(&algo_iter, olm_1tk_obj);
    const gchar *keys_algo;
    JsonNode *keys_node;
    while (json_object_iter_next(&algo_iter, &keys_algo, &keys_node)) {
        /* We're expecting keys_algo to be "curve25519" and keys_node to be an
         * object with a set of keys.
         */
        JsonObjectIter keys_iter;
        JsonObject *keys_obj = matrix_json_node_get_object(keys_node);
        json_object_iter_init(&keys_iter, keys_obj);
        const gchar *key_id;
        JsonNode *key_node;
        while (json_object_iter_next(&keys_iter, &key_id, &key_node)) {
            const gchar *key_string = matrix_json_node_get_string(key_node);

            JsonObject *signed_key = json_object_new();
            json_object_set_string_member(signed_key, "key", key_string);
            ret = matrix_sign_json(conn, signed_key);
            if (ret) {
                g_object_unref(signed_key);
                goto out;
            }
            gchar *signed_key_name = g_strdup_printf("signed_%s:%s", keys_algo,
                                                       key_id);
            json_object_set_object_member(otk_json,
                                               signed_key_name, signed_key);
            g_free(signed_key_name);
        }
    }

    matrix_api_upload_keys(conn, NULL, otk_json,
        key_upload_callback,
        matrix_api_error, matrix_api_bad_response, (void *)1);
    otk_json = NULL; /* matrix_api_upload_keys frees with its json */
    ret = 0;
out:
    g_object_unref(json_parser);
    if (otk_json)
        g_object_unref(otk_json);
    g_free(random_buffer);
    g_free(olm_1t_keys_json);

    return ret;
}

/* Called from sync with an object of the form:
 *           "device_one_time_keys_count" : {
 *               "signed_curve25519" : 100
 *           },
 */
void matrix_e2e_handle_sync_key_counts(PurpleConnection *pc, JsonObject *count_object,
                                       gboolean force_send)
{
    gboolean need_to_send = force_send;
    gboolean valid_counts = FALSE;
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);
    size_t max_keys = olm_account_max_number_of_one_time_keys(conn->e2e->oa);
    size_t to_create = max_keys;

    if (!force_send) {
        JsonObjectIter iter;
        const gchar *key_algo;
        JsonNode *key_count_node;
        json_object_iter_init(&iter, count_object);
        while (json_object_iter_next(&iter, &key_algo, &key_count_node)) {
            valid_counts = TRUE;
            gint64 count = matrix_json_node_get_int(key_count_node);
            if (count < max_keys / 2) {
                to_create = (max_keys / 2) - count;
                need_to_send = TRUE;
            }
            purple_debug_info("matrixprpl", "%s: %s: %ld\n",
                           __func__, key_algo, count);
        }
    }

    need_to_send |= !valid_counts;
    if (need_to_send) {
        purple_debug_info("matrixprpl", "%s: need to send\n",__func__);
        send_one_time_keys(conn, to_create);
    }
}

/* Called back when we've successfully uploaded the device keys
 * we use 'user_data' = 1 to indicate we did an upload of one time
 * keys.
 */
static void key_upload_callback(MatrixConnectionData *conn,
                                gpointer user_data,
                                struct _JsonNode *json_root,
                                const char *body,
                                size_t body_len, const char *content_type)
{
    /* The server responds with a count of the one time keys on the server */
    JsonObject *top_object = matrix_json_node_get_object(json_root);
    JsonObject *key_counts = matrix_json_object_get_object_member(top_object,
                                       "one_time_key_counts");

    purple_debug_info("matrixprpl",
                      "%s: json_root=%p top_object=%p key_counts=%p\n",
                      __func__, json_root, top_object, key_counts);
    /* True if it's a response to a key upload */
    if (user_data) {
        /* Tell Olm that these one time keys are uploaded */
        olm_account_mark_keys_as_published(conn->e2e->oa);
        matrix_store_e2e_account(conn);
    }

    matrix_e2e_handle_sync_key_counts(conn->pc, key_counts, !key_counts);
}

static void close_e2e_db(MatrixConnectionData *conn)
{
    sqlite3_close(conn->e2e->db);
    conn->e2e->db = NULL;
}

/* 'check' and 'create' are SQL statements; call check, if it returns no result
 * then run 'create'.
 * typically for checking for the existence of a table and creating it if it didn't
 * exist.
 */
static int ensure_table(MatrixConnectionData *conn, const char *check, const char *create)
{
    PurpleConnection *pc = conn->pc;
    int ret;
    sqlite3_stmt *dbstmt;
    ret = sqlite3_prepare_v2(conn->e2e->db, check, -1, &dbstmt, NULL);
    if (ret != SQLITE_OK || !dbstmt) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
            "Failed to check e2e db table list (prep)");
        return -1;
    }
    ret = sqlite3_step(dbstmt);
    sqlite3_finalize(dbstmt);
    purple_debug_info("matrixprpl", "%s:db table query %d\n", __func__, ret);
    if (ret == SQLITE_ROW) {
        /* Already exists */
        return 0;
    }
    ret = sqlite3_prepare_v2(conn->e2e->db, create, -1, &dbstmt, NULL);
    if (ret != SQLITE_OK || !dbstmt) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
            "Failed to create e2e db table (prep)");
        return -1;
    }
    ret = sqlite3_step(dbstmt);
    sqlite3_finalize(dbstmt);
    if (ret != SQLITE_DONE) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
            "Failed to create e2e db table (step)");
        return -1;
    }

    return 0;
}
static int open_e2e_db(MatrixConnectionData *conn)
{
    PurpleConnection *pc = conn->pc;
    int ret;
    const char *purple_username = 
               purple_account_get_username(purple_connection_get_account(pc));
    char *cfilename = g_strdup_printf("matrix-%s-%s.db", conn->user_id,
                                       purple_username);
    const char *escaped_filename = purple_escape_filename(cfilename);
    g_free(cfilename);
    char *full_path = g_strdup_printf("%s/%s", purple_user_dir(),
                                               escaped_filename);
    ret = sqlite3_open(full_path, &conn->e2e->db);
    purple_debug_info("matrixprpl", "Opened e2e db at %s %d\n", full_path, ret);
    g_free(full_path);
    if (ret) {
        purple_connection_error_reason(pc,
            PURPLE_CONNECTION_ERROR_OTHER_ERROR,
            "Failed to open e2e db");
        return ret;
    }

    ret = ensure_table(conn,
                 "SELECT name FROM sqlite_master WHERE type='table' AND name='olmsessions'",
                 "CREATE TABLE olmsessions (sender_name text, sender_key text,"
                 "                          session_pickle text,"
                 "                          PRIMARY KEY (sender_name, sender_key))");

    if (ret) {
        close_e2e_db(conn);
        return ret;
    }

    return 0;
}

/*
 * Get a set of device keys for ourselves.  Either by retreiving it from our store
 * or by generating a new set.
 *
 * Returns: 0 on success
 */
int matrix_e2e_get_device_keys(MatrixConnectionData *conn, const gchar *device_id)
{
    PurpleConnection *pc = conn->pc;
    JsonObject * json_dev_keys = NULL;
    OlmAccount *account = olm_account(g_malloc0(olm_account_size()));
    char *pickled_account = NULL;
    void *random_pot = NULL;
    int ret = 0;

    if (!conn->e2e) {
        conn->e2e = g_new0(MatrixE2EData,1);
        conn->e2e->device_id = g_strdup(device_id);
        conn->e2e->olm_session_hash = g_hash_table_new_full(olm_inbound_hash,
                                                       olm_inbound_equality,
                                                       olm_hash_key_destroy,
                                                       olm_hash_value_destroy);
    }
    conn->e2e->oa = account;

    /* Try and restore olm account from settings; may fail, may work
     * or may say there were no settings stored.
     */
    ret = matrix_restore_e2e_account(conn);
    purple_debug_info("matrixprpl",
                      "restore_e2e_account says %d\n", ret);
    if (ret < 0) {
        goto out;
    }

    if (ret == 0) {
        /* No stored account - create one */
        size_t needed_random = olm_create_account_random_length(account);
        random_pot = get_random(needed_random);
        if (!random_pot) {
            purple_connection_error_reason(pc,
                    PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                    "Unable to get randomness");
            ret = -1;
            goto out;
        };

        if (olm_create_account(account, random_pot, needed_random) ==
            olm_error()) {
            purple_connection_error_reason(pc,
                    PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                    olm_account_last_error(account));
            ret = -1;
            goto out;
        }
        ret = matrix_store_e2e_account(conn);
        if (ret) {
            goto out;
        }
    }

    /* Open the e2e db - an sqlite db held for the account */
    ret = open_e2e_db(conn);
    if (ret) {
        goto out;
    }

    /* Form a device keys object for an upload,
     * from https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#post-matrix-client-unstable-keys-upload
     */
    json_dev_keys = json_object_new();
    json_object_set_string_member(json_dev_keys, "user_id", conn->user_id);
    json_object_set_string_member(json_dev_keys, "device_id", device_id);
    /* Add 'algorithms' array - is there a way to get libolm to tell us the list of what's supported */
    /* the output of olm_account_identity_keys isn't quite right for it */
    JsonArray *algorithms = json_array_new();
    json_array_add_string_element(algorithms, "m.olm.curve25519-aes-sha256");
    json_array_add_string_element(algorithms, "m.megolm.v1.aes-sha");
    json_object_set_array_member(json_dev_keys, "algorithms", algorithms);

    /* Add 'keys' entry */
    JsonObject *json_keys = json_object_new();
    gchar **algorithm_strings, **key_strings;
    int num_algorithms = get_id_keys(pc, account, &algorithm_strings,
                                     &key_strings);
    if (num_algorithms < 1) {
        json_object_unref(json_keys);
        goto out;
    }

    int alg;

    for(alg = 0; alg < num_algorithms; alg++) {
        GString *algdev = g_string_new(NULL);
        g_string_printf(algdev, "%s:%s", algorithm_strings[alg], device_id);
        gchar *alg_dev_char = g_string_free(algdev, FALSE);
        json_object_set_string_member(json_keys, alg_dev_char,
                                      key_strings[alg]);

        if (!strcmp(algorithm_strings[alg], "curve25519")) {
            conn->e2e->curve25519_pubkey = key_strings[alg];
        } else if (!strcmp(algorithm_strings[alg], "ed25519")) {
            conn->e2e->ed25519_pubkey = key_strings[alg];
        } else {
            g_free(key_strings[alg]);
        }
        g_free(algorithm_strings[alg]);
        g_free(alg_dev_char);
    }
    g_free(algorithm_strings);
    g_free(key_strings);
    json_object_set_object_member(json_dev_keys, "keys", json_keys);

    /* Sign */
    if (matrix_sign_json(conn, json_dev_keys)) {
        goto out;
    }

    /* Send the keys */
    matrix_api_upload_keys(conn, json_dev_keys, NULL /* TODO: one time keys */,
        key_upload_callback,
        matrix_api_error, matrix_api_bad_response, (void *)0);
    json_dev_keys = NULL; /* api_upload_keys frees it with it's whole json */

    ret = 0;

out:
    if (json_dev_keys)
         json_object_unref(json_dev_keys);
    g_free(pickled_account);
    g_free(random_pot);

    if (ret) {
        matrix_e2e_cleanup_connection(conn);
    }
    return ret;
}

void matrix_e2e_cleanup_conversation(PurpleConversation *conv)
{
    MatrixE2ERoomData *result = purple_conversation_get_data(conv,
                                                     PURPLE_CONV_E2E_STATE);
    if (result) {
        g_hash_table_destroy(result->megolm_sessions_inbound);
        g_free(result);
        purple_conversation_set_data(conv, PURPLE_CONV_E2E_STATE, NULL);
    }
}

void matrix_e2e_cleanup_connection(MatrixConnectionData *conn)
{
    GList *ptr;
    for(ptr = purple_get_conversations(); ptr != NULL; ptr = g_list_next(ptr))
    {
        PurpleConversation *conv = ptr->data;
        matrix_e2e_cleanup_conversation(conv);
    }
    if (conn->e2e) {
        close_e2e_db(conn);
        g_hash_table_destroy(conn->e2e->olm_session_hash);
        g_free(conn->e2e->curve25519_pubkey);
        g_free(conn->e2e->oa);
        g_free(conn->e2e->device_id);
        g_free(conn->e2e);
        conn->e2e = NULL;
    }
}

/* See: https://matrix.org/docs/guides/e2e_implementation.html#handling-an-m-room-key-event
 */
static int handle_m_room_key(PurpleConnection *pc, MatrixConnectionData *conn,
                             const gchar *sender, const gchar *sender_key, const gchar *sender_device,
                             JsonObject *mrk)
{
    int ret = 0;
    purple_debug_info("matrixprpl", "%s\n", __func__);
    JsonObject *mrk_content;
    const gchar *mrk_algo;
    PurpleConversation *conv;
    mrk_content = matrix_json_object_get_object_member(mrk, "content");
    mrk_algo = matrix_json_object_get_string_member(mrk_content, "algorithm");
    OlmInboundGroupSession *in_mo_session = NULL;

    if (!mrk_algo || strcmp(mrk_algo, "m.megolm.v1.aes-sha2")) {
        purple_debug_info("matrixprpl", "%s: Not megolm (%s)\n",
                               __func__, mrk_algo);
        ret = -1;
        goto out;
    }

    const gchar *mrk_room_id, *mrk_session_id, *mrk_session_key;
    mrk_room_id = matrix_json_object_get_string_member(mrk_content, "room_id");
    mrk_session_id = matrix_json_object_get_string_member(mrk_content,
                                                               "session_id");
    mrk_session_key = matrix_json_object_get_string_member(mrk_content,
                                                               "session_key");

    conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
                                                 mrk_room_id, pc->account);
    if (!conv) {
        purple_debug_info("matrixprpl", "%s: Unknown room %s\n",
                          __func__, mrk_room_id);
        ret = -1;
        goto out;
    }

    /* Search for an existing session with the matching room_id,
     * sender_key, session_id */
    in_mo_session = get_inbound_megolm_session(conv, sender_key,
                                               sender, mrk_session_id,
                                               sender_device);
      
    if (!in_mo_session) {
        /* Bah, no match, lets make one */
        in_mo_session =  olm_inbound_group_session(g_malloc(
                               olm_inbound_group_session_size()));
        if (olm_init_inbound_group_session(in_mo_session,
                                       (const uint8_t *)mrk_session_key,
                                       strlen(mrk_session_key)) ==
                olm_error()) {
            purple_debug_info("matrixprpl",
                       "%s: megolm inbound session creation failed: %s\n",
                       __func__,
                       olm_inbound_group_session_last_error(in_mo_session));
            ret = -1;
            goto out;
        }

        store_inbound_megolm_session(conv, sender_key, sender,
                                     mrk_session_id, sender_device,
                                     in_mo_session);
    }

out:
    if (ret) {
        if (in_mo_session) {
            olm_clear_inbound_group_session(in_mo_session);
        }
        g_free(in_mo_session);
    }
    return ret;
}

/* Called from decypt_olm after we've decrypted an olm message.
 */
static int handle_decrypted_olm(PurpleConnection *pc,
                                MatrixConnectionData *conn,
                                const gchar *sender,
                                const gchar *sender_key, gchar *plaintext)
{
    JsonParser *json_parser = json_parser_new();
    GError *err = NULL;
    int ret = 0;

    purple_debug_info("matrixprpl", "%s: %s\n", __func__, plaintext);
    if (!json_parser_load_from_data(json_parser, plaintext, strlen(plaintext),
                                       &err)) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Failed to parse decrypted olm JSON");
        purple_debug_info("matrixprpl",
                "unable to parse decrypted olm JSON: %s",
                err->message);
        g_error_free(err);
        ret = -1;
        goto out;

    }
    JsonNode *pt_node = json_parser_get_root(json_parser);
    JsonObject *pt_body = matrix_json_node_get_object(pt_node);

    /* The spec says we need to check these actually match */
    const gchar *pt_sender, *pt_sender_device, *pt_recipient, *pt_recipient_ed;
    const gchar *pt_type;
    pt_sender = matrix_json_object_get_string_member(pt_body, "sender");
    pt_sender_device = matrix_json_object_get_string_member(pt_body,
                                                            "sender_device");
    pt_recipient = matrix_json_object_get_string_member(pt_body, "recipient");
    JsonObject *pt_recipient_keys =
               matrix_json_object_get_object_member(pt_body, "recipient_keys");
    pt_recipient_ed = matrix_json_object_get_string_member(pt_recipient_keys,
                                                           "ed25519");
    pt_type = matrix_json_object_get_string_member(pt_body, "type");

    if (!pt_sender || !pt_sender_device || !pt_recipient ||
        !pt_recipient_ed || !pt_type) {
        purple_debug_info("matrixprpl",
                          "%s: Missing field\n", __func__);
        ret = -1;
        goto out;
    }
    if (strcmp(sender, pt_sender)) {
        purple_debug_info("matrixprpl",
           "%s: Mismatch on sender '%s' vs '%s'\n",
           __func__, sender, pt_sender);
        ret = -1;
        goto out;
    }
    if (strcmp(conn->user_id, pt_recipient)) {
        purple_debug_info("matrixprpl",
                          "%s: Mismatch on recipient '%s' vs '%s'\n",
                          __func__, conn->user_id, pt_recipient);
        ret = -1;
        goto out;
    }
    if (strcmp(conn->e2e->ed25519_pubkey, pt_recipient_ed)) {
        purple_debug_info("matrixprpl",
           "%s: Mismatch on recipient key '%s' vs '%s' pt_recipient_keys=%p\n",
           __func__, conn->e2e->ed25519_pubkey, pt_recipient_ed,
           pt_recipient_keys);
        ret = -1;
        goto out;
    }

    /* TODO: check the device against the keys in use, stash somewhere? */
    if (!strcmp(pt_type, "m.room_key")) {
        ret = handle_m_room_key(pc, conn, pt_sender, sender_key,
                                pt_sender_device, pt_body);
    } else {
        purple_debug_info("matrixprpl",
                          "%s: Got '%s' from '%s'/'%s'\n",
                          __func__, pt_type, pt_sender_device, pt_sender);
    }
out:
    g_object_unref(json_parser);
    return ret;
}

/*
 * See:
 * https://matrix.org/docs/guides/e2e_implementation.html#m-olm-v1-curve25519-aes-sha2
 * TODO: All the error paths in this function need to clean up!
 */
static void decrypt_olm(PurpleConnection *pc, MatrixConnectionData *conn, JsonObject *cevent,
                                   JsonObject *cevent_content)
{
    const gchar *cevent_sender;
    const gchar *sender_key;
    JsonObject *cevent_ciphertext;
    gchar *cevent_body_copy = NULL;
    gchar *plaintext = NULL;
    size_t max_plaintext_len = 0;
    cevent_sender = matrix_json_object_get_string_member(cevent, "sender");
    sender_key = matrix_json_object_get_string_member(cevent_content,
                                                       "sender_key");
    cevent_ciphertext = matrix_json_object_get_object_member(cevent_content,
                                                               "ciphertext");
    /* TODO: Look up sender_key - I think we need to check this against device
     * list from user? */
    MatrixOlmSession *mos = NULL;
    OlmSession *session = NULL;

    if (!cevent_ciphertext || !sender_key) {
        purple_debug_info("matrixprpl",
                               "%s: no ciphertext or sender_key in olm event\n",
                               __func__);
        goto err;
    }
    JsonObject *our_ciphertext;
    our_ciphertext = matrix_json_object_get_object_member(cevent_ciphertext,
                                               conn->e2e->curve25519_pubkey);
    if (!our_ciphertext) {
        purple_debug_info("matrixprpl",
                          "%s: No ciphertext with our curve25519 pubkey\n",
                          __func__);
        goto err;
    }
    JsonNode *type_node = matrix_json_object_get_member(our_ciphertext, "type");
    if (!type_node) {
        purple_debug_info("matrixprpl", "%s: No type node\n", __func__);
        goto err;
    }

    gint64 type = matrix_json_node_get_int(type_node);
    purple_debug_info("matrixprpl",
                      "%s: Type %zd olm encrypted message from %s\n",
                      __func__, (size_t)type, cevent_sender);
    if (!type) {
        /* A 'prekey' message to establish an Olm session */
        const gchar *cevent_body;
        cevent_body = matrix_json_object_get_string_member(our_ciphertext,
                                                           "body");
        mos = find_olm_session(conn, cevent_sender, sender_key, cevent_body);
        if (!mos) {
            cevent_body_copy = g_strdup(cevent_body);
            /* OK, no existing session, lets create one */
            session = olm_session(g_malloc0(olm_session_size()));

            if (olm_create_inbound_session_from(session, conn->e2e->oa,
                                               sender_key, strlen(sender_key),
                                               cevent_body_copy,
                                               strlen(cevent_body)) ==
                olm_error()) {
                purple_debug_info("matrixprpl",
                    "%s: olm prekey inbound_session_from failed with %s\n",
                    __func__, olm_session_last_error(session));
                g_free(session);
                goto err;
            }
            if (olm_remove_one_time_keys(conn->e2e->oa, session) ==
                olm_error()) {
                purple_debug_info("matrixprpl",
                    "%s: Failed to remove 1tk from inbound session "
                    " creation: %s\n",
                    __func__, olm_account_last_error(conn->e2e->oa));
                g_free(session);
                goto err;
            }
            mos = store_olm_session(conn, session, cevent_sender, sender_key);
            if (!mos) {
                g_free(session);
                goto err;
            }
            if (matrix_store_e2e_account(conn)) {
                g_free(session);
                goto err;
            }
        }
        session = mos->session;
        cevent_body_copy = g_strdup(cevent_body);
        max_plaintext_len = olm_decrypt_max_plaintext_length(session,
                                       0 /* Prekey */,
                                       cevent_body_copy,
                                       strlen(cevent_body_copy));
        if (max_plaintext_len == olm_error()) {
            purple_debug_info("matrixprpl",
                              "%s: Failed to get plaintext length %s\n",
                              __func__, olm_session_last_error(session));
            goto err;
        }
        plaintext = g_malloc0(max_plaintext_len + 1);
        cevent_body_copy = g_strdup(cevent_body);

        size_t pt_len = olm_decrypt(session, 0 /* Prekey */, cevent_body_copy,
                                       strlen(cevent_body),
                                       plaintext, max_plaintext_len);
        if (pt_len == olm_error() || pt_len >= max_plaintext_len) {
            purple_debug_info("matrixprpl",
                              "%s: Failed to decrypt inbound session creation"
                              " event: %s\n",
                              __func__, olm_session_last_error(session));
            goto err;
        }
        update_olm_session(conn, mos);
        plaintext[pt_len] = '\0';
        handle_decrypted_olm(pc, conn, cevent_sender, sender_key, plaintext);
    } else {
        purple_debug_info("matrixprpl", "%s: Type %zd olm\n", __func__, type);
    }
    if (plaintext) {
        clear_mem(plaintext, max_plaintext_len);
    }
    g_free(plaintext);
    g_free(cevent_body_copy);

    matrix_store_e2e_account(conn);
    return;

err:
    if (plaintext) {
        clear_mem(plaintext, max_plaintext_len);
    }
    g_free(plaintext);
    g_free(cevent_body_copy);
}

/*
 * See:
 * https://matrix.org/docs/guides/e2e_implementation.html#handling-an-m-room-encrypted-event
 * For decrypting d2d messages
 * TODO: We really need to build a queue of stuff to decrypt, especially since they take multiple
 * messages to deal with when we have to fetch stuff/validate a device id
 */
void matrix_e2e_decrypt_d2d(PurpleConnection *pc, JsonObject *cevent)
{
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);
    const gchar *cevent_type;
    const gchar *cevent_sender;
    cevent_type = matrix_json_object_get_string_member(cevent, "type");
    cevent_sender = matrix_json_object_get_string_member(cevent, "sender");
    purple_debug_info("matrixprpl", "%s: %s from %s\n", __func__, cevent_type,
                       cevent_sender);

    if (strcmp(cevent_type, "m.room.encrypted")) {
        purple_debug_info("matrixprpl", "%s: %s unexpected type\n",
                               __func__, cevent_type);
        goto out;
    }

    JsonObject *cevent_content = matrix_json_object_get_object_member(cevent,
                                                               "content");
    const gchar *cevent_algo;
    cevent_algo = matrix_json_object_get_string_member(cevent_content,
                                                       "algorithm");
    if (!cevent_algo) {
        purple_debug_info("matrixprpl",
           "%s: Encrypted event doesn't have algorithm entry\n", __func__);
        goto out;
    }

    if (!strcmp(cevent_algo, "m.olm.v1.curve25519-aes-sha2")) {
        decrypt_olm(pc, conn, cevent, cevent_content);
    } else if (!strcmp(cevent_algo, "m.megolm.v1.aes-sha2")) {
        purple_debug_info("matrixprpl",
           "%s: It's megolm - unexpected for d2d!\n", __func__);
    } else {
        purple_debug_info("matrixprpl",
           "%s: Unknown crypto algorithm %s\n", __func__, cevent_algo);
    }

out:
    return;
}

/*
 * If succesful returns a JsonParser on the decrypted event.
 */
JsonParser *matrix_e2e_decrypt_room(PurpleConversation *conv,
                                    struct _JsonObject *cevent)
{
    JsonObject *cevent_content;
    const gchar *cevent_sender, *cevent_sender_key, *cevent_session_id;
    const gchar *algorithm, *cevent_ciphertext, *cevent_device_id;
    gchar *dupe_ciphertext = NULL;
    gchar *plaintext = NULL;
    size_t maxlen = 0;
    JsonParser *plaintext_parser = NULL;

    cevent_sender = matrix_json_object_get_string_member(cevent, "sender");
    cevent_content = matrix_json_object_get_object_member(cevent, "content");
    cevent_sender_key = matrix_json_object_get_string_member(cevent_content,
                                                               "sender_key");
    cevent_session_id = matrix_json_object_get_string_member(cevent_content,
                                                               "session_id");
    cevent_device_id = matrix_json_object_get_string_member(cevent_content,
                                                               "device_id");
    algorithm = matrix_json_object_get_string_member(cevent_content,
                                                       "algorithm");
    cevent_ciphertext = matrix_json_object_get_string_member(cevent_content,
                                                               "ciphertext");

    if (!algorithm || strcmp(algorithm, "m.megolm.v1.aes-sha2")) {
        purple_debug_info("matrixprpl", "%s: Bad algorithm %s\n",
                               __func__, algorithm);
        goto out;
    }

    if (!cevent_sender || !cevent_content || !cevent_sender_key ||
        !cevent_session_id || !cevent_device_id || !cevent_ciphertext) {
        purple_debug_info("matrixprpl",
                          "%s: Missing field sender: %s content: %p "
                          "sender_key: %s session_id: %s device_id: %s "
                          "ciphertext: %s\n",
                               __func__, cevent_sender, cevent_content,
                               cevent_sender_key, cevent_session_id,
                               cevent_device_id, cevent_ciphertext);
        goto out;
    }

    OlmInboundGroupSession *oigs = get_inbound_megolm_session(conv,
                                               cevent_sender_key,
                                               cevent_sender, cevent_session_id,
                                               cevent_device_id);
    if (!oigs) {
        // TODO: Queue this message and decrypt it when we get the session?
        // TODO: Check device verification state?
        purple_debug_info("matrixprpl",
                          "%s: No Megolm session for %s/%s/%s/%s\n", __func__,
                          cevent_device_id, cevent_sender, cevent_sender_key,
                          cevent_session_id);
        goto out;
    }
    purple_debug_info("matrixprpl",
                      "%s: have Megolm session %p for %s/%s/%s/%s\n",
                      __func__, oigs, cevent_device_id, cevent_sender,
                      cevent_sender_key, cevent_session_id);
    dupe_ciphertext = g_strdup(cevent_ciphertext);
    maxlen = olm_group_decrypt_max_plaintext_length(oigs,
                       (uint8_t *)dupe_ciphertext,
                       strlen(dupe_ciphertext));
    if (maxlen == olm_error()) {
        purple_debug_info("matrixprpl",
                          "%s: olm_group_decrypt_max_plaintext_length says "
                          "%s for %s/%s/%s/%s\n",
                __func__, olm_inbound_group_session_last_error(oigs),
                cevent_device_id, cevent_sender, cevent_sender_key,
                cevent_session_id);
        goto out;
    }
    dupe_ciphertext = g_strdup(cevent_ciphertext);
    plaintext = g_malloc0(maxlen+1);
    uint32_t index;
    size_t decrypt_len = olm_group_decrypt(oigs, (uint8_t *)dupe_ciphertext,
                                           strlen(dupe_ciphertext),
                                           (uint8_t *)plaintext, maxlen,
                                           &index);
    if (decrypt_len == olm_error()) {
        purple_debug_info("matrixprpl",
                "%s: olm_group_decrypt says %s for %s/%s/%s/%s\n",
                __func__, olm_inbound_group_session_last_error(oigs),
                cevent_device_id, cevent_sender, cevent_sender_key,
                cevent_session_id);
        goto out;
    }

    if (decrypt_len > maxlen) {
        purple_debug_info("matrixprpl",
                "%s: olm_group_decrypt len=%zd max was supposed to be %zd\n",
                __func__, decrypt_len, maxlen);
        goto out;
    }
    // TODO: Stash index somewhere - supposed to check it for validity
    plaintext[decrypt_len] = '\0';
    purple_debug_info("matrixprpl",
                      "%s: Decrypted megolm event as '%s' index=%zd\n",
                      __func__, plaintext, (size_t)index);

    plaintext_parser = json_parser_new();
    GError *err = NULL;
    if (!json_parser_load_from_data(plaintext_parser,
                                    plaintext, strlen(plaintext), &err)) {
        purple_debug_info("matrixprpl",
                          "%s: Failed to json parse decrypted plain text: %s\n",
                          __func__, plaintext);
        g_object_unref(plaintext_parser);
        goto out;
    }

out:
    g_free(dupe_ciphertext);
    if (plaintext) {
        clear_mem(plaintext, maxlen);
    }
    g_free(plaintext);

    return plaintext_parser;
}

/* Parse the 'file' object on media to extract keys, returns
 * FALSE if there's a problem, returns TRUE whether or not there's
 * any crypto information, returns TRUE and allocates *crypt
 * with the keys if the crypto info is there.
 * The *crypt should be g_free'd by someone later.
 */
gboolean matrix_e2e_parse_media_decrypt_info(MatrixMediaCryptInfo **crypt,
                                             JsonObject *file_obj)
{
    const gchar *v_str = matrix_json_object_get_string_member(file_obj, "v");
    const gchar *iv_str = matrix_json_object_get_string_member(file_obj, "iv");
    /* RFC7517 JSON Web Key */
    JsonObject *key_obj = matrix_json_object_get_object_member(file_obj,
                                                               "key");
    JsonObject *hashes_obj = matrix_json_object_get_object_member(file_obj,
                                                               "hashes");
    const gchar *alg_str = matrix_json_object_get_string_member(key_obj, "alg");
    const gchar *kty_str = matrix_json_object_get_string_member(key_obj, "kty");
    const gchar *k_str = matrix_json_object_get_string_member(key_obj, "k");
    const gchar *sha256_str = matrix_json_object_get_string_member(hashes_obj,
                                                                   "sha256");
    if (!v_str || strcmp(v_str, "v2")) {
        purple_debug_info("matrixprpl", "bad media decrypt version\n");
        return FALSE;
    }
    if (!iv_str || !alg_str || !kty_str || !k_str || !sha256_str) {
        purple_debug_info("matrixprpl", "missing media decrypt field\n");
        /* Potentially this is OK, just not crypted */
        return TRUE;
    }
    if (strcmp(alg_str, "A256CTR") || strcmp(kty_str, "oct")) {
        purple_debug_info("matrixprpl", "media decrypt: bad alg/kty :%s/%s\n",
                alg_str, kty_str);
        return FALSE;
    }
    if (strlen(iv_str) != 22 || strlen(sha256_str) != 43 ||
        strlen(k_str) != 43) {
        purple_debug_info("matrixprpl", "media decrypt: Bad key/sha len:"
                " iv: %s sha256: %s k: %s\n", iv_str, sha256_str, k_str);
        return FALSE;
    }

    /* The k_str isn't a simple base64, it's a JSON web signature that needs
     * to be decoded first.
     */
    gchar tmp_str[48];
    gsize k_len, iv_len, sha256_len;
    matrix_json_jws_tobase64(tmp_str, k_str);
    guchar *decoded_k = g_base64_decode(tmp_str, &k_len);
    matrix_json_jws_tobase64(tmp_str, iv_str);
    guchar *decoded_iv = g_base64_decode(tmp_str, &iv_len);
    matrix_json_jws_tobase64(tmp_str, sha256_str);
    guchar *decoded_sha256 = g_base64_decode(tmp_str, &sha256_len);

    if (k_len != 32 || iv_len != 16 || sha256_len != 32) {
        purple_debug_info("matrixprpl", "media decrypt: Bad base64 decode:"
                " iv: %s=%zd sha256: %s=%zd k: %s=%zd\n",
                iv_str, iv_len, sha256_str, sha256_len,  k_str, k_len);
        g_free(decoded_k);
        g_free(decoded_iv);
        g_free(decoded_sha256);
        return FALSE;
    }
    purple_debug_info("matrixprpl", "media decrypt: got decrypt keys\n");

    *crypt = g_new0(MatrixMediaCryptInfo, 1);
    memcpy((*crypt)->sha256, decoded_sha256, 32);
    memcpy((*crypt)->aes_k, decoded_k, 32);
    memcpy((*crypt)->aes_iv, decoded_iv, 16);

    clear_mem((char *)decoded_k, sizeof(decoded_k));
    g_free(decoded_k);
    g_free(decoded_iv);
    g_free(decoded_sha256);

    return TRUE;
}

/* Decrypt media (in or inlen) into a buffer it allocates (*out)
 * returns NULL or an error string.
 */
const char *matrix_e2e_decrypt_media(MatrixMediaCryptInfo *crypt,
                                     size_t inlen, const void *in, void **out)
{
    char *fail_str = NULL;
    gcry_error_t gcry_err;
    gcry_cipher_hd_t cipher_hd;
    gboolean copen = FALSE;

    *out = NULL;
    gcry_err = gcry_cipher_open(&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0);
    if (gcry_err) {
        fail_str = "failed to open cipher";
        goto err;
    }
    copen = TRUE;
    gcry_err = gcry_cipher_setkey(cipher_hd, crypt->aes_k, 32);
    if (gcry_err) {
        fail_str = "failed to set key";
        goto err;
    }
    /* Note: this is only working if we use setctr not setiv */
    gcry_err = gcry_cipher_setctr(cipher_hd, crypt->aes_iv, 16);
    if (gcry_err) {
        fail_str = "failed to set iv";
        goto err;
    }
    *out = g_malloc(inlen); /* Do I need to round this to block len? */
    gcry_cipher_final(cipher_hd);
    gcry_err = gcry_cipher_decrypt(cipher_hd, *out, inlen, in, inlen);
    if (gcry_err) {
        g_free(*out);
        fail_str = "failed to decrypt";
        goto err;
    }

    gcry_cipher_close(cipher_hd);

    return NULL;

err:
    g_free(*out);
    *out = NULL;
    if (copen) gcry_cipher_close(cipher_hd);

    return fail_str;
}

static void action_device_info(PurplePluginAction *action)
{
    PurpleConnection *pc = (PurpleConnection *) action->context;

    if (!pc) return;
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);
    if (!conn || !conn->e2e) return;
    char *title = g_strdup_printf("Device info for %s", conn->user_id);
    char *body = g_strdup_printf("Device ID: %s"
                                 "<br>Device Key: %s",
                                 conn->e2e->device_id,
                                 conn->e2e->ed25519_pubkey);
    purple_notify_formatted(pc, title, title, NULL, body, NULL, NULL);
    g_free(title);
    g_free(body);
}

/* Hook for adding purple 'action' menu items */
GList *matrix_e2e_actions(GList *list)
{
    list = g_list_append(list,
                         purple_plugin_action_new(_("Device info"),
                                                 action_device_info));
    return list;
}

#else
/* ==== Stubs for when e2e is configured out of the build === */
void matrix_e2e_decrypt_d2d(PurpleConnection *pc, JsonObject *cevent)
{
}

JsonParser *matrix_e2e_decrypt_room(PurpleConversation *conv,
                                    struct _JsonObject *cevent)
{
    return NULL;
}

int matrix_e2e_get_device_keys(MatrixConnectionData *conn, const gchar *device_id)
{
    return -1;
}

void matrix_e2e_cleanup_connection(MatrixConnectionData *conn)
{
}

void matrix_e2e_handle_sync_key_counts(PurpleConnection *pc, JsonObject *count_object,
                                       gboolean force_send)
{
}

void matrix_e2e_cleanup_conversation(PurpleConversation *conv)
{
}

gboolean matrix_e2e_parse_media_decrypt_info(MatrixMediaCryptInfo **crypt,
                                             JsonObject *file_obj)
{
    JsonObject *key_obj = matrix_json_object_get_object_member(file_obj,
                                                               "key");
    if (key_obj) {
        /* This has a key but with crypto turned off we'll have to fail */
        return FALSE;
    }

    return TRUE;
}

const char *matrix_e2e_decrypt_media(MatrixMediaCryptInfo *crypt,
                                     size_t inlen, const void *in, void **out)
{
    return "Crypto not available";
}


GList *matrix_e2e_actions(GList *list)
{
    return list;
}

#endif
