From a643dbc0c15b0b40fc16f144757295c39c0c5920 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Tue, 19 May 2026 23:08:32 +0200 Subject: [PATCH 1/5] fix(otr): prevent sending message when contact goes offline Don't automatically end an OTR session when a contact disconnects. Now we have clearer separation: `chatwin->is_otr` tracks whether the user wants to use OTR. `otr_is_secure` tracks whether the encryption is still activated. We can now use this when sending a message to make sure we don't send unencrypted text in case the contact goes offline. We will display a hint that the user first needs to stop to use otr by disabling it (`/otr end`). This as well improves the situation when the connection flickers (contact goes offline/online). Fixes: https://github.com/profanity-im/profanity/issues/952 Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 2 +- src/event/server_events.c | 10 +----- src/otr/otr.c | 8 ++++- src/plugins/api.c | 2 +- src/ui/chatwin.c | 6 ++-- src/ui/titlebar.c | 62 +++++++++++++++++++++++++++--------- src/ui/ui.h | 2 +- tests/unittests/ui/stub_ui.c | 2 +- 8 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 25649a953..0c0f04d12 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -8000,7 +8000,7 @@ cmd_otr_end(ProfWin* window, const char* const command, gchar** args) return TRUE; } - chatwin_otr_unsecured(chatwin); + chatwin_otr_unsecured(chatwin, TRUE); otr_end_session(chatwin->barejid); return TRUE; #else diff --git a/src/event/server_events.c b/src/event/server_events.c index f7bce3c6c..871a55739 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -195,7 +195,7 @@ sv_ev_lost_connection(void) char* barejid = curr->data; ProfChatWin* chatwin = wins_get_chat(barejid); if (chatwin && otr_is_secure(barejid)) { - chatwin_otr_unsecured(chatwin); + chatwin_otr_unsecured(chatwin, FALSE); otr_end_session(barejid); } curr = g_slist_next(curr); @@ -798,14 +798,6 @@ sv_ev_contact_offline(char* barejid, char* resource, char* status) ui_contact_offline(barejid, resource, status); } -#ifdef HAVE_LIBOTR - ProfChatWin* chatwin = wins_get_chat(barejid); - if (chatwin && otr_is_secure(barejid)) { - chatwin_otr_unsecured(chatwin); - otr_end_session(chatwin->barejid); - } -#endif - rosterwin_roster(); chat_session_remove(barejid); } diff --git a/src/otr/otr.c b/src/otr/otr.c index c4e178bc1..e5b536f1c 100644 --- a/src/otr/otr.c +++ b/src/otr/otr.c @@ -318,6 +318,12 @@ otr_on_message_send(ProfChatWin* chatwin, const char* const message, gboolean re return TRUE; } + if (chatwin->is_otr && !otr_is_secure(chatwin->barejid)) { + win_println((ProfWin*)chatwin, THEME_ERROR, "-", "%s", "Failed to send message: OTR session is not active."); + win_println((ProfWin*)chatwin, THEME_DEFAULT, "-", "%s", "Use '/otr end' to intentionally send unencrypted messages."); + return TRUE; + } + // tag and send for policy opportunistic if (policy == PROF_OTRPOLICY_OPPORTUNISTIC) { auto_char char* otr_tagged_msg = otr_tag_message(message); @@ -691,7 +697,7 @@ otr_decrypt_message(const char* const from, const char* const message, gboolean* otrl_context_force_plaintext(context); ProfChatWin* chatwin = wins_get_chat(from); if (chatwin) { - chatwin_otr_unsecured(chatwin); + chatwin_otr_unsecured(chatwin, FALSE); } } } diff --git a/src/plugins/api.c b/src/plugins/api.c index d3b8dcebd..32a7dad4e 100644 --- a/src/plugins/api.c +++ b/src/plugins/api.c @@ -519,7 +519,7 @@ api_encryption_reset(const char* const barejid) #ifdef HAVE_LIBOTR if (chatwin->is_otr) { - chatwin_otr_unsecured(chatwin); + chatwin_otr_unsecured(chatwin, TRUE); otr_end_session(chatwin->barejid); } #endif diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c index 2105556f7..007d2102c 100644 --- a/src/ui/chatwin.c +++ b/src/ui/chatwin.c @@ -169,11 +169,13 @@ chatwin_otr_secured(ProfChatWin* chatwin, gboolean trusted) } void -chatwin_otr_unsecured(ProfChatWin* chatwin) +chatwin_otr_unsecured(ProfChatWin* chatwin, gboolean disable_otr) { assert(chatwin != NULL); - chatwin->is_otr = FALSE; + if (disable_otr) { + chatwin->is_otr = FALSE; + } chatwin->otr_is_trusted = FALSE; ProfWin* window = (ProfWin*)chatwin; diff --git a/src/ui/titlebar.c b/src/ui/titlebar.c index 119458216..07a5d2be4 100644 --- a/src/ui/titlebar.c +++ b/src/ui/titlebar.c @@ -28,6 +28,9 @@ #endif #include "xmpp/roster_list.h" #include "xmpp/chat_session.h" +#ifdef HAVE_LIBOTR +#include "otr/otr.h" +#endif static WINDOW* win; static contact_presence_t current_presence; @@ -454,8 +457,10 @@ _show_privacy(ProfChatWin* chatwin) int bracket_attrs = theme_attrs(THEME_TITLE_BRACKET); int encrypted_attrs = theme_attrs(THEME_TITLE_ENCRYPTED); int unencrypted_attrs = theme_attrs(THEME_TITLE_UNENCRYPTED); +#if defined(HAVE_LIBOTR) || defined(HAVE_OMEMO) int trusted_attrs = theme_attrs(THEME_TITLE_TRUSTED); int untrusted_attrs = theme_attrs(THEME_TITLE_UNTRUSTED); +#endif if (chatwin->enctext) { wprintw(win, " "); @@ -491,34 +496,61 @@ _show_privacy(ProfChatWin* chatwin) wprintw(win, "["); wattroff(win, bracket_attrs); wattron(win, encrypted_attrs); - wprintw(win, "OTR"); - wattroff(win, encrypted_attrs); - wattron(win, bracket_attrs); - wprintw(win, "]"); - wattroff(win, bracket_attrs); - if (chatwin->otr_is_trusted) { - wprintw(win, " "); +#ifdef HAVE_LIBOTR + if (otr_is_secure(chatwin->barejid)) { + wprintw(win, "OTR"); + wattroff(win, encrypted_attrs); wattron(win, bracket_attrs); - wprintw(win, "["); + wprintw(win, "]"); wattroff(win, bracket_attrs); - wattron(win, trusted_attrs); - wprintw(win, "trusted"); - wattroff(win, trusted_attrs); + if (chatwin->otr_is_trusted) { + wprintw(win, " "); + wattron(win, bracket_attrs); + wprintw(win, "["); + wattroff(win, bracket_attrs); + wattron(win, trusted_attrs); + wprintw(win, "trusted"); + wattroff(win, trusted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); + } else { + wprintw(win, " "); + wattron(win, bracket_attrs); + wprintw(win, "["); + wattroff(win, bracket_attrs); + wattron(win, untrusted_attrs); + wprintw(win, "untrusted"); + wattroff(win, untrusted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); + } + } else { + wprintw(win, "OTR"); + wattroff(win, encrypted_attrs); wattron(win, bracket_attrs); wprintw(win, "]"); wattroff(win, bracket_attrs); - } else { + wprintw(win, " "); wattron(win, bracket_attrs); wprintw(win, "["); wattroff(win, bracket_attrs); - wattron(win, untrusted_attrs); - wprintw(win, "untrusted"); - wattroff(win, untrusted_attrs); + wattron(win, unencrypted_attrs); + wprintw(win, "inactive"); + wattroff(win, unencrypted_attrs); wattron(win, bracket_attrs); wprintw(win, "]"); wattroff(win, bracket_attrs); } +#else + wprintw(win, "OTR"); + wattroff(win, encrypted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); +#endif return; } diff --git a/src/ui/ui.h b/src/ui/ui.h index 31b0a961d..e4e7983ca 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -112,7 +112,7 @@ void chatwin_contact_offline(ProfChatWin* chatwin, char* resource, char* status) gchar* chatwin_get_string(ProfChatWin* chatwin); #ifdef HAVE_LIBOTR void chatwin_otr_secured(ProfChatWin* chatwin, gboolean trusted); -void chatwin_otr_unsecured(ProfChatWin* chatwin); +void chatwin_otr_unsecured(ProfChatWin* chatwin, gboolean disable_otr); void chatwin_otr_trust(ProfChatWin* chatwin); void chatwin_otr_untrust(ProfChatWin* chatwin); void chatwin_otr_smp_event(ProfChatWin* chatwin, prof_otr_smp_event_t event, void* data); diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c index 39956f1e1..b909920bb 100644 --- a/tests/unittests/ui/stub_ui.c +++ b/tests/unittests/ui/stub_ui.c @@ -97,7 +97,7 @@ chatwin_otr_secured(ProfChatWin* chatwin, gboolean trusted) { } void -chatwin_otr_unsecured(ProfChatWin* chatwin) +chatwin_otr_unsecured(ProfChatWin* chatwin, gboolean disable_otr) { } void From 83936e81b844005a24d11d800d574205d5e4e03d Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 22:17:02 +0200 Subject: [PATCH 2/5] docs: Explain OTR titlebar status in manpage Signed-off-by: Michael Vetter --- docs/profanity.1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/profanity.1 b/docs/profanity.1 index 556b7ed2f..5b9669034 100755 --- a/docs/profanity.1 +++ b/docs/profanity.1 @@ -156,6 +156,14 @@ OMEMO is the only encryption option in Profanity that also supports encryption f .TP .BR OTR OTR (/otr) is defined in XEP-0364. It uses a combination of the AES symmetric-key algorithm, the Diffie–Hellman key exchange, and the SHA-1 hash function. It offers deniable authentication and Perfect Forward Secrecy. To initialize a session both clients need to be online at the same time. A session is between two clients, so multiclient chats are not working. Which is a feature not a bug. OTR does by design not work for MUCs. +.PP +The OTR session status is shown in the title bar of the chat window: +.br +- \fB[OTR] [inactive]\fR: OTR is enabled but the secure session is currently inactive or ended (can happen when the contact went offline). Outgoing messages are blocked from being sent in plaintext. You must start OTR again or use \fB/otr end\fR to disable the safety guard. +.br +- \fB[OTR] [untrusted]\fR: OTR session is active but the contact's fingerprint is unverified. +.br +- \fB[OTR] [trusted]\fR: OTR session is active and the contact's identity is authenticated. .TP .BR OpenPGP OpenPGP (/pgp) is defined in XEP-0027. Is uses a public and secret key. It is also known as Legacy OpenPGP and has been deprecated. It doesn't provide protection against replay attacks. MUCs and file transfer via HTTP upload are not specified and thus not supported. From 06dff998d11ac1d597e1cfe9fa5befa794d20e6a Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 22:33:48 +0200 Subject: [PATCH 3/5] docs: Explain OTR titlebar meanings in /help otr Signed-off-by: Michael Vetter --- src/command/cmd_defs.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 9e13450d8..d3b1f6cce 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1808,7 +1808,11 @@ static const struct cmd_t command_defs[] = { "/otr char ", "/otr sendfile on|off") CMD_DESC( - "Off The Record (OTR) commands to manage keys, and perform OTR encryption during chat sessions.") + "Off The Record (OTR) commands to manage keys, and perform OTR encryption during chat sessions.\n" + "The title bar will show the OTR session status:\n" + "[OTR] [inactive]: session inactive, plaintext blocked\n" + "[OTR] [untrusted]: session active, unverified fingerprint\n" + "[OTR] [trusted]: session active, authenticated.") CMD_ARGS( { "libver", "Show which version of the libotr library is being used." }, { "gen", "Generate your private key." }, From e463d81094991dae4a7dbcff25b0f23002f84113 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 22:34:14 +0200 Subject: [PATCH 4/5] chore: Improve OMEMO help formatting Signed-off-by: Michael Vetter --- src/command/cmd_defs.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index d3b1f6cce..c0d0bbd32 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -2332,8 +2332,8 @@ static const struct cmd_t command_defs[] = { CMD_DESC( "OMEMO commands to manage keys, and perform encryption during chat sessions.\n" "The title bar will show the OMEMO session status:\n" - "[OMEMO][trusted] - All active devices for the contact are trusted.\n" - "[OMEMO][untrusted] - One or more active devices for the contact are untrusted.\n") + "[OMEMO] [trusted] - All active devices for the contact are trusted.\n" + "[OMEMO] [untrusted] - One or more active devices for the contact are untrusted.") CMD_ARGS( { "gen", "Generate OMEMO cryptographic materials for current account." }, { "start []", "Start an OMEMO session with contact, or current recipient if omitted." }, From 782a8f9abc9f9bb2ccece2f20cd4a3ad6fb67fc4 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 22:41:35 +0200 Subject: [PATCH 5/5] docs: Explain OMEMO titlebar status in manpage Signed-off-by: Michael Vetter --- docs/profanity.1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/profanity.1 b/docs/profanity.1 index 5b9669034..f79dd62f4 100755 --- a/docs/profanity.1 +++ b/docs/profanity.1 @@ -153,6 +153,12 @@ You can only enable one of them per correspondent at a time. OMEMO (/omemo) is defined in XEP-0384. It uses an implementation of the Signal protocol for key management and to synchronize messages among different clients. It works even when other clients are offline. And offers Perfect Forward Secrecy and Plausible deniability. Servers need to support PEP (XEP-0163). We implement the "siacs" version of OMEMO. Version 0.3.0, which is currently the widest adopted option. OMEMO is the only encryption option in Profanity that also supports encryption for MUCs (XEP-0045) and file transfer via HTTP upload (XEP-0363). +.PP +The OMEMO session status is shown in the title bar of the chat window: +.br +- \fB[OMEMO] [untrusted]\fR: OMEMO is enabled, but one or more active devices for the contact are untrusted. +.br +- \fB[OMEMO] [trusted]\fR: OMEMO is enabled, and all active devices for the contact are trusted. .TP .BR OTR OTR (/otr) is defined in XEP-0364. It uses a combination of the AES symmetric-key algorithm, the Diffie–Hellman key exchange, and the SHA-1 hash function. It offers deniable authentication and Perfect Forward Secrecy. To initialize a session both clients need to be online at the same time. A session is between two clients, so multiclient chats are not working. Which is a feature not a bug. OTR does by design not work for MUCs.