From 4d49c2b7463f91a13695a957b498afb760404475 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Sat, 7 Mar 2026 00:31:44 +0100 Subject: [PATCH] feat: Provide descriptive fallback messages for OMEMO decryption failures If an incoming OMEMO message failed to decrypt (due to missing session keys or untrusted identities), Profanity would fall back to displaying the raw XMPP body. This usually contained a generic string like "This message is encrypted with OMEMO,". We wrote the detailed reason in the debug logs but the user only saw the fallback message and probably wondered *why* the message wasn't displayed properly. I'm unsure if we should display the fallback message as well though. --- src/omemo/omemo.c | 34 ++++++++++++- src/omemo/omemo.h | 3 +- src/xmpp/message.c | 76 +++++++++++++++++++++++++++--- src/xmpp/omemo.c | 32 ++++++++++++- src/xmpp/omemo.h | 3 +- src/xmpp/xmpp.h | 12 +++++ tests/unittests/omemo/stub_omemo.c | 21 +++++++++ 7 files changed, 169 insertions(+), 12 deletions(-) diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c index 2fe05234..98aa8af6 100644 --- a/src/omemo/omemo.c +++ b/src/omemo/omemo.c @@ -1014,13 +1014,21 @@ out: char* omemo_on_message_recv(const char* const from_jid, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, - const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted) + const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted, omemo_error_t* error) { unsigned char* plaintext = NULL; auto_jid Jid* sender = NULL; auto_jid Jid* from = jid_create(from_jid); + + if (error) { + *error = OMEMO_ERR_NONE; + } + if (!from) { log_error("[OMEMO][RECV] Invalid jid %s", from_jid); + if (error) { + *error = OMEMO_ERR_INVALID_JID; + } return NULL; } @@ -1036,6 +1044,9 @@ omemo_on_message_recv(const char* const from_jid, uint32_t sid, if (!key) { log_warning("[OMEMO][RECV] received a message with no corresponding key"); + if (error) { + *error = OMEMO_ERR_NO_KEY; + } return NULL; } @@ -1052,6 +1063,9 @@ omemo_on_message_recv(const char* const from_jid, uint32_t sid, g_list_free(roster); if (!sender) { log_warning("[OMEMO][RECV] cannot find MUC message sender fulljid"); + if (error) { + *error = OMEMO_ERR_MUC_SENDER_NOT_FOUND; + } return NULL; } } else { @@ -1069,6 +1083,9 @@ omemo_on_message_recv(const char* const from_jid, uint32_t sid, res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); if (res != 0) { log_error("[OMEMO][RECV] cannot create session cipher"); + if (error) { + *error = OMEMO_ERR_NO_SESSION; + } return NULL; } @@ -1127,13 +1144,23 @@ omemo_on_message_recv(const char* const from_jid, uint32_t sid, session_cipher_free(cipher); if (res != 0) { - log_error("[OMEMO][RECV] cannot decrypt message key"); + log_error("[OMEMO][RECV] cannot decrypt message key: %d", res); + if (error) { + if (!*trusted) { + *error = OMEMO_ERR_NOT_TRUSTED; + } else { + *error = OMEMO_ERR_DECRYPT_FAILED; + } + } return NULL; } if (signal_buffer_len(plaintext_key) != AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH) { log_error("[OMEMO][RECV] invalid key length"); signal_buffer_free(plaintext_key); + if (error) { + *error = OMEMO_ERR_DECRYPT_FAILED; + } return NULL; } @@ -1146,6 +1173,9 @@ omemo_on_message_recv(const char* const from_jid, uint32_t sid, if (res != 0) { log_error("[OMEMO][RECV] cannot decrypt message: %s", gcry_strerror(res)); free(plaintext); + if (error) { + *error = OMEMO_ERR_DECRYPT_FAILED; + } return NULL; } diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h index 47ebaaf7..62d28197 100644 --- a/src/omemo/omemo.h +++ b/src/omemo/omemo.h @@ -98,8 +98,9 @@ void omemo_start_muc_sessions(const char* const roomjid); void omemo_start_device_session(const char* const jid, uint32_t device_id, GList* prekeys, uint32_t signed_prekey_id, const unsigned char* const signed_prekey, size_t signed_prekey_len, const unsigned char* const signature, size_t signature_len, const unsigned char* const identity_key, size_t identity_key_len); gboolean omemo_loaded(void); + char* omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id); -char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted); +char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted, omemo_error_t* error); char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res); gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment); diff --git a/src/xmpp/message.c b/src/xmpp/message.c index bc07b073..4562cc87 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -356,6 +356,7 @@ message_init(void) ProfMessage* message = g_new0(ProfMessage, 1); message->enc = PROF_MSG_ENC_NONE; + message->omemo_err = OMEMO_ERR_NONE; message->trusted = true; message->type = PROF_MSG_TYPE_UNINITIALIZED; @@ -1110,8 +1111,29 @@ _handle_groupchat(xmpp_stanza_t* const stanza) // check omemo encryption #ifdef HAVE_OMEMO - message->plain = omemo_receive_message(stanza, &message->trusted); - if (message->plain != NULL) { + 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) { + switch (message->omemo_err) { + case OMEMO_ERR_NO_KEY: + message->plain = g_strdup("OMEMO message received but no key for this device found."); + break; + case OMEMO_ERR_NOT_TRUSTED: + message->plain = g_strdup("OMEMO message received but sender identity is untrusted."); + break; + case OMEMO_ERR_NO_SESSION: + message->plain = g_strdup("OMEMO message received but no session found. Try '/omemo start'."); + break; + case OMEMO_ERR_DECRYPT_FAILED: + message->plain = g_strdup("OMEMO message received but decryption failed."); + break; + default: + message->plain = g_strdup("OMEMO message received but could not be decrypted."); + break; + } + } + } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; } #endif @@ -1270,8 +1292,29 @@ _handle_muc_private_message(xmpp_stanza_t* const stanza) // check omemo encryption #ifdef HAVE_OMEMO - message->plain = omemo_receive_message(stanza, &message->trusted); - if (message->plain != NULL) { + 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) { + switch (message->omemo_err) { + case OMEMO_ERR_NO_KEY: + message->plain = g_strdup("OMEMO message received but no key for this device found."); + break; + case OMEMO_ERR_NOT_TRUSTED: + message->plain = g_strdup("OMEMO message received but sender identity is untrusted."); + break; + case OMEMO_ERR_NO_SESSION: + message->plain = g_strdup("OMEMO message received but no session found. Try '/omemo start'."); + break; + case OMEMO_ERR_DECRYPT_FAILED: + message->plain = g_strdup("OMEMO message received but decryption failed."); + break; + default: + message->plain = g_strdup("OMEMO message received but could not be decrypted."); + break; + } + } + } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; } #endif @@ -1437,8 +1480,29 @@ _handle_chat(xmpp_stanza_t* const stanza, gboolean is_mam, gboolean is_carbon, c #ifdef HAVE_OMEMO // check omemo encryption - message->plain = omemo_receive_message(stanza, &message->trusted); - if (message->plain != NULL) { + 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) { + switch (message->omemo_err) { + case OMEMO_ERR_NO_KEY: + message->plain = g_strdup("OMEMO message received but no key for this device found."); + break; + case OMEMO_ERR_NOT_TRUSTED: + message->plain = g_strdup("OMEMO message received but sender identity is untrusted."); + break; + case OMEMO_ERR_NO_SESSION: + message->plain = g_strdup("OMEMO message received but no session found. Try '/omemo start'."); + break; + case OMEMO_ERR_DECRYPT_FAILED: + message->plain = g_strdup("OMEMO message received but decryption failed."); + break; + default: + message->plain = g_strdup("OMEMO message received but could not be decrypted."); + break; + } + } + } else if (message->plain != NULL) { message->enc = PROF_MSG_ENC_OMEMO; } #endif diff --git a/src/xmpp/omemo.c b/src/xmpp/omemo.c index d41588a6..d53b83d7 100644 --- a/src/xmpp/omemo.c +++ b/src/xmpp/omemo.c @@ -350,7 +350,7 @@ out: } char* -omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted) +omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted, omemo_error_t* error) { char* plaintext = NULL; const char* type = xmpp_stanza_get_type(stanza); @@ -360,6 +360,10 @@ omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted) char* iv_text = NULL; char* payload_text = NULL; + if (error) { + *error = OMEMO_ERR_NONE; + } + xmpp_stanza_t* encrypted = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_OMEMO); if (!encrypted) { return NULL; @@ -367,40 +371,64 @@ omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted) xmpp_stanza_t* header = xmpp_stanza_get_child_by_name(encrypted, "header"); if (!header) { + if (error) { + *error = OMEMO_ERR_OTHER; + } return NULL; } const char* sid_text = xmpp_stanza_get_attribute(header, "sid"); if (!sid_text) { + if (error) { + *error = OMEMO_ERR_OTHER; + } return NULL; } uint32_t sid = strtoul(sid_text, NULL, 10); xmpp_stanza_t* iv = xmpp_stanza_get_child_by_name(header, "iv"); if (!iv) { + if (error) { + *error = OMEMO_ERR_OTHER; + } return NULL; } iv_text = xmpp_stanza_get_text(iv); if (!iv_text) { + if (error) { + *error = OMEMO_ERR_OTHER; + } return NULL; } size_t iv_len; iv_raw = g_base64_decode(iv_text, &iv_len); if (!iv_raw) { + if (error) { + *error = OMEMO_ERR_OTHER; + } return NULL; } xmpp_stanza_t* payload = xmpp_stanza_get_child_by_name(encrypted, "payload"); if (!payload) { + if (error) { + *error = OMEMO_ERR_OTHER; + } goto quit; } payload_text = xmpp_stanza_get_text(payload); if (!payload_text) { + if (error) { + *error = OMEMO_ERR_OTHER; + } goto quit; } size_t payload_len; payload_raw = g_base64_decode(payload_text, &payload_len); if (!payload_raw) { + if (error) { + *error = OMEMO_ERR_OTHER; + } goto quit; } @@ -439,7 +467,7 @@ omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted) plaintext = omemo_on_message_recv(from, sid, iv_raw, iv_len, keys, payload_raw, payload_len, - g_strcmp0(type, STANZA_TYPE_GROUPCHAT) == 0, trusted); + g_strcmp0(type, STANZA_TYPE_GROUPCHAT) == 0, trusted, error); if (keys) { g_list_free_full(keys, (GDestroyNotify)omemo_key_free); diff --git a/src/xmpp/omemo.h b/src/xmpp/omemo.h index 9f29cb96..e26029e1 100644 --- a/src/xmpp/omemo.h +++ b/src/xmpp/omemo.h @@ -44,4 +44,5 @@ void omemo_devicelist_request(const char* const jid); void omemo_bundle_publish(gboolean first); void omemo_bundle_request(const char* const jid, uint32_t device_id, ProfIqCallback func, ProfIqFreeCallback free_func, void* userdata); int omemo_start_device_session_handle_bundle(xmpp_stanza_t* const stanza, void* const userdata); -char* omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted); + +char* omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted, omemo_error_t* error); diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h index 5fd214c9..620fb66e 100644 --- a/src/xmpp/xmpp.h +++ b/src/xmpp/xmpp.h @@ -150,6 +150,17 @@ typedef enum { PROF_MSG_TYPE_MUCPM } prof_msg_type_t; +typedef enum { + OMEMO_ERR_NONE = 0, + OMEMO_ERR_NO_KEY, + OMEMO_ERR_NOT_TRUSTED, + OMEMO_ERR_NO_SESSION, + OMEMO_ERR_DECRYPT_FAILED, + OMEMO_ERR_INVALID_JID, + OMEMO_ERR_MUC_SENDER_NOT_FOUND, + OMEMO_ERR_OTHER +} omemo_error_t; + typedef struct prof_message_t { Jid* from_jid; @@ -172,6 +183,7 @@ typedef struct prof_message_t char* plain; GDateTime* timestamp; prof_enc_t enc; + omemo_error_t omemo_err; gboolean trusted; gboolean is_mam; prof_msg_type_t type; diff --git a/tests/unittests/omemo/stub_omemo.c b/tests/unittests/omemo/stub_omemo.c index c445eb23..7d1afd5f 100644 --- a/tests/unittests/omemo/stub_omemo.c +++ b/tests/unittests/omemo/stub_omemo.c @@ -129,6 +129,27 @@ omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res) }; void omemo_free(void* a) {}; +char* +omemo_on_message_recv(const char* const from, uint32_t sid, + const unsigned char* const iv, size_t iv_len, + GList* keys, const unsigned char* const payload, + size_t payload_len, gboolean muc, gboolean* trusted, omemo_error_t* error) +{ + if (error) { + *error = OMEMO_ERR_NONE; + } + return NULL; +} + +char* +omemo_receive_message(xmpp_stanza_t* const stanza, gboolean* trusted, omemo_error_t* error) +{ + if (error) { + *error = OMEMO_ERR_NONE; + } + return NULL; +} + uint32_t omemo_device_id() {