diff --git a/docs/profanity-ox.md b/docs/profanity-ox.md new file mode 100644 index 00000000..1a01e210 --- /dev/null +++ b/docs/profanity-ox.md @@ -0,0 +1,78 @@ +# Profanity - OpenPGP for XMPP + +Implementation of XEP-0373 - OpenPGP for XMPP (OX) in profanity. + +## Overview +The current version (2020-05-23) of profanity provides *XEP-0027: Current Jabber +OpenPGP Usage* via the `/pgp` command. This XEP is *Obsolete*. We should +implement *XEP-0373 - OpenPGP for XMPP (OX)* in profanity. + +## pgp + +``` +14:37:52 - Synopsis +14:37:52 - /pgp libver +14:37:52 - /pgp keys +14:37:52 - /pgp contacts +14:37:52 - /pgp setkey +14:37:52 - /pgp start [] +14:37:52 - /pgp end +14:37:52 - /pgp log on|off|redact +14:37:52 - /pgp char +14:37:52 - +14:37:52 - Description +14:37:52 - Open PGP commands to manage keys, and perform PGP encryption during chat sessions. See the /account command to set your own PGP key. +14:37:52 - +14:37:52 - Arguments +14:37:52 - libver : Show which version of the libgpgme library is being used. +14:37:52 - keys : List all keys known to the system. +14:37:52 - contacts : Show contacts with assigned public keys. +14:37:52 - setkey : Manually associate a contact with a public key. +14:37:52 - start [] : Start PGP encrypted chat, current contact will be used if not specified. +14:37:52 - end : End PGP encrypted chat with the current recipient. +14:37:52 - log on|off : Enable or disable plaintext logging of PGP encrypted messages. +14:37:52 - log redact : Log PGP encrypted messages, but replace the contents with [redacted]. This is the default. +14:37:52 - char : Set the character to be displayed next to PGP encrypted messages. +``` +## OX +We should implement the `/ox` command which can be used for XEP-0373 instead of +XEP-0027. + +``` +/ox keys - List all public keys known to the system (gnupg's keyring) +/ox contacts - Shows contacts with an assigned public key. +``` + +The `keys` command will list all public keys of gnupg's Keyring, independent if +the key is in use for XMPP or not. + +In profanity we are going to implement the key lookup with a XMPP-URI as OpenPGP +User-ID. An OpenPGP public key can only be used, if the owner of the public key +created an User-ID with the XMPP-URI as Name. https://xmpp.org/extensions/xep-0373.html#openpgp-user-ids +It's not required and possible to assign a contact to an public key. + +``` +sec rsa3072 2020-05-01 [SC] [verfällt: 2022-05-01] + 7FA1EB8644BAC07E7F18E7C9F121E6A6F3A0C7A5 +uid [ ultimativ ] Doctor Snuggles +uid [ ultimativ ] xmpp:doctor.snuggles@domain.tld +ssb rsa3072 2020-05-01 [E] [verfällt: 2022-05-01] +``` + +The `contacts` command will show all contacts of the roster with a public key in +the keyring, if there is a xmpp user-id within the public key. + +OX provides the elements: ``, `` and ``. Profanity +implements signcrypt, only. + + +## Keys command +The command `keys` is independent of the XEP. Should we move common commands +(e.g. /pgp keys /ox keys) to /openpgp which will will be the function which are +related to gnupg itself. + +## Appendix + +* https://xmpp.org/extensions/xep-0373.html - 0.4.0 (2018-07-30) + + diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 09de573c..eefca0cc 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -78,6 +78,7 @@ static char* _otr_autocomplete(ProfWin *window, const char *const input, gboolea #endif #ifdef HAVE_LIBGPGME static char* _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous); +static char* _ox_autocomplete(ProfWin *window, const char *const input, gboolean previous); #endif #ifdef HAVE_OMEMO static char* _omemo_autocomplete(ProfWin *window, const char *const input, gboolean previous); @@ -224,6 +225,9 @@ static Autocomplete receipts_ac; static Autocomplete pgp_ac; static Autocomplete pgp_log_ac; static Autocomplete pgp_sendfile_ac; +static Autocomplete ox_ac; +static Autocomplete ox_log_ac; +static Autocomplete ox_sendfile_ac; #endif static Autocomplete tls_ac; static Autocomplete titlebar_ac; @@ -261,6 +265,13 @@ static Autocomplete correction_ac; static Autocomplete avatar_ac; static Autocomplete executable_ac; +/*! + * \brief Initialization of auto completion for commands. + * + * This function implements the auto completion for profanity's commands. + * + */ + void cmd_ac_init(void) { @@ -842,6 +853,29 @@ cmd_ac_init(void) pgp_sendfile_ac = autocomplete_new(); autocomplete_add(pgp_sendfile_ac, "on"); autocomplete_add(pgp_sendfile_ac, "off"); + + // XEP-0373: OX + ox_ac = autocomplete_new(); + autocomplete_add(ox_ac, "keys"); + autocomplete_add(ox_ac, "contacts"); + autocomplete_add(ox_ac, "start"); + autocomplete_add(ox_ac, "end"); + autocomplete_add(ox_ac, "log"); + autocomplete_add(ox_ac, "char"); + autocomplete_add(ox_ac, "sendfile"); + autocomplete_add(ox_ac, "announce"); + autocomplete_add(ox_ac, "discover"); + autocomplete_add(ox_ac, "request"); + + pgp_log_ac = autocomplete_new(); + autocomplete_add(ox_log_ac, "on"); + autocomplete_add(ox_log_ac, "off"); + autocomplete_add(ox_log_ac, "redact"); + + pgp_sendfile_ac = autocomplete_new(); + autocomplete_add(ox_sendfile_ac, "on"); + autocomplete_add(ox_sendfile_ac, "off"); + #endif tls_ac = autocomplete_new(); @@ -1707,6 +1741,7 @@ _cmd_ac_complete_params(ProfWin *window, const char *const input, gboolean previ #endif #ifdef HAVE_LIBGPGME g_hash_table_insert(ac_funcs, "/pgp", _pgp_autocomplete); + g_hash_table_insert(ac_funcs, "/ox", _ox_autocomplete); #endif #ifdef HAVE_OMEMO g_hash_table_insert(ac_funcs, "/omemo", _omemo_autocomplete); @@ -2420,6 +2455,68 @@ _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous) return NULL; } + +/*! + * \brief Auto completion for XEP-0373: OpenPGP for XMPP command. + * + * + */ +static char* +_ox_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, "/ox start", roster_contact_autocomplete, previous, NULL); + if (found) { + return found; + } + } + + found = autocomplete_param_with_ac(input, "/ox log", ox_log_ac, TRUE, previous); + if (found) { + return found; + } + + found = autocomplete_param_with_ac(input, "/ox sendfile", ox_sendfile_ac, TRUE, previous); + if (found) { + return found; + } + + gboolean result; + gchar **args = parse_args(input, 2, 3, &result); + if ((strncmp(input, "/ox", 4) == 0) && (result == TRUE)) { + GString *beginning = g_string_new("/ox "); + g_string_append(beginning, args[0]); + if (args[1]) { + g_string_append(beginning, " "); + g_string_append(beginning, args[1]); + } + found = autocomplete_param_with_func(input, beginning->str, p_gpg_autocomplete_key, previous, NULL); + g_string_free(beginning, TRUE); + if (found) { + g_strfreev(args); + return found; + } + } + g_strfreev(args); + + if (conn_status == JABBER_CONNECTED) { + found = autocomplete_param_with_func(input, "/ox setkey", roster_barejid_autocomplete, previous, NULL); + if (found) { + return found; + } + } + + found = autocomplete_param_with_ac(input, "/ox", ox_ac, TRUE, previous); + if (found) { + return found; + } + + return NULL; +} #endif #ifdef HAVE_OMEMO diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 85140430..815fe9d8 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1691,6 +1691,48 @@ static struct cmd_t command_defs[] = "/pgp char P") }, +// XEP-0373: OpenPGP for XMPP +#ifdef HAVE_LIBGPGME + { "/ox", + parse_args, 1, 3, NULL, + CMD_NOSUBFUNCS + CMD_MAINFUNC(cmd_ox) + CMD_TAGS( + CMD_TAG_CHAT, + CMD_TAG_UI) + CMD_SYN( + "/ox keys", + "/ox contacts", + "/ox start []", + "/ox end", + "/ox log on|off|redact", + "/ox char ", + "/ox sendfile on|off", + "/ox announce ", + "/ox discover", + "/ox request ") + CMD_DESC( + "OpenPGP (OX) commands to manage keys, and perform PGP encryption during chat sessions. ") + CMD_ARGS( + { "keys", "List all keys known to the system." }, + { "contacts", "Show contacts with assigned public keys." }, + { "start []", "Start PGP encrypted chat, current contact will be used if not specified." }, + { "end", "End PGP encrypted chat with the current recipient." }, + { "log on|off", "Enable or disable plaintext logging of PGP encrypted messages." }, + { "log redact", "Log PGP encrypted messages, but replace the contents with [redacted]. This is the default." }, + { "char ", "Set the character to be displayed next to PGP encrypted messages." }, + { "announce ", "Announce a public key by pushing it on the XMPP Server"}, + { "discover ", "Discover public keys of a jid "}, + { "request ", "Request public keys"}, + { "sendfile on|off", "Allow /sendfile to send unencrypted files while otherwise using PGP."}) + CMD_EXAMPLES( + "/ox log off", + "/ox start odin@valhalla.edda", + "/ox end", + "/ox char X") + }, +#endif // HAVE_LIBGPGME + { "/otr", parse_args, 1, 3, NULL, CMD_SUBFUNCS( diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 735e197b..7d779e87 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -7418,6 +7418,169 @@ cmd_pgp(ProfWin *window, const char *const command, gchar **args) #endif } +#ifdef HAVE_LIBGPGME + +/*! + * \brief Command for XEP-0373: OpenPGP for XMPP + * + */ + +gboolean +cmd_ox(ProfWin *window, const char *const command, gchar **args) +{ + if (args[0] == NULL) { + cons_bad_cmd_usage(command); + return TRUE; + } + + // The '/ox keys' command - same like in pgp + // Should we move this to a common command + // e.g. '/openpgp keys'?. + else if (g_strcmp0(args[0], "keys") == 0) { + GHashTable *keys = p_gpg_list_keys(); + if (!keys || g_hash_table_size(keys) == 0) { + cons_show("No keys found"); + return TRUE; + } + + cons_show("OpenPGP keys:"); + GList *keylist = g_hash_table_get_keys(keys); + GList *curr = keylist; + while (curr) { + ProfPGPKey *key = g_hash_table_lookup(keys, curr->data); + cons_show(" %s", key->name); + cons_show(" ID : %s", key->id); + char *format_fp = p_gpg_format_fp_str(key->fp); + cons_show(" Fingerprint : %s", format_fp); + free(format_fp); + if (key->secret) { + cons_show(" Type : PUBLIC, PRIVATE"); + } else { + cons_show(" Type : PUBLIC"); + } + curr = g_list_next(curr); + } + g_list_free(keylist); + p_gpg_free_keys(keys); + return TRUE; + } + + else if (g_strcmp0(args[0], "contacts") == 0) { + GHashTable *keys = ox_gpg_public_keys(); + cons_show("OpenPGP keys:"); + GList *keylist = g_hash_table_get_keys(keys); + GList *curr = keylist; + + + GSList *roster_list = NULL; + jabber_conn_status_t conn_status = connection_get_status(); + if (conn_status != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + } else { + roster_list = roster_get_contacts(ROSTER_ORD_NAME); + } + + while (curr) { + ProfPGPKey *key = g_hash_table_lookup(keys, curr->data); + PContact contact = NULL; + if (roster_list) { + GSList *curr_c = roster_list; + while ( !contact && curr_c){ + contact = curr_c->data; + const char *jid = p_contact_barejid(contact); + GString* xmppuri = g_string_new("xmpp:"); + g_string_append(xmppuri, jid); + if( g_strcmp0(key->name, xmppuri->str)) { + contact = NULL; + } + curr_c = g_slist_next(curr_c); + } + } + + if(contact) { + cons_show("%s - %s", key->fp, key->name); + } else { + cons_show("%s - %s (not in roster)", key->fp, key->name); + } + curr = g_list_next(curr); + } + + } else if (g_strcmp0(args[0], "start") == 0) { + jabber_conn_status_t conn_status = connection_get_status(); + if (conn_status != JABBER_CONNECTED) { + cons_show("You must be connected to start OX encrpytion."); + return TRUE; + } + + if (window->type != WIN_CHAT && args[1] == NULL) { + cons_show("You must be in a regular chat window to start OX encrpytion."); + return TRUE; + } + + ProfChatWin *chatwin = NULL; + + if (args[1]) { + char *contact = args[1]; + char *barejid = roster_barejid_from_name(contact); + if (barejid == NULL) { + barejid = contact; + } + + chatwin = wins_get_chat(barejid); + if (!chatwin) { + chatwin = chatwin_new(barejid); + } + ui_focus_win((ProfWin*)chatwin); + } else { + chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + } + + if (chatwin->is_otr) { + win_println(window, THEME_DEFAULT, "!", "You must end the OTR session to start OX encryption."); + return TRUE; + } + + if (chatwin->pgp_send) { + win_println(window, THEME_DEFAULT, "!", "You must end the PGP session to start OX encryption."); + return TRUE; + } + + if (chatwin->is_ox) { + win_println(window, THEME_DEFAULT, "!", "You have already started OX encryption."); + return TRUE; + } + + ProfAccount *account = accounts_get_account(session_get_account_name()); + + if ( !ox_is_private_key_available(account->jid) ) { + win_println(window, THEME_DEFAULT, "!", "No private OpenPGP found, cannot start OX encryption."); + account_free(account); + return TRUE; + } + account_free(account); + + if (!ox_is_public_key_available(chatwin->barejid)) { + win_println(window, THEME_DEFAULT, "!", "No OX-OpenPGP key found for %s.", chatwin->barejid); + return TRUE; + } + + chatwin->is_ox = TRUE; + win_println(window, THEME_DEFAULT, "!", "OX encryption enabled."); + return TRUE; + } else if (g_strcmp0(args[0], "push") == 0) { + if( args[1] ) { + cons_show("Push file...%s ", args[1] ); + } else { + cons_show("Filename is required"); + } + } else { + cons_show("OX not implemented"); + } + return TRUE; +} +#endif // HAVE_LIBGPGME + gboolean cmd_otr_char(ProfWin *window, const char *const command, gchar **args) { diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index d0d37efa..b75755cb 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -107,6 +107,9 @@ gboolean cmd_msg(ProfWin *window, const char *const command, gchar **args); gboolean cmd_nick(ProfWin *window, const char *const command, gchar **args); gboolean cmd_notify(ProfWin *window, const char *const command, gchar **args); gboolean cmd_pgp(ProfWin *window, const char *const command, gchar **args); +#ifdef HAVE_LIBGPGME +gboolean cmd_ox(ProfWin *window, const char *const command, gchar **args); +#endif // HAVE_LIBGPGME gboolean cmd_outtype(ProfWin *window, const char *const command, gchar **args); gboolean cmd_prefs(ProfWin *window, const char *const command, gchar **args); gboolean cmd_priority(ProfWin *window, const char *const command, gchar **args); diff --git a/src/config/preferences.c b/src/config/preferences.c index 093d7679..87f1acad 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -62,6 +62,7 @@ #define PREF_GROUP_OTR "otr" #define PREF_GROUP_PGP "pgp" #define PREF_GROUP_OMEMO "omemo" +#define PREF_GROUP_OX "ox" #define PREF_GROUP_MUC "muc" #define PREF_GROUP_PLUGINS "plugins" @@ -942,6 +943,18 @@ prefs_set_pgp_char(char *ch) return _prefs_set_encryption_char(ch, PREF_GROUP_PGP, "pgp.char"); } +char* +prefs_get_ox_char(void) +{ + return _prefs_get_encryption_char("%", PREF_GROUP_OX, "ox.char"); +} + +gboolean +prefs_set_ox_char(char *ch) +{ + return _prefs_set_encryption_char(ch, PREF_GROUP_OX, "ox.char"); +} + char* prefs_get_omemo_char(void) { diff --git a/src/config/preferences.h b/src/config/preferences.h index 5a38bfec..920342b8 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -244,6 +244,9 @@ char* prefs_get_pgp_char(void); gboolean prefs_set_pgp_char(char *ch); char* prefs_get_omemo_char(void); gboolean prefs_set_omemo_char(char *ch); +// XEP-0373: OpenPGP for XMPP +char* prefs_get_ox_char(void); +gboolean prefs_set_ox_char(char *ch); char prefs_get_roster_header_char(void); void prefs_set_roster_header_char(char ch); diff --git a/src/database.c b/src/database.c index 3cc14d0d..271c64b6 100644 --- a/src/database.c +++ b/src/database.c @@ -288,6 +288,8 @@ static prof_msg_type_t _get_message_type_type(const char *const type) { static const char* _get_message_enc_str(prof_enc_t enc) { switch (enc) { + case PROF_MSG_ENC_OX: + return "ox"; case PROF_MSG_ENC_PGP: return "pgp"; case PROF_MSG_ENC_OTR: @@ -298,7 +300,7 @@ static const char* _get_message_enc_str(prof_enc_t enc) { return "none"; } - return "none"; + return "none"; // do not return none - return NULL } static void diff --git a/src/event/client_events.c b/src/event/client_events.c index 177a6559..15261760 100644 --- a/src/event/client_events.c +++ b/src/event/client_events.c @@ -138,6 +138,15 @@ cl_ev_send_msg_correct(ProfChatWin *chatwin, const char *const msg, const char * log_database_add_outgoing_chat(id, chatwin->barejid, plugin_msg, replace_id, PROF_MSG_ENC_OMEMO); chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_ENC_OMEMO, request_receipt, replace_id); free(id); +#endif + } else if (chatwin->is_ox) { +#ifdef HAVE_LIBGPGME + // XEP-0373: OpenPGP for XMPP + char *id = message_send_chat_ox(chatwin->barejid, plugin_msg, request_receipt, replace_id); + chat_log_pgp_msg_out(chatwin->barejid, plugin_msg, NULL); + log_database_add_outgoing_chat(id, chatwin->barejid, plugin_msg, replace_id, PROF_MSG_ENC_OX); + chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_ENC_OX, request_receipt, replace_id); + free(id); #endif } else if (chatwin->pgp_send) { #ifdef HAVE_LIBGPGME diff --git a/src/event/server_events.c b/src/event/server_events.c index 1684a7d2..22ac9d10 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -516,6 +516,22 @@ _sv_ev_incoming_pgp(ProfChatWin *chatwin, gboolean new_win, ProfMessage *message #endif } +static void +_sv_ev_incoming_ox(ProfChatWin *chatwin, gboolean new_win, ProfMessage *message, gboolean logit) +{ +#ifdef HAVE_LIBGPGME + //_clean_incoming_message(message); + chatwin_incoming_msg(chatwin, message, new_win); + log_database_add_incoming(message); + if (logit) { + chat_log_pgp_msg_in(message); + } + chatwin->pgp_recv = TRUE; + //p_gpg_free_decrypted(message->plain); + message->plain = NULL; +} +#endif + static void _sv_ev_incoming_otr(ProfChatWin *chatwin, gboolean new_win, ProfMessage *message) { @@ -604,7 +620,9 @@ sv_ev_incoming_message(ProfMessage *message) #endif } - if (message->encrypted) { + if( message->enc == PROF_MSG_ENC_OX) { + _sv_ev_incoming_ox(chatwin, new_win, message, TRUE); + } else if (message->encrypted) { if (chatwin->is_otr) { win_println((ProfWin*)chatwin, THEME_DEFAULT, "-", "PGP encrypted message received whilst in OTR session."); } else { @@ -638,7 +656,9 @@ sv_ev_incoming_carbon(ProfMessage *message) #endif } - if (message->encrypted) { + if (message->enc == PROF_MSG_ENC_OX) { + _sv_ev_incoming_ox(chatwin, new_win, message, FALSE); + } else if (message->encrypted) { _sv_ev_incoming_pgp(chatwin, new_win, message, FALSE); } else if (message->enc == PROF_MSG_ENC_OMEMO) { _sv_ev_incoming_omemo(chatwin, new_win, message, FALSE); diff --git a/src/pgp/gpg.c b/src/pgp/gpg.c index 528b772e..b50dd73f 100644 --- a/src/pgp/gpg.c +++ b/src/pgp/gpg.c @@ -72,6 +72,9 @@ static char* _remove_header_footer(char *str, const char *const footer); static char* _add_header_footer(const char *const str, const char *const header, const char *const footer); static void _save_pubkeys(void); +static gpgme_key_t _ox_key_lookup(const char *const barejid, gboolean secret_only); +static gboolean _ox_key_is_usable(gpgme_key_t key, const char *const barejid, gboolean secret); + void _p_gpg_free_pubkeyid(ProfPGPPubKeyId *pubkeyid) { @@ -787,6 +790,219 @@ p_gpg_format_fp_str(char *fp) return result; } +/*! + * \brief Public keys with XMPP-URI. + * + * This function will look for all public key with a XMPP-URI as UID. + * + */ +GHashTable* ox_gpg_public_keys(void){ + gpgme_error_t error; + GHashTable *result = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)_p_gpg_free_key); + + gpgme_ctx_t ctx; + error = gpgme_new(&ctx); + + if (error) { + log_error("OX - gpgme_new failed: %s %s", gpgme_strsource(error), gpgme_strerror(error)); + return NULL; + } + + error = gpgme_op_keylist_start(ctx, NULL, 0); // all public keys + if (error == GPG_ERR_NO_ERROR) { + gpgme_key_t key; + error = gpgme_op_keylist_next(ctx, &key); + if ( error != GPG_ERR_EOF && error != GPG_ERR_NO_ERROR) { + log_error("OX: gpgme_op_keylist_next %s %s", gpgme_strsource(error), gpgme_strerror(error)); + g_hash_table_destroy(result); + return NULL; + } + while (!error) { + // Looking for XMPP URI UID + gpgme_user_id_t uid = key->uids; + gpgme_user_id_t xmppid = NULL; + while (!xmppid && uid) { + if( uid->name && strlen(uid->name) >= 10 ) { + if( strstr(uid->name, "xmpp:") == uid->name ) { + xmppid = uid; + } + } + uid = uid->next; + } + + if(xmppid) { + // Build Key information about all subkey + gpgme_subkey_t sub = key->subkeys; + + ProfPGPKey *p_pgpkey = _p_gpg_key_new(); + p_pgpkey->id = strdup(sub->keyid); + p_pgpkey->name = strdup(xmppid->uid); + p_pgpkey->fp = strdup(sub->fpr); + if (sub->can_encrypt) p_pgpkey->encrypt = TRUE; + if (sub->can_authenticate) p_pgpkey->authenticate = TRUE; + if (sub->can_certify) p_pgpkey->certify = TRUE; + if (sub->can_sign) p_pgpkey->sign = TRUE; + + sub = sub->next; + while (sub) { + if (sub->can_encrypt) p_pgpkey->encrypt = TRUE; + if (sub->can_authenticate) p_pgpkey->authenticate = TRUE; + if (sub->can_certify) p_pgpkey->certify = TRUE; + if (sub->can_sign) p_pgpkey->sign = TRUE; + + sub = sub->next; + } + + g_hash_table_insert(result, strdup(p_pgpkey->name), p_pgpkey); + + } + gpgme_key_unref(key); + error = gpgme_op_keylist_next(ctx, &key); + } + } + gpgme_release(ctx); + + //autocomplete_clear(key_ac); + // GList *ids = g_hash_table_get_keys(result); + // GList *curr = ids; + // while (curr) { + // ProfPGPKey *key = g_hash_table_lookup(result, curr->data); + // autocomplete_add(key_ac, key->id); + // curr = curr->next; + // } + // g_list_free(ids); + + return result; + +} + +char* p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient_barejid , const char* const message) { + setlocale (LC_ALL, ""); + gpgme_check_version (NULL); + gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL)); + gpgme_ctx_t ctx; + gpgme_error_t error = gpgme_new (&ctx); + if(GPG_ERR_NO_ERROR != error ) { + printf("gpgme_new: %d\n", error); + return NULL; + } + error = gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + } + gpgme_set_armor(ctx,0); + gpgme_set_textmode(ctx,0); + gpgme_set_offline(ctx,1); + gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + } + + gpgme_key_t recp[3]; + recp[0] = NULL, + recp[1] = NULL; + + char* xmpp_jid_me = alloca( (strlen(sender_barejid)+6) * sizeof(char) ); + char* xmpp_jid_recipient = alloca( (strlen(recipient_barejid)+6) * sizeof(char) ); + + strcpy(xmpp_jid_me, "xmpp:"); + strcpy(xmpp_jid_recipient, "xmpp:"); + strcat(xmpp_jid_me, sender_barejid); + strcat(xmpp_jid_recipient,recipient_barejid); + + gpgme_signers_clear(ctx); + + // lookup own key + recp[0] = _ox_key_lookup(sender_barejid, TRUE); + if(error != 0) { + log_error("Key not found for %s. GpgME Error: %s", xmpp_jid_me, gpgme_strerror(error)); + return NULL; + } + + error = gpgme_signers_add(ctx,recp[0]); + if(error != 0) { + log_error("gpgme_signers_add %s. GpgME Error: %s", xmpp_jid_me, gpgme_strerror(error)); + return NULL; + } + + + // lookup key of recipient + recp[1] = _ox_key_lookup(recipient_barejid, FALSE); + if(error != 0) { + log_error("Key not found for %s. GpgME Error: %s", xmpp_jid_recipient, gpgme_strerror(error)); + return NULL; + } + recp[2] = NULL; + log_debug("%s <%s>", recp[0]->uids->name, recp[0]->uids->email); + log_debug("%s <%s>", recp[1]->uids->name, recp[1]->uids->email); + + gpgme_encrypt_flags_t flags = 0; + + gpgme_data_t plain; + gpgme_data_t cipher; + + error = gpgme_data_new (&plain); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + return NULL; + } + + error = gpgme_data_new_from_mem(&plain, message, strlen(message),0); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + return NULL; + } + error = gpgme_data_new (&cipher); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + return NULL; + } + + error = gpgme_op_encrypt_sign ( ctx, recp, flags, plain, cipher); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + return NULL; + } + + size_t len; + char *cipher_str = gpgme_data_release_and_get_mem(cipher, &len); + char* result = g_base64_encode( (unsigned char*) cipher_str,len); + gpgme_key_release (recp[0]); + gpgme_key_release (recp[1]); + gpgme_release (ctx); + return result; + +} + +gboolean ox_is_private_key_available(const char *const barejid) { + g_assert(barejid); + gboolean result = FALSE; + + gpgme_key_t key = _ox_key_lookup(barejid, TRUE); + if(key) { + if (_ox_key_is_usable(key, barejid, TRUE) ) { + result = TRUE; + } + gpgme_key_unref(key); + } + + return result; +} + +gboolean ox_is_public_key_available(const char *const barejid) { + g_assert(barejid); + gboolean result = FALSE; + gpgme_key_t key = _ox_key_lookup(barejid, FALSE); + if(key) { + if (_ox_key_is_usable(key, barejid, FALSE) ) { + result = TRUE; + } + gpgme_key_unref(key); + } + return result; +} + + static char* _remove_header_footer(char *str, const char *const footer) { @@ -837,3 +1053,126 @@ _save_pubkeys(void) g_chmod(pubsloc, S_IRUSR | S_IWUSR); g_free(g_pubkeys_data); } + +static gpgme_key_t _ox_key_lookup(const char *const barejid, gboolean secret_only) { + g_assert(barejid); + log_debug("Looking for %s key: %s", secret_only == TRUE ? "Private" : "Public", barejid); + gpgme_key_t key = NULL; + gpgme_error_t error; + + gpgme_ctx_t ctx; + error = gpgme_new(&ctx); + + if (error) { + log_error("OX - gpgme_new failed: %s %s", gpgme_strsource(error), gpgme_strerror(error)); + return NULL; + } + + error = gpgme_op_keylist_start(ctx, NULL, secret_only); + if (error == GPG_ERR_NO_ERROR) { + error = gpgme_op_keylist_next(ctx, &key); + if ( error != GPG_ERR_EOF && error != GPG_ERR_NO_ERROR) { + log_error("OX: gpgme_op_keylist_next %s %s", gpgme_strsource(error), gpgme_strerror(error)); + return NULL; + } + + GString* xmppuri = g_string_new("xmpp:"); + g_string_append(xmppuri,barejid); + + while (!error) { + // Looking for XMPP URI UID + gpgme_user_id_t uid = key->uids; + + while ( uid ) { + if( uid->name && strlen(uid->name) >= 10 ) { + if( g_strcmp0(uid->name, xmppuri->str) == 0 ) { + gpgme_release(ctx); + return key; + } + } + uid = uid->next; + } + gpgme_key_unref(key); + error = gpgme_op_keylist_next(ctx, &key); + } + } + gpgme_release(ctx); + + return key; +} + +static gboolean _ox_key_is_usable(gpgme_key_t key, const char *const barejid, gboolean secret) { + gboolean result = TRUE; + + if(key->revoked) result = FALSE; + if(key->expired) result = FALSE; + if(key->disabled) result = FALSE; + + return result; +} + +/*! + * @brief XMPP-OX: Decrypt OX Message. + * + * + * + * @param base64 base64_encode OpenPGP message. + * + * @result decrypt XMPP OX Message NULL terminated C-String + */ +char* p_ox_gpg_decrypt(char* base64) { + setlocale (LC_ALL, ""); + gpgme_check_version (NULL); + gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL)); + gpgme_ctx_t ctx; + gpgme_error_t error = gpgme_new (&ctx); + if(GPG_ERR_NO_ERROR != error ) { + printf("gpgme_new: %d\n", error); + return NULL; + } + error = gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + } + gpgme_set_armor(ctx,0); + gpgme_set_textmode(ctx,0); + gpgme_set_offline(ctx,1); + gpgme_set_keylist_mode(ctx, GPGME_KEYLIST_MODE_LOCAL); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + } + + gpgme_data_t plain = NULL; + gpgme_data_t cipher = NULL; + + gsize s; + guchar* encypted = g_base64_decode(base64, &s); + error = gpgme_data_new_from_mem(&cipher, (char*)encypted, s,0); + if(error != 0) { + log_error("GpgME Error gpgme_data_new_from_mem: %s", gpgme_strerror(error)); + return NULL; + } + + error = gpgme_data_new (&plain); + if(error != 0) { + log_error("GpgME Error: %s", gpgme_strerror(error)); + return NULL; + } + + error = gpgme_op_decrypt_verify(ctx, cipher, plain); + if(error != 0) { + log_error("GpgME Error gpgme_op_decrypt: %s", gpgme_strerror(error)); + error = gpgme_op_decrypt(ctx, cipher, plain); + if ( error != 0 ) { + return NULL; + } + } + size_t len; + char *plain_str = gpgme_data_release_and_get_mem(plain, &len); + char* result = malloc(len+1); + strcpy(result, plain_str); + result[len] = '\0'; + return result; +} + + diff --git a/src/pgp/gpg.h b/src/pgp/gpg.h index 0417c8a5..3eae6032 100644 --- a/src/pgp/gpg.h +++ b/src/pgp/gpg.h @@ -72,4 +72,20 @@ char* p_gpg_autocomplete_key(const char *const search_str, gboolean previous, vo void p_gpg_autocomplete_key_reset(void); char* p_gpg_format_fp_str(char *fp); +char* p_ox_gpg_signcrypt(const char* const sender_barejid, const char* const recipient_barejid , const char* const message); + +char* p_ox_gpg_decrypt(char* base64); + +/*! + * \brief List of public keys with xmpp-URI. + * + * @returns GHashTable* with GString* xmpp-uri and ProfPGPKey* value. Empty + * hash, if there is no key. NULL in case of error. + * + */ +GHashTable* ox_gpg_public_keys(void); + +gboolean ox_is_private_key_available(const char *const barejid); +gboolean ox_is_public_key_available(const char *const barejid); + #endif diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c index 5c17b0d4..35ad803f 100644 --- a/src/ui/chatwin.c +++ b/src/ui/chatwin.c @@ -90,6 +90,9 @@ chatwin_new(const char *const barejid) if (prefs_get_boolean(PREF_MAM)) { iq_mam_request(chatwin); } + // XEP-0373: OpenPGP for XMPP + chatwin->is_ox = FALSE; + return chatwin; } @@ -344,6 +347,8 @@ chatwin_outgoing_msg(ProfChatWin *chatwin, const char *const message, char *id, enc_char = prefs_get_pgp_char(); } else if (enc_mode == PROF_MSG_ENC_OMEMO) { enc_char = prefs_get_omemo_char(); + } else if (enc_mode == PROF_MSG_ENC_OX) { + enc_char = prefs_get_ox_char(); } else { enc_char = strdup("-"); } diff --git a/src/ui/mucwin.c b/src/ui/mucwin.c index cb0167d0..66f33a4b 100644 --- a/src/ui/mucwin.c +++ b/src/ui/mucwin.c @@ -503,6 +503,7 @@ mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *c ProfWin *window = (ProfWin*)mucwin; char *mynick = muc_nick(mucwin->roomjid); + // displayed message char char *ch; if (mucwin->message_char) { ch = strdup(mucwin->message_char); @@ -512,6 +513,8 @@ mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *c ch = prefs_get_pgp_char(); } else if (enc_mode == PROF_MSG_ENC_OMEMO) { ch = prefs_get_omemo_char(); + } else if (enc_mode == PROF_MSG_ENC_OX) { + ch = prefs_get_omemo_char(); } else { ch = strdup("-"); } diff --git a/src/ui/titlebar.c b/src/ui/titlebar.c index b557b59a..0a688e10 100644 --- a/src/ui/titlebar.c +++ b/src/ui/titlebar.c @@ -439,6 +439,20 @@ _show_privacy(ProfChatWin *chatwin) return; } + // XEP-0373: OpenPGP for XMPP + if (chatwin->is_ox) { + wprintw(win, " "); + wattron(win, bracket_attrs); + wprintw(win, "["); + wattroff(win, bracket_attrs); + wattron(win, encrypted_attrs); + wprintw(win, "OX"); + wattroff(win, encrypted_attrs); + wattron(win, bracket_attrs); + wprintw(win, "]"); + wattroff(win, bracket_attrs); + } + if (chatwin->is_otr) { wprintw(win, " "); wattron(win, bracket_attrs); diff --git a/src/ui/win_types.h b/src/ui/win_types.h index 5da1765a..cb6834c6 100644 --- a/src/ui/win_types.h +++ b/src/ui/win_types.h @@ -155,6 +155,7 @@ typedef struct prof_chat_win_t { gboolean pgp_send; gboolean pgp_recv; gboolean is_omemo; + gboolean is_ox; // XEP-0373: OpenPGP for XMPP char *resource_override; gboolean history_shown; unsigned long memcheck; diff --git a/src/ui/window.c b/src/ui/window.c index 763f389d..88092329 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1144,6 +1144,8 @@ win_print_incoming(ProfWin *window, const char *const display_name_from, ProfMes enc_char = prefs_get_otr_char(); } else if (message->enc == PROF_MSG_ENC_PGP) { enc_char = prefs_get_pgp_char(); + } else if (message->enc == PROF_MSG_ENC_OX) { // XEP-0373: OpenPGP for XMPP + enc_char = prefs_get_ox_char(); } else if (message->enc == PROF_MSG_ENC_OMEMO) { enc_char = prefs_get_omemo_char(); } else { diff --git a/src/xmpp/message.c b/src/xmpp/message.c index d578786e..ddd2d5e6 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -85,10 +85,13 @@ static void _handle_conference(xmpp_stanza_t *const stanza); static void _handle_captcha(xmpp_stanza_t *const stanza); static void _handle_receipt_received(xmpp_stanza_t *const stanza); static void _handle_chat(xmpp_stanza_t *const stanza, gboolean is_mam); +static void _handle_ox_chat(xmpp_stanza_t *const stanza, ProfMessage *message, gboolean is_mam); static gboolean _handle_mam(xmpp_stanza_t *const stanza); static void _send_message_stanza(xmpp_stanza_t *const stanza); +static xmpp_stanza_t* _openpgp_signcrypt(xmpp_ctx_t* ctx, const char* const to, const char* const text); + static GHashTable *pubsub_event_handlers; static int @@ -353,6 +356,7 @@ message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean } account_free(account); #else + // ? message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id); xmpp_message_set_body(message, msg); #endif @@ -376,6 +380,70 @@ message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean return id; } +// XEP-0373: OpenPGP for XMPP + +char* +message_send_chat_ox(const char *const barejid, const char *const msg, gboolean request_receipt, const char *const replace_id) +{ + xmpp_ctx_t * const ctx = connection_get_ctx(); + + char *state = chat_session_get_state(barejid); + char *jid = chat_session_get_jid(barejid); + char *id = connection_create_stanza_id(); + + xmpp_stanza_t *message = NULL; + Jid *jidp = jid_create(jid); + + char *account_name = session_get_account_name(); + ProfAccount *account = accounts_get_account(account_name); + + message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id); + xmpp_message_set_body(message, "This message is encrypted (XEP-0373: OpenPGP for XMPP)."); + + xmpp_stanza_t *openpgp = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(openpgp, STANZA_NAME_OPENPGP); + xmpp_stanza_set_ns(openpgp, STANZA_NS_OPENPGP_0); + + xmpp_stanza_t * signcrypt = _openpgp_signcrypt(ctx, barejid, msg); + char* c; + size_t s; + xmpp_stanza_to_text(signcrypt, &c,&s); + char* signcrypt_e = p_ox_gpg_signcrypt(account->jid, barejid, c); + if( signcrypt_e == NULL ) { + log_error("Message not signcrypted."); + return NULL; + } + // BASE64_OPENPGP_MESSAGE + xmpp_stanza_t* base64_openpgp_message = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(base64_openpgp_message,signcrypt_e); + xmpp_stanza_add_child(openpgp, base64_openpgp_message); + xmpp_stanza_add_child(message, openpgp); + + xmpp_stanza_to_text(message, &c,&s); + + account_free(account); + jid_destroy(jidp); + free(jid); + + if (state) { + stanza_attach_state(ctx, message, state); + } + + if (request_receipt) { + stanza_attach_receipt_request(ctx, message); + } + + if (replace_id) { + stanza_attach_correction(ctx, message, replace_id); + } + + _send_message_stanza(message); + xmpp_stanza_release(message); + + + return id; +} + char* message_send_chat_otr(const char *const barejid, const char *const msg, gboolean request_receipt, const char *const replace_id) { @@ -1140,6 +1208,12 @@ _handle_carbons(xmpp_stanza_t *const stanza) if (x) { message->encrypted = xmpp_stanza_get_text(x); } + // OX + xmpp_stanza_t *ox = xmpp_stanza_get_child_by_ns(message_stanza, STANZA_NS_OPENPGP_0); + if( ox ) { + message->enc=PROF_MSG_ENC_OX; + _handle_ox_chat(message_stanza,message, FALSE); + } //TODO: maybe also add is_carbon maybe even an enum with outgoing/incoming //could be that then we can have sv_ev_carbon no incoming/outgoing @@ -1260,6 +1334,12 @@ _handle_chat(xmpp_stanza_t *const stanza, gboolean is_mam) message->encrypted = xmpp_stanza_get_text(encrypted); } + xmpp_stanza_t *ox = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_OPENPGP_0); + if( ox ) { + message->enc=PROF_MSG_ENC_OX; + _handle_ox_chat(stanza,message, FALSE); + } + if (message->plain || message->body || message->encrypted) { sv_ev_incoming_message(message); @@ -1290,6 +1370,35 @@ _handle_chat(xmpp_stanza_t *const stanza, gboolean is_mam) message_free(message); } + +/*! + * @brief Handle incoming XMMP-OX chat message. + * + * + */ +static void _handle_ox_chat(xmpp_stanza_t *const stanza, ProfMessage *message, gboolean is_mam) { + xmpp_stanza_t *ox = stanza_get_child_by_name_and_ns(stanza, "openpgp", STANZA_NS_OPENPGP_0); + message->plain = p_ox_gpg_decrypt(xmpp_stanza_get_text(ox)); + + // Implementation for libstrophe 0.10. + /* + xmpp_stanza_t *x = xmpp_stanza_new_from_string(connection_get_ctx(), message->plain); + xmpp_stanza_t *p = xmpp_stanza_get_child_by_name(x, "payload"); + xmpp_stanza_t *b = xmpp_stanza_get_child_by_name(p, "body"); + message->plain = xmpp_stanza_get_text(b); + if(message->plain == NULL ) { + message->plain = xmpp_stanza_get_text(stanza); + } + message->encrypted = xmpp_stanza_get_text(ox); + */ + + if(message->plain == NULL ) { + message->plain = xmpp_stanza_get_text(stanza); + } + message->encrypted = xmpp_stanza_get_text(ox); + +} + static gboolean _handle_mam(xmpp_stanza_t *const stanza) { @@ -1372,3 +1481,64 @@ message_is_sent_by_us(const ProfMessage *const message, bool checkOID) { return ret; } + + +xmpp_stanza_t* _openpgp_signcrypt(xmpp_ctx_t* ctx, const char* const to, const char* const text) { + + time_t now = time(NULL); + struct tm* tm = localtime(&now); + char buf[255]; + strftime(buf, sizeof(buf), "%FT%T%z", tm); + int randnr = rand() % 5; + char rpad_data[randnr]; + for(int i = 0; i < randnr-1; i++) { + rpad_data[i] = 'c'; + } + rpad_data[randnr-1] = '\0'; + + // signcrypt + xmpp_stanza_t *signcrypt = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(signcrypt, "signcrypt"); + xmpp_stanza_set_ns(signcrypt, "urn:xmpp:openpgp:0"); + // to + xmpp_stanza_t *s_to = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(s_to, "to"); + xmpp_stanza_set_attribute(s_to, "jid", to); + // time + xmpp_stanza_t *time = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(time, "time"); + xmpp_stanza_set_attribute(time, "stamp", buf); + xmpp_stanza_set_name(time, "time"); + // rpad + xmpp_stanza_t *rpad = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(rpad, "rpad"); + xmpp_stanza_t *rpad_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(rpad_text, rpad_data); + // payload + xmpp_stanza_t *payload= xmpp_stanza_new(ctx); + xmpp_stanza_set_name(payload, "payload"); + // body + xmpp_stanza_t *body = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(body, "body"); + xmpp_stanza_set_ns(body, "jabber:client"); + // text + xmpp_stanza_t *body_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(body_text, text); + xmpp_stanza_add_child(signcrypt,s_to); + xmpp_stanza_add_child(signcrypt,time); + xmpp_stanza_add_child(signcrypt,rpad); + xmpp_stanza_add_child(rpad,rpad_text); + xmpp_stanza_add_child(signcrypt,payload); + xmpp_stanza_add_child(payload, body); + xmpp_stanza_add_child(body, body_text); + + xmpp_stanza_release(body_text); + xmpp_stanza_release(body); + xmpp_stanza_release(rpad_text); + xmpp_stanza_release(rpad); + xmpp_stanza_release(time); + xmpp_stanza_release(s_to); + + return signcrypt; +} + diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h index b20af1cd..9d1f6d38 100644 --- a/src/xmpp/stanza.h +++ b/src/xmpp/stanza.h @@ -62,6 +62,8 @@ #define STANZA_NAME_PRESENCE "presence" #define STANZA_NAME_PRIORITY "priority" #define STANZA_NAME_X "x" +// XEP-0373: OpenPGP for XMPP +#define STANZA_NAME_OPENPGP "openpgp" #define STANZA_NAME_SHOW "show" #define STANZA_NAME_STATUS "status" #define STANZA_NAME_IQ "iq" @@ -192,6 +194,8 @@ #define STANZA_NS_RECEIPTS "urn:xmpp:receipts" #define STANZA_NS_SIGNED "jabber:x:signed" #define STANZA_NS_ENCRYPTED "jabber:x:encrypted" +// XEP-0373: OpenPGP for XMPP +#define STANZA_NS_OPENPGP_0 "urn:xmpp:openpgp:0" #define STANZA_NS_HTTP_UPLOAD "urn:xmpp:http:upload" #define STANZA_NS_X_OOB "jabber:x:oob" #define STANZA_NS_BLOCKING "urn:xmpp:blocking" diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h index 1444cffe..c097387b 100644 --- a/src/xmpp/xmpp.h +++ b/src/xmpp/xmpp.h @@ -126,7 +126,8 @@ typedef enum { PROF_MSG_ENC_NONE, PROF_MSG_ENC_OTR, PROF_MSG_ENC_PGP, - PROF_MSG_ENC_OMEMO + PROF_MSG_ENC_OMEMO, + PROF_MSG_ENC_OX } prof_enc_t; typedef enum { @@ -192,6 +193,8 @@ const char* connection_get_profanity_identifier(void); char* message_send_chat(const char *const barejid, const char *const msg, const char *const oob_url, gboolean request_receipt, const char *const replace_id); char* message_send_chat_otr(const char *const barejid, const char *const msg, gboolean request_receipt, const char *const replace_id); char* message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean request_receipt, const char *const replace_id); +// XEP-0373: OpenPGP for XMPP +char* message_send_chat_ox(const char *const barejid, const char *const msg, gboolean request_receipt, const char *const replace_id); 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, const char *const replace_id); char* message_send_private(const char *const fulljid, 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, const char *const replace_id);