From 36784738fcc19122ed6df04ff7f53e4266c77707 Mon Sep 17 00:00:00 2001 From: John Hernandez <129467592+H3rnand3zzz@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:33:17 +0200 Subject: [PATCH] Add optional pgp public key autoimport Refactor `p_gpg_list_keys` Add `/pgp autoimport` command, it's not described in XEP-0027, but used in some clients, such as PSI, Pidgin. It will autoimport keys received with `/pgp sendpub`, in plain text as a message, or using features, provided in other clients. It doesn't autoassign them, but shows command to assign, letting user to decide. Improve documentation for some preexisting functions Add contact argument to `/pgp sendpub` --- src/command/cmd_ac.c | 18 +++ src/command/cmd_defs.c | 5 +- src/command/cmd_funcs.c | 44 ++++++- src/config/preferences.c | 4 + src/config/preferences.h | 1 + src/pgp/gpg.c | 204 ++++++++++++++++++++++++++------- src/pgp/gpg.h | 2 + src/ui/chatwin.c | 27 ++++- tests/unittests/pgp/stub_gpg.c | 14 ++- tests/unittests/test_cmd_pgp.c | 2 +- 10 files changed, 269 insertions(+), 52 deletions(-) diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index e2affcf6..c14648d9 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -238,6 +238,7 @@ static Autocomplete reconnect_ac; static Autocomplete pgp_ac; static Autocomplete pgp_log_ac; static Autocomplete pgp_sendfile_ac; +static Autocomplete pgp_autoimport_ac; static Autocomplete ox_ac; static Autocomplete ox_log_ac; #endif @@ -911,6 +912,7 @@ cmd_ac_init(void) autocomplete_add(pgp_ac, "char"); autocomplete_add(pgp_ac, "sendfile"); autocomplete_add(pgp_ac, "sendpub"); + autocomplete_add(pgp_ac, "autoimport"); pgp_log_ac = autocomplete_new(); autocomplete_add(pgp_log_ac, "on"); @@ -921,6 +923,10 @@ cmd_ac_init(void) autocomplete_add(pgp_sendfile_ac, "on"); autocomplete_add(pgp_sendfile_ac, "off"); + pgp_autoimport_ac = autocomplete_new(); + autocomplete_add(pgp_autoimport_ac, "on"); + autocomplete_add(pgp_autoimport_ac, "off"); + ox_ac = autocomplete_new(); autocomplete_add(ox_ac, "keys"); autocomplete_add(ox_ac, "contacts"); @@ -1675,6 +1681,7 @@ cmd_ac_reset(ProfWin* window) autocomplete_reset(pgp_ac); autocomplete_reset(pgp_log_ac); autocomplete_reset(pgp_sendfile_ac); + autocomplete_reset(pgp_autoimport_ac); autocomplete_reset(ox_ac); autocomplete_reset(ox_log_ac); #endif @@ -1857,6 +1864,7 @@ cmd_ac_uninit(void) autocomplete_free(pgp_ac); autocomplete_free(pgp_log_ac); autocomplete_free(pgp_sendfile_ac); + autocomplete_free(pgp_autoimport_ac); autocomplete_free(ox_ac); autocomplete_free(ox_log_ac); #endif @@ -2737,6 +2745,11 @@ _pgp_autocomplete(ProfWin* window, const char* const input, gboolean previous) if (found) { return found; } + + found = autocomplete_param_with_func(input, "/pgp sendpub", roster_contact_autocomplete, previous, NULL); + if (found) { + return found; + } } found = autocomplete_param_with_ac(input, "/pgp log", pgp_log_ac, TRUE, previous); @@ -2749,6 +2762,11 @@ _pgp_autocomplete(ProfWin* window, const char* const input, gboolean previous) return found; } + found = autocomplete_param_with_ac(input, "/pgp autoimport", pgp_autoimport_ac, TRUE, previous); + if (found) { + return found; + } + gboolean result; auto_gcharv gchar** args = parse_args(input, 2, 3, &result); if ((strncmp(input, "/pgp", 4) == 0) && (result == TRUE)) { diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index d3f21260..34776875 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1706,7 +1706,7 @@ static const struct cmd_t command_defs[] = { "/pgp log on|off|redact", "/pgp char ", "/pgp sendfile on|off", - "/pgp sendpub") + "/pgp sendpub []") CMD_DESC( "Open PGP commands to manage keys, and perform PGP encryption during chat sessions. " "See the /account command to set your own PGP key.") @@ -1721,7 +1721,8 @@ static const struct cmd_t command_defs[] = { { "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." }, { "sendfile on|off", "Allow /sendfile to send unencrypted files while otherwise using PGP." }, - { "sendpub", "Used in chat. Sends a message to the current recipient with your PGP public key." }) + { "autoimport on|off", "Autoimport PGP keys from messages." }, + { "sendpub []", "Sends a message to the current recipient with your PGP public key, current contact will be used if not specified." }) CMD_EXAMPLES( "/pgp log off", "/pgp setkey odin@valhalla.edda BA19CACE5A9592C5", diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index a4662aa8..da475030 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -7361,7 +7361,9 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args) } cons_bad_cmd_usage(command); return TRUE; - } else if (g_strcmp0(args[0], "log") == 0) { + } + + if (g_strcmp0(args[0], "log") == 0) { char* choice = args[1]; if (g_strcmp0(choice, "on") == 0) { prefs_set_string(PREF_PGP_LOG, "on"); @@ -7384,6 +7386,11 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args) return TRUE; } + if (g_strcmp0(args[0], "autoimport") == 0) { + _cmd_set_boolean_preference(args[1], "PGP keys autoimport from messages", PREF_PGP_PUBKEY_AUTOIMPORT); + return TRUE; + } + if (g_strcmp0(args[0], "keys") == 0) { GHashTable* keys = p_gpg_list_keys(); if (!keys || g_hash_table_size(keys) == 0) { @@ -7494,7 +7501,7 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args) } if (window->type != WIN_CHAT && args[1] == NULL) { - cons_show("You must be in a regular chat window to start PGP encryption."); + cons_show("You must set recipient in an argument or be in a regular chat window to start PGP encryption."); return TRUE; } @@ -7587,11 +7594,17 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args) } if (g_strcmp0(args[0], "sendpub") == 0) { - if (window->type != WIN_CHAT) { - cons_show_error("Please, use this command only in chat windows."); + jabber_conn_status_t conn_status = connection_get_status(); + if (conn_status != JABBER_CONNECTED) { + cons_show("You must be connected to share your PGP public key."); return TRUE; } - ProfChatWin* chatwin = (ProfChatWin*)window; + + if (window->type != WIN_CHAT && args[1] == NULL) { + cons_show("You must set recipient in an argument or use this command in a regular chat window to share your PGP key."); + return TRUE; + } + ProfAccount* account = accounts_get_account(session_get_account_name()); if (account->pgp_keyid == NULL) { @@ -7605,7 +7618,28 @@ cmd_pgp(ProfWin* window, const char* const command, gchar** args) account_free(account); 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); + } + cl_ev_send_msg(chatwin, pubkey, NULL); + win_update_entry_message((ProfWin*)chatwin, chatwin->last_msg_id, "[you shared your PGP key]"); cons_show("PGP key has been shared with %s.", chatwin->barejid); account_free(account); return TRUE; diff --git a/src/config/preferences.c b/src/config/preferences.c index c10f4d83..5dffe22a 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -1863,6 +1863,7 @@ _get_group(preference_t pref) return PREF_GROUP_OTR; case PREF_PGP_LOG: case PREF_PGP_SENDFILE: + case PREF_PGP_PUBKEY_AUTOIMPORT: return PREF_GROUP_PGP; case PREF_BOOKMARK_INVITE: case PREF_ROOM_LIST_CACHE: @@ -2084,6 +2085,8 @@ _get_key(preference_t pref) return "log"; case PREF_PGP_SENDFILE: return "sendfile"; + case PREF_PGP_PUBKEY_AUTOIMPORT: + return "pgp.pubkey.autoimport"; case PREF_TLS_CERTPATH: return "tls.certpath"; case PREF_TLS_SHOW: @@ -2215,6 +2218,7 @@ _get_default_boolean(preference_t pref) case PREF_STROPHE_SM_ENABLED: case PREF_STROPHE_SM_RESEND: return TRUE; + case PREF_PGP_PUBKEY_AUTOIMPORT: default: return FALSE; } diff --git a/src/config/preferences.h b/src/config/preferences.h index ecb28485..ceac115a 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -147,6 +147,7 @@ typedef enum { PREF_TITLEBAR_MUC_TITLE_NAME, PREF_PGP_LOG, PREF_PGP_SENDFILE, + PREF_PGP_PUBKEY_AUTOIMPORT, PREF_TLS_CERTPATH, PREF_TLS_SHOW, PREF_LASTACTIVITY, diff --git a/src/pgp/gpg.c b/src/pgp/gpg.c index bd25a58f..09bcdadc 100644 --- a/src/pgp/gpg.c +++ b/src/pgp/gpg.c @@ -53,10 +53,12 @@ #include "tools/autocomplete.h" #include "ui/ui.h" -#define PGP_SIGNATURE_HEADER "-----BEGIN PGP SIGNATURE-----" -#define PGP_SIGNATURE_FOOTER "-----END PGP SIGNATURE-----" -#define PGP_MESSAGE_HEADER "-----BEGIN PGP MESSAGE-----" -#define PGP_MESSAGE_FOOTER "-----END PGP MESSAGE-----" +#define PGP_SIGNATURE_HEADER "-----BEGIN PGP SIGNATURE-----" +#define PGP_SIGNATURE_FOOTER "-----END PGP SIGNATURE-----" +#define PGP_MESSAGE_HEADER "-----BEGIN PGP MESSAGE-----" +#define PGP_MESSAGE_FOOTER "-----END PGP MESSAGE-----" +#define PGP_PUBLIC_KEY_HEADER "-----BEGIN PGP PUBLIC KEY BLOCK-----" +#define PGP_PUBLIC_KEY_FOOTER "-----END PGP PUBLIC KEY BLOCK-----" static const char* libversion = NULL; static GHashTable* pubkeys; @@ -73,6 +75,7 @@ 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 char* _gpgme_data_to_char(gpgme_data_t data); static void _save_pubkeys(void); +static ProfPGPKey* _gpgme_key_to_ProfPGPKey(gpgme_key_t key); void _p_gpg_free_pubkeyid(ProfPGPPubKeyId* pubkeyid) @@ -304,6 +307,21 @@ p_gpg_free_key(ProfPGPKey* key) } } +/** + * Retrieve a list of GPG keys and create a hash table of ProfPGPKey objects. + * + * This function utilizes the GPGME library to retrieve both public and secret keys. + * It iterates over the keys and their subkeys to populate a hash table with ProfPGPKey objects. + * The key name is used as the key in the hash table, and the ProfPGPKey object is the corresponding value. + * + * @return A newly created GHashTable* containing ProfPGPKey objects, with the key name as the key. + * Returns NULL if an error occurs during key retrieval or if memory allocation fails. + * + * @note The returned hash table should be released using p_gpg_free_keys() + * when they are no longer needed to avoid memory leaks. + * + * @note This function may perform additional operations, such as autocomplete, related to the retrieved keys. + */ GHashTable* p_gpg_list_keys(void) { @@ -312,48 +330,23 @@ p_gpg_list_keys(void) gpgme_ctx_t ctx; error = gpgme_new(&ctx); - if (error) { - log_error("GPG: Could not list keys. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + log_error("GPG: Could not create GPGME context. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + g_hash_table_destroy(result); return NULL; } error = gpgme_op_keylist_start(ctx, NULL, 0); if (error == GPG_ERR_NO_ERROR) { gpgme_key_t key; + error = gpgme_op_keylist_next(ctx, &key); while (!error) { - gpgme_subkey_t sub = key->subkeys; - - ProfPGPKey* p_pgpkey = p_gpg_key_new(); - p_pgpkey->id = strdup(sub->keyid); - p_pgpkey->name = strdup(key->uids->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; + ProfPGPKey* p_pgpkey = _gpgme_key_to_ProfPGPKey(key); + if (p_pgpkey != NULL) { + g_hash_table_insert(result, strdup(p_pgpkey->name), p_pgpkey); } - g_hash_table_insert(result, strdup(p_pgpkey->name), p_pgpkey); - gpgme_key_unref(key); error = gpgme_op_keylist_next(ctx, &key); } @@ -382,6 +375,7 @@ p_gpg_list_keys(void) gpgme_release(ctx); + // TODO: move autocomplete in other place autocomplete_clear(key_ac); GList* ids = g_hash_table_get_keys(result); GList* curr = ids; @@ -761,10 +755,13 @@ p_gpg_format_fp_str(char* fp) } /** - * \brief Function to extract specific public key from PGP - * \param keyid Key ID that will be used to search key in the current PGP context, if NULL returns null - * \returns null-terminated char* string with the the public key in armored format, that must be free'd to avoid memory leaks - * or NULL on error + * Returns the public key data for the given key ID. + * + * @param keyid The key ID for which to retrieve the public key data. + * If the key ID is empty or NULL, returns NULL. + * @return The public key data as a null-terminated char* string allocated using malloc. + * The returned string should be freed by the caller. + * Returns NULL on error, and errors are written to the error log. */ char* p_gpg_get_pubkey(const char* keyid) @@ -802,10 +799,135 @@ cleanup: return _gpgme_data_to_char(data); } +/** + * Validate that the provided buffer has the format of an armored public key. + * + * This function only briefly checks for presence of armored header and footer. + * + * @param buffer The buffer containing the key data. + * @return TRUE if the buffer has the expected header and footer of an armored public key, FALSE otherwise. + */ +gboolean +p_gpg_is_public_key_format(const char* buffer) +{ + if (buffer == NULL || buffer[0] == '\0') { + return false; + } + + const char* headerPos = strstr(buffer, PGP_PUBLIC_KEY_HEADER); + if (headerPos == NULL) { + return false; + } + + const char* footerPos = strstr(buffer, PGP_PUBLIC_KEY_FOOTER); + + return (footerPos != NULL && footerPos > headerPos); +} + +/** + * Imports a PGP public key(s) from a buffer. + * + * @param buffer The buffer containing the PGP key data. + * @return A pointer to the first imported ProfPGPKey structure, or NULL if an error occurs. + * + * @note The caller is responsible for freeing the memory of the returned ProfPGPKey structure + * by calling p_gpg_free_key() to avoid resource leaks. + */ +ProfPGPKey* +p_gpg_import_pubkey(const char* buffer) +{ + gpgme_ctx_t ctx; + ProfPGPKey* result = NULL; + gpgme_error_t error = gpgme_new(&ctx); + if (error != GPG_ERR_NO_ERROR) { + log_error("GPG: Error creating GPGME context"); + goto out; + } + + gpgme_data_t key_data; + error = gpgme_data_new_from_mem(&key_data, buffer, strlen(buffer), 1); + if (error != GPG_ERR_NO_ERROR) { + log_error("GPG: Error creating GPGME data from buffer"); + goto out; + } + + error = gpgme_op_import(ctx, key_data); + + gpgme_data_release(key_data); + + if (error != GPG_ERR_NO_ERROR) { + log_error("GPG: Error importing key data"); + goto out; + } + + gpgme_import_result_t import_result = gpgme_op_import_result(ctx); + gpgme_import_status_t status = import_result->imports; + gboolean is_valid = (status && status->result == GPG_ERR_NO_ERROR); + + if (!is_valid) { + log_error("GPG: Error importing PGP key (%s).", status ? gpgme_strerror(status->result) : "Invalid import status."); + goto out; + } + + gpgme_key_t key = NULL; + error = gpgme_get_key(ctx, status->fpr, &key, 0); + if (error != GPG_ERR_NO_ERROR) { + log_error("GPG: Unable to find imported PGP key (%s).", gpgme_strerror(error)); + goto out; + } + + result = _gpgme_key_to_ProfPGPKey(key); + gpgme_key_release(key); + +out: + gpgme_release(ctx); + return result; +} + +/** + * Converts a GPGME key struct to a ProfPGPKey struct. + * + * @param key The GPGME key struct to convert. + * @return A newly allocated ProfPGPKey struct populated with the converted data, + * or NULL if an error occurs. + * + * @note The caller is responsible for freeing the memory of the returned ProfPGPKey structure + * by calling p_gpg_free_key() to avoid resource leaks. + */ +static ProfPGPKey* +_gpgme_key_to_ProfPGPKey(gpgme_key_t key) +{ + if (key == NULL) { + return NULL; + } + + ProfPGPKey* p_pgpkey = p_gpg_key_new(); + gpgme_subkey_t sub = key->subkeys; + p_pgpkey->id = strdup(sub->keyid); + p_pgpkey->name = strdup(key->uids->uid); + p_pgpkey->fp = strdup(sub->fpr); + + 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; + } + return p_pgpkey; +} + /** * Convert a gpgme_data_t object to a null-terminated char* string. - * The returned string is allocated using malloc and should be freed by the caller. - * If an error occurs or the data is empty, NULL is returned and errors written to the error log. + * + * @param data The gpgme_data_t object to convert. + * @return The converted string allocated using malloc, which should be freed by the caller. + * If an error occurs or the data is empty, NULL is returned and errors are written to the error log. */ static char* _gpgme_data_to_char(gpgme_data_t data) diff --git a/src/pgp/gpg.h b/src/pgp/gpg.h index 23d32fa7..883aaf73 100644 --- a/src/pgp/gpg.h +++ b/src/pgp/gpg.h @@ -74,6 +74,8 @@ 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_gpg_get_pubkey(const char* const keyid); +gboolean p_gpg_is_public_key_format(const char* buffer); +ProfPGPKey* p_gpg_import_pubkey(const char* buffer); ProfPGPKey* p_gpg_key_new(void); void p_gpg_free_key(ProfPGPKey* key); diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c index 0c540998..6ae2c681 100644 --- a/src/ui/chatwin.c +++ b/src/ui/chatwin.c @@ -53,6 +53,9 @@ #ifdef HAVE_LIBOTR #include "otr/otr.h" #endif +#ifdef HAVE_LIBGPGME +#include "pgp/gpg.h" +#endif #ifdef HAVE_OMEMO #include "omemo/omemo.h" #endif @@ -320,6 +323,7 @@ chatwin_incoming_msg(ProfChatWin* chatwin, ProfMessage* message, gboolean win_cr char* old_plain = message->plain; message->plain = plugins_pre_chat_message_display(message->from_jid->barejid, message->from_jid->resourcepart, message->plain); + gboolean show_message = true; ProfWin* window = (ProfWin*)chatwin; int num = wins_get_num(window); @@ -333,12 +337,29 @@ chatwin_incoming_msg(ProfChatWin* chatwin, ProfMessage* message, gboolean win_cr } free(mybarejid); +#ifdef HAVE_LIBGPGME + if (prefs_get_boolean(PREF_PGP_PUBKEY_AUTOIMPORT)) { + if (p_gpg_is_public_key_format(message->plain)) { + ProfPGPKey* key = p_gpg_import_pubkey(message->plain); + if (key != NULL) { + show_message = false; + win_println(window, THEME_DEFAULT, "-", "Received and imported PGP key %s: \"%s\". To assign it to the correspondent using /pgp setkey %s %s", key->fp, key->name, display_name, key->id); + p_gpg_free_key(key); + } else { + win_println(window, THEME_DEFAULT, "-", "Received PGP key, but couldn't import PGP key above."); + } + } + } +#endif + gboolean is_current = wins_is_current(window); gboolean notify = prefs_do_chat_notify(is_current) && !message->is_mam; // currently viewing chat window with sender if (wins_is_current(window)) { - win_print_incoming(window, display_name, message); + if (show_message) { + win_print_incoming(window, display_name, message); + } title_bar_set_typing(FALSE); status_bar_active(num, WIN_CHAT, chatwin->barejid); @@ -377,7 +398,9 @@ chatwin_incoming_msg(ProfChatWin* chatwin, ProfMessage* message, gboolean win_cr } win_insert_last_read_position_marker((ProfWin*)chatwin, chatwin->barejid); - win_print_incoming(window, display_name, message); + if (show_message) { + win_print_incoming(window, display_name, message); + } } if (!message->is_mam) { diff --git a/tests/unittests/pgp/stub_gpg.c b/tests/unittests/pgp/stub_gpg.c index e1efae72..6333e723 100644 --- a/tests/unittests/pgp/stub_gpg.c +++ b/tests/unittests/pgp/stub_gpg.c @@ -103,4 +103,16 @@ char* p_gpg_get_pubkey(const char* const keyid) { return NULL; -} \ No newline at end of file +} + +gboolean +p_gpg_is_public_key_format(const char* buffer) +{ + return TRUE; +} + +ProfPGPKey* +p_gpg_import_pubkey(const char* buffer) +{ + return NULL; +} diff --git a/tests/unittests/test_cmd_pgp.c b/tests/unittests/test_cmd_pgp.c index f9d66e2b..35df812f 100644 --- a/tests/unittests/test_cmd_pgp.c +++ b/tests/unittests/test_cmd_pgp.c @@ -69,7 +69,7 @@ cmd_pgp_start_shows_message_when_no_arg_in_wintype(win_type_t wintype) will_return(connection_get_status, JABBER_CONNECTED); - expect_cons_show("You must be in a regular chat window to start PGP encryption."); + expect_cons_show("You must set recipient in an argument or be in a regular chat window to start PGP encryption."); gboolean result = cmd_pgp(&window, CMD_PGP, args); assert_true(result);