diff --git a/Makefile.am b/Makefile.am index e9a1c365..872c0b02 100644 --- a/Makefile.am +++ b/Makefile.am @@ -166,6 +166,7 @@ unittest_sources = \ tests/unittests/test_cmd_disconnect.c tests/unittests/test_cmd_disconnect.h \ tests/unittests/test_callbacks.c tests/unittests/test_callbacks.h \ tests/unittests/test_plugins_disco.c tests/unittests/test_plugins_disco.h \ + tests/unittests/test_forced_encryption.c tests/unittests/test_forced_encryption.h \ tests/unittests/unittests.c functionaltest_sources = \ diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 62faf705..0caba5ba 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -136,6 +136,7 @@ static char* _mood_autocomplete(ProfWin* window, const char* const input, gboole static char* _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous); +static char* _force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context); @@ -296,6 +297,8 @@ static Autocomplete vcard_name_ac; static Autocomplete vcard_set_param_ac; static Autocomplete vcard_togglable_param_ac; static Autocomplete vcard_address_type_ac; +static Autocomplete force_encryption_ac; +static Autocomplete force_encryption_policy_ac; static Autocomplete* all_acs[] = { &commands_ac, @@ -448,6 +451,8 @@ static Autocomplete* all_acs[] = { &vcard_set_param_ac, &vcard_togglable_param_ac, &vcard_address_type_ac, + &force_encryption_ac, + &force_encryption_policy_ac }; static GHashTable* ac_funcs = NULL; @@ -1343,6 +1348,13 @@ cmd_ac_init(void) autocomplete_add(vcard_address_type_ac, "domestic"); autocomplete_add(vcard_address_type_ac, "international"); + autocomplete_add(force_encryption_ac, "on"); + autocomplete_add(force_encryption_ac, "off"); + autocomplete_add(force_encryption_ac, "policy"); + + autocomplete_add(force_encryption_policy_ac, "block"); + autocomplete_add(force_encryption_policy_ac, "resend-to-confirm"); + if (ac_funcs != NULL) g_hash_table_destroy(ac_funcs); ac_funcs = g_hash_table_new(g_str_hash, g_str_equal); @@ -1415,6 +1427,7 @@ cmd_ac_init(void) g_hash_table_insert(ac_funcs, "/win", _win_autocomplete); g_hash_table_insert(ac_funcs, "/wins", _wins_autocomplete); g_hash_table_insert(ac_funcs, "/wintitle", _wintitle_autocomplete); + g_hash_table_insert(ac_funcs, "/force-encryption", _force_encryption_autocomplete); } void @@ -4406,3 +4419,17 @@ _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous) return result; } + +static char* +_force_encryption_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + char* result = NULL; + + result = autocomplete_param_with_ac(input, "/force-encryption policy", force_encryption_policy_ac, TRUE, previous); + if (result) { + return result; + } + + result = autocomplete_param_with_ac(input, "/force-encryption", force_encryption_ac, TRUE, previous); + return result; +} \ No newline at end of file diff --git a/src/event/client_events.c b/src/event/client_events.c index cae3d46d..7b68bdc1 100644 --- a/src/event/client_events.c +++ b/src/event/client_events.c @@ -42,6 +42,7 @@ #include "log.h" #include "chatlog.h" #include "database.h" +#include "client_events.h" #include "config/preferences.h" #include "event/common.h" #include "plugins/plugins.h" @@ -130,38 +131,6 @@ cl_ev_presence_send(const resource_presence_t presence_type, const int idle_secs presence_send(presence_type, idle_secs, signed_status); } -// Checks if an unencrypted message can be sent based on encryption preferences. -// Returns TRUE if allowed, FALSE if blocked. -static gboolean -_allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg) -{ - if (!prefs_get_boolean(PREF_FORCE_ENCRYPTION)) { - return TRUE; // Encryption not enforced - } - - auto_gchar gchar* force_enc_mode = prefs_get_string(PREF_FORCE_ENCRYPTION_MODE); - - if (g_strcmp0(force_enc_mode, "block") == 0) { - win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or /force-encryption off."); - return FALSE; - } - - if (g_strcmp0(force_enc_mode, "resend-to-confirm") == 0) { - // We do not use chatwin->last_message because it should be set only if it's sent. - guint msg_hash = g_str_hash(msg); - if (msg_hash != chatwin->last_attempted_msg_hash) { - win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or press Enter again to send without encryption. Use /force-encryption off to disable this protection."); - chatwin->last_attempted_msg_hash = msg_hash; - inp_set_line(msg); // reset message to prevent the need to retype it - return FALSE; - } - return TRUE; - } - - win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: invalid encryption mode (%s). Use '/force-encryption policy resend-to-confirm'.", force_enc_mode); - return FALSE; -} - void cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char* const oob_url, gboolean correct_last_msg) { @@ -210,7 +179,7 @@ cl_ev_send_msg_correct(ProfChatWin* chatwin, const char* const msg, const char* #ifdef HAVE_LIBOTR handled = otr_on_message_send(chatwin, message, request_receipt, replace_id); #endif - if (!handled && _allow_unencrypted_message(chatwin, message)) { + if (!handled && allow_unencrypted_message(chatwin, message)) { auto_char char* id = message_send_chat(chatwin->barejid, message, oob_url, request_receipt, replace_id); chat_log_msg_out(chatwin->barejid, message, NULL); log_database_add_outgoing_chat(id, chatwin->barejid, message, replace_id, PROF_MSG_ENC_NONE); @@ -284,3 +253,34 @@ cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* plugins_post_priv_message_send(privwin->fulljid, message); } } + +gboolean +allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg) +{ + if (!prefs_get_boolean(PREF_FORCE_ENCRYPTION)) { + return TRUE; // Encryption not enforced + } + + auto_gchar gchar* force_enc_mode = prefs_get_string(PREF_FORCE_ENCRYPTION_MODE); + + if (g_strcmp0(force_enc_mode, "block") == 0) { + win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or /force-encryption off."); + return FALSE; + } + + if (g_strcmp0(force_enc_mode, "resend-to-confirm") == 0) { + // We do not use chatwin->last_message because it should be set only if it's sent. + guint msg_hash = g_str_hash(msg); + if (msg_hash == chatwin->last_attempted_msg_hash) { + chatwin->last_attempted_msg_hash = 0; // reset hash + return TRUE; + } + win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or press Enter again to send without encryption. Use /force-encryption off to disable this protection."); + chatwin->last_attempted_msg_hash = msg_hash; + inp_set_line(msg); // reset message to prevent the need to retype it + return FALSE; + } + + win_println((ProfWin*)chatwin, THEME_ERROR, "-", "Message not sent: invalid encryption mode (%s). Use '/force-encryption policy resend-to-confirm'.", force_enc_mode); + return FALSE; +} diff --git a/src/event/client_events.h b/src/event/client_events.h index a35e97b5..e5f638f9 100644 --- a/src/event/client_events.h +++ b/src/event/client_events.h @@ -52,4 +52,8 @@ void cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, con void cl_ev_send_muc_msg(ProfMucWin* mucwin, const char* const msg, const char* const oob_url); void cl_ev_send_priv_msg(ProfPrivateWin* privwin, const char* const msg, const char* const oob_url); +// Checks if an unencrypted message can be sent based on encryption preferences. +// Returns TRUE if allowed, FALSE if blocked. +gboolean allow_unencrypted_message(ProfChatWin* chatwin, const char* const msg); + #endif diff --git a/tests/unittests/test_forced_encryption.c b/tests/unittests/test_forced_encryption.c new file mode 100644 index 00000000..de2967c8 --- /dev/null +++ b/tests/unittests/test_forced_encryption.c @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include "event/client_events.h" +#include "command/cmd_funcs.h" +#include "config/preferences.h" +#include "ui/stub_ui.h" + +#define CMD_FORCE_ENCRYPTION "/force-encryption" + +// Test: /force-encryption with no arguments shows error. +void +test_cmd_force_encryption_no_args_bad_cmd(void** state) +{ + char* args[] = { NULL }; + expect_string(cons_bad_cmd_usage, cmd, CMD_FORCE_ENCRYPTION); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); +} + +// Test: /force-encryption on enables encryption. +void +test_cmd_force_encryption_toggles_on(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, FALSE); + char* args[] = { "on", NULL }; + expect_cons_show("Forces encryption enabled."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + assert_true(prefs_get_boolean(PREF_FORCE_ENCRYPTION)); +} + +// Test: /force-encryption off disables encryption. +void +test_cmd_force_encryption_toggles_off(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, TRUE); + char* args[] = { "off", NULL }; + expect_cons_show("Forces encryption disabled."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + assert_false(prefs_get_boolean(PREF_FORCE_ENCRYPTION)); +} + +// Test: /force-encryption off when already off. +void +test_cmd_force_encryption_toggles_off_already(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, FALSE); + char* args[] = { "off", NULL }; + expect_cons_show("Forces encryption is already disabled."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + assert_false(prefs_get_boolean(PREF_FORCE_ENCRYPTION)); +} + +// Test: /force-encryption on when already on. +void +test_cmd_force_encryption_toggles_on_already(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, TRUE); + char* args[] = { "on", NULL }; + expect_cons_show("Forces encryption is already enabled."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + assert_true(prefs_get_boolean(PREF_FORCE_ENCRYPTION)); +} + +// Test: /force-encryption policy block sets block mode. +void +test_cmd_force_encryption_sets_policy_block(void** state) +{ + char* args[] = { "policy", "block", NULL }; + expect_cons_show("Force encryption policy has been set to \"block\"."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + auto_gchar gchar* pref_force_enc_mode = prefs_get_string(PREF_FORCE_ENCRYPTION_MODE); + assert_string_equal(pref_force_enc_mode, "block"); +} + +// Test: /force-encryption policy resend-to-confirm sets resend mode. +void +test_cmd_force_encryption_sets_policy_resend(void** state) +{ + char* args[] = { "policy", "resend-to-confirm", NULL }; + expect_cons_show("Force encryption policy has been set to \"resend-to-confirm\"."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); + auto_gchar gchar* pref_force_enc_mode = prefs_get_string(PREF_FORCE_ENCRYPTION_MODE); + assert_string_equal(pref_force_enc_mode, "resend-to-confirm"); +} + +// Test: Unencrypted message blocked when policy is block. +void +test_allow_unencrypted_message_blocks(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, TRUE); + prefs_set_string(PREF_FORCE_ENCRYPTION_MODE, "block"); + ProfChatWin win = {}; + char* msg = "test message"; + expect_win_println("Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or /force-encryption off."); + assert_false(allow_unencrypted_message(&win, msg)); +} + +// Test: Unencrypted message prompts confirmation, then allows on second attempt. +void +test_allow_unencrypted_message_confirms_on_second_attempt(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, TRUE); + prefs_set_string(PREF_FORCE_ENCRYPTION_MODE, "resend-to-confirm"); + ProfChatWin win = { .last_attempted_msg_hash = 0 }; + char* msg = "test message"; + expect_win_println("Message not sent: encryption required. Enable (omemo/pgp/otr) encryption or press Enter again to send without encryption. Use /force-encryption off to disable this protection."); + assert_false(allow_unencrypted_message(&win, msg)); + assert_int_not_equal(win.last_attempted_msg_hash, 0); + assert_true(allow_unencrypted_message(&win, msg)); + assert_int_equal(win.last_attempted_msg_hash, 0); // ensure that value resets since we don't want to approve the same message twice +} + +// Test: Invalid policy argument fails gracefully. +void +test_cmd_force_encryption_invalid_policy(void** state) +{ + char* args[] = { "policy", "invalid", NULL }; + expect_cons_show_error("Invalid policy: \"invalid\". See '/help force-encryption' for details."); + assert_true(cmd_force_encryption(NULL, CMD_FORCE_ENCRYPTION, args)); +} + +// Test: Invalid encryption mode blocks message. +void +test_allow_unencrypted_message_invalid_mode(void** state) +{ + prefs_set_boolean(PREF_FORCE_ENCRYPTION, TRUE); + prefs_set_string(PREF_FORCE_ENCRYPTION_MODE, "invalid"); + ProfChatWin win = { .last_attempted_msg_hash = 0 }; + char* msg = "test message"; + expect_win_println("Message not sent: invalid encryption mode (invalid). Use '/force-encryption policy resend-to-confirm'."); + assert_false(allow_unencrypted_message(&win, msg)); + assert_int_equal(win.last_attempted_msg_hash, 0); +} diff --git a/tests/unittests/test_forced_encryption.h b/tests/unittests/test_forced_encryption.h new file mode 100644 index 00000000..58da0983 --- /dev/null +++ b/tests/unittests/test_forced_encryption.h @@ -0,0 +1,16 @@ +#ifndef TEST_FORCED_ENCRYPTION_H +#define TEST_FORCED_ENCRYPTION_H + +void test_cmd_force_encryption_no_args_bad_cmd(void** state); +void test_cmd_force_encryption_toggles_on(void** state); +void test_cmd_force_encryption_toggles_off(void** state); +void test_cmd_force_encryption_toggles_off_already(void** state); +void test_cmd_force_encryption_toggles_on_already(void** state); +void test_cmd_force_encryption_sets_policy_block(void** state); +void test_cmd_force_encryption_sets_policy_resend(void** state); +void test_allow_unencrypted_message_blocks(void** state); +void test_allow_unencrypted_message_confirms_on_second_attempt(void** state); +void test_cmd_force_encryption_invalid_policy(void** state); +void test_allow_unencrypted_message_invalid_mode(void** state); + +#endif // TEST_FORCED_ENCRYPTION_H \ No newline at end of file diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index e0d4d792..7b94ebcf 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -24,6 +24,7 @@ #include "test_cmd_presence.h" #include "test_cmd_otr.h" #include "test_cmd_pgp.h" +#include "test_forced_encryption.h" #include "test_jid.h" #include "test_parser.h" #include "test_roster_list.h" @@ -644,6 +645,19 @@ main(int argc, char* argv[]) cmocka_unit_test(does_not_add_duplicate_feature), cmocka_unit_test(removes_plugin_features), cmocka_unit_test(does_not_remove_feature_when_more_than_one_reference), + + // Forced encryption + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_no_args_bad_cmd, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_toggles_on, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_toggles_off, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_toggles_off_already, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_toggles_on_already, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_sets_policy_block, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_sets_policy_resend, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_blocks, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_confirms_on_second_attempt, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_cmd_force_encryption_invalid_policy, load_preferences, close_preferences), + cmocka_unit_test_setup_teardown(test_allow_unencrypted_message_invalid_mode, load_preferences, close_preferences), }; return cmocka_run_group_tests(all_tests, NULL, NULL); }