diff options
author | Michael Vetter <jubalh@iodoru.org> | 2019-04-11 10:58:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-11 10:58:22 +0200 |
commit | 61df0c8e8513a1aa9912e37019a63778ec3ed06c (patch) | |
tree | a52850f2f5fc225759c2287d1672ed22b5ef7f0a /src | |
parent | 6b064cfde4456c25bd9dbcbfe0a79262ebcb3599 (diff) | |
parent | f75e1d7a7b05c68f03b6b13163ac9f2b8824e7df (diff) | |
download | profani-tty-61df0c8e8513a1aa9912e37019a63778ec3ed06c.tar.gz |
Merge pull request #1039 from paulfariello/feature/omemo
Add basic OMEMO support.
Diffstat (limited to 'src')
42 files changed, 4962 insertions, 99 deletions
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 58ad758a..0cc28bb3 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -57,6 +57,10 @@ #include "pgp/gpg.h" #endif +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#endif + static char* _sub_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _notify_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _theme_autocomplete(ProfWin *window, const char *const input, gboolean previous); @@ -69,6 +73,7 @@ static char* _group_autocomplete(ProfWin *window, const char *const input, gbool static char* _bookmark_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _otr_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous); +static char* _omemo_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _connect_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _alias_autocomplete(ProfWin *window, const char *const input, gboolean previous); static char* _join_autocomplete(ProfWin *window, const char *const input, gboolean previous); @@ -157,6 +162,8 @@ static Autocomplete bookmark_property_ac; static Autocomplete otr_ac; static Autocomplete otr_log_ac; static Autocomplete otr_policy_ac; +static Autocomplete omemo_ac; +static Autocomplete omemo_log_ac; static Autocomplete connect_property_ac; static Autocomplete tls_property_ac; static Autocomplete alias_ac; @@ -237,6 +244,7 @@ cmd_ac_init(void) autocomplete_add(prefs_ac, "presence"); autocomplete_add(prefs_ac, "otr"); autocomplete_add(prefs_ac, "pgp"); + autocomplete_add(prefs_ac, "omemo"); notify_ac = autocomplete_new(); autocomplete_add(notify_ac, "chat"); @@ -574,6 +582,21 @@ cmd_ac_init(void) autocomplete_add(otr_policy_ac, "opportunistic"); autocomplete_add(otr_policy_ac, "always"); + omemo_ac = autocomplete_new(); + autocomplete_add(omemo_ac, "gen"); + autocomplete_add(omemo_ac, "log"); + autocomplete_add(omemo_ac, "start"); + autocomplete_add(omemo_ac, "end"); + autocomplete_add(omemo_ac, "trust"); + autocomplete_add(omemo_ac, "untrust"); + autocomplete_add(omemo_ac, "fingerprint"); + autocomplete_add(omemo_ac, "clear_device_list"); + + omemo_log_ac = autocomplete_new(); + autocomplete_add(omemo_log_ac, "on"); + autocomplete_add(omemo_log_ac, "off"); + autocomplete_add(omemo_log_ac, "redact"); + connect_property_ac = autocomplete_new(); autocomplete_add(connect_property_ac, "server"); autocomplete_add(connect_property_ac, "port"); @@ -983,6 +1006,9 @@ cmd_ac_reset(ProfWin *window) #ifdef HAVE_LIBGPGME p_gpg_autocomplete_key_reset(); #endif +#ifdef HAVE_OMEMO + omemo_fingerprint_autocomplete_reset(); +#endif autocomplete_reset(help_ac); autocomplete_reset(help_commands_ac); autocomplete_reset(notify_ac); @@ -1052,6 +1078,8 @@ cmd_ac_reset(ProfWin *window) autocomplete_reset(otr_ac); autocomplete_reset(otr_log_ac); autocomplete_reset(otr_policy_ac); + autocomplete_reset(omemo_ac); + autocomplete_reset(omemo_log_ac); autocomplete_reset(connect_property_ac); autocomplete_reset(tls_property_ac); autocomplete_reset(alias_ac); @@ -1179,6 +1207,8 @@ cmd_ac_uninit(void) autocomplete_free(otr_ac); autocomplete_free(otr_log_ac); autocomplete_free(otr_policy_ac); + autocomplete_free(omemo_ac); + autocomplete_free(omemo_log_ac); autocomplete_free(connect_property_ac); autocomplete_free(tls_property_ac); autocomplete_free(alias_ac); @@ -1438,6 +1468,7 @@ _cmd_ac_complete_params(ProfWin *window, const char *const input, gboolean previ g_hash_table_insert(ac_funcs, "/autoconnect", _autoconnect_autocomplete); g_hash_table_insert(ac_funcs, "/otr", _otr_autocomplete); g_hash_table_insert(ac_funcs, "/pgp", _pgp_autocomplete); + g_hash_table_insert(ac_funcs, "/omemo", _omemo_autocomplete); g_hash_table_insert(ac_funcs, "/connect", _connect_autocomplete); g_hash_table_insert(ac_funcs, "/alias", _alias_autocomplete); g_hash_table_insert(ac_funcs, "/join", _join_autocomplete); @@ -2118,6 +2149,57 @@ _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous) } static char* +_omemo_autocomplete(ProfWin *window, const char *const input, gboolean previous) +{ + char *found = NULL; + + jabber_conn_status_t conn_status = connection_get_status(); + + if (conn_status == JABBER_CONNECTED) { + found = autocomplete_param_with_func(input, "/omemo start", roster_contact_autocomplete, previous); + if (found) { + return found; + } + } + + found = autocomplete_param_with_func(input, "/omemo fingerprint", roster_contact_autocomplete, previous); + if (found) { + return found; + } + +#ifdef HAVE_OMEMO + if (window->type == WIN_CHAT) { + found = autocomplete_param_with_func(input, "/omemo trust", omemo_fingerprint_autocomplete, previous); + if (found) { + return found; + } + } else { + found = autocomplete_param_with_func(input, "/omemo trust", roster_contact_autocomplete, previous); + if (found) { + return found; + } + + found = autocomplete_param_no_with_func(input, "/omemo trust", 4, omemo_fingerprint_autocomplete, previous); + if (found) { + return found; + } + } +#endif + + found = autocomplete_param_with_ac(input, "/omemo log", omemo_log_ac, TRUE, previous); + if (found) { + return found; + } + + found = autocomplete_param_with_ac(input, "/omemo", omemo_ac, TRUE, previous); + if (found) { + return found; + } + + return NULL; +} + +static char* _plugins_autocomplete(ProfWin *window, const char *const input, gboolean previous) { char *result = NULL; diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 4447020b..ee86aaba 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -2134,7 +2134,7 @@ static struct cmd_t command_defs[] = CMD_MAINFUNC(cmd_prefs) CMD_NOTAGS CMD_SYN( - "/prefs [ui|desktop|chat|log|conn|presence|otr|pgp]") + "/prefs [ui|desktop|chat|log|conn|presence|otr|pgp|omemo]") CMD_DESC( "Show preferences for different areas of functionality. " "Passing no arguments shows all preferences.") @@ -2146,7 +2146,8 @@ static struct cmd_t command_defs[] = { "conn", "Connection handling preferences." }, { "presence", "Chat presence preferences." }, { "otr", "Off The Record preferences." }, - { "pgp", "OpenPGP preferences." }) + { "pgp", "OpenPGP preferences." }, + { "omemo", "OMEMO preferences." }) CMD_NOEXAMPLES }, @@ -2328,7 +2329,51 @@ static struct cmd_t command_defs[] = CMD_EXAMPLES( "/cmd list", "/cmd exec ping") - } + }, + + { "/omemo", + parse_args, 1, 3, NULL, + CMD_SUBFUNCS( + { "gen", cmd_omemo_gen }, + { "log", cmd_omemo_log }, + { "start", cmd_omemo_start }, + { "end", cmd_omemo_end }, + { "trust", cmd_omemo_trust }, + { "untrust", cmd_omemo_untrust }, + { "fingerprint", cmd_omemo_fingerprint }, + { "char", cmd_omemo_char }, + { "clear_device_list", cmd_omemo_clear_device_list }) + CMD_NOMAINFUNC + CMD_TAGS( + CMD_TAG_CHAT, + CMD_TAG_UI) + CMD_SYN( + "/omemo gen", + "/omemo log on|off|redact", + "/omemo start [<contact>]", + "/omemo trust [<contact>] <fingerprint>", + "/omemo end", + "/omemo fingerprint [<contact>]", + "/omemo char <char>", + "/omemo clear_device_list") + CMD_DESC( + "OMEMO commands to manage keys, and perform encryption during chat sessions.") + CMD_ARGS( + { "gen", "Generate OMEMO crytographic materials for current account." }, + { "start [<contact>]", "Start an OMEMO session with contact, or current recipient if omitted." }, + { "end", "End the current OMEMO session." }, + { "log on|off", "Enable or disable plaintext logging of OMEMO encrypted messages." }, + { "log redact", "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." }, + { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." }, + { "char <char>", "Set the character to be displayed next to OMEMO encrypted messages." }, + { "clear_device_list", "Clear your own device list on server side. Each client will reannounce itself when connected back."}) + CMD_EXAMPLES( + "/omemo gen", + "/omemo start buddy@buddychat.org", + "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a", + "/omemo untrust buddy@buddychat.org c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a", + "/omemo char *") + }, }; static GHashTable *search_index; diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index b2f0ee7f..fe289f0b 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -85,6 +85,11 @@ #include "pgp/gpg.h" #endif +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#include "xmpp/omemo.h" +#endif + #ifdef HAVE_GTK #include "ui/tray.h" #endif @@ -1674,6 +1679,10 @@ cmd_prefs(ProfWin *window, const char *const command, gchar **args) cons_show(""); cons_show_pgp_prefs(); cons_show(""); + } else if (strcmp(args[0], "omemo") == 0) { + cons_show(""); + cons_show_omemo_prefs(); + cons_show(""); } else { cons_bad_cmd_usage(command); } @@ -2141,17 +2150,67 @@ cmd_msg(ProfWin *window, const char *const command, gchar **args) } ui_focus_win((ProfWin*)chatwin); +#ifdef HAVE_OMEMO +#ifndef HAVE_LIBOTR + if (omemo_is_trusted_jid(barejid)) { + omemo_start_session(barejid); + chatwin->is_omemo = TRUE; + } + + if (msg) { + cl_ev_send_msg(chatwin, msg, NULL); + } + + return TRUE; +#endif +#endif + +#ifdef HAVE_OMEMO +#ifdef HAVE_LIBOTR + if (omemo_is_trusted_jid(barejid) && otr_is_secure(barejid)) { + win_println(window, THEME_DEFAULT, '!', "Chat could be either OMEMO or OTR encrypted. Use '/omemo start %s' or '/otr start %s' to start a session.", usr, usr); + return TRUE; + } else if (omemo_is_trusted_jid(barejid)) { + omemo_start_session(barejid); + chatwin->is_omemo = TRUE; + } + if (msg) { cl_ev_send_msg(chatwin, msg, NULL); } else { + if (otr_is_secure(barejid)) { + chatwin_otr_secured(chatwin, otr_is_trusted(barejid)); + } + } + + return TRUE; +#endif +#endif + +#ifndef HAVE_OMEMO #ifdef HAVE_LIBOTR + if (msg) { + cl_ev_send_msg(chatwin, msg, NULL); + } else { if (otr_is_secure(barejid)) { chatwin_otr_secured(chatwin, otr_is_trusted(barejid)); } + } + + return TRUE; #endif +#endif + +#ifndef HAVE_OMEMO +#ifndef HAVE_LIBOTR + if (msg) { + cl_ev_send_msg(chatwin, msg, NULL); } return TRUE; +#endif +#endif + } } @@ -7304,6 +7363,11 @@ cmd_otr_start(ProfWin *window, const char *const command, gchar **args) return TRUE; } + if (chatwin->is_omemo) { + win_println(window, THEME_DEFAULT, '!', "You must disable OMEMO before starting an OTR session."); + return TRUE; + } + if (chatwin->is_otr) { win_println(window, THEME_DEFAULT, '!', "You are already in an OTR session."); return TRUE; @@ -7872,3 +7936,432 @@ _cmd_set_boolean_preference(gchar *arg, const char *const command, g_string_free(enabled, TRUE); g_string_free(disabled, TRUE); } + +gboolean +cmd_omemo_gen(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to initialize OMEMO."); + return TRUE; + } + + if (omemo_loaded()) { + cons_show("OMEMO crytographic materials have already been generated."); + return TRUE; + } + + ProfAccount *account = accounts_get_account(session_get_account_name()); + omemo_generate_crypto_materials(account); + cons_show("OMEMO crytographic materials generated."); + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_start(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to load OMEMO information."); + return TRUE; + } + + if (!omemo_loaded()) { + win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'"); + return TRUE; + } + + // recipient supplied + if (args[1]) { + char *contact = args[1]; + char *barejid = roster_barejid_from_name(contact); + if (barejid == NULL) { + barejid = contact; + } + + ProfChatWin *chatwin = wins_get_chat(barejid); + if (!chatwin) { + chatwin = chatwin_new(barejid); + } + ui_focus_win((ProfWin*)chatwin); + + if (chatwin->pgp_send) { + win_println(window, THEME_DEFAULT, '!', "You must disable PGP encryption before starting an OMEMO session."); + return TRUE; + } + + if (chatwin->is_otr) { + win_println(window, THEME_DEFAULT, '!', "You must disable OTR encryption before starting an OMEMO session."); + return TRUE; + } + + if (chatwin->is_omemo) { + win_println(window, THEME_DEFAULT, '!', "You are already in an OMEMO session."); + return TRUE; + } + + omemo_start_session(barejid); + chatwin->is_omemo = TRUE; + } else { + if (window->type == WIN_CHAT) { + ProfChatWin *chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + if (chatwin->pgp_send) { + win_println(window, THEME_DEFAULT, '!', "You must disable PGP encryption before starting an OMEMO session."); + return TRUE; + } + + if (chatwin->is_otr) { + win_println(window, THEME_DEFAULT, '!', "You must disable OTR encryption before starting an OMEMO session."); + return TRUE; + } + + if (chatwin->is_omemo) { + win_println(window, THEME_DEFAULT, '!', "You are already in an OMEMO session."); + return TRUE; + } + + omemo_start_session(chatwin->barejid); + chatwin->is_omemo = TRUE; + } else if (window->type == WIN_MUC) { + ProfMucWin *mucwin = (ProfMucWin*)window; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + + /* TODO: Check room is configured correctly, no anonymous and access to + * full jid */ + omemo_start_muc_sessions(mucwin->roomjid); + + mucwin->is_omemo = TRUE; + } else { + win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to start an OMEMO session."); + return TRUE; + } + + } + + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_char(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (args[1] == NULL) { + cons_bad_cmd_usage(command); + } else if (strlen(args[1]) != 1) { + cons_bad_cmd_usage(command); + } else { + prefs_set_omemo_char(args[1][0]); + cons_show("OMEMO char set to %c.", args[1][0]); + } + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_log(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + char *choice = args[1]; + if (g_strcmp0(choice, "on") == 0) { + prefs_set_string(PREF_OMEMO_LOG, "on"); + cons_show("OMEMO messages will be logged as plaintext."); + if (!prefs_get_boolean(PREF_CHLOG)) { + cons_show("Chat logging is currently disabled, use '/chlog on' to enable."); + } + } else if (g_strcmp0(choice, "off") == 0) { + prefs_set_string(PREF_OMEMO_LOG, "off"); + cons_show("OMEMO message logging disabled."); + } else if (g_strcmp0(choice, "redact") == 0) { + prefs_set_string(PREF_OMEMO_LOG, "redact"); + cons_show("OMEMO messages will be logged as '[redacted]'."); + if (!prefs_get_boolean(PREF_CHLOG)) { + cons_show("Chat logging is currently disabled, use '/chlog on' to enable."); + } + } else { + cons_bad_cmd_usage(command); + } + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_end(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to load OMEMO information."); + return TRUE; + } + + if (window->type == WIN_CHAT) { + ProfChatWin *chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + + if (!chatwin->is_omemo) { + win_println(window, THEME_DEFAULT, '!', "You are not currently in an OMEMO session."); + return TRUE; + } + + chatwin->is_omemo = FALSE; + } else if (window->type == WIN_MUC) { + ProfMucWin *mucwin = (ProfMucWin*)window; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + + if (!mucwin->is_omemo) { + win_println(window, THEME_DEFAULT, '!', "You are not currently in an OMEMO session."); + return TRUE; + } + + mucwin->is_omemo = FALSE; + } else { + win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to start an OMEMO session."); + return TRUE; + } + + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_fingerprint(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to load OMEMO information."); + return TRUE; + } + + if (!omemo_loaded()) { + win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'"); + return TRUE; + } + + Jid *jid; + if (!args[1]) { + if (window->type == WIN_CONSOLE) { + char *fingerprint = omemo_own_fingerprint(TRUE); + cons_show("Your OMEMO fingerprint: %s", fingerprint); + free(fingerprint); + jid = jid_create(connection_get_fulljid()); + } else if (window->type == WIN_CHAT) { + ProfChatWin *chatwin = (ProfChatWin*)window; + jid = jid_create(chatwin->barejid); + } else { + win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to print fingerprint without providing the contact."); + return TRUE; + } + } else { + char *barejid = roster_barejid_from_name(args[1]); + if (barejid) { + jid = jid_create(barejid); + } else { + jid = jid_create(args[1]); + if (!jid) { + cons_show("%s is not a valid jid", args[1]); + return TRUE; + } + } + } + + GList *fingerprints = omemo_known_device_identities(jid->barejid); + GList *fingerprint; + + if (!fingerprints) { + win_println(window, THEME_DEFAULT, '-', "There is no known fingerprints for %s", jid->barejid); + return TRUE; + } + + for (fingerprint = fingerprints; fingerprint != NULL; fingerprint = fingerprint->next) { + char *formatted_fingerprint = omemo_format_fingerprint(fingerprint->data); + gboolean trusted = omemo_is_trusted_identity(jid->barejid, fingerprint->data); + + win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", jid->barejid, formatted_fingerprint, trusted ? " (trusted)" : ""); + + free(formatted_fingerprint); + } + + g_list_free(fingerprints); + + win_println(window, THEME_DEFAULT, '-', "You can trust it with '/omemo trust <fingerprint>'"); + win_println(window, THEME_DEFAULT, '-', "You can untrust it with '/omemo untrust <fingerprint>'"); + + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_trust(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to load OMEMO information."); + return TRUE; + } + + if (!args[1]) { + cons_bad_cmd_usage(command); + return TRUE; + } + + if (!omemo_loaded()) { + win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'"); + return TRUE; + } + + char *fingerprint; + char *barejid; + + /* Contact not provided */ + if (!args[2]) { + fingerprint = args[1]; + + if (window->type != WIN_CHAT) { + win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to trust a device without providing the contact."); + return TRUE; + } + + ProfChatWin *chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + barejid = chatwin->barejid; + } else { + fingerprint = args[2]; + char *contact = args[1]; + barejid = roster_barejid_from_name(contact); + if (barejid == NULL) { + barejid = contact; + } + } + + omemo_trust(barejid, fingerprint); + + char *unformatted_fingerprint = malloc(strlen(fingerprint)); + int i; + int j; + for (i = 0, j = 0; fingerprint[i] != '\0'; i++) { + if (!g_ascii_isxdigit(fingerprint[i])) { + continue; + } + unformatted_fingerprint[j++] = fingerprint[i]; + } + + unformatted_fingerprint[j] = '\0'; + gboolean trusted = omemo_is_trusted_identity(barejid, unformatted_fingerprint); + + win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", barejid, fingerprint, trusted ? " (trusted)" : ""); + + free(unformatted_fingerprint); + + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_untrust(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to load OMEMO information."); + return TRUE; + } + + if (!args[1]) { + cons_bad_cmd_usage(command); + return TRUE; + } + + if (!omemo_loaded()) { + win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'"); + return TRUE; + } + + char *fingerprint; + char *barejid; + + /* Contact not provided */ + if (!args[2]) { + fingerprint = args[1]; + + if (window->type != WIN_CHAT) { + win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to trust a device without providing the contact."); + return TRUE; + } + + ProfChatWin *chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + barejid = chatwin->barejid; + } else { + fingerprint = args[2]; + char *contact = args[1]; + barejid = roster_barejid_from_name(contact); + if (barejid == NULL) { + barejid = contact; + } + } + + omemo_untrust(barejid, fingerprint); + + char *unformatted_fingerprint = malloc(strlen(fingerprint)); + int i; + int j; + for (i = 0, j = 0; fingerprint[i] != '\0'; i++) { + if (!g_ascii_isxdigit(fingerprint[i])) { + continue; + } + unformatted_fingerprint[j++] = fingerprint[i]; + } + + unformatted_fingerprint[j] = '\0'; + gboolean trusted = omemo_is_trusted_identity(barejid, unformatted_fingerprint); + + win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", barejid, fingerprint, trusted ? " (trusted)" : ""); + + free(unformatted_fingerprint); + + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} + +gboolean +cmd_omemo_clear_device_list(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_OMEMO + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You must be connected with an account to initialize OMEMO."); + return TRUE; + } + + omemo_devicelist_publish(NULL); + cons_show("Cleared OMEMO device list"); + return TRUE; +#else + cons_show("This version of Profanity has not been built with OMEMO support enabled"); + return TRUE; +#endif +} diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index 89166ba1..249b50fe 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -214,4 +214,14 @@ gboolean cmd_wins_swap(ProfWin *window, const char *const command, gchar **args) gboolean cmd_form_field(ProfWin *window, char *tag, gchar **args); +gboolean cmd_omemo_gen(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_char(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_log(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_start(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_end(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_fingerprint(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_trust(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_untrust(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_omemo_clear_device_list(ProfWin *window, const char *const command, gchar **args); + #endif diff --git a/src/config/files.h b/src/config/files.h index 1d8d2890..f7dfa29a 100644 --- a/src/config/files.h +++ b/src/config/files.h @@ -50,6 +50,7 @@ #define DIR_CHATLOGS "chatlogs" #define DIR_OTR "otr" #define DIR_PGP "pgp" +#define DIR_OMEMO "omemo" #define DIR_PLUGINS "plugins" void files_create_directories(void); diff --git a/src/config/preferences.c b/src/config/preferences.c index 265e11db..65e7a64d 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -58,6 +58,7 @@ #define PREF_GROUP_ALIAS "alias" #define PREF_GROUP_OTR "otr" #define PREF_GROUP_PGP "pgp" +#define PREF_GROUP_OMEMO "omemo" #define PREF_GROUP_MUC "muc" #define PREF_GROUP_PLUGINS "plugins" @@ -823,6 +824,33 @@ prefs_set_pgp_char(char ch) } char +prefs_get_omemo_char(void) +{ + char result = '~'; + + char *resultstr = g_key_file_get_string(prefs, PREF_GROUP_OMEMO, "omemo.char", NULL); + if (!resultstr) { + result = '~'; + } else { + result = resultstr[0]; + } + free(resultstr); + + return result; +} + +void +prefs_set_omemo_char(char ch) +{ + char str[2]; + str[0] = ch; + str[1] = '\0'; + + g_key_file_set_string(prefs, PREF_GROUP_OMEMO, "omemo.char", str); + _save_prefs(); +} + +char prefs_get_roster_header_char(void) { char result = 0; @@ -1657,6 +1685,8 @@ _get_group(preference_t pref) return PREF_GROUP_MUC; case PREF_PLUGINS_SOURCEPATH: return PREF_GROUP_PLUGINS; + case PREF_OMEMO_LOG: + return PREF_GROUP_OMEMO; default: return NULL; } @@ -1871,6 +1901,8 @@ _get_key(preference_t pref) return "statusbar.chat"; case PREF_STATUSBAR_ROOM: return "statusbar.room"; + case PREF_OMEMO_LOG: + return "log"; default: return NULL; } @@ -1989,6 +2021,8 @@ _get_default_string(preference_t pref) return "user"; case PREF_STATUSBAR_ROOM: return "room"; + case PREF_OMEMO_LOG: + return "redact"; default: return NULL; } diff --git a/src/config/preferences.h b/src/config/preferences.h index 65dee327..a4d82967 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -148,6 +148,7 @@ typedef enum { PREF_STATUSBAR_SELF, PREF_STATUSBAR_CHAT, PREF_STATUSBAR_ROOM, + PREF_OMEMO_LOG, } preference_t; typedef struct prof_alias_t { @@ -216,6 +217,8 @@ char prefs_get_otr_char(void); void prefs_set_otr_char(char ch); char prefs_get_pgp_char(void); void prefs_set_pgp_char(char ch); +char prefs_get_omemo_char(void); +void prefs_set_omemo_char(char ch); char prefs_get_roster_header_char(void); void prefs_set_roster_header_char(char ch); diff --git a/src/event/client_events.c b/src/event/client_events.c index 3b6218ea..c2149985 100644 --- a/src/event/client_events.c +++ b/src/event/client_events.c @@ -54,6 +54,10 @@ #include "pgp/gpg.h" #endif +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#endif + jabber_conn_status_t cl_ev_connect_jid(const char *const jid, const char *const passwd, const char *const altdomain, const int port, const char *const tls_policy) { @@ -94,6 +98,9 @@ cl_ev_disconnect(void) #ifdef HAVE_LIBGPGME p_gpg_on_disconnect(); #endif +#ifdef HAVE_OMEMO + omemo_on_disconnect(); +#endif } void @@ -141,9 +148,10 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo return; } -// OTR suported, PGP supported +// OTR suported, PGP supported, OMEMO unsupported #ifdef HAVE_LIBOTR #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (chatwin->pgp_send) { char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt); chat_log_pgp_msg_out(chatwin->barejid, plugin_msg); @@ -164,10 +172,12 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo return; #endif #endif +#endif -// OTR supported, PGP unsupported +// OTR supported, PGP unsupported, OMEMO unsupported #ifdef HAVE_LIBOTR #ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt); if (!handled) { char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); @@ -181,10 +191,12 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo return; #endif #endif +#endif -// OTR unsupported, PGP supported +// OTR unsupported, PGP supported, OMEMO unsupported #ifndef HAVE_LIBOTR #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (chatwin->pgp_send) { char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt); chat_log_pgp_msg_out(chatwin->barejid, plugin_msg); @@ -202,10 +214,120 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo return; #endif #endif +#endif + +// OTR unsupported, PGP unsupported, OMEMO supported +#ifndef HAVE_LIBOTR +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (chatwin->is_omemo) { + char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE); + chat_log_omemo_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt); + free(id); + } else { + char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); + chat_log_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt); + free(id); + } + + plugins_post_chat_message_send(chatwin->barejid, plugin_msg); + free(plugin_msg); + return; +#endif +#endif +#endif + +// OTR supported, PGP unsupported, OMEMO supported +#ifdef HAVE_LIBOTR +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (chatwin->is_omemo) { + char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE); + chat_log_omemo_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt); + free(id); + } else { + gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt); + if (!handled) { + char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); + chat_log_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt); + free(id); + } + } + + plugins_post_chat_message_send(chatwin->barejid, plugin_msg); + free(plugin_msg); + return; +#endif +#endif +#endif + +// OTR unsupported, PGP supported, OMEMO supported +#ifndef HAVE_LIBOTR +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (chatwin->is_omemo) { + char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE); + chat_log_omemo_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt); + free(id); + } else if (chatwin->pgp_send) { + char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt); + chat_log_pgp_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PGP, request_receipt); + free(id); + } else { + char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); + chat_log_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt); + free(id); + } + + plugins_post_chat_message_send(chatwin->barejid, plugin_msg); + free(plugin_msg); + return; +#endif +#endif +#endif -// OTR unsupported, PGP unsupported +// OTR supported, PGP supported, OMEMO supported +#ifdef HAVE_LIBOTR +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (chatwin->is_omemo) { + char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE); + chat_log_omemo_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt); + free(id); + } else if (chatwin->pgp_send) { + char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt); + chat_log_pgp_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PGP, request_receipt); + free(id); + } else { + gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt); + if (!handled) { + char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); + chat_log_msg_out(chatwin->barejid, plugin_msg); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt); + free(id); + } + } + + plugins_post_chat_message_send(chatwin->barejid, plugin_msg); + free(plugin_msg); + return; +#endif +#endif +#endif + +// OTR unsupported, PGP unsupported, OMEMO unsupported #ifndef HAVE_LIBOTR #ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt); chat_log_msg_out(chatwin->barejid, plugin_msg); chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt); @@ -216,6 +338,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo return; #endif #endif +#endif } void @@ -226,10 +349,34 @@ cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg, const char *const return; } - message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url); +#ifdef HAVE_OMEMO + if (mucwin->is_omemo) { + char *id = omemo_on_message_send((ProfWin *)mucwin, plugin_msg, FALSE, TRUE); + groupchat_log_omemo_msg_out(mucwin->roomjid, plugin_msg); + mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_OMEMO); + free(id); + } else { + char *id = message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url); + groupchat_log_msg_out(mucwin->roomjid, plugin_msg); + mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_PLAIN); + free(id); + } + + plugins_post_room_message_send(mucwin->roomjid, plugin_msg); + free(plugin_msg); + return; +#endif + +#ifndef HAVE_OMEMO + char *id = message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url); + groupchat_log_msg_out(mucwin->roomjid, plugin_msg); + mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_PLAIN); + free(id); plugins_post_room_message_send(mucwin->roomjid, plugin_msg); free(plugin_msg); + return; +#endif } void diff --git a/src/event/server_events.c b/src/event/server_events.c index 69883141..36db8ebe 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -59,6 +59,10 @@ #include "pgp/gpg.h" #endif +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#endif + #include "ui/ui.h" void @@ -76,6 +80,10 @@ sv_ev_login_account_success(char *account_name, gboolean secured) p_gpg_on_connect(account->jid); #endif +#ifdef HAVE_OMEMO + omemo_on_connect(account); +#endif + ui_handle_login_account_success(account, secured); // attempt to rejoin rooms with passwords @@ -166,6 +174,18 @@ sv_ev_roster_received(void) const char *fulljid = connection_get_fulljid(); plugins_on_connect(account_name, fulljid); + +#ifdef HAVE_OMEMO + omemo_start_sessions(); +#endif +} + +void +sv_ev_connection_features_received(void) +{ +#ifdef HAVE_OMEMO + omemo_publish_crypto_materials(); +#endif } void @@ -252,22 +272,23 @@ sv_ev_room_history(const char *const room_jid, const char *const nick, } void -sv_ev_room_message(const char *const room_jid, const char *const nick, const char *const message) +sv_ev_room_message(const char *const room_jid, const char *const nick, const char *const message, const char *const id, gboolean omemo) { - if (prefs_get_boolean(PREF_GRLOG)) { - Jid *jid = jid_create(connection_get_fulljid()); - groupchat_log_chat(jid->barejid, room_jid, nick, message); - jid_destroy(jid); - } - ProfMucWin *mucwin = wins_get_muc(room_jid); if (!mucwin) { return; } - char *new_message = plugins_pre_room_message_display(room_jid, nick, message); char *mynick = muc_nick(mucwin->roomjid); + if (omemo) { + groupchat_log_omemo_msg_in(room_jid, nick, message); + } else { + groupchat_log_msg_in(room_jid, nick, message); + } + + char *new_message = plugins_pre_room_message_display(room_jid, nick, message); + gboolean whole_word = prefs_get_boolean(PREF_NOTIFY_MENTION_WHOLE_WORD); gboolean case_sensitive = prefs_get_boolean(PREF_NOTIFY_MENTION_CASE_SENSITIVE); char *message_search = case_sensitive ? strdup(new_message) : g_utf8_strdown(new_message, -1); @@ -281,7 +302,11 @@ sv_ev_room_message(const char *const room_jid, const char *const nick, const cha GList *triggers = prefs_message_get_triggers(new_message); - mucwin_message(mucwin, nick, new_message, mentions, triggers); + if (omemo) { + mucwin_incoming_msg(mucwin, nick, new_message, id, mentions, triggers, PROF_MSG_OMEMO); + } else { + mucwin_incoming_msg(mucwin, nick, new_message, id, mentions, triggers, PROF_MSG_PLAIN); + } g_slist_free(mentions); @@ -370,7 +395,7 @@ sv_ev_delayed_private_message(const char *const fulljid, char *message, GDateTim } void -sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message) +sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message, gboolean omemo) { ProfChatWin *chatwin = wins_get_chat(barejid); if (!chatwin) { @@ -380,6 +405,7 @@ sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message) chat_state_active(chatwin->state); #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (pgp_message) { char *decrypted = p_gpg_decrypt(pgp_message); if (decrypted) { @@ -390,9 +416,44 @@ sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message) } else { chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN); } -#else + return; +#endif +#endif + +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (omemo) { + chatwin_outgoing_carbon(chatwin, message, PROF_MSG_OMEMO); + } else { + chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN); + } + return; +#endif +#endif + +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (omemo) { + chatwin_outgoing_carbon(chatwin, message, PROF_MSG_OMEMO); + } else if (pgp_message) { + char *decrypted = p_gpg_decrypt(pgp_message); + if (decrypted) { + chatwin_outgoing_carbon(chatwin, decrypted, PROF_MSG_PGP); + } else { + chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN); + } + } else { + chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN); + } + return; +#endif +#endif + +#ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN); #endif +#endif } #ifdef HAVE_LIBGPGME @@ -433,6 +494,16 @@ _sv_ev_incoming_otr(ProfChatWin *chatwin, gboolean new_win, char *barejid, char } #endif +#ifdef HAVE_OMEMO +static void +_sv_ev_incoming_omemo(ProfChatWin *chatwin, gboolean new_win, char *barejid, char *resource, char *message, GDateTime *timestamp) +{ + chatwin_incoming_msg(chatwin, resource, message, timestamp, new_win, PROF_MSG_OMEMO); + chat_log_omemo_msg_in(barejid, message, timestamp); + chatwin->pgp_recv = FALSE; +} +#endif + static void _sv_ev_incoming_plain(ProfChatWin *chatwin, gboolean new_win, char *barejid, char *resource, char *message, GDateTime *timestamp) { @@ -442,7 +513,7 @@ _sv_ev_incoming_plain(ProfChatWin *chatwin, gboolean new_win, char *barejid, cha } void -sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp) +sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp, gboolean omemo) { gboolean new_win = FALSE; ProfChatWin *chatwin = wins_get_chat(barejid); @@ -452,9 +523,10 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m new_win = TRUE; } -// OTR suported, PGP supported +// OTR suported, PGP supported, OMEMO unsupported #ifdef HAVE_LIBOTR #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (pgp_message) { if (chatwin->is_otr) { win_println((ProfWin*)chatwin, THEME_DEFAULT, '-', "PGP encrypted message received whilst in OTR session."); @@ -468,19 +540,23 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m return; #endif #endif +#endif -// OTR supported, PGP unsupported +// OTR supported, PGP unsupported, OMEMO unsupported #ifdef HAVE_LIBOTR #ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp); rosterwin_roster(); return; #endif #endif +#endif -// OTR unsupported, PGP supported +// OTR unsupported, PGP supported, OMEMO unsupported #ifndef HAVE_LIBOTR #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (pgp_message) { _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp); } else { @@ -490,19 +566,90 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m return; #endif #endif +#endif -// OTR unsupported, PGP unsupported +// OTR suported, PGP supported, OMEMO supported +#ifdef HAVE_LIBOTR +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (pgp_message) { + if (chatwin->is_otr) { + win_println((ProfWin*)chatwin, THEME_DEFAULT, '-', "PGP encrypted message received whilst in OTR session."); + } else { // PROF_ENC_NONE, PROF_ENC_PGP + _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp); + } + } else if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp); + } else { + _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp); + } + rosterwin_roster(); + return; +#endif +#endif +#endif + +// OTR supported, PGP unsupported, OMEMO supported +#ifdef HAVE_LIBOTR +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp); + } else { + _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp); + } + rosterwin_roster(); + return; +#endif +#endif +#endif + +// OTR unsupported, PGP supported, OMEMO supported +#ifndef HAVE_LIBOTR +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (pgp_message) { + _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp); + } else if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp); + } else { + _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp); + } + rosterwin_roster(); + return; +#endif +#endif +#endif + +// OTR unsupported, PGP unsupported, OMEMO supported +#ifndef HAVE_LIBOTR +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp); + } else { + _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp); + } + rosterwin_roster(); + return; +#endif +#endif +#endif + +// OTR unsupported, PGP unsupported, OMEMO unsupported #ifndef HAVE_LIBOTR #ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp); rosterwin_roster(); return; #endif #endif +#endif } void -sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message) +sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message, gboolean omemo) { gboolean new_win = FALSE; ProfChatWin *chatwin = wins_get_chat(barejid); @@ -513,15 +660,50 @@ sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_me } #ifdef HAVE_LIBGPGME +#ifndef HAVE_OMEMO if (pgp_message) { _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, NULL); } else { _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL); } -#else - _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL); + rosterwin_roster(); + return; +#endif #endif + +#ifdef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (pgp_message) { + _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, NULL); + } else if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, NULL); + } else { + _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL); + } rosterwin_roster(); + return; +#endif +#endif + +#ifndef HAVE_LIBGPGME +#ifdef HAVE_OMEMO + if (omemo) { + _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, NULL); + } else { + _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL); + } + rosterwin_roster(); + return; +#endif +#endif + +#ifndef HAVE_LIBGPGME +#ifndef HAVE_OMEMO + _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL); + rosterwin_roster(); + return; +#endif +#endif } void diff --git a/src/event/server_events.h b/src/event/server_events.h index cc261487..74016ceb 100644 --- a/src/event/server_events.h +++ b/src/event/server_events.h @@ -46,10 +46,10 @@ void sv_ev_room_invite(jabber_invite_t invite_type, void sv_ev_room_broadcast(const char *const room_jid, const char *const message); void sv_ev_room_subject(const char *const room, const char *const nick, const char *const subject); void sv_ev_room_history(const char *const room_jid, const char *const nick, - GDateTime *timestamp, const char *const message); + GDateTime *timestamp, const char *const message, gboolean omemo); void sv_ev_room_message(const char *const room_jid, const char *const nick, - const char *const message); -void sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp); + const char *const message, const char *const id, gboolean omemo); +void sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp, gboolean omemo); void sv_ev_incoming_private_message(const char *const fulljid, char *message); void sv_ev_delayed_private_message(const char *const fulljid, char *message, GDateTime *timestamp); void sv_ev_typing(char *barejid, char *resource); @@ -73,8 +73,8 @@ void sv_ev_room_occupent_kicked(const char *const room, const char *const nick, void sv_ev_room_banned(const char *const room, const char *const actor, const char *const reason); void sv_ev_room_occupent_banned(const char *const room, const char *const nick, const char *const actor, const char *const reason); -void sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message); -void sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message); +void sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message, gboolean omemo); +void sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message, gboolean omemo); void sv_ev_xmpp_stanza(const char *const msg); void sv_ev_muc_self_online(const char *const room, const char *const nick, gboolean config_required, const char *const role, const char *const affiliation, const char *const actor, const char *const reason, @@ -85,6 +85,7 @@ void sv_ev_muc_occupant_online(const char *const room, const char *const nick, c void sv_ev_roster_update(const char *const barejid, const char *const name, GSList *groups, const char *const subscription, gboolean pending_out); void sv_ev_roster_received(void); +void sv_ev_connection_features_received(void); int sv_ev_certfail(const char *const errormsg, TLSCertificate *cert); void sv_ev_lastactivity_response(const char *const from, const int seconds, const char *const msg); void sv_ev_bookmark_autojoin(Bookmark *bookmark); diff --git a/src/log.c b/src/log.c index 0133a6cf..b679eb1a 100644 --- a/src/log.c +++ b/src/log.c @@ -48,6 +48,7 @@ #include "config/files.h" #include "config/preferences.h" #include "xmpp/xmpp.h" +#include "xmpp/muc.h" #define PROF "prof" @@ -89,6 +90,8 @@ static void _rotate_log_file(void); static char* _log_string_from_level(log_level_t level); static void _chat_log_chat(const char *const login, const char *const other, const gchar *const msg, chat_log_direction_t direction, GDateTime *timestamp); +static void _groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick, + const gchar *const msg); void log_debug(const char *const msg, ...) @@ -305,6 +308,23 @@ chat_log_pgp_msg_out(const char *const barejid, const char *const msg) } void +chat_log_omemo_msg_out(const char *const barejid, const char *const msg) +{ + if (prefs_get_boolean(PREF_CHLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG); + if (strcmp(pref_omemo_log, "on") == 0) { + _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL); + } else if (strcmp(pref_omemo_log, "redact") == 0) { + _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_OUT_LOG, NULL); + } + prefs_free_string(pref_omemo_log); + jid_destroy(jidp); + } +} + +void chat_log_otr_msg_in(const char *const barejid, const char *const msg, gboolean was_decrypted, GDateTime *timestamp) { if (prefs_get_boolean(PREF_CHLOG)) { @@ -339,6 +359,23 @@ chat_log_pgp_msg_in(const char *const barejid, const char *const msg, GDateTime } void +chat_log_omemo_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp) +{ + if (prefs_get_boolean(PREF_CHLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG); + if (strcmp(pref_omemo_log, "on") == 0) { + _chat_log_chat(jidp->barejid, barejid, msg, PROF_IN_LOG, timestamp); + } else if (strcmp(pref_omemo_log, "redact") == 0) { + _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_IN_LOG, timestamp); + } + prefs_free_string(pref_omemo_log); + jid_destroy(jidp); + } +} + +void chat_log_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp) { if (prefs_get_boolean(PREF_CHLOG)) { @@ -406,7 +443,66 @@ _chat_log_chat(const char *const login, const char *const other, const char *con } void -groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick, const gchar *const msg) +groupchat_log_msg_out(const gchar *const room, const gchar *const msg) +{ + if (prefs_get_boolean(PREF_GRLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + char *mynick = muc_nick(room); + _groupchat_log_chat(jidp->barejid, room, mynick, msg); + jid_destroy(jidp); + } +} + +void +groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg) +{ + if (prefs_get_boolean(PREF_GRLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + _groupchat_log_chat(jidp->barejid, room, nick, msg); + jid_destroy(jidp); + } +} + +void +groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg) +{ + if (prefs_get_boolean(PREF_CHLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG); + char *mynick = muc_nick(room); + if (strcmp(pref_omemo_log, "on") == 0) { + _groupchat_log_chat(jidp->barejid, room, mynick, msg); + } else if (strcmp(pref_omemo_log, "redact") == 0) { + _groupchat_log_chat(jidp->barejid, room, mynick, "[redacted]"); + } + prefs_free_string(pref_omemo_log); + jid_destroy(jidp); + } +} + +void +groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg) +{ + if (prefs_get_boolean(PREF_CHLOG)) { + const char *jid = connection_get_fulljid(); + Jid *jidp = jid_create(jid); + char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG); + if (strcmp(pref_omemo_log, "on") == 0) { + _groupchat_log_chat(jidp->barejid, room, nick, msg); + } else if (strcmp(pref_omemo_log, "redact") == 0) { + _groupchat_log_chat(jidp->barejid, room, nick, "[redacted]"); + } + prefs_free_string(pref_omemo_log); + jid_destroy(jidp); + } +} + +void +_groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick, + const gchar *const msg) { struct dated_chat_log *dated_log = g_hash_table_lookup(groupchat_logs, room); diff --git a/src/log.h b/src/log.h index 43a34ca1..1f45545c 100644 --- a/src/log.h +++ b/src/log.h @@ -71,16 +71,21 @@ void chat_log_init(void); void chat_log_msg_out(const char *const barejid, const char *const msg); void chat_log_otr_msg_out(const char *const barejid, const char *const msg); void chat_log_pgp_msg_out(const char *const barejid, const char *const msg); +void chat_log_omemo_msg_out(const char *const barejid, const char *const msg); void chat_log_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp); void chat_log_otr_msg_in(const char *const barejid, const char *const msg, gboolean was_decrypted, GDateTime *timestamp); void chat_log_pgp_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp); +void chat_log_omemo_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp); void chat_log_close(void); GSList* chat_log_get_previous(const gchar *const login, const gchar *const recipient); void groupchat_log_init(void); -void groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick, - const gchar *const msg); + +void groupchat_log_msg_out(const gchar *const room, const gchar *const msg); +void groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg); +void groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg); +void groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg); #endif diff --git a/src/main.c b/src/main.c index d2392a2b..6060ca27 100644 --- a/src/main.c +++ b/src/main.c @@ -138,6 +138,12 @@ main(int argc, char **argv) g_print("PGP support: Disabled\n"); #endif +#ifdef HAVE_OMEMO + g_print("OMEMO support: Enabled\n"); +#else + g_print("OMEMO support: Disabled\n"); +#endif + #ifdef HAVE_C g_print("C plugins: Enabled\n"); #else diff --git a/src/omemo/crypto.c b/src/omemo/crypto.c new file mode 100644 index 00000000..9d64a701 --- /dev/null +++ b/src/omemo/crypto.c @@ -0,0 +1,331 @@ +#include <assert.h> +#include <signal/signal_protocol.h> +#include <signal/signal_protocol_types.h> +#include <gcrypt.h> + +#include "log.h" +#include "omemo/omemo.h" +#include "omemo/crypto.h" + +int +omemo_crypto_init(void) +{ + if (!gcry_check_version(GCRYPT_VERSION)) { + return -1; + } + + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); + + return 0; +} + +int +omemo_random_func(uint8_t *data, size_t len, void *user_data) +{ + gcry_randomize(data, len, GCRY_VERY_STRONG_RANDOM); + return 0; +} + +int +omemo_hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data) +{ + gcry_error_t res; + gcry_mac_hd_t hd; + + res = gcry_mac_open(&hd, GCRY_MAC_HMAC_SHA256, 0, NULL); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *hmac_context = hd; + res = gcry_mac_setkey(hd, key, key_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + return 0; +} + +int +omemo_hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data) +{ + gcry_error_t res; + + res = gcry_mac_write(hmac_context, data, data_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + return 0; +} + +int +omemo_hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data) +{ + gcry_error_t res; + size_t mac_len = 32; + unsigned char out[mac_len]; + + res = gcry_mac_read(hmac_context, out, &mac_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *output = signal_buffer_create(out, mac_len); + return 0; +} + +void +omemo_hmac_sha256_cleanup_func(void *hmac_context, void *user_data) +{ + gcry_mac_close(hmac_context); +} + +int +omemo_sha512_digest_init_func(void **digest_context, void *user_data) +{ + gcry_error_t res; + gcry_md_hd_t hd; + + res = gcry_md_open(&hd, GCRY_MD_SHA512, 0); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *digest_context = hd; + + return 0; +} + +int +omemo_sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *user_data) +{ + gcry_md_write(digest_context, data, data_len); + + return 0; +} + +int +omemo_sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data) +{ + gcry_error_t res; + unsigned char out[64]; + + res = gcry_md_extract(digest_context, GCRY_MD_SHA512, out, 64); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *output = signal_buffer_create(out, 64); + return 0; +} + +void +omemo_sha512_digest_cleanup_func(void *digest_context, void *user_data) +{ + gcry_md_close(digest_context); +} + +int +omemo_encrypt_func(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, void *user_data) +{ + gcry_cipher_hd_t hd; + unsigned char *padded_plaintext; + unsigned char *ciphertext; + size_t ciphertext_len; + int mode; + int algo; + uint8_t padding = 0; + + switch (key_len) { + case 32: + algo = GCRY_CIPHER_AES256; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + mode = GCRY_CIPHER_MODE_CBC; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + gcry_cipher_open(&hd, algo, mode, GCRY_CIPHER_SECURE); + + gcry_cipher_setkey(hd, key, key_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + gcry_cipher_setiv(hd, iv, iv_len); + padding = 16 - (plaintext_len % 16); + break; + default: + assert(FALSE); + } + + padded_plaintext = malloc(plaintext_len + padding); + memcpy(padded_plaintext, plaintext, plaintext_len); + memset(padded_plaintext + plaintext_len, padding, padding); + + ciphertext_len = plaintext_len + padding; + ciphertext = malloc(ciphertext_len); + gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, padded_plaintext, plaintext_len + padding); + + *output = signal_buffer_create(ciphertext, ciphertext_len); + free(padded_plaintext); + free(ciphertext); + + gcry_cipher_close(hd); + + return SG_SUCCESS; +} + +int +omemo_decrypt_func(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, void *user_data) +{ + int ret = SG_SUCCESS; + gcry_cipher_hd_t hd; + unsigned char *plaintext; + size_t plaintext_len; + int mode; + int algo; + uint8_t padding = 0; + + switch (key_len) { + case 32: + algo = GCRY_CIPHER_AES256; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + mode = GCRY_CIPHER_MODE_CBC; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + gcry_cipher_open(&hd, algo, mode, GCRY_CIPHER_SECURE); + + gcry_cipher_setkey(hd, key, key_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + gcry_cipher_setiv(hd, iv, iv_len); + break; + default: + assert(FALSE); + } + + plaintext_len = ciphertext_len; + plaintext = malloc(plaintext_len); + gcry_cipher_decrypt(hd, plaintext, plaintext_len, ciphertext, ciphertext_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + padding = plaintext[plaintext_len - 1]; + break; + default: + assert(FALSE); + } + + int i; + for (i = 0; i < padding; i++) { + if (plaintext[plaintext_len - 1 - i] != padding) { + ret = SG_ERR_UNKNOWN; + goto out; + } + } + + *output = signal_buffer_create(plaintext, plaintext_len - padding); + +out: + free(plaintext); + + gcry_cipher_close(hd); + + return ret; +} + +int +aes128gcm_encrypt(unsigned char *ciphertext, size_t *ciphertext_len, unsigned char *tag, size_t *tag_len, const unsigned char *const plaintext, size_t plaintext_len, const unsigned char *const iv, const unsigned char *const key) +{ + gcry_error_t res; + gcry_cipher_hd_t hd; + + res = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + res = gcry_cipher_setkey(hd, key, AES128_GCM_KEY_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + res = gcry_cipher_setiv(hd, iv, AES128_GCM_IV_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_encrypt(hd, ciphertext, *ciphertext_len, plaintext, plaintext_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_gettag(hd, tag, *tag_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + +out: + gcry_cipher_close(hd); + return res; +} + +int +aes128gcm_decrypt(unsigned char *plaintext, size_t *plaintext_len, const unsigned char *const ciphertext, size_t ciphertext_len, const unsigned char *const iv, const unsigned char *const key, const unsigned char *const tag) +{ + gcry_error_t res; + gcry_cipher_hd_t hd; + + res = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setkey(hd, key, AES128_GCM_KEY_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setiv(hd, iv, AES128_GCM_IV_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_decrypt(hd, plaintext, *plaintext_len, ciphertext, ciphertext_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_checktag(hd, tag, AES128_GCM_TAG_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + +out: + gcry_cipher_close(hd); + return res; +} diff --git a/src/omemo/crypto.h b/src/omemo/crypto.h new file mode 100644 index 00000000..4b882455 --- /dev/null +++ b/src/omemo/crypto.h @@ -0,0 +1,148 @@ +#include <signal/signal_protocol_types.h> + +#define AES128_GCM_KEY_LENGTH 16 +#define AES128_GCM_IV_LENGTH 16 +#define AES128_GCM_TAG_LENGTH 16 + +int omemo_crypto_init(void); +/** +* Callback for a secure random number generator. +* This function shall fill the provided buffer with random bytes. +* +* @param data pointer to the output buffer +* @param len size of the output buffer +* @return 0 on success, negative on failure +*/ +int omemo_random_func(uint8_t *data, size_t len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall initialize an HMAC context with the provided key. +* +* @param hmac_context private HMAC context pointer +* @param key pointer to the key +* @param key_len length of the key +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall update the HMAC context with the provided data +* +* @param hmac_context private HMAC context pointer +* @param data pointer to the data +* @param data_len length of the data +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall finalize an HMAC calculation and populate the output +* buffer with the result. +* +* @param hmac_context private HMAC context pointer +* @param output buffer to be allocated and populated with the result +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall free the private context allocated in +* hmac_sha256_init_func. +* +* @param hmac_context private HMAC context pointer +*/ +void omemo_hmac_sha256_cleanup_func(void *hmac_context, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall initialize a digest context. +* +* @param digest_context private digest context pointer +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_init_func(void **digest_context, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall update the digest context with the provided data. +* +* @param digest_context private digest context pointer +* @param data pointer to the data +* @param data_len length of the data +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall finalize the digest calculation, populate the +* output buffer with the result, and prepare the context for reuse. +* +* @param digest_context private digest context pointer +* @param output buffer to be allocated and populated with the result +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall free the private context allocated in +* sha512_digest_init_func. +* +* @param digest_context private digest context pointer +*/ +void omemo_sha512_digest_cleanup_func(void *digest_context, void *user_data); + +/** +* Callback for an AES encryption implementation. +* +* @param output buffer to be allocated and populated with the ciphertext +* @param cipher specific cipher variant to use, either SG_CIPHER_AES_CTR_NOPADDING or SG_CIPHER_AES_CBC_PKCS5 +* @param key the encryption key +* @param key_len length of the encryption key +* @param iv the initialization vector +* @param iv_len length of the initialization vector +* @param plaintext the plaintext to encrypt +* @param plaintext_len length of the plaintext +* @return 0 on success, negative on failure +*/ +int omemo_encrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, + void *user_data); + +/** +* Callback for an AES decryption implementation. +* +* @param output buffer to be allocated and populated with the plaintext +* @param cipher specific cipher variant to use, either SG_CIPHER_AES_CTR_NOPADDING or SG_CIPHER_AES_CBC_PKCS5 +* @param key the encryption key +* @param key_len length of the encryption key +* @param iv the initialization vector +* @param iv_len length of the initialization vector +* @param ciphertext the ciphertext to decrypt +* @param ciphertext_len length of the ciphertext +* @return 0 on success, negative on failure +*/ +int omemo_decrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, + void *user_data); + +int aes128gcm_encrypt(unsigned char *ciphertext, size_t *ciphertext_len, + unsigned char *tag, size_t *tag_len, + const unsigned char *const plaintext, size_t plaintext_len, + const unsigned char *const iv, const unsigned char *const key); + +int aes128gcm_decrypt(unsigned char *plaintext, + size_t *plaintext_len, const unsigned char *const ciphertext, + size_t ciphertext_len, const unsigned char *const iv, + const unsigned char *const key, const unsigned char *const tag); diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c new file mode 100644 index 00000000..7b3855dd --- /dev/null +++ b/src/omemo/omemo.c @@ -0,0 +1,1410 @@ +#include <sys/time.h> +#include <sys/stat.h> + +#include <assert.h> +#include <errno.h> +#include <glib.h> +#include <pthread.h> +#include <signal/key_helper.h> +#include <signal/protocol.h> +#include <signal/signal_protocol.h> +#include <signal/session_builder.h> +#include <signal/session_cipher.h> +#include <gcrypt.h> + +#include "config/account.h" +#include "config/files.h" +#include "log.h" +#include "omemo/crypto.h" +#include "omemo/omemo.h" +#include "omemo/store.h" +#include "ui/ui.h" +#include "ui/window_list.h" +#include "xmpp/connection.h" +#include "xmpp/muc.h" +#include "xmpp/omemo.h" +#include "xmpp/roster_list.h" +#include "xmpp/xmpp.h" + +static gboolean loaded; + +static void _generate_pre_keys(int count); +static void _generate_signed_pre_key(void); +static void _load_identity(void); +static void _load_trust(void); +static void _load_sessions(void); +static void _lock(void *user_data); +static void _unlock(void *user_data); +static void _omemo_log(int level, const char *message, size_t len, void *user_data); +static gboolean _handle_own_device_list(const char *const jid, GList *device_list); +static gboolean _handle_device_list_start_session(const char *const jid, GList *device_list); +static char * _omemo_fingerprint(ec_public_key *identity, gboolean formatted); +static unsigned char *_omemo_fingerprint_decode(const char *const fingerprint, size_t *len); +static void _cache_device_identity(const char *const jid, uint32_t device_id, ec_public_key *identity); +static void _g_hash_table_free(GHashTable *hash_table); + +typedef gboolean (*OmemoDeviceListHandler)(const char *const jid, GList *device_list); + +struct omemo_context_t { + pthread_mutexattr_t attr; + pthread_mutex_t lock; + signal_context *signal; + uint32_t device_id; + GHashTable *device_list; + GHashTable *device_list_handler; + ratchet_identity_key_pair *identity_key_pair; + uint32_t registration_id; + uint32_t signed_pre_key_id; + signal_protocol_store_context *store; + GHashTable *session_store; + GHashTable *pre_key_store; + GHashTable *signed_pre_key_store; + identity_key_store_t identity_key_store; + GHashTable *device_ids; + GString *identity_filename; + GKeyFile *identity_keyfile; + GString *trust_filename; + GKeyFile *trust_keyfile; + GString *sessions_filename; + GKeyFile *sessions_keyfile; + GHashTable *known_devices; + Autocomplete fingerprint_ac; +}; + +static omemo_context omemo_ctx; + +void +omemo_init(void) +{ + log_info("OMEMO: initialising"); + if (omemo_crypto_init() != 0) { + cons_show("Error initializing OMEMO crypto"); + } + + pthread_mutexattr_init(&omemo_ctx.attr); + pthread_mutexattr_settype(&omemo_ctx.attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&omemo_ctx.lock, &omemo_ctx.attr); + + omemo_ctx.fingerprint_ac = autocomplete_new(); +} + +void +omemo_on_connect(ProfAccount *account) +{ + GError *error = NULL; + + if (signal_context_create(&omemo_ctx.signal, &omemo_ctx) != 0) { + cons_show("Error initializing OMEMO context"); + return; + } + + if (signal_context_set_log_function(omemo_ctx.signal, _omemo_log) != 0) { + cons_show("Error initializing OMEMO log"); + } + + signal_crypto_provider crypto_provider = { + .random_func = omemo_random_func, + .hmac_sha256_init_func = omemo_hmac_sha256_init_func, + .hmac_sha256_update_func = omemo_hmac_sha256_update_func, + .hmac_sha256_final_func = omemo_hmac_sha256_final_func, + .hmac_sha256_cleanup_func = omemo_hmac_sha256_cleanup_func, + .sha512_digest_init_func = omemo_sha512_digest_init_func, + .sha512_digest_update_func = omemo_sha512_digest_update_func, + .sha512_digest_final_func = omemo_sha512_digest_final_func, + .sha512_digest_cleanup_func = omemo_sha512_digest_cleanup_func, + .encrypt_func = omemo_encrypt_func, + .decrypt_func = omemo_decrypt_func, + .user_data = NULL + }; + + if (signal_context_set_crypto_provider(omemo_ctx.signal, &crypto_provider) != 0) { + cons_show("Error initializing OMEMO crypto"); + return; + } + + signal_context_set_locking_functions(omemo_ctx.signal, _lock, _unlock); + + signal_protocol_store_context_create(&omemo_ctx.store, omemo_ctx.signal); + + omemo_ctx.session_store = session_store_new(); + signal_protocol_session_store session_store = { + .load_session_func = load_session, + .get_sub_device_sessions_func = get_sub_device_sessions, + .store_session_func = store_session, + .contains_session_func = contains_session, + .delete_session_func = delete_session, + .delete_all_sessions_func = delete_all_sessions, + .destroy_func = NULL, + .user_data = omemo_ctx.session_store + }; + signal_protocol_store_context_set_session_store(omemo_ctx.store, &session_store); + + omemo_ctx.pre_key_store = pre_key_store_new(); + signal_protocol_pre_key_store pre_key_store = { + .load_pre_key = load_pre_key, + .store_pre_key = store_pre_key, + .contains_pre_key = contains_pre_key, + .remove_pre_key = remove_pre_key, + .destroy_func = NULL, + .user_data = omemo_ctx.pre_key_store + }; + signal_protocol_store_context_set_pre_key_store(omemo_ctx.store, &pre_key_store); + + omemo_ctx.signed_pre_key_store = signed_pre_key_store_new(); + signal_protocol_signed_pre_key_store signed_pre_key_store = { + .load_signed_pre_key = load_signed_pre_key, + .store_signed_pre_key = store_signed_pre_key, + .contains_signed_pre_key = contains_signed_pre_key, + .remove_signed_pre_key = remove_signed_pre_key, + .destroy_func = NULL, + .user_data = omemo_ctx.signed_pre_key_store + }; + signal_protocol_store_context_set_signed_pre_key_store(omemo_ctx.store, &signed_pre_key_store); + + identity_key_store_new(&omemo_ctx.identity_key_store); + signal_protocol_identity_key_store identity_key_store = { + .get_identity_key_pair = get_identity_key_pair, + .get_local_registration_id = get_local_registration_id, + .save_identity = save_identity, + .is_trusted_identity = is_trusted_identity, + .destroy_func = NULL, + .user_data = &omemo_ctx.identity_key_store + }; + signal_protocol_store_context_set_identity_key_store(omemo_ctx.store, &identity_key_store); + + + loaded = FALSE; + omemo_ctx.device_list = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)g_list_free); + omemo_ctx.device_list_handler = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); + omemo_ctx.known_devices = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)_g_hash_table_free); + + omemo_ctx.fingerprint_ac = autocomplete_new(); + + char *omemodir = files_get_data_path(DIR_OMEMO); + GString *basedir = g_string_new(omemodir); + free(omemodir); + gchar *account_dir = str_replace(account->jid, "@", "_at_"); + g_string_append(basedir, "/"); + g_string_append(basedir, account_dir); + g_string_append(basedir, "/"); + free(account_dir); + + omemo_ctx.identity_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.identity_filename, "identity.txt"); + omemo_ctx.trust_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.trust_filename, "trust.txt"); + omemo_ctx.sessions_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.sessions_filename, "sessions.txt"); + + + errno = 0; + int res = g_mkdir_with_parents(basedir->str, S_IRWXU); + if (res == -1) { + char *errmsg = strerror(errno); + if (errmsg) { + log_error("OMEMO: error creating directory: %s, %s", basedir->str, errmsg); + } else { + log_error("OMEMO: creating directory: %s", basedir->str); + } + } + + g_string_free(basedir, TRUE); + + omemo_devicelist_subscribe(); + + omemo_ctx.identity_keyfile = g_key_file_new(); + omemo_ctx.trust_keyfile = g_key_file_new(); + omemo_ctx.sessions_keyfile = g_key_file_new(); + + if (g_key_file_load_from_file(omemo_ctx.identity_keyfile, omemo_ctx.identity_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_identity(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading identity from: %s, %s", omemo_ctx.identity_filename->str, error->message); + return; + } + + error = NULL; + if (g_key_file_load_from_file(omemo_ctx.trust_keyfile, omemo_ctx.trust_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_trust(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading trust from: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } + + error = NULL; + if (g_key_file_load_from_file(omemo_ctx.sessions_keyfile, omemo_ctx.sessions_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_sessions(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading sessions from: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } +} + +void +omemo_on_disconnect(void) +{ + signal_protocol_signed_pre_key_remove_key(omemo_ctx.store, omemo_ctx.signed_pre_key_id); + _g_hash_table_free(omemo_ctx.signed_pre_key_store); + + GHashTableIter iter; + gpointer id; + + g_hash_table_iter_init(&iter, omemo_ctx.pre_key_store); + while (g_hash_table_iter_next(&iter, &id, NULL)) { + signal_protocol_pre_key_remove_key(omemo_ctx.store, GPOINTER_TO_INT(id)); + } + + _g_hash_table_free(omemo_ctx.pre_key_store); + + g_string_free(omemo_ctx.identity_filename, TRUE); + g_key_file_free(omemo_ctx.identity_keyfile); + g_string_free(omemo_ctx.trust_filename, TRUE); + g_key_file_free(omemo_ctx.trust_keyfile); + g_string_free(omemo_ctx.sessions_filename, TRUE); + g_key_file_free(omemo_ctx.sessions_keyfile); +} + +void +omemo_generate_crypto_materials(ProfAccount *account) +{ + if (loaded) { + return; + } + + log_info("Generate long term OMEMO cryptography metarials"); + + /* Device ID */ + gcry_randomize(&omemo_ctx.device_id, 4, GCRY_VERY_STRONG_RANDOM); + omemo_ctx.device_id &= 0x7fffffff; + g_key_file_set_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_DEVICE_ID, omemo_ctx.device_id); + log_info("OMEMO: device id: %d", omemo_ctx.device_id); + + /* Identity key */ + signal_protocol_key_helper_generate_identity_key_pair(&omemo_ctx.identity_key_pair, omemo_ctx.signal); + + ec_public_key_serialize(&omemo_ctx.identity_key_store.public, ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair)); + char *identity_key_public = g_base64_encode(signal_buffer_data(omemo_ctx.identity_key_store.public), signal_buffer_len(omemo_ctx.identity_key_store.public)); + g_key_file_set_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC, identity_key_public); + g_free(identity_key_public); + + ec_private_key_serialize(&omemo_ctx.identity_key_store.private, ratchet_identity_key_pair_get_private(omemo_ctx.identity_key_pair)); + char *identity_key_private = g_base64_encode(signal_buffer_data(omemo_ctx.identity_key_store.private), signal_buffer_len(omemo_ctx.identity_key_store.private)); + g_key_file_set_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE, identity_key_private); + g_free(identity_key_private); + + /* Registration ID */ + signal_protocol_key_helper_generate_registration_id(&omemo_ctx.registration_id, 0, omemo_ctx.signal); + g_key_file_set_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_REGISTRATION_ID, omemo_ctx.registration_id); + + /* Pre keys */ + _generate_pre_keys(100); + + /* Signed pre key */ + _generate_signed_pre_key(); + + omemo_identity_keyfile_save(); + + loaded = TRUE; + + omemo_publish_crypto_materials(); + omemo_start_sessions(); +} + +void +omemo_publish_crypto_materials(void) +{ + if (loaded != TRUE) { + log_error("OMEMO: cannot publish crypto materials before they are generated"); + return; + } + + Jid *jid = jid_create(connection_get_fulljid()); + + /* Ensure we get our current device list, and it gets updated with our + * device_id */ + g_hash_table_insert(omemo_ctx.device_list_handler, strdup(jid->barejid), _handle_own_device_list); + omemo_devicelist_request(jid->barejid); + + omemo_bundle_publish(true); + + jid_destroy(jid); +} + +void +omemo_start_sessions(void) +{ + GSList *contacts = roster_get_contacts(ROSTER_ORD_NAME); + if (contacts) { + GSList *curr = contacts; + for (curr = contacts; curr != NULL; curr = g_slist_next(curr)){ + PContact contact = curr->data; + const char *jid = p_contact_barejid(contact); + omemo_start_session(jid); + } + } +} + +void +omemo_start_session(const char *const barejid) +{ + log_info("OMEMO: start session with %s", barejid); + GList *device_list = g_hash_table_lookup(omemo_ctx.device_list, barejid); + if (!device_list) { + log_info("OMEMO: missing device list for %s", barejid); + omemo_devicelist_request(barejid); + g_hash_table_insert(omemo_ctx.device_list_handler, strdup(barejid), _handle_device_list_start_session); + return; + } + + GList *device_id; + for (device_id = device_list; device_id != NULL; device_id = device_id->next) { + omemo_bundle_request(barejid, GPOINTER_TO_INT(device_id->data), omemo_start_device_session_handle_bundle, free, strdup(barejid)); + } +} + +void +omemo_start_muc_sessions(const char *const roomjid) +{ + GList *roster = muc_roster(roomjid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + Jid *jid = jid_create(occupant->jid); + omemo_start_session(jid->barejid); + jid_destroy(jid); + } + g_list_free(roster); +} + +gboolean +omemo_loaded(void) +{ + return loaded; +} + +uint32_t +omemo_device_id(void) +{ + return omemo_ctx.device_id; +} + +void +omemo_identity_key(unsigned char **output, size_t *length) +{ + signal_buffer *buffer = NULL; + ec_public_key_serialize(&buffer, ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair)); + *length = signal_buffer_len(buffer); + *output = malloc(*length); + memcpy(*output, signal_buffer_data(buffer), *length); + signal_buffer_free(buffer); +} + +void +omemo_signed_prekey(unsigned char **output, size_t *length) +{ + session_signed_pre_key *signed_pre_key; + signal_buffer *buffer = NULL; + + if (signal_protocol_signed_pre_key_load_key(omemo_ctx.store, &signed_pre_key, omemo_ctx.signed_pre_key_id) != SG_SUCCESS) { + *output = NULL; + *length = 0; + return; + } + + ec_public_key_serialize(&buffer, ec_key_pair_get_public(session_signed_pre_key_get_key_pair(signed_pre_key))); + SIGNAL_UNREF(signed_pre_key); + *length = signal_buffer_len(buffer); + *output = malloc(*length); + memcpy(*output, signal_buffer_data(buffer), *length); + signal_buffer_free(buffer); +} + +void +omemo_signed_prekey_signature(unsigned char **output, size_t *length) +{ + session_signed_pre_key *signed_pre_key; + + if (signal_protocol_signed_pre_key_load_key(omemo_ctx.store, &signed_pre_key, omemo_ctx.signed_pre_key_id) != SG_SUCCESS) { + *output = NULL; + *length = 0; + return; + } + + *length = session_signed_pre_key_get_signature_len(signed_pre_key); + *output = malloc(*length); + memcpy(*output, session_signed_pre_key_get_signature(signed_pre_key), *length); + SIGNAL_UNREF(signed_pre_key); +} + +void +omemo_prekeys(GList **prekeys, GList **ids, GList **lengths) +{ + GHashTableIter iter; + gpointer id; + + g_hash_table_iter_init(&iter, omemo_ctx.pre_key_store); + while (g_hash_table_iter_next(&iter, &id, NULL)) { + session_pre_key *pre_key; + int ret; + ret = signal_protocol_pre_key_load_key(omemo_ctx.store, &pre_key, GPOINTER_TO_INT(id)); + if (ret != SG_SUCCESS) { + continue; + } + + signal_buffer *public_key; + ec_public_key_serialize(&public_key, ec_key_pair_get_public(session_pre_key_get_key_pair(pre_key))); + SIGNAL_UNREF(pre_key); + size_t length = signal_buffer_len(public_key); + unsigned char *prekey_value = malloc(length); + memcpy(prekey_value, signal_buffer_data(public_key), length); + signal_buffer_free(public_key); + + *prekeys = g_list_append(*prekeys, prekey_value); + *ids = g_list_append(*ids, GINT_TO_POINTER(id)); + *lengths = g_list_append(*lengths, GINT_TO_POINTER(length)); + } +} + +void +omemo_set_device_list(const char *const from, GList * device_list) +{ + Jid *jid; + if (from) { + jid = jid_create(from); + } else { + jid = jid_create(connection_get_fulljid()); + } + + g_hash_table_insert(omemo_ctx.device_list, strdup(jid->barejid), device_list); + + OmemoDeviceListHandler handler = g_hash_table_lookup(omemo_ctx.device_list_handler, jid->barejid); + if (handler) { + gboolean keep = handler(jid->barejid, device_list); + if (!keep) { + g_hash_table_remove(omemo_ctx.device_list_handler, jid->barejid); + } + } + + jid_destroy(jid); +} + +GKeyFile * +omemo_identity_keyfile(void) +{ + return omemo_ctx.identity_keyfile; +} + +void +omemo_identity_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.identity_keyfile, omemo_ctx.identity_filename->str, &error)) { + log_error("OMEMO: error saving identity to: %s, %s", omemo_ctx.identity_filename->str, error->message); + } +} + +GKeyFile * +omemo_trust_keyfile(void) +{ + return omemo_ctx.trust_keyfile; +} + +void +omemo_trust_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.trust_keyfile, omemo_ctx.trust_filename->str, &error)) { + log_error("OMEMO: error saving trust to: %s, %s", omemo_ctx.trust_filename->str, error->message); + } +} + +GKeyFile * +omemo_sessions_keyfile(void) +{ + return omemo_ctx.sessions_keyfile; +} + +void +omemo_sessions_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.sessions_keyfile, omemo_ctx.sessions_filename->str, &error)) { + log_error("OMEMO: error saving sessions to: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } +} + +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_raw, size_t signed_prekey_len, + const unsigned char *const signature, size_t signature_len, + const unsigned char *const identity_key_raw, size_t identity_key_len) +{ + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = device_id, + }; + + ec_public_key *identity_key; + curve_decode_point(&identity_key, identity_key_raw, identity_key_len, omemo_ctx.signal); + _cache_device_identity(jid, device_id, identity_key); + + gboolean trusted = is_trusted_identity(&address, (uint8_t *)identity_key_raw, identity_key_len, &omemo_ctx.identity_key_store); + + if (!trusted) { + goto out; + } + + if (!contains_session(&address, omemo_ctx.session_store)) { + int res; + session_pre_key_bundle *bundle; + signal_protocol_address *address; + + address = malloc(sizeof(signal_protocol_address)); + address->name = strdup(jid); + address->name_len = strlen(jid); + address->device_id = device_id; + + session_builder *builder; + res = session_builder_create(&builder, omemo_ctx.store, address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create session builder for %s device %d", jid, device_id); + goto out; + } + + int prekey_index; + gcry_randomize(&prekey_index, sizeof(int), GCRY_STRONG_RANDOM); + prekey_index %= g_list_length(prekeys); + omemo_key_t *prekey = g_list_nth_data(prekeys, prekey_index); + + ec_public_key *prekey_public; + curve_decode_point(&prekey_public, prekey->data, prekey->length, omemo_ctx.signal); + ec_public_key *signed_prekey; + curve_decode_point(&signed_prekey, signed_prekey_raw, signed_prekey_len, omemo_ctx.signal); + + res = session_pre_key_bundle_create(&bundle, 0, device_id, prekey->id, prekey_public, signed_prekey_id, signed_prekey, signature, signature_len, identity_key); + if (res != 0) { + log_error("OMEMO: cannot create pre key bundle for %s device %d", jid, device_id); + goto out; + } + + res = session_builder_process_pre_key_bundle(builder, bundle); + if (res != 0) { + log_error("OMEMO: cannot process pre key bundle for %s device %d", jid, device_id); + goto out; + } + + log_info("OMEMO: create session with %s device %d", jid, device_id); + } + +out: + SIGNAL_UNREF(identity_key); +} + +char * +omemo_on_message_send(ProfWin *win, const char *const message, gboolean request_receipt, gboolean muc) +{ + char *id = NULL; + int res; + Jid *jid = jid_create(connection_get_fulljid()); + GList *keys = NULL; + + unsigned char *key; + unsigned char *iv; + unsigned char *ciphertext; + unsigned char *tag; + unsigned char *key_tag; + size_t ciphertext_len, tag_len; + + ciphertext_len = strlen(message); + ciphertext = malloc(ciphertext_len); + tag_len = AES128_GCM_TAG_LENGTH; + tag = gcry_malloc_secure(tag_len); + key_tag = gcry_malloc_secure(AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH); + + key = gcry_random_bytes_secure(AES128_GCM_KEY_LENGTH, GCRY_VERY_STRONG_RANDOM); + iv = gcry_random_bytes_secure(AES128_GCM_IV_LENGTH, GCRY_VERY_STRONG_RANDOM); + + res = aes128gcm_encrypt(ciphertext, &ciphertext_len, tag, &tag_len, (const unsigned char * const)message, strlen(message), iv, key); + if (res != 0) { + log_error("OMEMO: cannot encrypt message"); + goto out; + } + + memcpy(key_tag, key, AES128_GCM_KEY_LENGTH); + memcpy(key_tag + AES128_GCM_KEY_LENGTH, tag, AES128_GCM_TAG_LENGTH); + + GList *recipients = NULL; + if (muc) { + ProfMucWin *mucwin = (ProfMucWin *)win; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + GList *roster = muc_roster(mucwin->roomjid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + Jid *jid = jid_create(occupant->jid); + if (!jid->barejid) { + log_warning("OMEMO: missing barejid for MUC %s occupant %s", mucwin->roomjid, occupant->nick); + } else { + recipients = g_list_append(recipients, strdup(jid->barejid)); + } + jid_destroy(jid); + } + g_list_free(roster); + } else { + ProfChatWin *chatwin = (ProfChatWin *)win; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + recipients = g_list_append(recipients, strdup(chatwin->barejid)); + } + + GList *device_ids_iter; + + GList *recipients_iter; + for (recipients_iter = recipients; recipients_iter != NULL; recipients_iter = recipients_iter->next) { + GList *recipient_device_id = NULL; + recipient_device_id = g_hash_table_lookup(omemo_ctx.device_list, recipients_iter->data); + if (!recipient_device_id) { + log_warning("OMEMO: cannot find device ids for %s", recipients_iter->data); + continue; + } + + for (device_ids_iter = recipient_device_id; device_ids_iter != NULL; device_ids_iter = device_ids_iter->next) { + int res; + ciphertext_message *ciphertext; + session_cipher *cipher; + signal_protocol_address address = { + .name = recipients_iter->data, + .name_len = strlen(recipients_iter->data), + .device_id = GPOINTER_TO_INT(device_ids_iter->data) + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create cipher for %s device id %d", address.name, address.device_id); + continue; + } + + res = session_cipher_encrypt(cipher, key_tag, AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH, &ciphertext); + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot encrypt key for %s device id %d", address.name, address.device_id); + continue; + } + signal_buffer *buffer = ciphertext_message_get_serialized(ciphertext); + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + key->length = signal_buffer_len(buffer); + key->data = malloc(key->length); + memcpy(key->data, signal_buffer_data(buffer), key->length); + key->device_id = GPOINTER_TO_INT(device_ids_iter->data); + key->prekey = ciphertext_message_get_type(ciphertext) == CIPHERTEXT_PREKEY_TYPE; + keys = g_list_append(keys, key); + SIGNAL_UNREF(ciphertext); + } + } + + g_list_free_full(recipients, free); + + if (!muc) { + GList *sender_device_id = g_hash_table_lookup(omemo_ctx.device_list, jid->barejid); + for (device_ids_iter = sender_device_id; device_ids_iter != NULL; device_ids_iter = device_ids_iter->next) { + int res; + ciphertext_message *ciphertext; + session_cipher *cipher; + signal_protocol_address address = { + .name = jid->barejid, + .name_len = strlen(jid->barejid), + .device_id = GPOINTER_TO_INT(device_ids_iter->data) + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create cipher for %s device id %d", address.name, address.device_id); + continue; + } + + res = session_cipher_encrypt(cipher, key_tag, AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH, &ciphertext); + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot encrypt key for %s device id %d", address.name, address.device_id); + continue; + } + signal_buffer *buffer = ciphertext_message_get_serialized(ciphertext); + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + key->length = signal_buffer_len(buffer); + key->data = malloc(key->length); + memcpy(key->data, signal_buffer_data(buffer), key->length); + key->device_id = GPOINTER_TO_INT(device_ids_iter->data); + key->prekey = ciphertext_message_get_type(ciphertext) == CIPHERTEXT_PREKEY_TYPE; + keys = g_list_append(keys, key); + SIGNAL_UNREF(ciphertext); + } + } + + if (muc) { + ProfMucWin *mucwin = (ProfMucWin *)win; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + id = message_send_chat_omemo(mucwin->roomjid, omemo_ctx.device_id, keys, iv, AES128_GCM_IV_LENGTH, ciphertext, ciphertext_len, request_receipt, TRUE); + } else { + ProfChatWin *chatwin = (ProfChatWin *)win; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + id = message_send_chat_omemo(chatwin->barejid, omemo_ctx.device_id, keys, iv, AES128_GCM_IV_LENGTH, ciphertext, ciphertext_len, request_receipt, FALSE); + } + +out: + jid_destroy(jid); + g_list_free_full(keys, (GDestroyNotify)omemo_key_free); + free(ciphertext); + gcry_free(key); + gcry_free(iv); + gcry_free(tag); + gcry_free(key_tag); + + return id; +} + +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) +{ + unsigned char *plaintext = NULL; + Jid *sender = NULL; + Jid *from = jid_create(from_jid); + if (!from) { + log_error("Invalid jid %s", from_jid); + goto out; + } + + int res; + GList *key_iter; + omemo_key_t *key = NULL; + for (key_iter = keys; key_iter != NULL; key_iter = key_iter->next) { + if (((omemo_key_t *)key_iter->data)->device_id == omemo_ctx.device_id) { + key = key_iter->data; + break; + } + } + + if (!key) { + log_warning("OMEMO: Received a message with no corresponding key"); + goto out; + } + + if (muc) { + GList *roster = muc_roster(from->barejid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + if (g_strcmp0(occupant->nick, from->resourcepart) == 0) { + sender = jid_create(occupant->jid); + break; + } + } + g_list_free(roster); + if (!sender) { + log_warning("OMEMO: cannot find MUC message sender fulljid"); + goto out; + } + } else { + sender = jid_create(from->barejid); + } + + session_cipher *cipher; + signal_buffer *plaintext_key; + signal_protocol_address address = { + .name = sender->barejid, + .name_len = strlen(sender->barejid), + .device_id = sid + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create session cipher"); + goto out; + } + + if (key->prekey) { + log_debug("OMEMO: decrypting message with prekey"); + pre_key_signal_message *message; + + pre_key_signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal); + + res = session_cipher_decrypt_pre_key_signal_message(cipher, message, NULL, &plaintext_key); + /* Replace used pre_key in bundle */ + uint32_t pre_key_id = pre_key_signal_message_get_pre_key_id(message); + ec_key_pair *ec_pair; + session_pre_key *new_pre_key; + curve_generate_key_pair(omemo_ctx.signal, &ec_pair); + session_pre_key_create(&new_pre_key, pre_key_id, ec_pair); + signal_protocol_pre_key_store_key(omemo_ctx.store, new_pre_key); + SIGNAL_UNREF(new_pre_key); + SIGNAL_UNREF(message); + SIGNAL_UNREF(ec_pair); + omemo_bundle_publish(true); + + if (res == 0) { + /* Start a new session */ + omemo_bundle_request(sender->barejid, sid, omemo_start_device_session_handle_bundle, free, strdup(sender->barejid)); + } + } else { + log_debug("OMEMO: decrypting message with existing session"); + signal_message *message; + signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal); + res = session_cipher_decrypt_signal_message(cipher, message, NULL, &plaintext_key); + SIGNAL_UNREF(message); + } + + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot decrypt message key"); + goto out; + } + + if (signal_buffer_len(plaintext_key) != AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH) { + log_error("OMEMO: invalid key length"); + signal_buffer_free(plaintext_key); + goto out; + } + + size_t plaintext_len = payload_len; + plaintext = malloc(plaintext_len + 1); + res = aes128gcm_decrypt(plaintext, &plaintext_len, payload, payload_len, iv, + signal_buffer_data(plaintext_key), + signal_buffer_data(plaintext_key) + AES128_GCM_KEY_LENGTH); + signal_buffer_free(plaintext_key); + if (res != 0) { + log_error("OMEMO: cannot decrypt message: %s", gcry_strerror(res)); + free(plaintext); + plaintext = NULL; + goto out; + } + + plaintext[plaintext_len] = '\0'; + +out: + jid_destroy(from); + jid_destroy(sender); + return (char *)plaintext; +} + +char * +omemo_format_fingerprint(const char *const fingerprint) +{ + char *output = malloc(strlen(fingerprint) + strlen(fingerprint) / 8); + + int i, j; + for (i = 0, j = 0; i < strlen(fingerprint); i++) { + if (i > 0 && i % 8 == 0) { + output[j++] = '-'; + } + output[j++] = fingerprint[i]; + } + + output[j] = '\0'; + + return output; +} + +char * +omemo_own_fingerprint(gboolean formatted) +{ + ec_public_key *identity = ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair); + return _omemo_fingerprint(identity, formatted); +} + +GList * +omemo_known_device_identities(const char *const jid) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + return NULL; + } + + return g_hash_table_get_keys(known_identities); +} + +gboolean +omemo_is_trusted_jid(const char *const jid) +{ + GHashTable *trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid); + if (!trusted) { + return FALSE; + } + + if (g_hash_table_size(trusted) > 0) { + return TRUE; + } + + return FALSE; +} + +gboolean +omemo_is_trusted_identity(const char *const jid, const char *const fingerprint) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + return FALSE; + } + + void *device_id = g_hash_table_lookup(known_identities, fingerprint); + if (!device_id) { + return FALSE; + } + + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = GPOINTER_TO_INT(device_id), + }; + + size_t fingerprint_len; + unsigned char *fingerprint_raw = _omemo_fingerprint_decode(fingerprint, &fingerprint_len); + unsigned char djb_type[] = {'\x05'}; + signal_buffer *buffer = signal_buffer_create(djb_type, 1); + buffer = signal_buffer_append(buffer, fingerprint_raw, fingerprint_len); + + gboolean trusted = is_trusted_identity(&address, signal_buffer_data(buffer), signal_buffer_len(buffer), &omemo_ctx.identity_key_store); + + free(fingerprint_raw); + signal_buffer_free(buffer); + + return trusted; +} + +static char * +_omemo_fingerprint(ec_public_key *identity, gboolean formatted) +{ + int i; + signal_buffer *identity_public_key; + + ec_public_key_serialize(&identity_public_key, identity); + size_t identity_public_key_len = signal_buffer_len(identity_public_key); + unsigned char *identity_public_key_data = signal_buffer_data(identity_public_key); + + /* Skip first byte corresponding to signal DJB_TYPE */ + identity_public_key_len--; + identity_public_key_data = &identity_public_key_data[1]; + + char *fingerprint = malloc(identity_public_key_len * 2 + 1); + + for (i = 0; i < identity_public_key_len; i++) { + fingerprint[i * 2] = (identity_public_key_data[i] & 0xf0) >> 4; + fingerprint[i * 2] += '0'; + if (fingerprint[i * 2] > '9') { + fingerprint[i * 2] += 0x27; + } + + fingerprint[(i * 2) + 1] = identity_public_key_data[i] & 0x0f; + fingerprint[(i * 2) + 1] += '0'; + if (fingerprint[(i * 2) + 1] > '9') { + fingerprint[(i * 2) + 1] += 0x27; + } + } + + fingerprint[i * 2] = '\0'; + signal_buffer_free(identity_public_key); + + if (!formatted) { + return fingerprint; + } else { + char *formatted_fingerprint = omemo_format_fingerprint(fingerprint); + free(fingerprint); + return formatted_fingerprint; + } +} + +static unsigned char * +_omemo_fingerprint_decode(const char *const fingerprint, size_t *len) +{ + unsigned char *output = malloc(strlen(fingerprint) / 2 + 1); + + int i; + int j; + for (i = 0, j = 0; i < strlen(fingerprint);) { + if (!g_ascii_isxdigit(fingerprint[i])) { + i++; + continue; + } + + output[j] = g_ascii_xdigit_value(fingerprint[i++]) << 4; + output[j] |= g_ascii_xdigit_value(fingerprint[i++]); + j++; + } + + *len = j; + + return output; +} + +void +omemo_trust(const char *const jid, const char *const fingerprint_formatted) +{ + size_t len; + + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + log_warning("OMEMO: cannot trust unknown device: %s", fingerprint_formatted); + cons_show("Cannot trust unknown device: %s", fingerprint_formatted); + return; + } + + /* Unformat fingerprint */ + char *fingerprint = malloc(strlen(fingerprint_formatted)); + int i; + int j; + for (i = 0, j = 0; fingerprint_formatted[i] != '\0'; i++) { + if (!g_ascii_isxdigit(fingerprint_formatted[i])) { + continue; + } + fingerprint[j++] = fingerprint_formatted[i]; + } + + fingerprint[j] = '\0'; + + uint32_t device_id = GPOINTER_TO_INT(g_hash_table_lookup(known_identities, fingerprint)); + free(fingerprint); + + if (!device_id) { + log_warning("OMEMO: cannot trust unknown device: %s", fingerprint_formatted); + cons_show("Cannot trust unknown device: %s", fingerprint_formatted); + return; + } + + /* TODO should not hardcode DJB_TYPE here + * should instead store identity key in known_identities along with + * device_id */ + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = device_id, + }; + unsigned char *fingerprint_raw = _omemo_fingerprint_decode(fingerprint_formatted, &len); + unsigned char djb_type[] = {'\x05'}; + signal_buffer *buffer = signal_buffer_create(djb_type, 1); + buffer = signal_buffer_append(buffer, fingerprint_raw, len); + save_identity(&address, signal_buffer_data(buffer), signal_buffer_len(buffer), &omemo_ctx.identity_key_store); + free(fingerprint_raw); + signal_buffer_free(buffer); + + omemo_bundle_request(jid, device_id, omemo_start_device_session_handle_bundle, free, strdup(jid)); +} + +void +omemo_untrust(const char *const jid, const char *const fingerprint_formatted) +{ + size_t len; + unsigned char *fingerprint = _omemo_fingerprint_decode(fingerprint_formatted, &len); + + GHashTableIter iter; + gpointer key, value; + + GHashTable *trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid); + if (!trusted) { + return; + } + + g_hash_table_iter_init(&iter, trusted); + while (g_hash_table_iter_next(&iter, &key, &value)) { + signal_buffer *buffer = value; + unsigned char *original = signal_buffer_data(buffer); + /* Skip DJB_TYPE byte */ + original++; + if ((signal_buffer_len(buffer) - 1) == len && memcmp(original, fingerprint, len) == 0) { + g_hash_table_remove(trusted, key); + } + } + free(fingerprint); +} + +static void +_lock(void *user_data) +{ + omemo_context *ctx = (omemo_context *)user_data; + pthread_mutex_lock(&ctx->lock); +} + +static void +_unlock(void *user_data) +{ + omemo_context *ctx = (omemo_context *)user_data; + pthread_mutex_unlock(&ctx->lock); +} + +static void +_omemo_log(int level, const char *message, size_t len, void *user_data) +{ + switch (level) { + case SG_LOG_ERROR: + log_error("OMEMO: %s", message); + break; + case SG_LOG_WARNING: + log_warning("OMEMO: %s", message); + break; + case SG_LOG_NOTICE: + case SG_LOG_INFO: + log_info("OMEMO: %s", message); + break; + case SG_LOG_DEBUG: + log_debug("OMEMO: %s", message); + break; + } +} + +static gboolean +_handle_own_device_list(const char *const jid, GList *device_list) +{ + if (!g_list_find(device_list, GINT_TO_POINTER(omemo_ctx.device_id))) { + device_list = g_list_copy(device_list); + device_list = g_list_append(device_list, GINT_TO_POINTER(omemo_ctx.device_id)); + g_hash_table_insert(omemo_ctx.device_list, strdup(jid), device_list); + omemo_devicelist_publish(device_list); + } + + GList *device_id; + for (device_id = device_list; device_id != NULL; device_id = device_id->next) { + omemo_bundle_request(jid, GPOINTER_TO_INT(device_id->data), omemo_start_device_session_handle_bundle, free, strdup(jid)); + } + + return TRUE; +} + +static gboolean +_handle_device_list_start_session(const char *const jid, GList *device_list) +{ + omemo_start_session(jid); + + return FALSE; +} + +void +omemo_key_free(omemo_key_t *key) +{ + if (key == NULL) { + return; + } + + free(key->data); + free(key); +} + +char* +omemo_fingerprint_autocomplete(const char *const search_str, gboolean previous) +{ + return autocomplete_complete(omemo_ctx.fingerprint_ac, search_str, FALSE, previous); +} + +void +omemo_fingerprint_autocomplete_reset(void) +{ + autocomplete_reset(omemo_ctx.fingerprint_ac); +} + +static void +_load_identity(void) +{ + log_info("Loading OMEMO identity"); + + /* Device ID */ + omemo_ctx.device_id = g_key_file_get_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_DEVICE_ID, NULL); + log_info("OMEMO: device id: %d", omemo_ctx.device_id); + + /* Registration ID */ + omemo_ctx.registration_id = g_key_file_get_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_REGISTRATION_ID, NULL); + + /* Identity key */ + char *identity_key_public_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC, NULL); + size_t identity_key_public_len; + unsigned char *identity_key_public = g_base64_decode(identity_key_public_b64, &identity_key_public_len); + g_free(identity_key_public_b64); + omemo_ctx.identity_key_store.public = signal_buffer_create(identity_key_public, identity_key_public_len); + + char *identity_key_private_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE, NULL); + size_t identity_key_private_len; + unsigned char *identity_key_private = g_base64_decode(identity_key_private_b64, &identity_key_private_len); + g_free(identity_key_private_b64); + omemo_ctx.identity_key_store.private = signal_buffer_create(identity_key_private, identity_key_private_len); + + ec_public_key *public_key; + curve_decode_point(&public_key, identity_key_public, identity_key_public_len, omemo_ctx.signal); + ec_private_key *private_key; + curve_decode_private_point(&private_key, identity_key_private, identity_key_private_len, omemo_ctx.signal); + ratchet_identity_key_pair_create(&omemo_ctx.identity_key_pair, public_key, private_key); + + g_free(identity_key_public); + g_free(identity_key_private); + + char **keys = NULL; + int i; + /* Pre keys */ + i = 0; + keys = g_key_file_get_keys(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_PREKEYS, NULL, NULL); + if (keys) { + for (i = 0; keys[i] != NULL; i++) { + char *pre_key_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_PREKEYS, keys[i], NULL); + size_t pre_key_len; + unsigned char *pre_key = g_base64_decode(pre_key_b64, &pre_key_len); + g_free(pre_key_b64); + signal_buffer *buffer = signal_buffer_create(pre_key, pre_key_len); + g_free(pre_key); + g_hash_table_insert(omemo_ctx.pre_key_store, GINT_TO_POINTER(strtoul(keys[i], NULL, 10)), buffer); + } + + g_strfreev(keys); + } + + /* Ensure we have at least 100 pre keys */ + if (i < 100) { + _generate_pre_keys(100 - i); + } + + /* Signed pre keys */ + i = 0; + keys = g_key_file_get_keys(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_SIGNED_PREKEYS, NULL, NULL); + if (keys) { + for (i = 0; keys[i] != NULL; i++) { + char *signed_pre_key_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_SIGNED_PREKEYS, keys[i], NULL); + size_t signed_pre_key_len; + unsigned char *signed_pre_key = g_base64_decode(signed_pre_key_b64, &signed_pre_key_len); + g_free(signed_pre_key_b64); + signal_buffer *buffer = signal_buffer_create(signed_pre_key, signed_pre_key_len); + g_free(signed_pre_key); + g_hash_table_insert(omemo_ctx.signed_pre_key_store, GINT_TO_POINTER(strtoul(keys[i], NULL, 10)), buffer); + omemo_ctx.signed_pre_key_id = strtoul(keys[i], NULL, 10); + } + g_strfreev(keys); + } + + if (i == 0) { + _generate_signed_pre_key(); + } + + loaded = TRUE; + + omemo_identity_keyfile_save(); + omemo_start_sessions(); +} + +static void +_load_trust(void) +{ + char **keys = NULL; + char **groups = g_key_file_get_groups(omemo_ctx.trust_keyfile, NULL); + if (groups) { + int i; + for (i = 0; groups[i] != NULL; i++) { + GHashTable *trusted; + + trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, groups[i]); + if (!trusted) { + trusted = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(omemo_ctx.identity_key_store.trusted, strdup(groups[i]), trusted); + } + + keys = g_key_file_get_keys(omemo_ctx.trust_keyfile, groups[i], NULL, NULL); + int j; + for (j = 0; keys[j] != NULL; j++) { + char *key_b64 = g_key_file_get_string(omemo_ctx.trust_keyfile, groups[i], keys[j], NULL); + size_t key_len; + unsigned char *key = g_base64_decode(key_b64, &key_len); + g_free(key_b64); + signal_buffer *buffer = signal_buffer_create(key, key_len); + g_free(key); + uint32_t device_id = strtoul(keys[j], NULL, 10); + g_hash_table_insert(trusted, GINT_TO_POINTER(device_id), buffer); + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +static void +_load_sessions(void) +{ + int i; + char **groups = g_key_file_get_groups(omemo_ctx.sessions_keyfile, NULL); + if (groups) { + for (i = 0; groups[i] != NULL; i++) { + int j; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(omemo_ctx.session_store, groups[i]); + if (!device_store) { + device_store = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(omemo_ctx.session_store, strdup(groups[i]), device_store); + } + + char **keys = g_key_file_get_keys(omemo_ctx.sessions_keyfile, groups[i], NULL, NULL); + for (j = 0; keys[j] != NULL; j++) { + uint32_t id = strtoul(keys[j], NULL, 10); + char *record_b64 = g_key_file_get_string(omemo_ctx.sessions_keyfile, groups[i], keys[j], NULL); + size_t record_len; + unsigned char *record = g_base64_decode(record_b64, &record_len); + g_free(record_b64); + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_free(record); + g_hash_table_insert(device_store, GINT_TO_POINTER(id), buffer); + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +static void +_cache_device_identity(const char *const jid, uint32_t device_id, ec_public_key *identity) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + known_identities = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); + g_hash_table_insert(omemo_ctx.known_devices, strdup(jid), known_identities); + } + + char *fingerprint = _omemo_fingerprint(identity, FALSE); + log_info("OMEMO: cache identity for %s:%d: %s", jid, device_id, fingerprint); + g_hash_table_insert(known_identities, strdup(fingerprint), GINT_TO_POINTER(device_id)); + + char *formatted_fingerprint = omemo_format_fingerprint(fingerprint); + autocomplete_add(omemo_ctx.fingerprint_ac, formatted_fingerprint); + free(formatted_fingerprint); + free(fingerprint); +} + +static void +_g_hash_table_free(GHashTable *hash_table) +{ + g_hash_table_remove_all(hash_table); + g_hash_table_unref(hash_table); +} + +static void +_generate_pre_keys(int count) +{ + unsigned int start; + gcry_randomize(&start, sizeof(unsigned int), GCRY_VERY_STRONG_RANDOM); + signal_protocol_key_helper_pre_key_list_node *pre_keys_head; + signal_protocol_key_helper_generate_pre_keys(&pre_keys_head, start, count, omemo_ctx.signal); + + signal_protocol_key_helper_pre_key_list_node *p; + for (p = pre_keys_head; p != NULL; p = signal_protocol_key_helper_key_list_next(p)) { + session_pre_key *prekey = signal_protocol_key_helper_key_list_element(p); + signal_protocol_pre_key_store_key(omemo_ctx.store, prekey); + } + signal_protocol_key_helper_key_list_free(pre_keys_head); +} + +static void +_generate_signed_pre_key(void) +{ + session_signed_pre_key *signed_pre_key; + struct timeval tv; + gettimeofday(&tv, NULL); + unsigned long long timestamp = (unsigned long long)(tv.tv_sec) * 1000 + (unsigned long long)(tv.tv_usec) / 1000; + + omemo_ctx.signed_pre_key_id = 1; + signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, omemo_ctx.identity_key_pair, omemo_ctx.signed_pre_key_id, timestamp, omemo_ctx.signal); + signal_protocol_signed_pre_key_store_key(omemo_ctx.store, signed_pre_key); + SIGNAL_UNREF(signed_pre_key); +} diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h new file mode 100644 index 00000000..166a5292 --- /dev/null +++ b/src/omemo/omemo.h @@ -0,0 +1,55 @@ +#include <glib.h> + +#include "ui/ui.h" +#include "config/account.h" + +#define OMEMO_ERR_UNSUPPORTED_CRYPTO -10000 +#define OMEMO_ERR_GCRYPT -20000 + +typedef struct omemo_context_t omemo_context; + +typedef struct omemo_key { + unsigned char *data; + size_t length; + gboolean prekey; + uint32_t device_id; + uint32_t id; +} omemo_key_t; + +void omemo_init(void); +void omemo_on_connect(ProfAccount *account); +void omemo_on_disconnect(void); +void omemo_generate_crypto_materials(ProfAccount *account); +void omemo_key_free(omemo_key_t *key); +void omemo_publish_crypto_materials(void); + +uint32_t omemo_device_id(void); +void omemo_identity_key(unsigned char **output, size_t *length); +void omemo_signed_prekey(unsigned char **output, size_t *length); +void omemo_signed_prekey_signature(unsigned char **output, size_t *length); +void omemo_prekeys(GList **prekeys, GList **ids, GList **lengths); +void omemo_set_device_list(const char *const jid, GList * device_list); +GKeyFile *omemo_identity_keyfile(void); +void omemo_identity_keyfile_save(void); +GKeyFile *omemo_trust_keyfile(void); +void omemo_trust_keyfile_save(void); +GKeyFile *omemo_sessions_keyfile(void); +void omemo_sessions_keyfile_save(void); +char *omemo_format_fingerprint(const char *const fingerprint); +char *omemo_own_fingerprint(gboolean formatted); +void omemo_trust(const char *const jid, const char *const fingerprint); +void omemo_untrust(const char *const jid, const char *const fingerprint); +GList *omemo_known_device_identities(const char *const jid); +gboolean omemo_is_trusted_jid(const char *const jid); +gboolean omemo_is_trusted_identity(const char *const jid, const char *const fingerprint); +char *omemo_fingerprint_autocomplete(const char *const search_str, gboolean previous); +void omemo_fingerprint_autocomplete_reset(void); + +void omemo_start_sessions(void); +void omemo_start_session(const char *const barejid); +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); +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); diff --git a/src/omemo/store.c b/src/omemo/store.c new file mode 100644 index 00000000..76b7449c --- /dev/null +++ b/src/omemo/store.c @@ -0,0 +1,382 @@ +#include <glib.h> +#include <signal/signal_protocol.h> + +#include "config.h" +#include "omemo/omemo.h" +#include "omemo/store.h" + +GHashTable * +session_store_new(void) +{ + return g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); +} + +GHashTable * +pre_key_store_new(void) +{ + return g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); +} + +GHashTable * +signed_pre_key_store_new(void) +{ + return g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); +} + +void +identity_key_store_new(identity_key_store_t *identity_key_store) +{ + identity_key_store->trusted = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)signal_buffer_free); + identity_key_store->private = NULL; + identity_key_store->public = NULL; +} + +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int +load_session(signal_buffer **record, const signal_protocol_address *address, + void *user_data) +#else +int +load_session(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_address *address, void *user_data) +#endif +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + *record = NULL; + return 0; + } + + signal_buffer *original = g_hash_table_lookup(device_store, GINT_TO_POINTER(address->device_id)); + if (!original) { + *record = NULL; + return 0; + } + *record = signal_buffer_copy(original); + return 1; +} + +int +get_sub_device_sessions(signal_int_list **sessions, const char *name, + size_t name_len, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + GHashTableIter iter; + gpointer key, value; + + device_store = g_hash_table_lookup(session_store, name); + if (!device_store) { + return SG_SUCCESS; + } + + *sessions = signal_int_list_alloc(); + g_hash_table_iter_init(&iter, device_store); + while (g_hash_table_iter_next(&iter, &key, &value)) { + signal_int_list_push_back(*sessions, GPOINTER_TO_INT(key)); + } + + + return SG_SUCCESS; +} + +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int +store_session(const signal_protocol_address *address, uint8_t *record, + size_t record_len, void *user_data) +#else +int +store_session(const signal_protocol_address *address, + uint8_t *record, size_t record_len, + uint8_t *user_record, size_t user_record_len, + void *user_data) +#endif +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, (void *)address->name); + if (!device_store) { + device_store = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(session_store, strdup(address->name), device_store); + } + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(device_store, GINT_TO_POINTER(address->device_id), buffer); + + + char *record_b64 = g_base64_encode(record, record_len); + char *device_id = g_strdup_printf("%d", address->device_id); + g_key_file_set_string(omemo_sessions_keyfile(), address->name, device_id, record_b64); + free(device_id); + g_free(record_b64); + + omemo_sessions_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_session(const signal_protocol_address *address, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + return 0; + } + + if (!g_hash_table_lookup(device_store, GINT_TO_POINTER(address->device_id))) { + return 0; + } + + return 1; +} + +int +delete_session(const signal_protocol_address *address, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + return SG_SUCCESS; + } + + return g_hash_table_remove(device_store, GINT_TO_POINTER(address->device_id)); +} + +int +delete_all_sessions(const char *name, size_t name_len, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, name); + if (!device_store) { + return SG_SUCCESS; + } + + guint len = g_hash_table_size(device_store); + g_hash_table_remove_all(device_store); + return len; +} + +int +load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data) +{ + signal_buffer *original; + GHashTable *pre_key_store = (GHashTable *)user_data; + + original = g_hash_table_lookup(pre_key_store, GINT_TO_POINTER(pre_key_id)); + if (original == NULL) { + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_copy(original); + return SG_SUCCESS; +} + +int +store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, + void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(pre_key_store, GINT_TO_POINTER(pre_key_id), buffer); + + /* Long term storage */ + char *pre_key_id_str = g_strdup_printf("%d", pre_key_id); + char *record_b64 = g_base64_encode(record, record_len); + g_key_file_set_string(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, pre_key_id_str, record_b64); + g_free(pre_key_id_str); + g_free(record_b64); + + omemo_identity_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_pre_key(uint32_t pre_key_id, void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + return g_hash_table_lookup(pre_key_store, GINT_TO_POINTER(pre_key_id)) != NULL; +} + +int +remove_pre_key(uint32_t pre_key_id, void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + int ret = g_hash_table_remove(pre_key_store, GINT_TO_POINTER(pre_key_id)); + + /* Long term storage */ + char *pre_key_id_str = g_strdup_printf("%d", pre_key_id); + g_key_file_remove_key(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, pre_key_id_str, NULL); + g_free(pre_key_id_str); + + omemo_identity_keyfile_save(); + + if (ret > 0) { + return SG_SUCCESS; + } else { + return SG_ERR_INVALID_KEY_ID; + } +} + +int +load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, + void *user_data) +{ + signal_buffer *original; + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + original = g_hash_table_lookup(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)); + if (!original) { + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_copy(original); + return SG_SUCCESS; +} + +int +store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, + size_t record_len, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id), buffer); + + /* Long term storage */ + char *signed_pre_key_id_str = g_strdup_printf("%d", signed_pre_key_id); + char *record_b64 = g_base64_encode(record, record_len); + g_key_file_set_string(omemo_identity_keyfile(), OMEMO_STORE_GROUP_SIGNED_PREKEYS, signed_pre_key_id_str, record_b64); + g_free(signed_pre_key_id_str); + g_free(record_b64); + + omemo_identity_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + return g_hash_table_lookup(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)) != NULL; +} + +int +remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + int ret = g_hash_table_remove(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)); + + /* Long term storage */ + char *signed_pre_key_id_str = g_strdup_printf("%d", signed_pre_key_id); + g_key_file_remove_key(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, signed_pre_key_id_str, NULL); + g_free(signed_pre_key_id_str); + + omemo_identity_keyfile_save(); + + return ret; +} + +int +get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, + void *user_data) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + *public_data = signal_buffer_copy(identity_key_store->public); + *private_data = signal_buffer_copy(identity_key_store->private); + + return SG_SUCCESS; +} + +int +get_local_registration_id(void *user_data, uint32_t *registration_id) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + *registration_id = identity_key_store->registration_id; + + return SG_SUCCESS; +} + +int +save_identity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len, void *user_data) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + signal_buffer *buffer = signal_buffer_create(key_data, key_len); + + GHashTable *trusted = g_hash_table_lookup(identity_key_store->trusted, strdup(address->name)); + if (!trusted) { + trusted = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(identity_key_store->trusted, strdup(address->name), trusted); + } + g_hash_table_insert(trusted, GINT_TO_POINTER(address->device_id), buffer); + + /* Long term storage */ + char *key_b64 = g_base64_encode(key_data, key_len); + char *device_id = g_strdup_printf("%d", address->device_id); + g_key_file_set_string(omemo_trust_keyfile(), address->name, strdup(device_id), key_b64); + g_free(device_id); + g_free(key_b64); + + omemo_trust_keyfile_save(); + + return SG_SUCCESS; +} + +int +is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len, void *user_data) +{ + int ret; + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + GHashTable *trusted = g_hash_table_lookup(identity_key_store->trusted, address->name); + if (!trusted) { + return 0; + } + + signal_buffer *buffer = signal_buffer_create(key_data, key_len); + signal_buffer *original = g_hash_table_lookup(trusted, GINT_TO_POINTER(address->device_id)); + + ret = original != NULL && signal_buffer_compare(buffer, original) == 0; + + signal_buffer_free(buffer); + + return ret; +} + +int +store_sender_key(const signal_protocol_sender_key_name *sender_key_name, + uint8_t *record, size_t record_len, uint8_t *user_record, + size_t user_record_len, void *user_data) +{ + return SG_SUCCESS; +} + +int +load_sender_key(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_sender_key_name *sender_key_name, + void *user_data) +{ + return SG_SUCCESS; +} diff --git a/src/omemo/store.h b/src/omemo/store.h new file mode 100644 index 00000000..d4096c90 --- /dev/null +++ b/src/omemo/store.h @@ -0,0 +1,250 @@ +#include <signal/signal_protocol.h> + +#include "config.h" + +#define OMEMO_STORE_GROUP_IDENTITY "identity" +#define OMEMO_STORE_GROUP_PREKEYS "prekeys" +#define OMEMO_STORE_GROUP_SIGNED_PREKEYS "signed_prekeys" +#define OMEMO_STORE_KEY_DEVICE_ID "device_id" +#define OMEMO_STORE_KEY_REGISTRATION_ID "registration_id" +#define OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC "identity_key_public" +#define OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE "identity_key_private" + +typedef struct { + signal_buffer *public; + signal_buffer *private; + uint32_t registration_id; + GHashTable *trusted; +} identity_key_store_t; + +GHashTable * session_store_new(void); +GHashTable * pre_key_store_new(void); +GHashTable * signed_pre_key_store_new(void); +void identity_key_store_new(identity_key_store_t *identity_key_store); + +/** + * Returns a copy of the serialized session record corresponding to the + * provided recipient ID + device ID tuple. + * + * @param record pointer to a freshly allocated buffer containing the + * serialized session record. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param address the address of the remote client + * @return 1 if the session was loaded, 0 if the session was not found, negative on failure + */ +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int load_session(signal_buffer **record, const signal_protocol_address *address, void *user_data); +#else +int load_session(signal_buffer **record, signal_buffer **user_record, const signal_protocol_address *address, void *user_data); +#endif + +/** + * Returns all known devices with active sessions for a recipient + * + * @param pointer to an array that will be allocated and populated with the result + * @param name the name of the remote client + * @param name_len the length of the name + * @return size of the sessions array, or negative on failure + */ +int get_sub_device_sessions(signal_int_list **sessions, const char *name, size_t name_len, void *user_data); + +/** + * Commit to storage the session record for a given + * recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @param record pointer to a buffer containing the serialized session + * record for the remote client + * @param record_len length of the serialized session record + * @return 0 on success, negative on failure + */ +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int store_session(const signal_protocol_address *address, uint8_t *record, size_t record_len, void *user_data); +#else +int store_session(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data); +#endif + +/** + * Determine whether there is a committed session record for a + * recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @return 1 if a session record exists, 0 otherwise. + */ +int contains_session(const signal_protocol_address *address, void *user_data); + +/** + * Remove a session record for a recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @return 1 if a session was deleted, 0 if a session was not deleted, negative on error + */ +int delete_session(const signal_protocol_address *address, void *user_data); + +/** + * Remove the session records corresponding to all devices of a recipient ID. + * + * @param name the name of the remote client + * @param name_len the length of the name + * @return the number of deleted sessions on success, negative on failure + */ +int delete_all_sessions(const char *name, size_t name_len, void *user_data); + +/** + * Load a local serialized PreKey record. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param pre_key_id the ID of the local serialized PreKey record + * @retval SG_SUCCESS if the key was found + * @retval SG_ERR_INVALID_KEY_ID if the key could not be found + */ +int load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data); + +/** + * Store a local serialized PreKey record. + * + * @param pre_key_id the ID of the PreKey record to store. + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data); + +/** + * Determine whether there is a committed PreKey record matching the + * provided ID. + * + * @param pre_key_id A PreKey record ID. + * @return 1 if the store has a record for the PreKey ID, 0 otherwise + */ +int contains_pre_key(uint32_t pre_key_id, void *user_data); + +/** + * Delete a PreKey record from local storage. + * + * @param pre_key_id The ID of the PreKey record to remove. + * @return 0 on success, negative on failure + */ +int remove_pre_key(uint32_t pre_key_id, void *user_data); + +/** + * Load a local serialized signed PreKey record. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param signed_pre_key_id the ID of the local signed PreKey record + * @retval SG_SUCCESS if the key was found + * @retval SG_ERR_INVALID_KEY_ID if the key could not be found + */ +int load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data); + +/** + * Store a local serialized signed PreKey record. + * + * @param signed_pre_key_id the ID of the signed PreKey record to store + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data); + +/** + * Determine whether there is a committed signed PreKey record matching + * the provided ID. + * + * @param signed_pre_key_id A signed PreKey record ID. + * @return 1 if the store has a record for the signed PreKey ID, 0 otherwise + */ +int contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); + +/** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signed_pre_key_id The ID of the signed PreKey record to remove. + * @return 0 on success, negative on failure + */ +int remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); + +/** + * Get the local client's identity key pair. + * + * @param public_data pointer to a newly allocated buffer containing the + * public key, if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param private_data pointer to a newly allocated buffer containing the + * private key, if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @return 0 on success, negative on failure + */ +int get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data); + +/** + * Return the local client's registration ID. + * + * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @param registration_id pointer to be set to the local client's + * registration ID, if it was successfully retrieved. + * @return 0 on success, negative on failure + */ +int get_local_registration_id(void *user_data, uint32_t *registration_id); + +/** + * Save a remote client's identity key + * <p> + * Store a remote client's identity key as trusted. + * The value of key_data may be null. In this case remove the key data + * from the identity store, but retain any metadata that may be kept + * alongside it. + * + * @param address the address of the remote client + * @param key_data Pointer to the remote client's identity key, may be null + * @param key_len Length of the remote client's identity key + * @return 0 on success, negative on failure + */ +int save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); + +/** + * Verify a remote client's identity key. + * + * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param address the address of the remote client + * @param identityKey The identity key to verify. + * @param key_data Pointer to the identity key to verify + * @param key_len Length of the identity key to verify + * @return 1 if trusted, 0 if untrusted, negative on failure + */ +int is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); + +/** + * Store a serialized sender key record for a given + * (groupId + senderId + deviceId) tuple. + * + * @param sender_key_name the (groupId + senderId + deviceId) tuple + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_sender_key(const signal_protocol_sender_key_name *sender_key_name, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data); + +/** + * Returns a copy of the sender key record corresponding to the + * (groupId + senderId + deviceId) tuple. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param sender_key_name the (groupId + senderId + deviceId) tuple + * @return 1 if the record was loaded, 0 if the record was not found, negative on failure + */ +int load_sender_key(signal_buffer **record, signal_buffer **user_record, const signal_protocol_sender_key_name *sender_key_name, void *user_data); diff --git a/src/plugins/api.c b/src/plugins/api.c index 4d8434e1..fc47f193 100644 --- a/src/plugins/api.c +++ b/src/plugins/api.c @@ -473,7 +473,7 @@ api_settings_int_set(const char *const group, const char *const key, int value) void api_incoming_message(const char *const barejid, const char *const resource, const char *const message) { - sv_ev_incoming_message((char*)barejid, (char*)resource, (char*)message, NULL, NULL); + sv_ev_incoming_message((char*)barejid, (char*)resource, (char*)message, NULL, NULL, FALSE); // TODO handle all states sv_ev_activity((char*)barejid, (char*)resource, FALSE); diff --git a/src/profanity.c b/src/profanity.c index 1d4a2c35..f21f02c0 100644 --- a/src/profanity.c +++ b/src/profanity.c @@ -80,6 +80,10 @@ #include "pgp/gpg.h" #endif +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#endif + static void _init(char *log_level); static void _shutdown(void); static void _connect_default(const char * const account); @@ -197,6 +201,9 @@ _init(char *log_level) #ifdef HAVE_LIBGPGME p_gpg_init(); #endif +#ifdef HAVE_OMEMO + omemo_init(); +#endif atexit(_shutdown); plugins_init(); #ifdef HAVE_GTK diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c index 98431a60..5064b194 100644 --- a/src/ui/chatwin.c +++ b/src/ui/chatwin.c @@ -305,6 +305,8 @@ chatwin_outgoing_msg(ProfChatWin *chatwin, const char *const message, char *id, enc_char = prefs_get_otr_char(); } else if (enc_mode == PROF_MSG_PGP) { enc_char = prefs_get_pgp_char(); + } else if (enc_mode == PROF_MSG_OMEMO) { + enc_char = prefs_get_omemo_char(); } if (request_receipt && id) { @@ -322,6 +324,8 @@ chatwin_outgoing_carbon(ProfChatWin *chatwin, const char *const message, prof_en char enc_char = '-'; if (enc_mode == PROF_MSG_PGP) { enc_char = prefs_get_pgp_char(); + } else if (enc_mode == PROF_MSG_OMEMO) { + enc_char = prefs_get_omemo_char(); } ProfWin *window = (ProfWin*)chatwin; diff --git a/src/ui/console.c b/src/ui/console.c index e5c12158..260658c8 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -1999,6 +1999,28 @@ cons_show_pgp_prefs(void) } void +cons_show_omemo_prefs(void) +{ + cons_show("OMEMO preferences:"); + cons_show(""); + + char *log_value = prefs_get_string(PREF_OMEMO_LOG); + if (strcmp(log_value, "on") == 0) { + cons_show("OMEMO logging (/omemo log) : ON"); + } else if (strcmp(log_value, "off") == 0) { + cons_show("OMEMO logging (/omemo log) : OFF"); + } else { + cons_show("OMEMO logging (/omemo log) : Redacted"); + } + prefs_free_string(log_value); + + char ch = prefs_get_omemo_char(); + cons_show("OMEMO char (/omemo char) : %c", ch); + + cons_alert(); +} + +void cons_show_themes(GSList *themes) { cons_show(""); @@ -2072,6 +2094,8 @@ cons_prefs(void) cons_show(""); cons_show_pgp_prefs(); cons_show(""); + cons_show_omemo_prefs(); + cons_show(""); cons_alert(); } diff --git a/src/ui/mucwin.c b/src/ui/mucwin.c index 0f9f4f2b..0122950a 100644 --- a/src/ui/mucwin.c +++ b/src/ui/mucwin.c @@ -478,29 +478,60 @@ _mucwin_print_triggers(ProfWin *window, const char *const message, GList *trigge } void -mucwin_message(ProfMucWin *mucwin, const char *const nick, const char *const message, GSList *mentions, GList *triggers) +mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *const id, prof_enc_t enc_mode) { assert(mucwin != NULL); + g_hash_table_insert(mucwin->sent_messages, strdup(id), NULL); + ProfWin *window = (ProfWin*)mucwin; char *mynick = muc_nick(mucwin->roomjid); char ch = '-'; if (mucwin->message_char) { ch = mucwin->message_char[0]; + } else if (enc_mode == PROF_MSG_OTR) { + ch = prefs_get_otr_char(); + } else if (enc_mode == PROF_MSG_PGP) { + ch = prefs_get_pgp_char(); + } else if (enc_mode == PROF_MSG_OMEMO) { + ch = prefs_get_omemo_char(); } - if (g_strcmp0(nick, mynick) != 0) { - if (g_slist_length(mentions) > 0) { - _mucwin_print_mention(window, message, nick, mynick, mentions, &ch); - } else if (triggers) { - win_print_them(window, THEME_ROOMTRIGGER, ch, nick); - _mucwin_print_triggers(window, message, triggers); - } else { - win_println_them_message(window, ch, nick, "%s", message); - } + win_println_me_message(window, ch, mynick, "%s", message); +} + +void +mucwin_incoming_msg(ProfMucWin *mucwin, const char *const nick, const char *const message, const char *const id, GSList *mentions, GList *triggers, prof_enc_t enc_mode) +{ + assert(mucwin != NULL); + + if (g_hash_table_remove(mucwin->sent_messages, id)) { + /* Ignore reflection messages */ + return; + } + + ProfWin *window = (ProfWin*)mucwin; + char *mynick = muc_nick(mucwin->roomjid); + + char ch = '-'; + if (mucwin->message_char) { + ch = mucwin->message_char[0]; + } else if (enc_mode == PROF_MSG_OTR) { + ch = prefs_get_otr_char(); + } else if (enc_mode == PROF_MSG_PGP) { + ch = prefs_get_pgp_char(); + } else if (enc_mode == PROF_MSG_OMEMO) { + ch = prefs_get_omemo_char(); + } + + if (g_slist_length(mentions) > 0) { + _mucwin_print_mention(window, message, nick, mynick, mentions, &ch); + } else if (triggers) { + win_print_them(window, THEME_ROOMTRIGGER, ch, nick); + _mucwin_print_triggers(window, message, triggers); } else { - win_println_me_message(window, ch, mynick, "%s", message); + win_println_them_message(window, ch, nick, "%s", message); } } diff --git a/src/ui/titlebar.c b/src/ui/titlebar.c index f519fdd2..e1758d81 100644 --- a/src/ui/titlebar.c +++ b/src/ui/titlebar.c @@ -321,6 +321,21 @@ _show_muc_privacy(ProfMucWin *mucwin) int bracket_attrs = theme_attrs(THEME_TITLE_BRACKET); int encrypted_attrs = theme_attrs(THEME_TITLE_ENCRYPTED); + if (mucwin->is_omemo) { + wprintw(win, " "); + wattron(win, bracket_attrs); + wprintw(win, "["); + wattroff(win, bracket_attrs); + wattron(win, encrypted_attrs); + wprintw(win, "OMEMO"); + wattroff(win, encrypted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); + + return; + } + if (mucwin->enctext) { wprintw(win, " "); wattron(win, bracket_attrs); @@ -421,6 +436,21 @@ _show_privacy(ProfChatWin *chatwin) return; } + if (chatwin->is_omemo) { + wprintw(win, " "); + wattron(win, bracket_attrs); + wprintw(win, "["); + wattroff(win, bracket_attrs); + wattron(win, encrypted_attrs); + wprintw(win, "OMEMO"); + wattroff(win, encrypted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); + + return; + } + if (prefs_get_boolean(PREF_ENC_WARN)) { wprintw(win, " "); wattron(win, bracket_attrs); diff --git a/src/ui/ui.h b/src/ui/ui.h index ad5a1216..b94fe475 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -56,7 +56,8 @@ typedef enum { PROF_MSG_PLAIN, PROF_MSG_OTR, - PROF_MSG_PGP + PROF_MSG_PGP, + PROF_MSG_OMEMO } prof_enc_t; // core UI @@ -161,7 +162,8 @@ void mucwin_occupant_role_and_affiliation_change(ProfMucWin *mucwin, const char const char *const role, const char *const affiliation, const char *const actor, const char *const reason); void mucwin_roster(ProfMucWin *mucwin, GList *occupants, const char *const presence); void mucwin_history(ProfMucWin *mucwin, const char *const nick, GDateTime *timestamp, const char *const message); -void mucwin_message(ProfMucWin *mucwin, const char *const nick, const char *const message, GSList *mentions, GList *triggers); +void mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *const id, prof_enc_t enc_mode); +void mucwin_incoming_msg(ProfMucWin *mucwin, const char *const nick, const char *const message, const char *const id, GSList *mentions, GList *triggers, prof_enc_t enc_mode); void mucwin_subject(ProfMucWin *mucwin, const char *const nick, const char *const subject); void mucwin_requires_config(ProfMucWin *mucwin); void mucwin_info(ProfMucWin *mucwin); @@ -250,6 +252,7 @@ void cons_show_presence_prefs(void); void cons_show_connection_prefs(void); void cons_show_otr_prefs(void); void cons_show_pgp_prefs(void); +void cons_show_omemo_prefs(void); void cons_show_account(ProfAccount *account); void cons_debug(const char *const msg, ...); void cons_show_error(const char *const cmd, ...); diff --git a/src/ui/win_types.h b/src/ui/win_types.h index 92618a36..e1e64bf9 100644 --- a/src/ui/win_types.h +++ b/src/ui/win_types.h @@ -152,6 +152,7 @@ typedef struct prof_chat_win_t { gboolean otr_is_trusted; gboolean pgp_send; gboolean pgp_recv; + gboolean is_omemo; char *resource_override; gboolean history_shown; unsigned long memcheck; @@ -167,9 +168,11 @@ typedef struct prof_muc_win_t { gboolean unread_mentions; gboolean unread_triggers; gboolean showjid; + gboolean is_omemo; unsigned long memcheck; char *enctext; char *message_char; + GHashTable *sent_messages; } ProfMucWin; typedef struct prof_conf_win_t ProfConfWin; diff --git a/src/ui/window.c b/src/ui/window.c index cc2c2062..12b6c15b 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -143,6 +143,7 @@ win_create_chat(const char *const barejid) new_win->otr_is_trusted = FALSE; new_win->pgp_recv = FALSE; new_win->pgp_send = FALSE; + new_win->is_omemo = FALSE; new_win->history_shown = FALSE; new_win->unread = 0; new_win->state = chat_state_new(); @@ -196,6 +197,8 @@ win_create_muc(const char *const roomjid) } new_win->enctext = NULL; new_win->message_char = NULL; + new_win->is_omemo = FALSE; + new_win->sent_messages = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); new_win->memcheck = PROFMUCWIN_MEMCHECK; @@ -1057,6 +1060,8 @@ win_print_incoming(ProfWin *window, GDateTime *timestamp, enc_char = prefs_get_otr_char(); } else if (enc_mode == PROF_MSG_PGP) { enc_char = prefs_get_pgp_char(); + } else if (enc_mode == PROF_MSG_OMEMO) { + enc_char = prefs_get_omemo_char(); } _win_printf(window, enc_char, 0, timestamp, NO_ME, THEME_TEXT_THEM, from, "%s", message); break; diff --git a/src/ui/window_list.c b/src/ui/window_list.c index 5ce68d63..43230b57 100644 --- a/src/ui/window_list.c +++ b/src/ui/window_list.c @@ -561,6 +561,7 @@ wins_close_by_num(int i) ProfMucWin *mucwin = (ProfMucWin*)window; autocomplete_remove(wins_ac, mucwin->roomjid); autocomplete_remove(wins_close_ac, mucwin->roomjid); + g_hash_table_remove_all(mucwin->sent_messages); break; } case WIN_PRIVATE: diff --git a/src/xmpp/connection.c b/src/xmpp/connection.c index 2adda46e..afcd8199 100644 --- a/src/xmpp/connection.c +++ b/src/xmpp/connection.c @@ -63,6 +63,7 @@ typedef struct prof_conn_t { char *domain; GHashTable *available_resources; GHashTable *features_by_jid; + GHashTable *requested_features; } ProfConnection; static ProfConnection conn; @@ -89,6 +90,7 @@ connection_init(void) conn.domain = NULL; conn.features_by_jid = NULL; conn.available_resources = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)resource_destroy); + conn.requested_features = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); } void @@ -231,6 +233,10 @@ connection_clear_data(void) if (conn.available_resources) { g_hash_table_remove_all(conn.available_resources); } + + if (conn.requested_features) { + g_hash_table_remove_all(conn.requested_features); + } } #ifdef HAVE_LIBMESODE @@ -314,11 +320,20 @@ connection_jid_for_feature(const char *const feature) } void +connection_request_features(void) +{ + /* We don't record it as a requested feature to avoid triggering th + * sv_ev_connection_features_received too soon */ + iq_disco_info_request_onconnect(conn.domain); +} + +void connection_set_disco_items(GSList *items) { GSList *curr = items; while (curr) { DiscoItem *item = curr->data; + g_hash_table_insert(conn.requested_features, strdup(item->jid), NULL); g_hash_table_insert(conn.features_by_jid, strdup(item->jid), g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL)); @@ -357,6 +372,14 @@ connection_get_fulljid(void) } } +void +connection_features_received(const char *const jid) +{ + if (g_hash_table_remove(conn.requested_features, jid) && g_hash_table_size(conn.requested_features) == 0) { + sv_ev_connection_features_received(); + } +} + GHashTable* connection_get_features(const char *const jid) { diff --git a/src/xmpp/connection.h b/src/xmpp/connection.h index 170bc2bf..044cf368 100644 --- a/src/xmpp/connection.h +++ b/src/xmpp/connection.h @@ -53,6 +53,8 @@ void connection_set_disco_items(GSList *items); xmpp_conn_t* connection_get_conn(void); xmpp_ctx_t* connection_get_ctx(void); char *connection_get_domain(void); +void connection_request_features(void); +void connection_features_received(const char *const jid); GHashTable* connection_get_features(const char *const jid); void connection_clear_data(void); diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index a77ef59b..d6e4c153 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -77,11 +77,11 @@ typedef struct p_room_info_data_t { gboolean display; } ProfRoomInfoData; -typedef struct p_id_handle_t { - ProfIdCallback func; - ProfIdFreeCallback free_func; +typedef struct p_iq_handle_t { + ProfIqCallback func; + ProfIqFreeCallback free_func; void *userdata; -} ProfIdHandler; +} ProfIqHandler; typedef struct privilege_set_t { char *item; @@ -205,7 +205,7 @@ _iq_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const us const char *id = xmpp_stanza_get_id(stanza); if (id) { - ProfIdHandler *handler = g_hash_table_lookup(id_handlers, id); + ProfIqHandler *handler = g_hash_table_lookup(id_handlers, id); if (handler) { int keep = handler->func(stanza, handler->userdata); if (!keep) { @@ -234,7 +234,7 @@ iq_handlers_init(void) GList *keys = g_hash_table_get_keys(id_handlers); GList *curr = keys; while (curr) { - ProfIdHandler *handler = g_hash_table_lookup(id_handlers, curr->data); + ProfIqHandler *handler = g_hash_table_lookup(id_handlers, curr->data); if (handler->free_func && handler->userdata) { handler->free_func(handler->userdata); } @@ -248,9 +248,9 @@ iq_handlers_init(void) } void -iq_id_handler_add(const char *const id, ProfIdCallback func, ProfIdFreeCallback free_func, void *userdata) +iq_id_handler_add(const char *const id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata) { - ProfIdHandler *handler = malloc(sizeof(ProfIdHandler)); + ProfIqHandler *handler = malloc(sizeof(ProfIqHandler)); handler->func = func; handler->free_func = free_func; handler->userdata = userdata; @@ -438,7 +438,7 @@ iq_room_info_request(const char *const room, gboolean display_result) cb_data->room = strdup(room); cb_data->display = display_result; - iq_id_handler_add(id, _room_info_response_id_handler, (ProfIdFreeCallback)_iq_free_room_data, cb_data); + iq_id_handler_add(id, _room_info_response_id_handler, (ProfIqFreeCallback)_iq_free_room_data, cb_data); free(id); @@ -651,7 +651,7 @@ iq_room_affiliation_set(const char *const room, const char *const jid, char *aff affiliation_set->item = strdup(jid); affiliation_set->privilege = strdup(affiliation); - iq_id_handler_add(id, _room_affiliation_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, affiliation_set); + iq_id_handler_add(id, _room_affiliation_set_result_id_handler, (ProfIqFreeCallback)_iq_free_affiliation_set, affiliation_set); iq_send_stanza(iq); xmpp_stanza_release(iq); @@ -670,7 +670,7 @@ iq_room_role_set(const char *const room, const char *const nick, char *role, role_set->item = strdup(nick); role_set->privilege = strdup(role); - iq_id_handler_add(id, _room_role_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, role_set); + iq_id_handler_add(id, _room_role_set_result_id_handler, (ProfIqFreeCallback)_iq_free_affiliation_set, role_set); iq_send_stanza(iq); xmpp_stanza_release(iq); @@ -697,7 +697,7 @@ iq_send_ping(const char *const target) const char *id = xmpp_stanza_get_id(iq); GDateTime *now = g_date_time_new_now_local(); - iq_id_handler_add(id, _manual_pong_id_handler, (ProfIdFreeCallback)g_date_time_unref, now); + iq_id_handler_add(id, _manual_pong_id_handler, (ProfIqFreeCallback)g_date_time_unref, now); iq_send_stanza(iq); xmpp_stanza_release(iq); @@ -2291,6 +2291,8 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t *const stanza, void *con } } + connection_features_received(from); + return 0; } diff --git a/src/xmpp/iq.h b/src/xmpp/iq.h index 025d5e9f..bc273db4 100644 --- a/src/xmpp/iq.h +++ b/src/xmpp/iq.h @@ -35,12 +35,12 @@ #ifndef XMPP_IQ_H #define XMPP_IQ_H -typedef int(*ProfIdCallback)(xmpp_stanza_t *const stanza, void *const userdata); -typedef void(*ProfIdFreeCallback)(void *userdata); +typedef int(*ProfIqCallback)(xmpp_stanza_t *const stanza, void *const userdata); +typedef void(*ProfIqFreeCallback)(void *userdata); void iq_handlers_init(void); void iq_send_stanza(xmpp_stanza_t *const stanza); -void iq_id_handler_add(const char *const id, ProfIdCallback func, ProfIdFreeCallback free_func, void *userdata); +void iq_id_handler_add(const char *const id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata); void iq_disco_info_request_onconnect(gchar *jid); void iq_disco_items_request_onconnect(gchar *jid); void iq_send_caps_request(const char *const to, const char *const id, const char *const node, const char *const ver); diff --git a/src/xmpp/message.c b/src/xmpp/message.c index adea5c10..47cf35d7 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -52,6 +52,7 @@ #include "pgp/gpg.h" #include "plugins/plugins.h" #include "ui/ui.h" +#include "ui/window_list.h" #include "xmpp/chat_session.h" #include "xmpp/muc.h" #include "xmpp/session.h" @@ -62,6 +63,17 @@ #include "xmpp/connection.h" #include "xmpp/xmpp.h" +#ifdef HAVE_OMEMO +#include "xmpp/omemo.h" +#include "omemo/omemo.h" +#endif + +typedef struct p_message_handle_t { + ProfMessageCallback func; + ProfMessageFreeCallback free_func; + void *userdata; +} ProfMessageHandler; + static int _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata); static void _handle_error(xmpp_stanza_t *const stanza); @@ -74,6 +86,8 @@ static void _handle_chat(xmpp_stanza_t *const stanza); static void _send_message_stanza(xmpp_stanza_t *const stanza); +static GHashTable *pubsub_event_handlers; + static int _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) { @@ -118,6 +132,23 @@ _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *con _handle_receipt_received(stanza); } + xmpp_stanza_t *event = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB_EVENT); + if (event) { + xmpp_stanza_t *child = xmpp_stanza_get_children(event); + if (child) { + const char *node = xmpp_stanza_get_attribute(child, STANZA_ATTR_NODE); + if (node) { + ProfMessageHandler *handler = g_hash_table_lookup(pubsub_event_handlers, node); + if (handler) { + int keep = handler->func(stanza, handler->userdata); + if (!keep) { + g_hash_table_remove(pubsub_event_handlers, node); + } + } + } + } + } + _handle_chat(stanza); return 1; @@ -129,6 +160,33 @@ message_handlers_init(void) xmpp_conn_t * const conn = connection_get_conn(); xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_handler_add(conn, _message_handler, NULL, STANZA_NAME_MESSAGE, NULL, ctx); + + if (pubsub_event_handlers) { + GList *keys = g_hash_table_get_keys(pubsub_event_handlers); + GList *curr = keys; + while (curr) { + ProfMessageHandler *handler = g_hash_table_lookup(pubsub_event_handlers, curr->data); + if (handler->free_func && handler->userdata) { + handler->free_func(handler->userdata); + } + curr = g_list_next(curr); + } + g_list_free(keys); + g_hash_table_destroy(pubsub_event_handlers); + } + + pubsub_event_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); +} + +void +message_pubsub_event_handler_add(const char *const node, ProfMessageCallback func, ProfMessageFreeCallback free_func, void *userdata) +{ + ProfMessageHandler *handler = malloc(sizeof(ProfMessageHandler)); + handler->func = func; + handler->free_func = free_func; + handler->userdata = userdata; + + g_hash_table_insert(pubsub_event_handlers, strdup(node), handler); } char* @@ -254,6 +312,118 @@ message_send_chat_otr(const char *const barejid, const char *const msg, gboolean return id; } +#ifdef HAVE_OMEMO +char* +message_send_chat_omemo(const char *const jid, uint32_t sid, GList *keys, + const unsigned char *const iv, size_t iv_len, + const unsigned char *const ciphertext, size_t ciphertext_len, + gboolean request_receipt, gboolean muc) +{ + char *state = chat_session_get_state(jid); + xmpp_ctx_t * const ctx = connection_get_ctx(); + char *id; + xmpp_stanza_t *message; + if (muc) { + id = connection_create_stanza_id("muc"); + message = xmpp_message_new(ctx, STANZA_TYPE_GROUPCHAT, jid, id); + stanza_attach_origin_id(ctx, message, id); + } else { + id = connection_create_stanza_id("msg"); + message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id); + } + + xmpp_stanza_t *encrypted = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(encrypted, "encrypted"); + xmpp_stanza_set_ns(encrypted, STANZA_NS_OMEMO); + + xmpp_stanza_t *header = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(header, "header"); + char *sid_text = g_strdup_printf("%d", sid); + xmpp_stanza_set_attribute(header, "sid", sid_text); + g_free(sid_text); + + GList *key_iter; + for (key_iter = keys; key_iter != NULL; key_iter = key_iter->next) { + omemo_key_t *key = (omemo_key_t *)key_iter->data; + + xmpp_stanza_t *key_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(key_stanza, "key"); + char *rid = g_strdup_printf("%d", key->device_id); + xmpp_stanza_set_attribute(key_stanza, "rid", rid); + g_free(rid); + if (key->prekey) { + xmpp_stanza_set_attribute(key_stanza, "prekey", "true"); + } + + gchar *key_raw = g_base64_encode(key->data, key->length); + xmpp_stanza_t *key_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(key_text, key_raw); + g_free(key_raw); + + xmpp_stanza_add_child(key_stanza, key_text); + xmpp_stanza_add_child(header, key_stanza); + xmpp_stanza_release(key_text); + xmpp_stanza_release(key_stanza); + } + + xmpp_stanza_t *iv_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(iv_stanza, "iv"); + + gchar *iv_raw = g_base64_encode(iv, iv_len); + xmpp_stanza_t *iv_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(iv_text, iv_raw); + g_free(iv_raw); + + xmpp_stanza_add_child(iv_stanza, iv_text); + xmpp_stanza_add_child(header, iv_stanza); + xmpp_stanza_release(iv_text); + xmpp_stanza_release(iv_stanza); + + xmpp_stanza_add_child(encrypted, header); + xmpp_stanza_release(header); + + xmpp_stanza_t *payload = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(payload, "payload"); + + gchar *ciphertext_raw = g_base64_encode(ciphertext, ciphertext_len); + xmpp_stanza_t *payload_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(payload_text, ciphertext_raw); + g_free(ciphertext_raw); + + xmpp_stanza_add_child(payload, payload_text); + xmpp_stanza_add_child(encrypted, payload); + xmpp_stanza_release(payload_text); + xmpp_stanza_release(payload); + + xmpp_stanza_add_child(message, encrypted); + xmpp_stanza_release(encrypted); + + xmpp_stanza_t *body = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(body, "body"); + xmpp_stanza_t *body_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(body_text, "You received a message encrypted with OMEMO but your client doesn't support OMEMO."); + xmpp_stanza_add_child(body, body_text); + xmpp_stanza_release(body_text); + xmpp_stanza_add_child(message, body); + xmpp_stanza_release(body); + + if (state) { + stanza_attach_state(ctx, message, state); + } + + stanza_attach_hints_store(ctx, message); + + if (request_receipt) { + stanza_attach_receipt_request(ctx, message); + } + + _send_message_stanza(message); + xmpp_stanza_release(message); + + return id; +} +#endif + void message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url) { @@ -273,23 +443,24 @@ message_send_private(const char *const fulljid, const char *const msg, const cha xmpp_stanza_release(message); } -void +char* message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = connection_create_stanza_id("muc"); xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_GROUPCHAT, roomjid, id); + stanza_attach_origin_id(ctx, message, id); xmpp_message_set_body(message, msg); - free(id); - if (oob_url) { stanza_attach_x_oob_url(ctx, message, oob_url); } _send_message_stanza(message); xmpp_stanza_release(message); + + return id; } void @@ -518,6 +689,14 @@ _handle_groupchat(xmpp_stanza_t *const stanza) { xmpp_ctx_t *ctx = connection_get_ctx(); char *message = NULL; + + const char *id = xmpp_stanza_get_id(stanza); + + xmpp_stanza_t *origin = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_STABLE_ID); + if (origin && g_strcmp0(xmpp_stanza_get_name(origin), STANZA_NAME_ORIGIN_ID) == 0) { + id = xmpp_stanza_get_attribute(origin, STANZA_ATTR_ID); + } + const char *room_jid = xmpp_stanza_get_from(stanza); Jid *jid = jid_create(room_jid); @@ -560,19 +739,28 @@ _handle_groupchat(xmpp_stanza_t *const stanza) return; } - message = xmpp_message_get_body(stanza); + // check omemo encryption + gboolean omemo = FALSE; +#ifdef HAVE_OMEMO + message = omemo_receive_message(stanza); + omemo = message != NULL; +#endif + if (!message) { - jid_destroy(jid); - return; + message = xmpp_message_get_body(stanza); + if (!message) { + jid_destroy(jid); + return; + } } // determine if the notifications happened whilst offline GDateTime *timestamp = stanza_get_delay(stanza); if (timestamp) { - sv_ev_room_history(jid->barejid, jid->resourcepart, timestamp, message); + sv_ev_room_history(jid->barejid, jid->resourcepart, timestamp, message, omemo); g_date_time_unref(timestamp); } else { - sv_ev_room_message(jid->barejid, jid->resourcepart, message); + sv_ev_room_message(jid->barejid, jid->resourcepart, message, id, omemo); } xmpp_free(ctx, message); @@ -675,6 +863,7 @@ _private_chat_handler(xmpp_stanza_t *const stanza, const char *const fulljid) static gboolean _handle_carbons(xmpp_stanza_t *const stanza) { + char *message_txt = NULL; xmpp_stanza_t *carbons = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CARBONS); if (!carbons) { return FALSE; @@ -708,10 +897,19 @@ _handle_carbons(xmpp_stanza_t *const stanza) return TRUE; } - char *message_txt = xmpp_message_get_body(message); + // check omemo encryption + gboolean omemo = FALSE; +#ifdef HAVE_OMEMO + message_txt = omemo_receive_message(message); + omemo = message_txt != NULL; +#endif + if (!message_txt) { - log_warning("Carbon received with no message."); - return TRUE; + message_txt = xmpp_message_get_body(message); + if (!message_txt) { + log_warning("Carbon received with no message."); + return TRUE; + } } Jid *my_jid = jid_create(connection_get_fulljid()); @@ -739,11 +937,11 @@ _handle_carbons(xmpp_stanza_t *const stanza) // if we are the recipient, treat as standard incoming message if (g_strcmp0(my_jid->barejid, jid_to->barejid) == 0) { - sv_ev_incoming_carbon(jid_from->barejid, jid_from->resourcepart, message_txt, enc_message); + sv_ev_incoming_carbon(jid_from->barejid, jid_from->resourcepart, message_txt, enc_message, omemo); // else treat as a sent message } else { - sv_ev_outgoing_carbon(jid_to->barejid, message_txt, enc_message); + sv_ev_outgoing_carbon(jid_to->barejid, message_txt, enc_message, omemo); } xmpp_ctx_t *ctx = connection_get_ctx(); @@ -760,6 +958,7 @@ _handle_carbons(xmpp_stanza_t *const stanza) static void _handle_chat(xmpp_stanza_t *const stanza) { + char *message = NULL; // ignore if type not chat or absent const char *type = xmpp_stanza_get_type(stanza); if (!(g_strcmp0(type, "chat") == 0 || type == NULL)) { @@ -772,6 +971,13 @@ _handle_chat(xmpp_stanza_t *const stanza) return; } + // check omemo encryption + gboolean omemo = FALSE; +#ifdef HAVE_OMEMO + message = omemo_receive_message(stanza); + omemo = message != NULL; +#endif + // ignore handled namespaces xmpp_stanza_t *conf = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CONFERENCE); xmpp_stanza_t *captcha = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CAPTCHA); @@ -801,19 +1007,24 @@ _handle_chat(xmpp_stanza_t *const stanza) // standard chat message, use jid without resource xmpp_ctx_t *ctx = connection_get_ctx(); GDateTime *timestamp = stanza_get_delay(stanza); - if (body) { - char *message = xmpp_stanza_get_text(body); - if (message) { - char *enc_message = NULL; - xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_ENCRYPTED); - if (x) { - enc_message = xmpp_stanza_get_text(x); - } - sv_ev_incoming_message(jid->barejid, jid->resourcepart, message, enc_message, timestamp); - xmpp_free(ctx, enc_message); + if (!message && body) { + message = xmpp_stanza_get_text(body); + } - _receipt_request_handler(stanza); + if (message) { + char *enc_message = NULL; + xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_ENCRYPTED); + if (x) { + enc_message = xmpp_stanza_get_text(x); + } + sv_ev_incoming_message(jid->barejid, jid->resourcepart, message, enc_message, timestamp, omemo); + xmpp_free(ctx, enc_message); + _receipt_request_handler(stanza); + + if (omemo) { + free(message); + } else { xmpp_free(ctx, message); } } diff --git a/src/xmpp/message.h b/src/xmpp/message.h index dee9be2d..0c81ca39 100644 --- a/src/xmpp/message.h +++ b/src/xmpp/message.h @@ -35,6 +35,10 @@ #ifndef XMPP_MESSAGE_H #define XMPP_MESSAGE_H +typedef int(*ProfMessageCallback)(xmpp_stanza_t *const stanza, void *const userdata); +typedef void(*ProfMessageFreeCallback)(void *userdata); + void message_handlers_init(void); +void message_pubsub_event_handler_add(const char *const node, ProfMessageCallback func, ProfMessageFreeCallback free_func, void *userdata); #endif diff --git a/src/xmpp/omemo.c b/src/xmpp/omemo.c new file mode 100644 index 00000000..4b77ef23 --- /dev/null +++ b/src/xmpp/omemo.c @@ -0,0 +1,448 @@ +#include <glib.h> + +#include "log.h" +#include "xmpp/connection.h" +#include "xmpp/form.h" +#include "xmpp/iq.h" +#include "xmpp/message.h" +#include "xmpp/stanza.h" + +#include "omemo/omemo.h" + +static int _omemo_receive_devicelist(xmpp_stanza_t *const stanza, void *const userdata); +static int _omemo_bundle_publish_result(xmpp_stanza_t *const stanza, void *const userdata); +static int _omemo_bundle_publish_configure(xmpp_stanza_t *const stanza, void *const userdata); +static int _omemo_bundle_publish_configure_result(xmpp_stanza_t *const stanza, void *const userdata); + +void +omemo_devicelist_subscribe(void) +{ + message_pubsub_event_handler_add(STANZA_NS_OMEMO_DEVICELIST, _omemo_receive_devicelist, NULL, NULL); + + caps_add_feature(XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY); +} + +void +omemo_devicelist_publish(GList *device_list) +{ + xmpp_ctx_t * const ctx = connection_get_ctx(); + xmpp_stanza_t *iq = stanza_create_omemo_devicelist_publish(ctx, device_list); + + if (connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS)) { + stanza_attach_publish_options(ctx, iq, "pubsub#access_model", "open"); + } + + iq_send_stanza(iq); + xmpp_stanza_release(iq); +} + +void +omemo_devicelist_request(const char * const jid) +{ + xmpp_ctx_t * const ctx = connection_get_ctx(); + char *id = connection_create_stanza_id("devicelist_request"); + + xmpp_stanza_t *iq = stanza_create_omemo_devicelist_request(ctx, id, jid); + iq_id_handler_add(id, _omemo_receive_devicelist, NULL, NULL); + + iq_send_stanza(iq); + + free(id); + xmpp_stanza_release(iq); +} + +void +omemo_bundle_publish(gboolean first) +{ + xmpp_ctx_t * const ctx = connection_get_ctx(); + unsigned char *identity_key = NULL; + size_t identity_key_length; + unsigned char *signed_prekey = NULL; + size_t signed_prekey_length; + unsigned char *signed_prekey_signature = NULL; + size_t signed_prekey_signature_length; + GList *prekeys = NULL, *ids = NULL, *lengths = NULL; + + omemo_identity_key(&identity_key, &identity_key_length); + omemo_signed_prekey(&signed_prekey, &signed_prekey_length); + omemo_signed_prekey_signature(&signed_prekey_signature, &signed_prekey_signature_length); + omemo_prekeys(&prekeys, &ids, &lengths); + + char *id = connection_create_stanza_id("omemo_bundle_publish"); + xmpp_stanza_t *iq = stanza_create_omemo_bundle_publish(ctx, id, + omemo_device_id(), identity_key, identity_key_length, signed_prekey, + signed_prekey_length, signed_prekey_signature, + signed_prekey_signature_length, prekeys, ids, lengths); + + g_list_free_full(prekeys, free); + g_list_free(lengths); + g_list_free(ids); + + if (connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS)) { + stanza_attach_publish_options(ctx, iq, "pubsub#access_model", "open"); + } + + iq_id_handler_add(id, _omemo_bundle_publish_result, NULL, GINT_TO_POINTER(first)); + + iq_send_stanza(iq); + + xmpp_stanza_release(iq); + free(identity_key); + free(signed_prekey); + free(signed_prekey_signature); + free(id); +} + +void +omemo_bundle_request(const char * const jid, uint32_t device_id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata) +{ + xmpp_ctx_t * const ctx = connection_get_ctx(); + char *id = connection_create_stanza_id("bundle_request"); + + xmpp_stanza_t *iq = stanza_create_omemo_bundle_request(ctx, id, jid, device_id); + iq_id_handler_add(id, func, free_func, userdata); + + iq_send_stanza(iq); + + free(id); + xmpp_stanza_release(iq); +} + +int +omemo_start_device_session_handle_bundle(xmpp_stanza_t *const stanza, void *const userdata) +{ + char *from = NULL; + const char *from_attr = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + if (!from_attr) { + Jid *jid = jid_create(connection_get_fulljid()); + from = strdup(jid->barejid); + jid_destroy(jid); + } else { + from = strdup(from_attr); + } + + if (g_strcmp0(from, userdata) != 0) { + return 1; + } + + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB); + if (!pubsub) { + return 1; + } + + xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(pubsub, "items"); + if (!items) { + return 1; + } + const char *node = xmpp_stanza_get_attribute(items, "node"); + char *device_id_str = strstr(node, ":"); + if (!device_id_str) { + return 1; + } + + uint32_t device_id = strtoul(++device_id_str, NULL, 10); + + xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item"); + if (!item) { + return 1; + } + + xmpp_stanza_t *bundle = xmpp_stanza_get_child_by_ns(item, STANZA_NS_OMEMO); + if (!bundle) { + return 1; + } + + xmpp_stanza_t *prekeys = xmpp_stanza_get_child_by_name(bundle, "prekeys"); + if (!prekeys) { + return 1; + } + + GList *prekeys_list = NULL; + xmpp_stanza_t *prekey; + for (prekey = xmpp_stanza_get_children(prekeys); prekey != NULL; prekey = xmpp_stanza_get_next(prekey)) { + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + + const char *prekey_id_text = xmpp_stanza_get_attribute(prekey, "preKeyId"); + if (!prekey_id_text) { + return 1; + } + key->id = strtoul(prekey_id_text, NULL, 10); + xmpp_stanza_t *prekey_text = xmpp_stanza_get_children(prekey); + if (!prekey_text) { + return 1; + } + char *prekey_b64 = xmpp_stanza_get_text(prekey_text); + key->data = g_base64_decode(prekey_b64, &key->length); + free(prekey_b64); + key->prekey = TRUE; + key->device_id = device_id; + + prekeys_list = g_list_append(prekeys_list, key); + } + + xmpp_stanza_t *signed_prekey = xmpp_stanza_get_child_by_name(bundle, "signedPreKeyPublic"); + if (!signed_prekey) { + return 1; + } + const char *signed_prekey_id_text = xmpp_stanza_get_attribute(signed_prekey, "signedPreKeyId"); + if (!signed_prekey_id_text) { + return 1; + } + uint32_t signed_prekey_id = strtoul(signed_prekey_id_text, NULL, 10); + xmpp_stanza_t *signed_prekey_text = xmpp_stanza_get_children(signed_prekey); + if (!signed_prekey_text) { + return 1; + } + size_t signed_prekey_len; + char *signed_prekey_b64 = xmpp_stanza_get_text(signed_prekey_text); + unsigned char *signed_prekey_raw = g_base64_decode(signed_prekey_b64, &signed_prekey_len); + free(signed_prekey_b64); + + xmpp_stanza_t *signed_prekey_signature = xmpp_stanza_get_child_by_name(bundle, "signedPreKeySignature"); + if (!signed_prekey_signature) { + return 1; + } + xmpp_stanza_t *signed_prekey_signature_text = xmpp_stanza_get_children(signed_prekey_signature); + if (!signed_prekey_signature_text) { + return 1; + } + size_t signed_prekey_signature_len; + char *signed_prekey_signature_b64 = xmpp_stanza_get_text(signed_prekey_signature_text); + unsigned char *signed_prekey_signature_raw = g_base64_decode(signed_prekey_signature_b64, &signed_prekey_signature_len); + free(signed_prekey_signature_b64); + + xmpp_stanza_t *identity_key = xmpp_stanza_get_child_by_name(bundle, "identityKey"); + if (!identity_key) { + return 1; + } + xmpp_stanza_t *identity_key_text = xmpp_stanza_get_children(identity_key); + if (!identity_key_text) { + return 1; + } + size_t identity_key_len; + char *identity_key_b64 = xmpp_stanza_get_text(identity_key_text); + unsigned char *identity_key_raw = g_base64_decode(identity_key_b64, &identity_key_len); + free(identity_key_b64); + + omemo_start_device_session(from, device_id, prekeys_list, signed_prekey_id, + signed_prekey_raw, signed_prekey_len, signed_prekey_signature_raw, + signed_prekey_signature_len, identity_key_raw, identity_key_len); + + free(from); + g_list_free_full(prekeys_list, (GDestroyNotify)omemo_key_free); + g_free(signed_prekey_raw); + g_free(identity_key_raw); + g_free(signed_prekey_signature_raw); + return 1; +} + +char * +omemo_receive_message(xmpp_stanza_t *const stanza) +{ + const char *type = xmpp_stanza_get_type(stanza); + + xmpp_stanza_t *encrypted = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_OMEMO); + if (!encrypted) { + return NULL; + } + + xmpp_stanza_t *header = xmpp_stanza_get_child_by_name(encrypted, "header"); + if (!header) { + return NULL; + } + + const char *sid_text = xmpp_stanza_get_attribute(header, "sid"); + if (!sid_text) { + return NULL; + } + uint32_t sid = strtoul(sid_text, NULL, 10); + + xmpp_stanza_t *iv = xmpp_stanza_get_child_by_name(header, "iv"); + if (!iv) { + return NULL; + } + char *iv_text = xmpp_stanza_get_text(iv); + if (!iv_text) { + return NULL; + } + size_t iv_len; + unsigned char *iv_raw = g_base64_decode(iv_text, &iv_len); + + xmpp_stanza_t *payload = xmpp_stanza_get_child_by_name(encrypted, "payload"); + if (!payload) { + return NULL; + } + char *payload_text = xmpp_stanza_get_text(payload); + if (!payload_text) { + return NULL; + } + size_t payload_len; + unsigned char *payload_raw = g_base64_decode(payload_text, &payload_len); + + GList *keys = NULL; + xmpp_stanza_t *key_stanza; + for (key_stanza = xmpp_stanza_get_children(header); key_stanza != NULL; key_stanza = xmpp_stanza_get_next(key_stanza)) { + if (g_strcmp0(xmpp_stanza_get_name(key_stanza), "key") != 0) { + continue; + } + + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + char *key_text = xmpp_stanza_get_text(key_stanza); + if (!key_text) { + goto skip; + } + + + const char *rid_text = xmpp_stanza_get_attribute(key_stanza, "rid"); + key->device_id = strtoul(rid_text, NULL, 10); + if (!key->device_id) { + goto skip; + } + key->data = g_base64_decode(key_text, &key->length); + free(key_text); + key->prekey = g_strcmp0(xmpp_stanza_get_attribute(key_stanza, "prekey"), "true") == 0; + keys = g_list_append(keys, key); + continue; + +skip: + free(key); + } + + const char *from = xmpp_stanza_get_from(stanza); + + char *plaintext = omemo_on_message_recv(from, sid, iv_raw, iv_len, + keys, payload_raw, payload_len, + g_strcmp0(type, STANZA_TYPE_GROUPCHAT) == 0); + + g_list_free_full(keys, (GDestroyNotify)omemo_key_free); + g_free(iv_raw); + g_free(payload_raw); + g_free(iv_text); + g_free(payload_text); + + return plaintext; +} + +static int +_omemo_receive_devicelist(xmpp_stanza_t *const stanza, void *const userdata) +{ + GList *device_list = NULL; + const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + + xmpp_stanza_t *root = NULL; + xmpp_stanza_t *event = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB_EVENT); + if (event) { + root = event; + } + + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB); + if (pubsub) { + root = pubsub; + } + + if (!root) { + return 1; + } + + xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(root, "items"); + if (!items) { + return 1; + } + + xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item"); + if (item) { + xmpp_stanza_t *list = xmpp_stanza_get_child_by_ns(item, STANZA_NS_OMEMO); + if (!list) { + return 1; + } + + xmpp_stanza_t *device; + for (device = xmpp_stanza_get_children(list); device != NULL; device = xmpp_stanza_get_next(device)) { + const char *id = xmpp_stanza_get_id(device); + device_list = g_list_append(device_list, GINT_TO_POINTER(strtoul(id, NULL, 10))); + } + } + omemo_set_device_list(from, device_list); + + return 1; +} + +static int +_omemo_bundle_publish_result(xmpp_stanza_t *const stanza, void *const userdata) +{ + const char *type = xmpp_stanza_get_type(stanza); + + if (g_strcmp0(type, STANZA_TYPE_ERROR) != 0) { + return 0; + } + + if (!GPOINTER_TO_INT(userdata)) { + log_error("OMEMO: definitely cannot publish bundle with an open access model"); + return 0; + } + + log_info("OMEMO: cannot publish bundle with open access model, trying to configure node"); + xmpp_ctx_t * const ctx = connection_get_ctx(); + Jid *jid = jid_create(connection_get_fulljid()); + char *id = connection_create_stanza_id("omemo_bundle_node_configure_request"); + char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, omemo_device_id()); + xmpp_stanza_t *iq = stanza_create_pubsub_configure_request(ctx, id, jid->barejid, node); + g_free(node); + + iq_id_handler_add(id, _omemo_bundle_publish_configure, NULL, userdata); + + iq_send_stanza(iq); + + xmpp_stanza_release(iq); + free(id); + jid_destroy(jid); + return 0; +} + +static int +_omemo_bundle_publish_configure(xmpp_stanza_t *const stanza, void *const userdata) +{ + /* TODO handle error */ + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_name(stanza, "pubsub"); + xmpp_stanza_t *configure = xmpp_stanza_get_child_by_name(pubsub, STANZA_NAME_CONFIGURE); + xmpp_stanza_t *x = xmpp_stanza_get_child_by_name(configure, "x"); + + DataForm* form = form_create(x); + char *tag = g_hash_table_lookup(form->var_to_tag, "pubsub#access_model"); + if (!tag) { + log_info("OMEMO: cannot configure bundle to an open access model"); + return 0; + } + form_set_value(form, tag, "open"); + + xmpp_ctx_t * const ctx = connection_get_ctx(); + Jid *jid = jid_create(connection_get_fulljid()); + char *id = connection_create_stanza_id("omemo_bundle_node_configure_submit"); + char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, omemo_device_id()); + xmpp_stanza_t *iq = stanza_create_pubsub_configure_submit(ctx, id, jid->barejid, node, form); + g_free(node); + + iq_id_handler_add(id, _omemo_bundle_publish_configure_result, NULL, userdata); + + iq_send_stanza(iq); + + xmpp_stanza_release(iq); + free(id); + jid_destroy(jid); + return 0; +} + +static int +_omemo_bundle_publish_configure_result(xmpp_stanza_t *const stanza, void *const userdata) +{ + const char *type = xmpp_stanza_get_type(stanza); + + if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { + log_error("OMEMO: cannot configure bundle to an open access model"); + return 0; + } + + omemo_bundle_publish(TRUE); + + return 0; +} diff --git a/src/xmpp/omemo.h b/src/xmpp/omemo.h new file mode 100644 index 00000000..f1fff7b7 --- /dev/null +++ b/src/xmpp/omemo.h @@ -0,0 +1,11 @@ +#include <glib.h> + +#include "xmpp/iq.h" + +void omemo_devicelist_subscribe(void); +void omemo_devicelist_publish(GList *device_list); +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); diff --git a/src/xmpp/roster.c b/src/xmpp/roster.c index 9be154e7..fe15515f 100644 --- a/src/xmpp/roster.c +++ b/src/xmpp/roster.c @@ -137,7 +137,7 @@ roster_send_add_to_group(const char *const group, PContact contact) } xmpp_ctx_t * const ctx = connection_get_ctx(); - iq_id_handler_add(unique_id, _group_add_id_handler, (ProfIdFreeCallback)_free_group_data, data); + iq_id_handler_add(unique_id, _group_add_id_handler, (ProfIqFreeCallback)_free_group_data, data); xmpp_stanza_t *iq = stanza_create_roster_set(ctx, unique_id, p_contact_barejid(contact), p_contact_name(contact), new_groups); iq_send_stanza(iq); @@ -180,7 +180,7 @@ roster_send_remove_from_group(const char *const group, PContact contact) data->name = strdup(p_contact_barejid(contact)); } - iq_id_handler_add(unique_id, _group_remove_id_handler, (ProfIdFreeCallback)_free_group_data, data); + iq_id_handler_add(unique_id, _group_remove_id_handler, (ProfIqFreeCallback)_free_group_data, data); xmpp_stanza_t *iq = stanza_create_roster_set(ctx, unique_id, p_contact_barejid(contact), p_contact_name(contact), new_groups); iq_send_stanza(iq); diff --git a/src/xmpp/session.c b/src/xmpp/session.c index de7fb7ac..675f23af 100644 --- a/src/xmpp/session.c +++ b/src/xmpp/session.c @@ -60,6 +60,11 @@ #include "xmpp/chat_session.h" #include "xmpp/jid.h" +#ifdef HAVE_OMEMO +#include "omemo/omemo.h" +#include "xmpp/omemo.h" +#endif + // for auto reconnect static struct { char *name; @@ -286,6 +291,12 @@ session_get_account_name(void) void session_login_success(gboolean secured) { + chat_sessions_init(); + + message_handlers_init(); + presence_handlers_init(); + iq_handlers_init(); + // logged in with account if (saved_account.name) { log_debug("Connection handler: logged in with account name: %s", saved_account.name); @@ -297,26 +308,20 @@ session_login_success(gboolean secured) accounts_add(saved_details.name, saved_details.altdomain, saved_details.port, saved_details.tls_policy); accounts_set_jid(saved_details.name, saved_details.jid); - sv_ev_login_account_success(saved_details.name, secured); saved_account.name = strdup(saved_details.name); saved_account.passwd = strdup(saved_details.passwd); _session_free_saved_details(); + sv_ev_login_account_success(saved_account.name, secured); } - chat_sessions_init(); - - message_handlers_init(); - presence_handlers_init(); - iq_handlers_init(); - roster_request(); bookmark_request(); blocking_request(); // items discovery + connection_request_features(); char *domain = connection_get_domain(); - iq_disco_info_request_onconnect(domain); iq_disco_items_request_onconnect(domain); if (prefs_get_boolean(PREF_CARBONS)){ diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c index 534ee06b..615de44f 100644 --- a/src/xmpp/stanza.c +++ b/src/xmpp/stanza.c @@ -396,6 +396,18 @@ stanza_attach_hints_no_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza) } xmpp_stanza_t* +stanza_attach_hints_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza) +{ + xmpp_stanza_t *store = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(store, "store"); + xmpp_stanza_set_ns(store, STANZA_NS_HINTS); + xmpp_stanza_add_child(stanza, store); + xmpp_stanza_release(store); + + return stanza; +} + +xmpp_stanza_t* stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza) { xmpp_stanza_t *receipet_request = xmpp_stanza_new(ctx); @@ -1821,6 +1833,45 @@ stanza_get_error_message(xmpp_stanza_t *stanza) } void +stanza_attach_publish_options(xmpp_ctx_t *const ctx, xmpp_stanza_t *const iq, const char *const option, const char *const value) +{ + xmpp_stanza_t *publish_options = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(publish_options, STANZA_NAME_PUBLISH_OPTIONS); + + xmpp_stanza_t *x = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(x, STANZA_NAME_X); + xmpp_stanza_set_ns(x, STANZA_NS_DATA); + xmpp_stanza_set_type(x, "submit"); + xmpp_stanza_add_child(publish_options, x); + + xmpp_stanza_t *form_type = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(form_type, STANZA_NAME_FIELD); + xmpp_stanza_set_attribute(form_type, STANZA_ATTR_VAR, "FORM_TYPE"); + xmpp_stanza_set_type(form_type, "hidden"); + xmpp_stanza_t *form_type_value = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(form_type_value, STANZA_NAME_VALUE); + xmpp_stanza_t *form_type_value_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(form_type_value_text, XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS); + xmpp_stanza_add_child(form_type_value, form_type_value_text); + xmpp_stanza_add_child(form_type, form_type_value); + xmpp_stanza_add_child(x, form_type); + + xmpp_stanza_t *access_model = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(access_model, STANZA_NAME_FIELD); + xmpp_stanza_set_attribute(access_model, STANZA_ATTR_VAR, option); + xmpp_stanza_t *access_model_value = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(access_model_value, STANZA_NAME_VALUE); + xmpp_stanza_t *access_model_value_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(access_model_value_text, value); + xmpp_stanza_add_child(access_model_value, access_model_value_text); + xmpp_stanza_add_child(access_model, access_model_value); + xmpp_stanza_add_child(x, access_model); + + xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(iq, STANZA_NS_PUBSUB); + xmpp_stanza_add_child(pubsub, publish_options); +} + +void stanza_attach_priority(xmpp_ctx_t *const ctx, xmpp_stanza_t *const presence, const int pri) { if (pri == 0) { @@ -2092,6 +2143,295 @@ stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room, return iq; } +xmpp_stanza_t* +stanza_create_omemo_devicelist_request(xmpp_ctx_t *ctx, const char *const id, + const char *const jid) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id); + xmpp_stanza_set_to(iq, jid); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *items = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(items, "items"); + xmpp_stanza_set_attribute(items, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST); + + xmpp_stanza_add_child(pubsub, items); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(items); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_omemo_devicelist_subscribe(xmpp_ctx_t *ctx, const char *const jid) +{ + char *id = connection_create_stanza_id("omemo_devicelist_subscribe"); + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id); + free(id); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *subscribe = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(subscribe, STANZA_NAME_SUBSCRIBE); + xmpp_stanza_set_attribute(subscribe, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST); + xmpp_stanza_set_attribute(subscribe, "jid", jid); + + xmpp_stanza_add_child(pubsub, subscribe); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(subscribe); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_omemo_devicelist_publish(xmpp_ctx_t *ctx, GList *const ids) +{ + char *id = connection_create_stanza_id("omemo_devicelist_publish"); + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id); + free(id); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *publish = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH); + xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST); + + xmpp_stanza_t *item = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(item, STANZA_NAME_ITEM); + xmpp_stanza_set_attribute(item, "id", "current"); + + xmpp_stanza_t *list = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(list, "list"); + xmpp_stanza_set_ns(list, "eu.siacs.conversations.axolotl"); + + GList *i; + for (i = ids; i != NULL; i = i->next) { + xmpp_stanza_t *device = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(device, "device"); + char *id = g_strdup_printf("%d", GPOINTER_TO_INT(i->data)); + xmpp_stanza_set_attribute(device, "id", id); + g_free(id); + + xmpp_stanza_add_child(list, device); + xmpp_stanza_release(device); + } + + xmpp_stanza_add_child(item, list); + xmpp_stanza_add_child(publish, item); + xmpp_stanza_add_child(pubsub, publish); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(list); + xmpp_stanza_release(item); + xmpp_stanza_release(publish); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_omemo_bundle_publish(xmpp_ctx_t *ctx, const char *const id, + uint32_t device_id, + const unsigned char * const identity_key, size_t identity_key_length, + const unsigned char * const signed_prekey, size_t signed_prekey_length, + const unsigned char * const signed_prekey_signature, size_t signed_prekey_signature_length, + GList *const prekeys, GList *const prekeys_id, GList *const prekeys_length) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *publish = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH); + char *node = g_strdup_printf("%s:%d", "eu.siacs.conversations.axolotl.bundles", device_id); + xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, node); + g_free(node); + + xmpp_stanza_t *item = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(item, STANZA_NAME_ITEM); + xmpp_stanza_set_attribute(item, "id", "current"); + + xmpp_stanza_t *bundle = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(bundle, "bundle"); + xmpp_stanza_set_ns(bundle, "eu.siacs.conversations.axolotl"); + + xmpp_stanza_t *signed_prekey_public_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(signed_prekey_public_stanza , "signedPreKeyPublic"); + xmpp_stanza_set_attribute(signed_prekey_public_stanza, "signedPreKeyId", "1"); + + xmpp_stanza_t *signed_prekey_public_stanza_text= xmpp_stanza_new(ctx); + char *signed_prekey_b64 = g_base64_encode(signed_prekey, signed_prekey_length); + xmpp_stanza_set_text(signed_prekey_public_stanza_text, signed_prekey_b64); + g_free(signed_prekey_b64); + xmpp_stanza_add_child(signed_prekey_public_stanza, signed_prekey_public_stanza_text); + xmpp_stanza_release(signed_prekey_public_stanza_text); + + xmpp_stanza_t *signed_prekey_signature_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(signed_prekey_signature_stanza , "signedPreKeySignature"); + + xmpp_stanza_t *signed_prekey_signature_stanza_text= xmpp_stanza_new(ctx); + char *signed_prekey_signature_b64 = g_base64_encode(signed_prekey_signature, signed_prekey_signature_length); + xmpp_stanza_set_text(signed_prekey_signature_stanza_text, signed_prekey_signature_b64); + g_free(signed_prekey_signature_b64); + xmpp_stanza_add_child(signed_prekey_signature_stanza, signed_prekey_signature_stanza_text); + xmpp_stanza_release(signed_prekey_signature_stanza_text); + + xmpp_stanza_t *identity_key_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(identity_key_stanza , "identityKey"); + + xmpp_stanza_t *identity_key_stanza_text= xmpp_stanza_new(ctx); + char *identity_key_b64 = g_base64_encode(identity_key, identity_key_length); + xmpp_stanza_set_text(identity_key_stanza_text, identity_key_b64); + g_free(identity_key_b64); + xmpp_stanza_add_child(identity_key_stanza, identity_key_stanza_text); + xmpp_stanza_release(identity_key_stanza_text); + + xmpp_stanza_t *prekeys_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(prekeys_stanza, "prekeys"); + + GList *p, *i, *l; + for (p = prekeys, i = prekeys_id, l = prekeys_length; p != NULL; p = p->next, i = i->next, l = l->next) { + xmpp_stanza_t *prekey = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(prekey, "preKeyPublic"); + char *id = g_strdup_printf("%d", GPOINTER_TO_INT(i->data)); + xmpp_stanza_set_attribute(prekey, "preKeyId", id); + g_free(id); + + xmpp_stanza_t *prekey_text = xmpp_stanza_new(ctx); + char *prekey_b64 = g_base64_encode(p->data, GPOINTER_TO_INT(l->data)); + xmpp_stanza_set_text(prekey_text, prekey_b64); + g_free(prekey_b64); + + xmpp_stanza_add_child(prekey, prekey_text); + xmpp_stanza_add_child(prekeys_stanza, prekey); + xmpp_stanza_release(prekey_text); + xmpp_stanza_release(prekey); + } + + xmpp_stanza_add_child(bundle, signed_prekey_public_stanza); + xmpp_stanza_add_child(bundle, signed_prekey_signature_stanza); + xmpp_stanza_add_child(bundle, identity_key_stanza); + xmpp_stanza_add_child(bundle, prekeys_stanza); + xmpp_stanza_add_child(item, bundle); + xmpp_stanza_add_child(publish, item); + xmpp_stanza_add_child(pubsub, publish); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(signed_prekey_public_stanza); + xmpp_stanza_release(signed_prekey_signature_stanza); + xmpp_stanza_release(identity_key_stanza); + xmpp_stanza_release(prekeys_stanza); + xmpp_stanza_release(bundle); + xmpp_stanza_release(item); + xmpp_stanza_release(publish); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_omemo_bundle_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, uint32_t device_id) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id); + xmpp_stanza_set_to(iq, jid); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB); + + xmpp_stanza_t *items = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(items, "items"); + char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, device_id); + xmpp_stanza_set_attribute(items, STANZA_ATTR_NODE, node); + g_free(node); + + xmpp_stanza_add_child(pubsub, items); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(items); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_pubsub_configure_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id); + xmpp_stanza_set_to(iq, jid); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB_OWNER); + + xmpp_stanza_t *configure = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(configure, STANZA_NAME_CONFIGURE); + xmpp_stanza_set_attribute(configure, STANZA_ATTR_NODE, node); + + xmpp_stanza_add_child(pubsub, configure); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(configure); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_create_pubsub_configure_submit(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node, DataForm *form) +{ + xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id); + xmpp_stanza_set_to(iq, jid); + + xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB); + xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB_OWNER); + + xmpp_stanza_t *configure = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(configure, STANZA_NAME_CONFIGURE); + xmpp_stanza_set_attribute(configure, STANZA_ATTR_NODE, node); + + xmpp_stanza_t *x = form_create_submission(form); + + xmpp_stanza_add_child(configure, x); + xmpp_stanza_add_child(pubsub, configure); + xmpp_stanza_add_child(iq, pubsub); + + xmpp_stanza_release(x); + xmpp_stanza_release(configure); + xmpp_stanza_release(pubsub); + + return iq; +} + +xmpp_stanza_t* +stanza_attach_origin_id(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const id) +{ + xmpp_stanza_t *origin_id = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(origin_id, STANZA_NAME_ORIGIN_ID); + xmpp_stanza_set_ns(origin_id, STANZA_NS_STABLE_ID); + xmpp_stanza_set_attribute(origin_id, STANZA_ATTR_ID, id); + + xmpp_stanza_add_child(stanza, origin_id); + + xmpp_stanza_release(origin_id); + + return stanza; +} + static void _stanza_add_unique_id(xmpp_stanza_t *stanza, char *prefix) { diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h index d3c3c9dc..e5e17ba4 100644 --- a/src/xmpp/stanza.h +++ b/src/xmpp/stanza.h @@ -82,6 +82,7 @@ #define STANZA_NAME_PUBSUB "pubsub" #define STANZA_NAME_PUBLISH "publish" #define STANZA_NAME_PUBLISH_OPTIONS "publish-options" +#define STANZA_NAME_SUBSCRIBE "subscribe" #define STANZA_NAME_FIELD "field" #define STANZA_NAME_STORAGE "storage" #define STANZA_NAME_NICK "nick" @@ -100,6 +101,8 @@ #define STANZA_NAME_GET "get" #define STANZA_NAME_URL "url" #define STANZA_NAME_COMMAND "command" +#define STANZA_NAME_CONFIGURE "configure" +#define STANZA_NAME_ORIGIN_ID "origin-id" // error conditions #define STANZA_NAME_BAD_REQUEST "bad-request" @@ -179,6 +182,8 @@ #define STANZA_NS_CONFERENCE "jabber:x:conference" #define STANZA_NS_CAPTCHA "urn:xmpp:captcha" #define STANZA_NS_PUBSUB "http://jabber.org/protocol/pubsub" +#define STANZA_NS_PUBSUB_OWNER "http://jabber.org/protocol/pubsub#owner" +#define STANZA_NS_PUBSUB_EVENT "http://jabber.org/protocol/pubsub#event" #define STANZA_NS_CARBONS "urn:xmpp:carbons:2" #define STANZA_NS_HINTS "urn:xmpp:hints" #define STANZA_NS_FORWARD "urn:xmpp:forward:0" @@ -189,6 +194,10 @@ #define STANZA_NS_X_OOB "jabber:x:oob" #define STANZA_NS_BLOCKING "urn:xmpp:blocking" #define STANZA_NS_COMMAND "http://jabber.org/protocol/commands" +#define STANZA_NS_OMEMO "eu.siacs.conversations.axolotl" +#define STANZA_NS_OMEMO_DEVICELIST "eu.siacs.conversations.axolotl.devicelist" +#define STANZA_NS_OMEMO_BUNDLES "eu.siacs.conversations.axolotl.bundles" +#define STANZA_NS_STABLE_ID "urn:xmpp:sid:0" #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo" @@ -228,8 +237,10 @@ xmpp_stanza_t* stanza_attach_state(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const xmpp_stanza_t* stanza_attach_carbons_private(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza); xmpp_stanza_t* stanza_attach_hints_no_copy(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza); xmpp_stanza_t* stanza_attach_hints_no_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza); +xmpp_stanza_t* stanza_attach_hints_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza); xmpp_stanza_t* stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza); xmpp_stanza_t* stanza_attach_x_oob_url(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const url); +xmpp_stanza_t* stanza_attach_origin_id(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const id); xmpp_stanza_t* stanza_create_room_join_presence(xmpp_ctx_t *const ctx, const char *const full_room_jid, const char *const passwd); @@ -284,6 +295,17 @@ xmpp_stanza_t* stanza_create_room_kick_iq(xmpp_ctx_t *const ctx, const char *con xmpp_stanza_t* stanza_create_command_exec_iq(xmpp_ctx_t *ctx, const char *const target, const char *const node); xmpp_stanza_t* stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room, const char *const node, const char *const sessionid, DataForm *form); +void stanza_attach_publish_options(xmpp_ctx_t *const ctx, xmpp_stanza_t *const publish, const char *const option, const char *const value); + +xmpp_stanza_t* stanza_create_omemo_devicelist_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid); +xmpp_stanza_t* stanza_create_omemo_devicelist_subscribe(xmpp_ctx_t *ctx, const char *const jid); +xmpp_stanza_t* stanza_create_omemo_devicelist_publish(xmpp_ctx_t *ctx, GList *const ids); +xmpp_stanza_t* stanza_create_omemo_bundle_publish(xmpp_ctx_t *ctx, const char *const id, uint32_t device_id, const unsigned char * const identity_key, size_t identity_key_length, const unsigned char * const signed_prekey, size_t signed_prekey_length, const unsigned char * const signed_prekey_signature, size_t signed_prekey_signature_length, GList *const prekeys, GList *const prekeys_id, GList *const prekeys_length); +xmpp_stanza_t* stanza_create_omemo_bundle_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, uint32_t device_id); + +xmpp_stanza_t* stanza_create_pubsub_configure_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node); +xmpp_stanza_t* stanza_create_pubsub_configure_submit(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node, DataForm *form); + int stanza_get_idle_time(xmpp_stanza_t *const stanza); void stanza_attach_priority(xmpp_ctx_t *const ctx, xmpp_stanza_t *const presence, const int pri); diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h index c9403090..d5330599 100644 --- a/src/xmpp/xmpp.h +++ b/src/xmpp/xmpp.h @@ -35,6 +35,8 @@ #ifndef XMPP_XMPP_H #define XMPP_XMPP_H +#include <stdint.h> + #include "config.h" #ifdef HAVE_LIBMESODE @@ -61,6 +63,9 @@ #define XMPP_FEATURE_LASTACTIVITY "jabber:iq:last" #define XMPP_FEATURE_MUC "http://jabber.org/protocol/muc" #define XMPP_FEATURE_COMMANDS "http://jabber.org/protocol/commands" +#define XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY "eu.siacs.conversations.axolotl.devicelist+notify" +#define XMPP_FEATURE_PUBSUB "http://jabber.org/protocol/pubsub" +#define XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS "http://jabber.org/protocol/pubsub#publish-options" typedef enum { JABBER_CONNECTING, @@ -139,8 +144,9 @@ char* message_send_chat(const char *const barejid, const char *const msg, const gboolean request_receipt); char* message_send_chat_otr(const char *const barejid, const char *const msg, gboolean request_receipt); char* message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean request_receipt); +char* message_send_chat_omemo(const char *const jid, uint32_t sid, GList *keys, const unsigned char *const iv, size_t iv_len, const unsigned char *const ciphertext, size_t ciphertext_len, gboolean request_receipt, gboolean muc); void message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url); -void message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url); +char* message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url); void message_send_groupchat_subject(const char *const roomjid, const char *const subject); void message_send_inactive(const char *const jid); void message_send_composing(const char *const jid); |