aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard van der Hoff <1389908+richvdh@users.noreply.github.com>2018-08-03 19:14:58 +0100
committerGitHub <noreply@github.com>2018-08-03 19:14:58 +0100
commit5a7166a3f54f85793c6b60662f8d12196aeaaeb0 (patch)
tree86b2de5cf9fc7f7b0dd4f505b44c28b030dc6cf1
parent49ea988ce7ca75ae5ea6ae1384707cea4d6c4f35 (diff)
parent1a47e5bce8c0fe95506b01cfa290ff682f88c2ef (diff)
downloadpurple-matrix-5a7166a3f54f85793c6b60662f8d12196aeaaeb0.tar.gz
Merge pull request #70 from penguin42/crypt-push4
E2E support
-rw-r--r--Makefile6
-rw-r--r--Makefile.common5
-rw-r--r--Makefile.mingw7
-rw-r--r--libmatrix.c11
-rw-r--r--libmatrix.h2
-rw-r--r--matrix-api.c46
-rw-r--r--matrix-api.h24
-rw-r--r--matrix-connection.c10
-rw-r--r--matrix-connection.h3
-rw-r--r--matrix-e2e.c1923
-rw-r--r--matrix-e2e.h41
-rw-r--r--matrix-json.c239
-rw-r--r--matrix-json.h15
-rw-r--r--matrix-room.c126
-rw-r--r--matrix-sync.c73
15 files changed, 2457 insertions, 74 deletions
diff --git a/Makefile b/Makefile
index 97b88e9..f914496 100644
--- a/Makefile
+++ b/Makefile
@@ -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);
+ }
}