/** * 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 #include #include #include #include #include "libmatrix.h" #include "matrix-api.h" #include "matrix-e2e.h" #include "matrix-json.h" #include "debug.h" /* json-glib */ #include #include "connection.h" #ifndef MATRIX_NO_E2E #include "olm/olm.h" #include 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" "
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