From 3926a779b90018bf1b8ad34e4120547c90c965d2 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 22 Jul 2026 13:23:21 +0200 Subject: [PATCH 1/3] fix(omemo): Ensure cleanup and prevent command execution on GCM verification failure Delete the decrypted file on disk if GCM decryption or authentication tag validation fails. Before unauthenticated plaintexts were left on disk. Additionally, avoid executing the configured open command template when decryption fails to prevent remote command execution on unauthenticated/corrupted payloads. Signed-off-by: Michael Vetter --- src/tools/aesgcm_download.c | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/tools/aesgcm_download.c b/src/tools/aesgcm_download.c index b7e7cd85db..09353c80d9 100644 --- a/src/tools/aesgcm_download.c +++ b/src/tools/aesgcm_download.c @@ -30,6 +30,7 @@ #include "ui/ui.h" #include "ui/window.h" #include "common.h" +#include "log.h" void* aesgcm_file_get(void* userdata) @@ -116,30 +117,35 @@ aesgcm_file_get(void* userdata) "Downloading '%s' failed: Failed to decrypt " "file (%s).", aesgcm_dl->url, gcry_strerror(crypt_res)); + log_error("OMEMO: AESGCM download file integrity check failed for '%s': %s", + aesgcm_dl->url, gcry_strerror(crypt_res)); + remove(aesgcm_dl->filename); } else { http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ONLINE, ENTRY_COMPLETED, "Downloading '%s': done\nSaved to '%s'", aesgcm_dl->url, aesgcm_dl->filename); win_mark_received(aesgcm_dl->window, aesgcm_dl->id); - } - if (aesgcm_dl->cmd_template != NULL) { - gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template, - aesgcm_dl->filename, - aesgcm_dl->filename); - - // TODO: Log the error. - if (!call_external(argv)) { - http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, - "Downloading '%s' failed: Unable to call " - "command '%s' with file at '%s' (%s).", - aesgcm_dl->url, - aesgcm_dl->cmd_template, - aesgcm_dl->filename, - "TODO: Log the error"); + if (aesgcm_dl->cmd_template != NULL) { + gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template, + aesgcm_dl->filename, + aesgcm_dl->filename); + + if (!call_external(argv)) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR, + "Downloading '%s' failed: Unable to call " + "command '%s' with file at '%s' (%s).", + aesgcm_dl->url, + aesgcm_dl->cmd_template, + aesgcm_dl->filename, + "Failed to run command"); + } + + g_strfreev(argv); } + } - g_strfreev(argv); + if (aesgcm_dl->cmd_template != NULL) { free(aesgcm_dl->cmd_template); } From abc4f89dffaa03ff0d1f4e58f755130e93773fb6 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 22 Jul 2026 13:29:31 +0200 Subject: [PATCH 2/3] fix(omemo): mitigate sensitive memory exposure and add padding bounds check Use libgcrypts secure memory functions for plaintext buffers in OMEMO encryption and decryption. This ensures decrypted chat messages do not reside in cleartext on the standard heap or get swapped to disk. And are zeroed later. Add safety and bounds checks on PKCS#5 padding during decryption to prevent potential OOB reads when processing malformed or truncated ciphertexts. Signed-off-by: Michael Vetter --- src/omemo/crypto.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/omemo/crypto.c b/src/omemo/crypto.c index 66a0bcfeaf..b19db31398 100644 --- a/src/omemo/crypto.c +++ b/src/omemo/crypto.c @@ -210,7 +210,7 @@ omemo_encrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_ assert(FALSE); } - padded_plaintext = malloc(plaintext_len + padding); + padded_plaintext = gcry_malloc_secure(plaintext_len + padding); memcpy(padded_plaintext, plaintext, plaintext_len); memset(padded_plaintext + plaintext_len, padding, padding); @@ -219,7 +219,7 @@ omemo_encrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_ gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, padded_plaintext, plaintext_len + padding); *output = signal_buffer_create(ciphertext, ciphertext_len); - free(padded_plaintext); + gcry_free(padded_plaintext); free(ciphertext); gcry_cipher_close(hd); @@ -268,17 +268,26 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_ } plaintext_len = ciphertext_len; - plaintext = malloc(plaintext_len); + plaintext = gcry_malloc_secure(plaintext_len); gcry_cipher_decrypt(hd, plaintext, plaintext_len, ciphertext, ciphertext_len); switch (cipher) { case SG_CIPHER_AES_CBC_PKCS5: + if (plaintext_len == 0) { + ret = SG_ERR_UNKNOWN; + goto out; + } padding = plaintext[plaintext_len - 1]; break; default: assert(FALSE); } + if (padding == 0 || padding > 16 || padding > plaintext_len) { + ret = SG_ERR_UNKNOWN; + goto out; + } + for (int i = 0; i < padding; i++) { if (plaintext[plaintext_len - 1 - i] != padding) { ret = SG_ERR_UNKNOWN; @@ -289,7 +298,7 @@ omemo_decrypt_func(signal_buffer** output, int cipher, const uint8_t* key, size_ *output = signal_buffer_create(plaintext, plaintext_len - padding); out: - free(plaintext); + gcry_free(plaintext); gcry_cipher_close(hd); From 8a6b40ccdb2d0f37c8f75c668218b8ad3252e87b Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 22 Jul 2026 13:53:17 +0200 Subject: [PATCH 3/3] fix: Zeroize account passwords and GPG passphrases on destruction Scrub sensitive cleartext strings using a `volatile` write pattern before they are released back to the heap. Use it in * Session saved PW * XMPP account passwords in `account_free()` * GPG passphrases and passphrase attempts Signed-off-by: Michael Vetter --- src/common.c | 11 +++++++++++ src/common.h | 2 ++ src/config/account.c | 1 + src/pgp/gpg.c | 2 ++ src/xmpp/session.c | 2 ++ 5 files changed, 18 insertions(+) diff --git a/src/common.c b/src/common.c index 3c694f57c1..c231504341 100644 --- a/src/common.c +++ b/src/common.c @@ -942,3 +942,14 @@ prof_get_version(void) return g_strdup_printf("%s", PACKAGE_VERSION); } } + +void +prof_wipe_string(char* str) +{ + if (str) { + volatile char* p = (volatile char*)str; + while (*p) { + *p++ = 0; + } + } +} diff --git a/src/common.h b/src/common.h index 3c18c56ddf..2a8c621e7d 100644 --- a/src/common.h +++ b/src/common.h @@ -135,6 +135,8 @@ free_keyfile(prof_keyfile_t* keyfile); resource = NULL; \ } while (0) +void prof_wipe_string(char* str); + typedef enum { CONTACT_OFFLINE, CONTACT_ONLINE, diff --git a/src/config/account.c b/src/config/account.c index 8bb9137a30..f7e06bd79f 100644 --- a/src/config/account.c +++ b/src/config/account.c @@ -180,6 +180,7 @@ account_free(ProfAccount* account) free(account->name); free(account->jid); + prof_wipe_string(account->password); free(account->password); free(account->eval_password); free(account->resource); diff --git a/src/pgp/gpg.c b/src/pgp/gpg.c index 3132ada83c..d8bd51e7d7 100644 --- a/src/pgp/gpg.c +++ b/src/pgp/gpg.c @@ -101,10 +101,12 @@ _p_gpg_close(void) key_ac = NULL; if (passphrase) { + prof_wipe_string(passphrase); GFREE_SET_NULL(passphrase); } if (passphrase_attempt) { + prof_wipe_string(passphrase_attempt); GFREE_SET_NULL(passphrase_attempt); } } diff --git a/src/xmpp/session.c b/src/xmpp/session.c index 3c747d4cef..06a3b8d019 100644 --- a/src/xmpp/session.c +++ b/src/xmpp/session.c @@ -616,6 +616,7 @@ static void _session_free_internals(void) { FREE_SET_NULL(saved_account.name); + prof_wipe_string(saved_account.passwd); FREE_SET_NULL(saved_account.passwd); GFREE_SET_NULL(reconnect.altdomain); if (last_moods) { @@ -630,6 +631,7 @@ _session_free_saved_details(void) { FREE_SET_NULL(saved_details.name); FREE_SET_NULL(saved_details.jid); + prof_wipe_string(saved_details.passwd); FREE_SET_NULL(saved_details.passwd); FREE_SET_NULL(saved_details.altdomain); FREE_SET_NULL(saved_details.tls_policy);