diff --git a/src/command/command.c b/src/command/command.c index c43e968e..dd160adc 100644 --- a/src/command/command.c +++ b/src/command/command.c @@ -869,18 +869,19 @@ static struct cmd_t command_defs[] = NULL } } }, { "/pgp", - cmd_pgp, parse_args, 1, 2, NULL, + cmd_pgp, parse_args, 1, 3, NULL, { "/pgp command [args..]", "Open PGP commands.", { "/pgp command [args..]", "---------------------", "Open PGP commands.", "", - "keys : List private keys.", - "libver : Show which version of the libgpgme library is being used.", - "fps : Show received fingerprints.", - "start [contact] : Start PGP encrypted chat, current contact will be used if not specified.", - "end : End PGP encrypted chat with the current recipient.", - "log on|off|redact : PGP message logging, default: redact.", + "keys : List all keys.", + "libver : Show which version of the libgpgme library is being used.", + "fps : Show known fingerprints.", + "setkey contact keyid : Manually associate a key ID with a JID.", + "start [contact] : Start PGP encrypted chat, current contact will be used if not specified.", + "end : End PGP encrypted chat with the current recipient.", + "log on|off|redact : PGP message logging, default: redact.", NULL } } }, { "/otr", @@ -1611,6 +1612,7 @@ cmd_init(void) pgp_ac = autocomplete_new(); autocomplete_add(pgp_ac, "keys"); autocomplete_add(pgp_ac, "fps"); + autocomplete_add(pgp_ac, "setkey"); autocomplete_add(pgp_ac, "libver"); autocomplete_add(pgp_ac, "start"); autocomplete_add(pgp_ac, "end"); @@ -2503,6 +2505,11 @@ _pgp_autocomplete(ProfWin *window, const char * const input) return found; } + found = autocomplete_param_with_func(input, "/pgp setkey", roster_barejid_autocomplete); + if (found) { + return found; + } + found = autocomplete_param_with_ac(input, "/pgp", pgp_ac, TRUE); if (found) { return found; diff --git a/src/command/commands.c b/src/command/commands.c index f865b873..65fdeb98 100644 --- a/src/command/commands.c +++ b/src/command/commands.c @@ -680,6 +680,9 @@ cmd_disconnect(ProfWin *window, gchar **args, struct cmd_help_t help) muc_invites_clear(); chat_sessions_clear(); ui_disconnected(); +#ifdef HAVE_LIBGPGME + p_gpg_on_disconnect(); +#endif free(jid); } else { cons_show("You are not currently connected."); @@ -4192,6 +4195,35 @@ cmd_pgp(ProfWin *window, gchar **args, struct cmd_help_t help) return TRUE; } + if (g_strcmp0(args[0], "setkey") == 0) { + jabber_conn_status_t conn_status = jabber_get_connection_status(); + if (conn_status != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + char *jid = args[1]; + if (!args[1]) { + cons_show("Usage: %s", help.usage); + return TRUE; + } + + char *keyid = args[2]; + if (!args[2]) { + cons_show("Usage: %s", help.usage); + return TRUE; + } + + gboolean res = p_gpg_addkey(jid, keyid); + if (!res) { + cons_show("Key ID not found."); + } else { + cons_show("Key %s set for %s.", keyid, jid); + } + + return TRUE; + } + if (g_strcmp0(args[0], "fps") == 0) { jabber_conn_status_t conn_status = jabber_get_connection_status(); if (conn_status != JABBER_CONNECTED) { @@ -4201,11 +4233,11 @@ cmd_pgp(ProfWin *window, gchar **args, struct cmd_help_t help) GHashTable *fingerprints = p_gpg_fingerprints(); GList *jids = g_hash_table_get_keys(fingerprints); if (!jids) { - cons_show("No PGP fingerprints received."); + cons_show("No PGP fingerprints available."); return TRUE; } - cons_show("Received PGP fingerprints:"); + cons_show("Known PGP fingerprints:"); GList *curr = jids; while (curr) { char *jid = curr->data; @@ -4314,6 +4346,7 @@ cmd_pgp(ProfWin *window, gchar **args, struct cmd_help_t help) return TRUE; } + cons_show("Usage: %s", help.usage); return TRUE; #else cons_show("This version of Profanity has not been built with PGP support enabled"); diff --git a/src/event/server_events.c b/src/event/server_events.c index 004e743b..ff0263cb 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -63,6 +63,10 @@ sv_ev_login_account_success(char *account_name) otr_on_connect(account); #endif +#ifdef HAVE_LIBGPGME + p_gpg_on_connect(account->jid); +#endif + ui_handle_login_account_success(account); // attempt to rejoin rooms with passwords @@ -97,6 +101,9 @@ sv_ev_lost_connection(void) muc_invites_clear(); chat_sessions_clear(); ui_disconnected(); +#ifdef HAVE_LIBGPGME + p_gpg_on_disconnect(); +#endif } void diff --git a/src/pgp/gpg.c b/src/pgp/gpg.c index ce40690f..e02a3595 100644 --- a/src/pgp/gpg.c +++ b/src/pgp/gpg.c @@ -35,12 +35,16 @@ #include #include #include +#include +#include #include +#include #include #include "pgp/gpg.h" #include "log.h" +#include "common.h" #define PGP_SIGNATURE_HEADER "-----BEGIN PGP SIGNATURE-----" #define PGP_SIGNATURE_FOOTER "-----END PGP SIGNATURE-----" @@ -50,8 +54,12 @@ static const char *libversion; static GHashTable *fingerprints; +static gchar *fpsloc; +static GKeyFile *fpskeyfile; + 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_fps(void); void p_gpg_init(void) @@ -67,6 +75,135 @@ void p_gpg_close(void) { g_hash_table_destroy(fingerprints); + fingerprints = NULL; + + g_key_file_free(fpskeyfile); + fpskeyfile = NULL; + + free(fpsloc); + fpsloc = NULL; +} + +void +p_gpg_on_connect(const char * const barejid) +{ + gchar *data_home = xdg_get_data_home(); + GString *fpsfile = g_string_new(data_home); + free(data_home); + + gchar *account_dir = str_replace(barejid, "@", "_at_"); + g_string_append(fpsfile, "/profanity/pgp/"); + g_string_append(fpsfile, account_dir); + free(account_dir); + + // mkdir if doesn't exist for account + errno = 0; + int res = g_mkdir_with_parents(fpsfile->str, S_IRWXU); + if (res == -1) { + char *errmsg = strerror(errno); + if (errmsg) { + log_error("Error creating directory: %s, %s", fpsfile->str, errmsg); + } else { + log_error("Error creating directory: %s", fpsfile->str); + } + } + + // create or read fingerprints keyfile + g_string_append(fpsfile, "/fingerprints"); + fpsloc = fpsfile->str; + g_string_free(fpsfile, FALSE); + + if (g_file_test(fpsloc, G_FILE_TEST_EXISTS)) { + g_chmod(fpsloc, S_IRUSR | S_IWUSR); + } + + fpskeyfile = g_key_file_new(); + g_key_file_load_from_file(fpskeyfile, fpsloc, G_KEY_FILE_KEEP_COMMENTS, NULL); + + // load each keyid + gsize len = 0; + gchar **jids = g_key_file_get_groups(fpskeyfile, &len); + + gpgme_ctx_t ctx; + gpgme_error_t error = gpgme_new(&ctx); + if (error) { + log_error("GPG: Failed to create gpgme context. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + g_strfreev(jids); + return; + } + + int i = 0; + for (i = 0; i < len; i++) { + GError *gerr = NULL; + gchar *jid = jids[i]; + gchar *keyid = g_key_file_get_string(fpskeyfile, jid, "keyid", &gerr); + if (gerr) { + log_error("Error loading PGP key id for %s", jid); + g_error_free(gerr); + } else { + gpgme_key_t key = NULL; + error = gpgme_get_key(ctx, keyid, &key, 1); + if (error || key == NULL) { + log_error("GPG: Failed to get key. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + continue; + } + + g_hash_table_replace(fingerprints, strdup(jid), strdup(key->subkeys->fpr)); + gpgme_key_release(key); + } + } + + gpgme_release(ctx); + g_strfreev(jids); + + _save_fps(); +} + +void +p_gpg_on_disconnect(void) +{ + if (fingerprints) { + g_hash_table_destroy(fingerprints); + fingerprints = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + } + + if (fpskeyfile) { + g_key_file_free(fpskeyfile); + fpskeyfile = NULL; + } + + if (fpsloc) { + free(fpsloc); + fpsloc = NULL; + } +} + +gboolean +p_gpg_addkey(const char * const jid, const char * const keyid) +{ + gpgme_ctx_t ctx; + gpgme_error_t error = gpgme_new(&ctx); + if (error) { + log_error("GPG: Failed to create gpgme context. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + return FALSE; + } + + gpgme_key_t key = NULL; + error = gpgme_get_key(ctx, keyid, &key, 1); + if (error || key == NULL) { + log_error("GPG: Failed to get key. %s %s", gpgme_strsource(error), gpgme_strerror(error)); + return FALSE; + } + + // save to ID keyfile + g_key_file_set_string(fpskeyfile, jid, "keyid", keyid); + _save_fps(); + + // update in memory fingerprint list + g_hash_table_replace(fingerprints, strdup(jid), strdup(key->subkeys->fpr)); + gpgme_key_release(key); + + return TRUE; } GSList * @@ -378,3 +515,13 @@ _add_header_footer(const char * const str, const char * const header, const char return result; } + +static void +_save_fps(void) +{ + gsize g_data_size; + gchar *g_fps_data = g_key_file_to_data(fpskeyfile, &g_data_size, NULL); + g_file_set_contents(fpsloc, g_fps_data, g_data_size, NULL); + g_chmod(fpsloc, S_IRUSR | S_IWUSR); + g_free(g_fps_data); +} diff --git a/src/pgp/gpg.h b/src/pgp/gpg.h index 74d86568..07c99465 100644 --- a/src/pgp/gpg.h +++ b/src/pgp/gpg.h @@ -43,7 +43,10 @@ typedef struct pgp_key_t { void p_gpg_init(void); void p_gpg_close(void); +void p_gpg_on_connect(const char * const barejid); +void p_gpg_on_disconnect(void); GSList* p_gpg_list_keys(void); +gboolean p_gpg_addkey(const char * const jid, const char * const keyid); GHashTable* p_gpg_fingerprints(void); gboolean p_gpg_available(const char * const barejid); const char* p_gpg_libver(void); diff --git a/tests/unittests/pgp/stub_gpg.c b/tests/unittests/pgp/stub_gpg.c index 03b58b55..671b2092 100644 --- a/tests/unittests/pgp/stub_gpg.c +++ b/tests/unittests/pgp/stub_gpg.c @@ -38,3 +38,12 @@ char * p_gpg_decrypt(const char * const barejid, const char * const cipher) { return NULL; } + +void p_gpg_on_connect(const char * const barejid) {} +void p_gpg_on_disconnect(void) {} + +gboolean p_gpg_addkey(const char * const jid, const char * const keyid) +{ + return TRUE; +} +