From 8e0139708fa8802350dfcd8036f93a7b45dc96a5 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 20 Jul 2026 10:48:28 +0200 Subject: [PATCH] fix(omemo): Correctly display our own sent OMEMO messages in MUCs and 1:1 chats We log our own messages when we send them. Thus we don't have the stable archive ID that the server assigns to it. When we get MAM history (or standard MUC history), we look for the ID, think the message is new and thus display a decryption error because we can't decrypt the message due to double-ratchet. Unify database update and retrieval functions using the prof_msg_type_t enum to stay DRY, and support both MUC and 1:1 chats. Update stable ID for self-reflected messages in our database. So later we can identify them correctly and load them from the DB. Signed-off-by: Michael Vetter --- src/database.c | 87 ++++++++++++++++++++++++ src/database.h | 2 + src/event/server_events.c | 20 +++++- src/xmpp/message.c | 48 +++++++++++-- tests/unittests/database/stub_database.c | 12 ++++ 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/src/database.c b/src/database.c index 2f3fcb66af..4991fe1f1f 100644 --- a/src/database.c +++ b/src/database.c @@ -896,3 +896,90 @@ _migrate_to_v3(void) return FALSE; } + +gboolean +log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id) +{ + if (!g_chatlog_database || !stanza_id || !archive_id || !room_or_contact_jid) { + return FALSE; + } + + const char* our_barejid = connection_get_jid()->barejid; + sqlite3_stmt* stmt = NULL; + gboolean success = FALSE; + const char* type_str = _get_message_type_str(type); + + if (!type_str) { + return FALSE; + } + + auto_sqlite char* query = sqlite3_mprintf( + "UPDATE `ChatLogs` " + "SET `archive_id` = %Q " + "WHERE `stanza_id` = %Q " + " AND `from_jid` = %Q " + " AND `to_jid` = %Q " + " AND `type` = %Q " + " AND `archive_id` IS NULL", + archive_id, stanza_id, our_barejid, room_or_contact_jid, type_str); + + if (query) { + if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL)) { + if (sqlite3_step(stmt) == SQLITE_DONE) { + success = TRUE; + } + sqlite3_finalize(stmt); + } + } + + return success; +} + +char* +log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id) +{ + if (!g_chatlog_database || !from_jid || !to_jid) { + return NULL; + } + + sqlite3_stmt* stmt = NULL; + char* decrypted_text = NULL; + auto_sqlite char* query = NULL; + const char* type_str = _get_message_type_str(type); + + if (!type_str) { + return NULL; + } + + if (stanza_id && strlen(stanza_id) > 0) { + query = sqlite3_mprintf( + "SELECT `message` FROM `ChatLogs` " + "WHERE `stanza_id` = %Q " + " AND `type` = %Q " + " AND ((`from_jid` = %Q AND `to_jid` = %Q) OR (`from_jid` = %Q AND `to_jid` = %Q)) " + "LIMIT 1", + stanza_id, type_str, from_jid, to_jid, to_jid, from_jid); + } else if (archive_id && strlen(archive_id) > 0) { + query = sqlite3_mprintf( + "SELECT `message` FROM `ChatLogs` " + "WHERE `archive_id` = %Q " + " AND `type` = %Q " + " AND ((`from_jid` = %Q AND `to_jid` = %Q) OR (`from_jid` = %Q AND `to_jid` = %Q)) " + "LIMIT 1", + archive_id, type_str, from_jid, to_jid, to_jid, from_jid); + } + + if (query) { + if (SQLITE_OK == sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL)) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* text = (const char*)sqlite3_column_text(stmt, 0); + if (text) { + decrypted_text = strdup(text); + } + } + sqlite3_finalize(stmt); + } + } + + return decrypted_text; +} diff --git a/src/database.h b/src/database.h index 129f25eab6..ae24b52176 100644 --- a/src/database.h +++ b/src/database.h @@ -27,6 +27,8 @@ int log_database_get_chat_count(const gchar* const contact_barejid, const char* int log_database_get_muc_count(const gchar* const room_jid, const char* start_time, const char* end_time); ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last); ProfMessage* log_database_get_limits_info_muc(const gchar* const room_jid, gboolean is_last); +gboolean log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id); +char* log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id); void log_database_close(void); #endif // DATABASE_H diff --git a/src/event/server_events.c b/src/event/server_events.c index 28752dcf44..26becb6cc2 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -330,8 +330,14 @@ sv_ev_room_message(ProfMessage* message) gboolean is_duplicate = FALSE; // only log message not coming from this client (but maybe same account, different client) - // our messages are logged when outgoing - if (!(g_strcmp0(mynick, message->from_jid->resourcepart) == 0 && message_is_sent_by_us(message, TRUE))) { + // our messages are logged when outgoing, and we update them when reflected + gboolean is_self_reflection = (g_strcmp0(mynick, message->from_jid->resourcepart) == 0 && message_is_sent_by_us(message, TRUE)); + + if (is_self_reflection) { + if (message->id && message->stanzaid) { + log_database_update_archive_id(PROF_MSG_TYPE_MUC, message->from_jid->barejid, message->id, message->stanzaid); + } + } else { if (!_log_muc(message)) { is_duplicate = TRUE; } @@ -651,6 +657,16 @@ _sv_ev_incoming_plain(ProfChatWin* chatwin, gboolean new_win, ProfMessage* messa void sv_ev_incoming_message(ProfMessage* message) { + gboolean is_self_reflection = (message->is_mam && equals_our_barejid(message->from_jid->barejid)); + + if (is_self_reflection) { + if (message->id && message->stanzaid && message->to_jid) { + log_database_update_archive_id(PROF_MSG_TYPE_CHAT, message->to_jid->barejid, message->id, message->stanzaid); + } + rosterwin_roster(); + return; + } + gboolean new_win = FALSE; ProfChatWin* chatwin; char* looking_for_jid = message->from_jid->barejid; diff --git a/src/xmpp/message.c b/src/xmpp/message.c index a9293d5587..f01d22497f 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -35,6 +35,7 @@ #include "xmpp/xmpp.h" #include "xmpp/form.h" #include "xmpp/iq.h" +#include "database.h" #ifdef HAVE_OMEMO #include "xmpp/omemo.h" @@ -1136,9 +1137,27 @@ _handle_groupchat(xmpp_stanza_t* const stanza, gboolean is_mam, const char* resu #ifdef HAVE_OMEMO message->plain = omemo_receive_message(stanza, &message->trusted, &message->omemo_err); if (message->omemo_err != OMEMO_ERR_NONE) { - message->enc = PROF_MSG_ENC_OMEMO; - if (message->plain == NULL) { - message->plain = omemo_error_to_string(message->omemo_err); + // Get previously decrypted plaintext from database + // Historical replays (MAM and standard delayed MUC history) fail decryption due to OMEMO key deletion. + // Realtime messages skip this. + char* db_decrypted = log_database_get_decrypted_message( + PROF_MSG_TYPE_MUC, + from_jid->barejid, + connection_get_jid()->barejid, + message->id, + message->stanzaid); + if (db_decrypted) { + if (message->plain) { + free(message->plain); + } + message->plain = db_decrypted; + message->omemo_err = OMEMO_ERR_NONE; + message->enc = PROF_MSG_ENC_OMEMO; + } else { + message->enc = PROF_MSG_ENC_OMEMO; + if (message->plain == NULL) { + message->plain = omemo_error_to_string(message->omemo_err); + } } } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; @@ -1487,9 +1506,26 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c // check omemo encryption message->plain = omemo_receive_message(stanza, &message->trusted, &message->omemo_err); if (message->omemo_err != OMEMO_ERR_NONE) { - message->enc = PROF_MSG_ENC_OMEMO; - if (message->plain == NULL) { - message->plain = omemo_error_to_string(message->omemo_err); + // Get previously decrypted plaintext from database + // Historical replays (MAM) fail decryption due to OMEMO key deletion/self-reflections. + char* db_decrypted = log_database_get_decrypted_message( + PROF_MSG_TYPE_CHAT, + message->from_jid->barejid, + message->to_jid ? message->to_jid->barejid : connection_get_jid()->barejid, + message->id, + message->stanzaid); + if (db_decrypted) { + if (message->plain) { + free(message->plain); + } + message->plain = db_decrypted; + message->omemo_err = OMEMO_ERR_NONE; + message->enc = PROF_MSG_ENC_OMEMO; + } else { + message->enc = PROF_MSG_ENC_OMEMO; + if (message->plain == NULL) { + message->plain = omemo_error_to_string(message->omemo_err); + } } } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; diff --git a/tests/unittests/database/stub_database.c b/tests/unittests/database/stub_database.c index cedfc16c84..8de1de65b0 100644 --- a/tests/unittests/database/stub_database.c +++ b/tests/unittests/database/stub_database.c @@ -37,3 +37,15 @@ void log_database_close(void) { } + +gboolean +log_database_update_archive_id(const prof_msg_type_t type, const char* const room_or_contact_jid, const char* const stanza_id, const char* const archive_id) +{ + return FALSE; +} + +char* +log_database_get_decrypted_message(const prof_msg_type_t type, const char* const from_jid, const char* const to_jid, const char* const stanza_id, const char* const archive_id) +{ + return NULL; +}