diff options
author | Richard van der Hoff <1389908+richvdh@users.noreply.github.com> | 2018-08-03 19:14:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-03 19:14:58 +0100 |
commit | 5a7166a3f54f85793c6b60662f8d12196aeaaeb0 (patch) | |
tree | 86b2de5cf9fc7f7b0dd4f505b44c28b030dc6cf1 | |
parent | 49ea988ce7ca75ae5ea6ae1384707cea4d6c4f35 (diff) | |
parent | 1a47e5bce8c0fe95506b01cfa290ff682f88c2ef (diff) | |
download | purple-matrix-5a7166a3f54f85793c6b60662f8d12196aeaaeb0.tar.gz |
Merge pull request #70 from penguin42/crypt-push4
E2E support
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | Makefile.common | 5 | ||||
-rw-r--r-- | Makefile.mingw | 7 | ||||
-rw-r--r-- | libmatrix.c | 11 | ||||
-rw-r--r-- | libmatrix.h | 2 | ||||
-rw-r--r-- | matrix-api.c | 46 | ||||
-rw-r--r-- | matrix-api.h | 24 | ||||
-rw-r--r-- | matrix-connection.c | 10 | ||||
-rw-r--r-- | matrix-connection.h | 3 | ||||
-rw-r--r-- | matrix-e2e.c | 1923 | ||||
-rw-r--r-- | matrix-e2e.h | 41 | ||||
-rw-r--r-- | matrix-json.c | 239 | ||||
-rw-r--r-- | matrix-json.h | 15 | ||||
-rw-r--r-- | matrix-room.c | 126 | ||||
-rw-r--r-- | matrix-sync.c | 73 |
15 files changed, 2457 insertions, 74 deletions
@@ -1,7 +1,7 @@ #!/usr/bin/make -f CC=gcc -LIBS=purple json-glib-1.0 glib-2.0 +LIBS=purple json-glib-1.0 glib-2.0 sqlite3 PKG_CONFIG=pkg-config CFLAGS+=$(shell $(PKG_CONFIG) --cflags $(LIBS)) @@ -9,6 +9,10 @@ CFLAGS+=-fPIC -DPIC LDLIBS+=$(shell $(PKG_CONFIG) --libs $(LIBS)) LDLIBS+=-lhttp_parser +ifndef MATRIX_NO_E2E +LDLIBS+=-lolm +endif + PLUGIN_DIR_PURPLE = $(shell $(PKG_CONFIG) --variable=plugindir purple) DATA_ROOT_DIR_PURPLE = $(shell $(PKG_CONFIG) --variable=datarootdir purple) diff --git a/Makefile.common b/Makefile.common index 05dc040..219f74f 100644 --- a/Makefile.common +++ b/Makefile.common @@ -8,7 +8,12 @@ CFLAGS += -DPURPLE_PLUGINS # generate .d files when compiling CPPFLAGS += -MMD +ifdef MATRIX_NO_E2E +CFLAGS+=-DMATRIX_NO_E2E +endif + OBJECTS = libmatrix.o matrix-api.o matrix-connection.o \ + matrix-e2e.o \ matrix-event.o \ matrix-json.o \ matrix-room.o \ diff --git a/Makefile.mingw b/Makefile.mingw index 1745354..33170c7 100644 --- a/Makefile.mingw +++ b/Makefile.mingw @@ -12,6 +12,13 @@ CFLAGS += -I$(PIDGIN_TREE_TOP)/libpurple -I$(JSON_GLIB_TOP)/include/json-glib-1. LDLIBS += -L$(PIDGIN_TREE_TOP)/libpurple -lpurple -L$(JSON_GLIB_TOP)/lib -ljson-glib-1.0 -L$(GLIB_TOP)/bin -lglib-2.0-0 -lgobject-2.0-0 LDLIBS += -L$(HTTP_PARSER_TOP) -lhttp_parser -static-libgcc +ifndef MATRIX_NO_E2E +OLM_TOP ?= $(WIN32_DEV_TOP)/olm +CFLAGS += -I$(OLM_TOP)/include +LDLIBS+= -L$(OLM_TOP)/build -lolm +endif + + PLUGIN_DIR_PURPLE = "C:\Program Files (x86)\Pidgin\plugins" DATA_ROOT_DIR_PURPLE = "C:\Program Files (x86)\Pidgin" diff --git a/libmatrix.c b/libmatrix.c index e0a1e64..5b2d4ae 100644 --- a/libmatrix.c +++ b/libmatrix.c @@ -35,6 +35,7 @@ #include "version.h" #include "matrix-connection.h" +#include "matrix-e2e.h" #include "matrix-room.h" #include "matrix-api.h" @@ -378,6 +379,14 @@ static void matrixprpl_destroy(PurplePlugin *plugin) { purple_debug_info("matrixprpl", "shutting down\n"); } +static GList *matrixprpl_actions(PurplePlugin *plugin, gpointer context) +{ + GList *list = NULL; + + list = matrix_e2e_actions(list); + + return list; +} static PurplePluginInfo info = { @@ -402,7 +411,7 @@ static PurplePluginInfo info = NULL, /* ui_info */ &prpl_info, /* extra_info */ NULL, /* prefs_info */ - NULL, /* actions */ + matrixprpl_actions, /* actions */ NULL, /* padding... */ NULL, NULL, diff --git a/libmatrix.h b/libmatrix.h index f4a8ce7..86ce891 100644 --- a/libmatrix.h +++ b/libmatrix.h @@ -115,6 +115,8 @@ #define PRPL_ACCOUNT_OPT_HOME_SERVER "home_server" #define PRPL_ACCOUNT_OPT_NEXT_BATCH "next_batch" #define PRPL_ACCOUNT_OPT_SKIP_OLD_MESSAGES "skip_old_messages" +/* Pickled account info from olm_pickle_account */ +#define PRPL_ACCOUNT_OPT_OLM_ACCOUNT_KEYS "olm_account_keys" /* defaults for account options */ #define DEFAULT_HOME_SERVER "https://matrix.org" diff --git a/matrix-api.c b/matrix-api.c index 71dc39d..7d1b6f1 100644 --- a/matrix-api.c +++ b/matrix-api.c @@ -1007,6 +1007,52 @@ MatrixApiRequestData *matrix_api_download_thumb(MatrixConnectionData *conn, return fetch_data; } +MatrixApiRequestData *matrix_api_upload_keys(MatrixConnectionData *conn, + JsonObject *device_keys, JsonObject *one_time_keys, + MatrixApiCallback callback, + MatrixApiErrorCallback error_callback, + MatrixApiBadResponseCallback bad_response_callback, + gpointer user_data) +{ + GString *url; + MatrixApiRequestData *fetch_data; + JsonNode *body_node; + JsonObject *top_obj; + JsonGenerator *generator; + gchar *json; + + url = g_string_new(conn->homeserver); + g_string_append(url, "_matrix/client/r0/keys/upload?access_token="); + g_string_append(url, purple_url_encode(conn->access_token)); + + top_obj = json_object_new(); + if (device_keys) { + json_object_set_object_member(top_obj, "device_keys", device_keys); + } + if (one_time_keys) { + json_object_set_object_member(top_obj, "one_time_keys", one_time_keys); + } + body_node = json_node_new(JSON_NODE_OBJECT); + json_node_set_object(body_node, top_obj); + json_object_unref(top_obj); + + generator = json_generator_new(); + json_generator_set_root(generator, body_node); + json = json_generator_to_data(generator, NULL); + g_object_unref(G_OBJECT(generator)); + json_node_free(body_node); + + fetch_data = matrix_api_start_full(url->str, "POST", + "Content-Type: application/json", json, NULL, 0, + conn, callback, error_callback, bad_response_callback, + user_data, 1024); + g_free(json); + g_string_free(url, TRUE); + + return fetch_data; +} + + #if 0 MatrixApiRequestData *matrix_api_get_room_state(MatrixConnectionData *conn, const gchar *room_id, diff --git a/matrix-api.h b/matrix-api.h index 089b9b3..763f587 100644 --- a/matrix-api.h +++ b/matrix-api.h @@ -362,6 +362,30 @@ MatrixApiRequestData *matrix_api_download_thumb(MatrixConnectionData *conn, MatrixApiBadResponseCallback bad_response_callback, gpointer user_data); +/** + * e2e: Upload keys; one or more of the device keys and the one time keys + * @param conn The connection with which to make the request + * @param device_keys (optional) Json Object with the signed device keys + * device_keys gets unreferenced + * @param one_time_keys (optional) Json Object with one time key set + * one_time_keys gets unreferenced + * @param callback Function to be called when the request completes + * @param error_callback Function to be called if there is an error making + * the request. If NULL, matrix_api_error will be + * used. + * @param bad_response_callback Function to be called if the API gives a non-200 + * response. If NULL, matrix_api_bad_response will be + * used. + * @param user_data Opaque data to be passed to the callbacks + * + */ +MatrixApiRequestData *matrix_api_upload_keys(MatrixConnectionData *conn, + struct _JsonObject *device_keys, struct _JsonObject *one_time_keys, + MatrixApiCallback callback, + MatrixApiErrorCallback error_callback, + MatrixApiBadResponseCallback bad_response_callback, + gpointer user_data); + #if 0 /** * Get the current state of a room diff --git a/matrix-connection.c b/matrix-connection.c index 91c660a..736782b 100644 --- a/matrix-connection.c +++ b/matrix-connection.c @@ -17,6 +17,7 @@ */ #include "matrix-connection.h" +#include "matrix-e2e.h" #include <string.h> @@ -53,6 +54,7 @@ void matrix_connection_free(PurpleConnection *pc) g_assert(conn != NULL); + matrix_e2e_cleanup_connection(conn); purple_connection_set_protocol_data(pc, NULL); g_free(conn->homeserver); @@ -167,6 +169,7 @@ static void _login_completed(MatrixConnectionData *conn, JsonObject *root_obj; const gchar *access_token; const gchar *next_batch; + const gchar *device_id; gboolean needs_full_state_sync = TRUE; root_obj = matrix_json_node_get_object(json_root); @@ -181,9 +184,12 @@ static void _login_completed(MatrixConnectionData *conn, conn->access_token = g_strdup(access_token); conn->user_id = g_strdup(matrix_json_object_get_string_member(root_obj, "user_id")); - purple_account_set_string(pc->account, "device_id", - matrix_json_object_get_string_member(root_obj, "device_id")); + device_id = matrix_json_object_get_string_member(root_obj, "device_id"); + purple_account_set_string(pc->account, "device_id", device_id); + if (device_id) { + matrix_e2e_get_device_keys(conn, device_id); + } /* start the sync loop */ next_batch = purple_account_get_string(pc->account, PRPL_ACCOUNT_OPT_NEXT_BATCH, NULL); diff --git a/matrix-connection.h b/matrix-connection.h index e5be783..b7d8e21 100644 --- a/matrix-connection.h +++ b/matrix-connection.h @@ -29,6 +29,7 @@ #include <glib.h> struct _PurpleConnection; +struct _MatrixE2EData; typedef struct _MatrixConnectionData { struct _PurpleConnection *pc; @@ -38,6 +39,8 @@ typedef struct _MatrixConnectionData { /* the active sync request */ struct _MatrixApiRequestData *active_sync; + /* All the end-2-end encryption magic */ + struct _MatrixE2EData *e2e; } MatrixConnectionData; diff --git a/matrix-e2e.c b/matrix-e2e.c new file mode 100644 index 0000000..a358185 --- /dev/null +++ b/matrix-e2e.c @@ -0,0 +1,1923 @@ +/** + * 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 diff --git a/matrix-e2e.h b/matrix-e2e.h new file mode 100644 index 0000000..5ab5ee1 --- /dev/null +++ b/matrix-e2e.h @@ -0,0 +1,41 @@ +/** + * 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 + */ + +#ifndef MATRIX_E2E_H +#define MATRIX_E2E_H + +#include <json-glib/json-glib.h> +#include "matrix-connection.h" + +typedef struct _MatrixE2EData MatrixE2EData; +typedef struct _PurpleConversation PurpleConversation; +typedef struct _MatrixMediaCryptInfo MatrixMediaCryptInfo; + +GList *matrix_e2e_actions(GList *list); +int matrix_e2e_get_device_keys(MatrixConnectionData *conn, const gchar *device_id); +void matrix_e2e_cleanup_connection(MatrixConnectionData *conn); +void matrix_e2e_cleanup_conversation(PurpleConversation *conv); +void matrix_e2e_decrypt_d2d(struct _PurpleConnection *pc, struct _JsonObject *event); +JsonParser *matrix_e2e_decrypt_room(struct _PurpleConversation *conv, struct _JsonObject *event); +gboolean matrix_e2e_parse_media_decrypt_info(MatrixMediaCryptInfo **crypt, + JsonObject *file_obj); +const char *matrix_e2e_decrypt_media(MatrixMediaCryptInfo *crypt, + size_t inlen, const void *in, void **out); +void matrix_e2e_handle_sync_key_counts(struct _PurpleConnection *pc, struct _JsonObject *count_object, gboolean force_send); + +#endif diff --git a/matrix-json.c b/matrix-json.c index 1fa944d..dcbb0bd 100644 --- a/matrix-json.c +++ b/matrix-json.c @@ -18,45 +18,50 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */ +#include <stdio.h> +#include <string.h> #include "matrix-json.h" +static GString *canonical_json_node(JsonNode *node, GString *result); +static GString *canonical_json_object(JsonObject *object, GString *result); + /* node */ const gchar *matrix_json_node_get_string(JsonNode *node) { - if(node == NULL) - return NULL; - if(JSON_NODE_TYPE(node) != JSON_NODE_VALUE) - return NULL; - return json_node_get_string(node); + if(node == NULL) + return NULL; + if(JSON_NODE_TYPE(node) != JSON_NODE_VALUE) + return NULL; + return json_node_get_string(node); } gint64 matrix_json_node_get_int(JsonNode *node) { - if(node == NULL) - return 0; - if(JSON_NODE_TYPE(node) != JSON_NODE_VALUE) - return 0; - return json_node_get_int(node); + if(node == NULL) + return 0; + if(JSON_NODE_TYPE(node) != JSON_NODE_VALUE) + return 0; + return json_node_get_int(node); } JsonObject *matrix_json_node_get_object (JsonNode *node) { - if(node == NULL) - return NULL; - if(JSON_NODE_TYPE(node) != JSON_NODE_OBJECT) - return NULL; - return json_node_get_object(node); + if(node == NULL) + return NULL; + if(JSON_NODE_TYPE(node) != JSON_NODE_OBJECT) + return NULL; + return json_node_get_object(node); } JsonArray *matrix_json_node_get_array(JsonNode *node) { - if(node == NULL) - return NULL; - if(JSON_NODE_TYPE(node) != JSON_NODE_ARRAY) - return NULL; - return json_node_get_array(node); + if(node == NULL) + return NULL; + if(JSON_NODE_TYPE(node) != JSON_NODE_ARRAY) + return NULL; + return json_node_get_array(node); } @@ -64,47 +69,47 @@ JsonArray *matrix_json_node_get_array(JsonNode *node) /* object */ JsonNode *matrix_json_object_get_member (JsonObject *object, - const gchar *member_name) + const gchar *member_name) { - g_assert(member_name != NULL); + g_assert(member_name != NULL); - if(object == NULL) - return NULL; + if(object == NULL) + return NULL; - return json_object_get_member(object, member_name); + return json_object_get_member(object, member_name); } const gchar *matrix_json_object_get_string_member(JsonObject *object, - const gchar *member_name) + const gchar *member_name) { - JsonNode *member; - member = matrix_json_object_get_member(object, member_name); + JsonNode *member; + member = matrix_json_object_get_member(object, member_name); return matrix_json_node_get_string(member); } gint64 matrix_json_object_get_int_member(JsonObject *object, - const gchar *member_name) + const gchar *member_name) { - JsonNode *member; - member = matrix_json_object_get_member(object, member_name); + JsonNode *member; + member = matrix_json_object_get_member(object, member_name); return matrix_json_node_get_int(member); } JsonObject *matrix_json_object_get_object_member(JsonObject *object, - const gchar *member_name) + const gchar *member_name) { - JsonNode *member; - member = matrix_json_object_get_member(object, member_name); + JsonNode *member; + member = matrix_json_object_get_member(object, member_name); return matrix_json_node_get_object(member); } JsonArray *matrix_json_object_get_array_member(JsonObject *object, - const gchar *member_name) + const gchar *member_name) { - JsonNode *member; - member = matrix_json_object_get_member(object, member_name); + JsonNode *member; + member = matrix_json_object_get_member(object, member_name); return matrix_json_node_get_array(member); } @@ -114,17 +119,161 @@ JsonArray *matrix_json_object_get_array_member(JsonObject *object, JsonNode *matrix_json_array_get_element(JsonArray *array, guint index) { - if(array == NULL) - return NULL; - if(json_array_get_length(array) <= index) - return NULL; - return json_array_get_element(array, index); + if(array == NULL) + return NULL; + if(json_array_get_length(array) <= index) + return NULL; + return json_array_get_element(array, index); } const gchar *matrix_json_array_get_string_element(JsonArray *array, guint index) { - JsonNode *element; - element = matrix_json_array_get_element(array, index); - return matrix_json_node_get_string(element); + JsonNode *element; + element = matrix_json_array_get_element(array, index); + return matrix_json_node_get_string(element); +} + +static gint canonical_json_sort(gconstpointer a, gconstpointer b) +{ + return strcmp((const gchar *)a, (const gchar *)b); +} + +static GString *canonical_json_value(JsonNode *node, GString *result) +{ + GType vt = json_node_get_value_type(node); + switch (vt) { + case G_TYPE_STRING: + /* TODO: I'm assuming our strings are nice UTF-8 strings already */ + result = g_string_append_c(result, '"'); + result = g_string_append(result, json_node_get_string(node)); + result = g_string_append_c(result, '"'); + break; + + default: + fprintf(stderr, "%s: Unknown value type %zd\n", __func__, + (size_t)vt); + /* TODO: Other value types */ + g_assert_not_reached(); + } + + return result; +} + +static GString *canonical_json_array(JsonArray *arr, GString *result) +{ + guint nelems, i; + result = g_string_append_c(result, '['); + nelems = json_array_get_length(arr); + for(i = 0; i < nelems; i++) { + if (i) result=g_string_append_c(result, ','); + result = canonical_json_node(json_array_get_element(arr, i), result); + } + result = g_string_append_c(result, ']'); + + return result; +} + +static GString *canonical_json_node(JsonNode *node, GString *result) +{ + switch (json_node_get_node_type(node)) { + case JSON_NODE_OBJECT: + result = canonical_json_object(json_node_get_object(node), result); + break; + + case JSON_NODE_ARRAY: + result = canonical_json_array(json_node_get_array(node), result); + break; + + case JSON_NODE_VALUE: + result = canonical_json_value(node, result); + break; + + case JSON_NODE_NULL: + result = g_string_append(result, "null"); + break; + } + return result; +} + +static GString *canonical_json_object(JsonObject *object, GString *result) +{ + gboolean first = TRUE; + result = result ? g_string_append_c(result, '{') : g_string_new("{"); + + /* This gets an unsorted list of member names */ + GList *members = json_object_get_members(object); + GList *cur; + + members = g_list_sort(members, canonical_json_sort); + for(cur=g_list_first(members); cur; cur=g_list_next(cur)) { + const gchar *cur_name = cur->data; + JsonNode *cur_node = json_object_get_member(object, cur_name); + if (!first) result=g_string_append_c(result, ','); + first = FALSE; + result = g_string_append_c(result, '"'); + result = g_string_append(result, cur_name); + result = g_string_append_c(result, '"'); + result = g_string_append_c(result, ':'); + result = canonical_json_node(cur_node, result); + } + + g_list_free(members); + + result = g_string_append_c(result, '}'); + return result; +} + +/* Produce a canonicalised string as defined in + * http://matrix.org/docs/spec/appendices.html#canonical-json + */ +GString *matrix_canonical_json(JsonObject *object) +{ + return canonical_json_object(object, NULL); +} + +/* Decode a json web signature (JWS) which is almost base64, + * its needs _ -> / and - -> + and some = padding. + * as https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C + * The output buffer should be upto 3 bytes longer than the input + * depending on the amount of = padding needed. + */ +void matrix_json_jws_tobase64(gchar *out, const gchar *in) +{ + unsigned int i; + for (i=0;in[i];i++) { + out[i] = in[i]; + switch (in[i]) { + case '-': + out[i] = '+'; + break; + + case '_': + out[i] = '/'; + break; + + default: + break; + } + } + while (i & 3) { + out[i] = '='; + i++; + } + out[i] = '\0'; +} + +/* Just dump the Json with the string prefix for debugging */ +void matrix_debug_jsonobject(const char *reason, JsonObject *object) +{ + JsonNode *tmp_top = json_node_new(JSON_NODE_OBJECT); + json_node_set_object(tmp_top, object); + JsonGenerator *generator = json_generator_new(); + json_generator_set_pretty(generator, TRUE); + json_generator_set_root(generator, tmp_top); + char *json = json_generator_to_data(generator, NULL); + fprintf(stderr, "%s: %s\n", reason, json); + g_free(json); + g_object_unref(generator); + json_node_free(tmp_top); } diff --git a/matrix-json.h b/matrix-json.h index b2c2820..8aed2d1 100644 --- a/matrix-json.h +++ b/matrix-json.h @@ -60,5 +60,20 @@ const gchar *matrix_json_array_get_string_element(JsonArray *array, guint index); +/* Produce a canonicalised string as defined in + * https://matrix.org/speculator/spec/drafts%2Fe2e/appendices.html#canonical-json + */ +GString *matrix_canonical_json(JsonObject *object); + +/* Decode a json web signature (JWS) which is almost base64, + * its needs _ -> / and - -> + and some = padding. + * as https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C + * The output buffer should be upto 3 bytes longer than the input + * depending on the amount of = padding needed. + */ +void matrix_json_jws_tobase64(gchar *out, const gchar *in); + +/* Just dump the Json with the string prefix for debugging */ +void matrix_debug_jsonobject(const char *reason, JsonObject *object); #endif /* MATRIX_JSON_H_ */ diff --git a/matrix-room.c b/matrix-room.c index 2fbb6a1..6350997 100644 --- a/matrix-room.c +++ b/matrix-room.c @@ -31,11 +31,14 @@ #include "libmatrix.h" #include "matrix-api.h" +#include "matrix-e2e.h" #include "matrix-event.h" #include "matrix-json.h" #include "matrix-roommembers.h" #include "matrix-statetable.h" +#include <gcrypt.h> + static gchar *_get_room_name(MatrixConnectionData *conn, PurpleConversation *conv); @@ -290,6 +293,9 @@ static void _on_state_update(const gchar *event_type, strcmp(event_type, "m.room.canonical_alias") == 0 || strcmp(event_type, "m.room.name") == 0) { _schedule_name_update(conv); + } else if (strcmp(event_type, "m.room.encryption") == 0) { + purple_debug_info("matrixprpl", + "Got m.room.encryption on_state_update\n"); } else if(strcmp(event_type, "m.typing") == 0) { _on_typing(conv, old_state, new_state); @@ -621,22 +627,49 @@ static void _send_image_hook(MatrixRoomEvent *event, gboolean just_free) } } -/* Passed through matrix_api_download_file all the way - * downto _image_download_complete - */ struct ReceiveImageData { PurpleConversation *conv; gint64 timestamp; const gchar *room_id; const gchar *sender_display_name; gchar *original_body; + MatrixMediaCryptInfo *crypt; }; +/* Deal with encrypted image data */ +static void _image_download_complete_crypt(struct ReceiveImageData *rid, + const char *raw_body, size_t raw_body_len) +{ + void *decrypted = NULL; + + const char *fail_str = matrix_e2e_decrypt_media(rid->crypt, + raw_body_len, raw_body, &decrypted); + + if (fail_str) { + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), + rid->sender_display_name, PURPLE_MESSAGE_RECV, + g_strdup_printf("%s (%s)", + rid->original_body, fail_str), rid->timestamp / 1000); + } else { + int img_id = purple_imgstore_add_with_id(decrypted, raw_body_len, NULL); + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), rid->sender_display_name, + PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_IMAGES, + g_strdup_printf("<IMG ID=\"%d\">", img_id), rid->timestamp / 1000); + } + + g_free(rid->crypt); + g_free(rid->original_body); + g_free(rid); +} + static void _image_download_complete(MatrixConnectionData *ma, gpointer user_data, JsonNode *json_root, const char *raw_body, size_t raw_body_len, const char *content_type) { struct ReceiveImageData *rid = user_data; + if (rid->crypt) { + return _image_download_complete_crypt(rid, raw_body, raw_body_len); + } if (is_known_image_type(content_type)) { /* Excellent - something to work with */ int img_id = purple_imgstore_add_with_id(g_memdup(raw_body, raw_body_len), @@ -669,6 +702,7 @@ static void _image_download_bad_response(MatrixConnectionData *ma, gpointer user purple_conversation_set_data(rid->conv, PURPLE_CONV_DATA_ACTIVE_SEND, NULL); g_free(escaped_body); + g_free(rid->crypt); g_free(rid->original_body); g_free(rid); } @@ -685,11 +719,11 @@ static void _image_download_error(MatrixConnectionData *ma, gpointer user_data, purple_conversation_set_data(rid->conv, PURPLE_CONV_DATA_ACTIVE_SEND, NULL); g_free(escaped_body); + g_free(rid->crypt); g_free(rid->original_body); g_free(rid); } - /* * Called from matrix_room_handle_timeline_event when it finds an m.video * or m.audio or m.file or m.image; msg_body has the fallback text, @@ -701,24 +735,31 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, JsonObject *json_content_object, const gchar *msg_type) { MatrixConnectionData *conn = _get_connection_data_from_conversation(conv); MatrixApiRequestData *fetch_data = NULL; + int is_image = !strcmp("m.image", msg_type); const gchar *url; GString *download_url; guint64 size = 0; const gchar *mime_type = "unknown"; + JsonObject *json_file_obj = NULL; JsonObject *json_info_object; url = matrix_json_object_get_string_member(json_content_object, "url"); if (!url) { - /* That seems odd, oh well, no point in getting upset */ - purple_debug_info("matrixprpl", "failed to get url for media\n"); - return FALSE; + /* Could be a new style format used by the e2e world */ + json_file_obj = matrix_json_object_get_object_member( + json_content_object, "file"); + if (json_file_obj) { + url = matrix_json_object_get_string_member(json_file_obj, + "url"); + } + if (!url) { + /* That seems odd, oh well, no point in getting upset */ + purple_debug_info("matrixprpl", "failed to get url for media\n"); + return FALSE; + } } download_url = get_download_url(conn->homeserver, url); - if (!download_url) { - purple_debug_error("matrixprpl", "failed to get download_url for media\n"); - return FALSE; - } /* the 'info' member is optional */ json_info_object = matrix_json_object_get_object_member(json_content_object, @@ -748,7 +789,6 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, * download that. Otherwise, only for m.image, ask for a server generated * thumbnail. */ - int is_image = !strcmp("m.image", msg_type); const gchar *thumb_url = ""; JsonObject *json_thumb_info; guint64 thumb_size = 0; @@ -773,6 +813,20 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, /* if an m.image is small, get that instead of the thumbnail */ thumb_url = url; thumb_size = size; + } else if (json_file_obj) { + /* In the world with file members, we've also got a thumbnail_file + * member with potentially quite different data. + */ + JsonObject *tmp_file_obj = matrix_json_object_get_object_member( + json_info_object, "thumbnail_file"); + if (tmp_file_obj) { + const char *tmp_url; + tmp_url = matrix_json_object_get_string_member(tmp_file_obj, "url"); + if (tmp_url) { + thumb_url = tmp_url; + json_file_obj = tmp_file_obj; + } + } } if (thumb_url || is_image) { struct ReceiveImageData *rid; @@ -783,6 +837,17 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, rid->room_id = room_id; rid->original_body = g_strdup(msg_body); + if (json_file_obj) { + /* It's almost certainly encrypted data, extract the info. + * Note if it is then we must fetch the raw, we can't ask for the server + * to generate a thumb. + */ + if (!matrix_e2e_parse_media_decrypt_info(&rid->crypt, json_file_obj)) { + g_free(rid); + return FALSE; + } + } + if (thumb_url && (thumb_size > 0) && (thumb_size < purple_max_media_size)) { fetch_data = matrix_api_download_file(conn, thumb_url, purple_max_media_size, @@ -790,7 +855,7 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, _image_download_error, _image_download_bad_response, rid); - } else if (thumb_url) { + } else if (thumb_url && !rid->crypt) { /* Ask the server to generate a thumbnail of the thumbnail. * Useful to improve the chance of showing something when the * original thumbnail is too big. @@ -802,7 +867,7 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, _image_download_error, _image_download_bad_response, rid); - } else { + } else if (!rid->crypt) { /* Ask the server to generate a thumbnail. Only for m.image. * TODO: Configure the size of thumbnails. * 640x480 is a good a width as any and reasonably likely to @@ -819,6 +884,9 @@ static gboolean _handle_incoming_media(PurpleConversation *conv, } purple_conversation_set_data(conv, PURPLE_CONV_DATA_ACTIVE_SEND, fetch_data); + if (!fetch_data) { + g_free(rid->crypt); + } return fetch_data != NULL; } return TRUE; @@ -928,6 +996,7 @@ void matrix_room_handle_timeline_event(PurpleConversation *conv, gchar *tmp_body = NULL; gchar *escaped_body = NULL; PurpleMessageFlags flags; + JsonParser *decrypted_parser = NULL; const gchar *sender_display_name; MatrixRoomMember *sender = NULL; @@ -947,6 +1016,31 @@ void matrix_room_handle_timeline_event(PurpleConversation *conv, return; } + if(!strcmp(event_type, "m.room.encrypted")) { + purple_debug_info("matrixprpl", "Got an m.room.encrypted!\n"); + decrypted_parser = matrix_e2e_decrypt_room(conv, json_event_obj); + if (!decrypted_parser) { + purple_debug_warning("matrixprpl", + "Failed to decrypt m.room.encrypted"); + return; + } + JsonNode *decrypted_node = json_parser_get_root(decrypted_parser); + JsonObject *decrypted_body; + decrypted_body = matrix_json_node_get_object(decrypted_node); + event_type = matrix_json_object_get_string_member(decrypted_body, + "type"); + json_content_obj = matrix_json_object_get_object_member(decrypted_body, + "content"); + // TODO: Check room_id matches + // TODO: Add some info about device trust etc + if (!event_type || !json_content_obj) { + purple_debug_warning("matrixprpl", + "Failed to find members of decrypted json"); + g_object_unref(decrypted_parser); + return; + } + } + if(strcmp(event_type, "m.room.message") != 0) { purple_debug_info("matrixprpl", "ignoring unknown room event %s\n", event_type); @@ -1013,6 +1107,9 @@ void matrix_room_handle_timeline_event(PurpleConversation *conv, serv_got_chat_in(conv->account->gc, g_str_hash(room_id), sender_display_name, flags, escaped_body, timestamp / 1000); g_free(escaped_body); + if (decrypted_parser) { + g_object_unref(decrypted_parser); + } } @@ -1081,6 +1178,7 @@ void matrix_room_leave_chat(PurpleConversation *conv) g_list_free_full(event_queue, (GDestroyNotify)matrix_event_free); purple_conversation_set_data(conv, PURPLE_CONV_DATA_EVENT_QUEUE, NULL); } + matrix_e2e_cleanup_conversation(conv); } diff --git a/matrix-sync.c b/matrix-sync.c index 84f8be1..eb69189 100644 --- a/matrix-sync.c +++ b/matrix-sync.c @@ -16,6 +16,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */ +#include <string.h> #include "matrix-sync.h" /* json-glib */ @@ -28,6 +29,7 @@ /* libmatrix */ #include "matrix-connection.h" +#include "matrix-e2e.h" #include "matrix-event.h" #include "matrix-json.h" #include "matrix-room.h" @@ -124,7 +126,8 @@ static PurpleChat *_ensure_blist_entry(PurpleAccount *acct, * handle a joined room within the sync response */ static void matrix_sync_room(const gchar *room_id, - JsonObject *room_data, PurpleConnection *pc) + JsonObject *room_data, PurpleConnection *pc, + gboolean handle_timeline) { JsonObject *state_object, *timeline_object, *ephemeral_object; JsonArray *state_array, *timeline_array, *ephemeral_array; @@ -150,20 +153,22 @@ static void matrix_sync_room(const gchar *room_id, matrix_room_complete_state_update(conv, !initial_sync); - /* parse the timeline events */ - timeline_object = matrix_json_object_get_object_member( - room_data, "timeline"); - timeline_array = matrix_json_object_get_array_member( - timeline_object, "events"); - if(timeline_array != NULL) - _parse_room_event_array(conv, timeline_array, FALSE); - /* parse the ephemeral events */ /* (uses the state table to track the state of who is typing and who isn't) */ ephemeral_object = matrix_json_object_get_object_member(room_data, "ephemeral"); ephemeral_array = matrix_json_object_get_array_member(ephemeral_object, "events"); if(ephemeral_array != NULL) _parse_room_event_array(conv, ephemeral_array, TRUE); + + if (handle_timeline) { + /* parse the timeline events */ + timeline_object = matrix_json_object_get_object_member( + room_data, "timeline"); + timeline_array = matrix_json_object_get_array_member( + timeline_object, "events"); + if(timeline_array != NULL) + _parse_room_event_array(conv, timeline_array, FALSE); + } } @@ -278,8 +283,8 @@ void matrix_sync_parse(PurpleConnection *pc, JsonNode *body, const gchar *room_id = elem->data; JsonObject *room_data = matrix_json_object_get_object_member( joined_rooms, room_id); - purple_debug_info("matrixprpl", "Syncing room %s\n", room_id); - matrix_sync_room(room_id, room_data, pc); + purple_debug_info("matrixprpl", "Syncing room (1)%s\n", room_id); + matrix_sync_room(room_id, room_data, pc, FALSE); } g_list_free(room_ids); } @@ -298,5 +303,51 @@ void matrix_sync_parse(PurpleConnection *pc, JsonNode *body, g_list_free(room_ids); } + /* Handle d2d messages so we can create any e2e sessions needed + * We need to do this after we created rooms/conversations, but before + * we handle timeline events that we might need to decrypt. + */ + JsonObject *to_device = matrix_json_object_get_object_member(rootObj, + "to_device"); + if (to_device) { + JsonArray *events = matrix_json_object_get_array_member(to_device, + "events"); + guint i = 0; + JsonNode *device_event; + while (device_event = matrix_json_array_get_element(events, i++), + device_event) { + JsonObject *event_obj = matrix_json_node_get_object(device_event); + const gchar *event_type; + event_type = matrix_json_object_get_string_member(event_obj, + "type"); + purple_debug_info("matrixprpl", "to_device: Got %s from %s\n", + event_type, + matrix_json_object_get_string_member(event_obj, "sender")); + if (!strcmp(event_type, "m.room.encrypted")) { + matrix_e2e_decrypt_d2d(pc, event_obj); + } else { + } + + } + } + + JsonObject *dev_key_counts = matrix_json_object_get_object_member(rootObj, + "device_one_time_keys_count"); + if (dev_key_counts) { + matrix_e2e_handle_sync_key_counts(pc, dev_key_counts, FALSE); + } + + /* Now go round the rooms again getting the timeline events */ + if (joined_rooms != NULL) { + room_ids = json_object_get_members(joined_rooms); + for(elem = room_ids; elem; elem = elem->next) { + const gchar *room_id = elem->data; + JsonObject *room_data = matrix_json_object_get_object_member( + joined_rooms, room_id); + purple_debug_info("matrixprpl", "Syncing room (2) %s\n", room_id); + matrix_sync_room(room_id, room_data, pc, TRUE); + } + g_list_free(room_ids); + } } |