[balsa/autocrypt: 3/11] Autocrypt support for Balsa
- From: Peter Bloomfield <peterb src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [balsa/autocrypt: 3/11] Autocrypt support for Balsa
- Date: Tue, 15 Jan 2019 01:16:07 +0000 (UTC)
commit 6eaf3870e7830c7a2279e2d3935e358376c40639
Author: Albrecht Dreß <albrecht dress arcor de>
Date: Thu Dec 27 22:17:06 2018 -0500
Autocrypt support for Balsa
- configure.ac, meson.build, meson_options.txt,
libbalsa/meson.build: add new option (Note: meson is not thoroughly
tested…)
- libbalsa/Makefile.am: add Autocrypt implementation sources
- libbalsa/autocrypt.[ch]: implementation of the Autocrypt feature
- libbalsa/identity.[ch]: add identity Autocrypt configuration
- libbalsa/libbalsa-gpgme-keys.[ch]: add function for exporting a
minimal public key for Autocrypt, add function for importing a binary
gpg key
- libbalsa/libbalsa-gpgme.[ch]: check and provide capabilities of the
underlying gpg engine, required for exporting the minimal public key
- libbalsa/send.c: include Autocrypt header if requested
- src/balsa-message.c: check for Autocrypt header in a new message
- src/balsa-mime-widget-crypto.c: offer importing a missing public key
from the Autocrypt database, fix memory leak in
create_import_keys_widget()
- src/main-window.c, ui/main-window.ui: add Autocrypt database viewer to
menus
- src/main.c: initialise Autocrypt database on startup
- src/sendmsg-window.c: implement Autocrypt recommendation on startup,
refactoring
Signed-off-by: Peter Bloomfield <PeterBloomfield bellsouth net>
configure.ac | 20 +
libbalsa/Makefile.am | 4 +
libbalsa/autocrypt.c | 912 +++++++++++++++++++++++++++++++++++++++++
libbalsa/autocrypt.h | 148 +++++++
libbalsa/identity.c | 78 ++++
libbalsa/identity.h | 4 +
libbalsa/libbalsa-gpgme-keys.c | 126 +++++-
libbalsa/libbalsa-gpgme-keys.h | 30 ++
libbalsa/libbalsa-gpgme.c | 56 +++
libbalsa/libbalsa-gpgme.h | 10 +
libbalsa/meson.build | 2 +
libbalsa/send.c | 14 +
meson.build | 15 +
meson_options.txt | 5 +
src/balsa-message.c | 34 +-
src/balsa-mime-widget-crypto.c | 41 +-
src/main-window.c | 16 +
src/main.c | 11 +
src/sendmsg-window.c | 288 ++++++++++---
ui/main-window.ui | 10 +
20 files changed, 1742 insertions(+), 82 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 2db4559f2..00b3dc4a8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -65,6 +65,10 @@ AC_ARG_WITH([gpgme],
[build with gpgme/GnuPG support (default=no, path to gpgme-config optional)]),
[ gpgmecfg=$withval ], [ gpgmecfg=no ])
+AC_ARG_ENABLE([autocrypt],
+ AC_HELP_STRING([--enable-autocrypt],
+ [build with Autocrypt support (see https://autocrypt.org/), default=no, requires gpgme and
sqlite3)]),
+ [autocrypt=$enableval], [autocrypt=no])
AC_ARG_WITH(canberra,
AC_HELP_STRING([--with-canberra],
@@ -360,6 +364,21 @@ if test x"$gpgmecfg" != xno ; then
fi
AM_CONDITIONAL([BUILD_WITH_GPGME], [test $gpgmecfg = "yes"])
+# Autocrypt support
+AC_MSG_CHECKING(whether to build with Autocrypt support)
+if test x"$gpgmecfg" != xno ; then
+ if test x"$autocrypt" != xno ; then
+ AC_MSG_RESULT([yes])
+ PKG_CHECK_MODULES(AUTOCRYPT, [sqlite3])
+ AC_DEFINE(ENABLE_AUTOCRYPT,1,[If defined, enable Autocrypt support])
+ BALSA_CFLAGS="$BALSA_CFLAGS $AUTOCRYPT_CFLAGS"
+ BALSA_LIBS="$BALSA_LIBS $AUTOCRYPT_LIBS"
+ else
+ AC_MSG_RESULT([no])
+ fi
+else
+ AC_MSG_RESULT([skipped, gpgme is disabled])
+fi
# OpenLDAP configuration.
#
@@ -716,6 +735,7 @@ echo " HTML widget: $use_html_widget"
echo " Use GNOME: $with_gnome"
echo " Use Canberra: $with_canberra"
echo " Use GPGME: $gpgmecfg"
+echo " Use Autocrypt: $autocrypt"
echo " Use LDAP: $with_ldap"
echo " Use GSS: $with_gss"
echo " Use SQLite: $with_sqlite"
diff --git a/libbalsa/Makefile.am b/libbalsa/Makefile.am
index d3420fedf..edf589a22 100644
--- a/libbalsa/Makefile.am
+++ b/libbalsa/Makefile.am
@@ -5,6 +5,8 @@ noinst_LIBRARIES = libbalsa.a
if BUILD_WITH_GPGME
libbalsa_gpgme_extra = \
+ autocrypt.h \
+ autocrypt.c \
libbalsa-gpgme.h \
libbalsa-gpgme.c \
libbalsa-gpgme-cb.h \
@@ -25,6 +27,8 @@ libbalsa_gpgme_extra_dist =
else
libbalsa_gpgme_extra =
libbalsa_gpgme_extra_dist = \
+ autocrypt.h \
+ autocrypt.c \
libbalsa-gpgme.h \
libbalsa-gpgme.c \
libbalsa-gpgme-cb.h \
diff --git a/libbalsa/autocrypt.c b/libbalsa/autocrypt.c
new file mode 100644
index 000000000..7c4be82c3
--- /dev/null
+++ b/libbalsa/autocrypt.c
@@ -0,0 +1,912 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2018 Stuart Parmenter and others,
+ * See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
+# include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+#if defined ENABLE_AUTOCRYPT
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <sqlite3.h>
+#include "libbalsa-gpgme.h"
+#include "libbalsa-gpgme-keys.h"
+#include "libbalsa-gpgme-widgets.h"
+#include "identity.h"
+#include "autocrypt.h"
+
+
+#ifdef G_LOG_DOMAIN
+# undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "autocrypt"
+
+
+/* the autocrypt SQL table contains the following data:
+ * addr: email address
+ * last_seen: time_t value when the last message from addr has been seen
+ * ac_timestamp: time_t value when the last message with a valid Autocrypt header from addr has been seen
+ * pubkey: raw (binary) public key data
+ * fingerprint: the fingerprint of pubkey, stored to avoid frequently importing pubkey into a temporary
context
+ * expires: the expiry time of pubkey (0 for never), stored to avoid frequently importing pubkey into a
temporary context
+ * prefer_encrypt: TRUE (1) if the prefer-encrypt=mutual attribute was given in the latest Autocrypt header
+ *
+ * notes: SQLite stores BOOLEAN as INTEGER
+ * We do not support key gossip, so storing everything in a flat table is sufficient */
+#define DB_SCHEMA \
+ "PRAGMA auto_vacuum = 1;" \
+ "CREATE TABLE autocrypt(" \
+ "addr TEXT PRIMARY KEY NOT NULL, " \
+ "last_seen BIGINT, " \
+ "ac_timestamp BIGINT, " \
+ "pubkey BLOB NOT NULL, " \
+ "fingerprint TEXT NOT NULL, " \
+ "expires BIGINT NOT NULL, " \
+ "prefer_encrypt BOOLEAN DEFAULT 0);"
+
+
+#define NUM_QUERIES 6U
+
+
+struct _AutocryptData {
+ gchar *addr;
+ time_t last_seen;
+ time_t ac_timestamp;
+ GBytes *keydata;
+ gchar *fingerprint;
+ time_t expires;
+ gboolean prefer_encrypt;
+};
+
+typedef struct _AutocryptData AutocryptData;
+
+
+enum {
+ AC_ADDRESS_COLUMN = 0,
+ AC_LAST_SEEN_COLUMN,
+ AC_TIMESTAMP_COLUMN,
+ AC_PREFER_ENCRYPT_COLUMN,
+ AC_KEY_PTR_COLUMN,
+ AC_DB_VIEW_COLUMNS
+};
+
+
+static void autocrypt_close(void);
+static AutocryptData *parse_autocrypt_header(const gchar *value);
+static gboolean eval_autocrypt_attr(const gchar *attr,
+ const gchar *value,
+ gboolean *seen,
+ AutocryptData *target);
+static void add_or_update_user_info(const AutocryptData *user_info,
+ time_t date_header,
+ gboolean update,
+ GError **error);
+static void update_last_seen(const gchar *addr,
+ time_t date_header,
+ GError **error);
+static AutocryptData *autocrypt_user_info(const gchar *mailbox,
+ GError **error)
+ G_GNUC_WARN_UNUSED_RESULT;
+static void autocrypt_free(AutocryptData *data);
+static AutocryptRecommend autocrypt_check_ia_list(gpgme_ctx_t gpgme_ctx,
+
InternetAddressList *recipients,
+ time_t
ref_time,
+ GList
**missing_keys,
+ GError
**error);
+static gboolean key_button_event_press_cb(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer data);
+
+
+static sqlite3 *autocrypt_db = NULL;
+static sqlite3_stmt *query[NUM_QUERIES] = { NULL, NULL, NULL, NULL, NULL, NULL };
+G_LOCK_DEFINE_STATIC(db_mutex);
+
+
+/* documentation: see header file */
+gboolean
+autocrypt_init(GError **error)
+{
+ static const gchar * const prepare_statements[NUM_QUERIES] = {
+ "SELECT * FROM autocrypt WHERE LOWER(addr) = ?",
+ "INSERT INTO autocrypt VALUES (?1, ?2, ?2, ?3, ?4, ?5, ?6)",
+ "UPDATE autocrypt SET last_seen = MAX(?2, last_seen), ac_timestamp = ?2, pubkey = ?3,
fingerprint = ?4,"
+ " expires = ?5, prefer_encrypt = ?6 WHERE addr = ?1",
+ "UPDATE autocrypt SET last_seen = ?2 WHERE addr = ?1 AND last_seen < ?2 AND ac_timestamp <
?2",
+ "SELECT pubkey FROM autocrypt WHERE fingerprint LIKE ?",
+ "SELECT addr, last_seen, ac_timestamp, prefer_encrypt, pubkey FROM autocrypt ORDER BY
LOWER(addr) ASC"
+ };
+ gboolean result;
+
+ G_LOCK(db_mutex);
+ if (autocrypt_db == NULL) {
+ gchar *db_path;
+ gboolean require_init;
+ int sqlite_res;
+
+ db_path = g_build_filename(g_get_home_dir(), ".balsa", "autocrypt.db", NULL);
+ require_init = (g_access(db_path, R_OK + W_OK) != 0);
+ sqlite_res = sqlite3_open(db_path, &autocrypt_db);
+ if (sqlite_res == SQLITE_OK) {
+ guint n;
+
+ /* write the schema if the database is new */
+ if (require_init) {
+ sqlite_res = sqlite3_exec(autocrypt_db, DB_SCHEMA, NULL, NULL, NULL);
+ }
+
+ /* always vacuum the database */
+ if (sqlite_res == SQLITE_OK) {
+ sqlite_res = sqlite3_exec(autocrypt_db, "VACUUM", NULL, NULL, NULL);
+ }
+
+ /* prepare statements */
+ for (n = 0U; (sqlite_res == SQLITE_OK) && (n < NUM_QUERIES); n++) {
+ sqlite_res = sqlite3_prepare_v2(autocrypt_db, prepare_statements[n], -1,
&query[n], NULL);
+ }
+ }
+ G_UNLOCK(db_mutex);
+
+ /* error checks... */
+ if (sqlite_res != SQLITE_OK) {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("cannot initialise Autocrypt
database “%s”: %s"), db_path,
+ sqlite3_errmsg(autocrypt_db));
+ autocrypt_close();
+ result = FALSE;
+ } else {
+ atexit(autocrypt_close);
+ result = TRUE;
+ }
+ g_free(db_path);
+ } else {
+ G_UNLOCK(db_mutex);
+ result = TRUE;
+ }
+
+ return result;
+}
+
+
+/* documentation: see header file */
+void
+autocrypt_from_message(LibBalsaMessage *message,
+ GError **error)
+{
+ const gchar *from_addr;
+ GMimeHeaderList *headers;
+ GMimeHeaderIter iter;
+ AutocryptData *autocrypt = NULL;
+
+ g_return_if_fail(LIBBALSA_IS_MESSAGE(message) && (message->headers != NULL) &&
(message->headers->from != NULL) &&
+ (message->headers->content_type != NULL) && GMIME_IS_OBJECT(message->mime_msg) &&
(autocrypt_db != NULL));
+
+ // FIXME - we should ignore spam - how can we detect it?
+
+ /* check for content types which shall be ignored */
+ if (autocrypt_ignore(message->headers->content_type)) {
+ g_debug("ignore %s/%s", g_mime_content_type_get_media_type(message->headers->content_type),
+ g_mime_content_type_get_media_subtype(message->headers->content_type));
+ return;
+ }
+
+ /* check for exactly one From: mailbox address - others shall be ignored */
+ if ((internet_address_list_length(message->headers->from) != 1) ||
+ !INTERNET_ADDRESS_IS_MAILBOX(internet_address_list_get_address(message->headers->from, 0))) {
+ g_debug("require exactly one From: address, ignored");
+ return;
+ }
+
+ /* ignore messages without a Date: header or with a date in the future */
+ if ((message->headers->date == 0) || (message->headers->date > time(NULL))) {
+ g_debug("no Date: header or value in the future, ignored");
+ return;
+ }
+
+ /* get the From: address (is a mailbox, checked above) */
+ from_addr =
+
internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(internet_address_list_get_address(message->headers->from,
0)));
+ g_debug("message from '%s', date %ld", from_addr, message->headers->date);
+
+ /* scan for Autocrypt headers */
+ headers = g_mime_object_get_header_list(GMIME_OBJECT(message->mime_msg));
+ if (g_mime_header_list_get_iter(headers, &iter)) {
+ do {
+ if ((g_ascii_strcasecmp(g_mime_header_iter_get_name(&iter), "Autocrypt") == 0) &&
+ (g_mime_header_iter_get_value(&iter) != NULL)) {
+ AutocryptData *new_data;
+
+ new_data = parse_autocrypt_header(g_mime_header_iter_get_value(&iter));
+ if (new_data != NULL) {
+ if (autocrypt == NULL) {
+ autocrypt = new_data;
+ } else {
+ g_info("more than one valid Autocrypt header, ignore message");
+ autocrypt_free(autocrypt);
+ autocrypt_free(new_data);
+ return;
+ }
+ } else {
+ /* ignore message with broken Autocrypt header */
+ autocrypt_free(autocrypt);
+ return;
+ }
+ }
+ } while (g_mime_header_iter_next(&iter));
+ }
+
+ /* check if addr matches From: - ignore otherwise */
+ if (autocrypt != NULL) {
+ if (g_ascii_strcasecmp(autocrypt->addr, from_addr) != 0) {
+ g_info("Autocrypt header for '%s' in message from '%s', ignore message", autocrypt->addr,
from_addr);
+ autocrypt_free(autocrypt);
+ return;
+ }
+ }
+
+ /* update the database */
+ G_LOCK(db_mutex);
+ if (autocrypt != NULL) {
+ AutocryptData *db_info;
+
+ db_info = autocrypt_user_info(autocrypt->addr, error);
+ if (db_info != NULL) {
+ if (message->headers->date > db_info->ac_timestamp) {
+ add_or_update_user_info(autocrypt, message->headers->date, TRUE, error);
+ } else {
+ g_info("message timestamp %ld not newer than autocrypt db timestamp %ld, ignore
message",
+ (long) message->headers->date, (long) db_info->ac_timestamp);
+ }
+ autocrypt_free(db_info);
+ } else {
+ add_or_update_user_info(autocrypt, message->headers->date, FALSE, error);
+ }
+ autocrypt_free(autocrypt);
+ } else {
+ update_last_seen(from_addr, message->headers->date, error);
+ }
+ G_UNLOCK(db_mutex);
+}
+
+
+/* documentation: see header file */
+gchar *
+autocrypt_header(const LibBalsaIdentity *identity, GError **error)
+{
+ const gchar *mailbox;
+ gchar *use_fpr = NULL;
+ gchar *result = NULL;
+ gchar *keydata;
+
+ g_return_val_if_fail((identity != NULL) && (identity->autocrypt_mode != AUTOCRYPT_DISABLE), NULL);
+ mailbox = internet_address_mailbox_get_addr(INTERNET_ADDRESS_MAILBOX(identity->ia));
+
+ /* no key fingerprint has been passed - try to find the fingerprint of a secret key matching the
passed mailbox */
+ if ((identity->force_gpg_key_id == NULL) || (identity->force_gpg_key_id[0] == '\0')) {
+ gpgme_ctx_t ctx;
+
+ ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+ if (ctx != NULL) {
+ GList *keys = NULL;
+
+ libbalsa_gpgme_list_keys(ctx, &keys, NULL, mailbox, TRUE, FALSE, FALSE, error);
+ if (keys != NULL) {
+ gpgme_key_t key = (gpgme_key_t) keys->data;
+
+ if ((key != NULL) && (key->subkeys != NULL)) {
+ use_fpr = g_strdup(key->subkeys->fpr);
+ }
+ g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+ }
+ gpgme_release(ctx);
+ }
+ g_debug("found fingerprint %s for '%s'", use_fpr, mailbox);
+ } else {
+ use_fpr = g_strdup(identity->force_gpg_key_id);
+ }
+
+ keydata = libbalsa_gpgme_export_autocrypt_key(use_fpr, mailbox, error);
+ g_free(use_fpr);
+ if (keydata != NULL) {
+ GString *buffer;
+ gssize ins_fws;
+
+ buffer = g_string_new(NULL);
+ g_string_append_printf(buffer, "addr=%s;", mailbox);
+ if (identity->autocrypt_mode == AUTOCRYPT_PREFER_ENCRYPT) {
+ g_string_append(buffer, "prefer-encrypt=mutual;");
+ }
+ g_string_append_printf(buffer, "keydata=%s", keydata);
+ for (ins_fws = 66U; ins_fws < (gssize) buffer->len; ins_fws += 78) {
+ g_string_insert(buffer, ins_fws, "\n\t");
+ }
+ result = g_string_free(buffer, FALSE);
+ }
+
+ return result;
+}
+
+
+/* documentation: see header file */
+gboolean
+autocrypt_ignore(GMimeContentType *content_type)
+{
+ g_return_val_if_fail(GMIME_IS_CONTENT_TYPE(content_type), TRUE);
+
+ return g_mime_content_type_is_type(content_type, "multipart", "report") ||
+ g_mime_content_type_is_type(content_type, "text", "calendar");
+}
+
+
+/* documentation: see header file */
+GBytes *
+autocrypt_get_key(const gchar *fingerprint, GError **error)
+{
+ gchar *param;
+ int sqlite_res;
+ GBytes *result = NULL;
+
+ g_return_val_if_fail(fingerprint != NULL, NULL);
+
+ /* prepend SQL "LIKE" wildcard */
+ param = g_strconcat("%", fingerprint, NULL);
+
+ sqlite_res = sqlite3_bind_text(query[4], 1, param, -1, SQLITE_STATIC);
+ if (sqlite_res == SQLITE_OK) {
+ sqlite_res = sqlite3_step(query[4]);
+ if (sqlite_res == SQLITE_ROW) {
+ result = g_bytes_new(sqlite3_column_blob(query[4], 0), sqlite3_column_bytes(query[4],
0));
+ sqlite_res = sqlite3_step(query[4]);
+ }
+
+ if (sqlite_res != SQLITE_DONE) {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data
for “%s”: %s"), fingerprint,
+ sqlite3_errmsg(autocrypt_db));
+ if (result != NULL) {
+ g_bytes_unref(result);
+ result = NULL;
+ }
+ }
+ } else {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data for
“%s”: %s"), fingerprint,
+ sqlite3_errmsg(autocrypt_db));
+ }
+ sqlite3_reset(query[4]);
+ g_free(param);
+
+ return result;
+}
+
+
+/* documentation: see header file */
+AutocryptRecommend
+autocrypt_recommendation(InternetAddressList *recipients, GList **missing_keys, GError **error)
+{
+ AutocryptRecommend result;
+ gpgme_ctx_t gpgme_ctx;
+
+ g_return_val_if_fail(IS_INTERNET_ADDRESS_LIST(recipients), AUTOCRYPT_ENCR_DISABLE);
+
+ /* create the gpgme context and set the protocol */
+ gpgme_ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+ if (gpgme_ctx == NULL) {
+ result = AUTOCRYPT_ENCR_ERROR;
+ } else {
+ result = autocrypt_check_ia_list(gpgme_ctx, recipients, time(NULL), missing_keys, error);
+ gpgme_release(gpgme_ctx);
+
+ if ((result == AUTOCRYPT_ENCR_ERROR) && (missing_keys != NULL) && (*missing_keys != NULL)) {
+ g_list_free_full(*missing_keys, (GDestroyNotify) g_bytes_unref);
+ *missing_keys = NULL;
+ }
+ }
+
+ return result;
+}
+
+
+/* documentation: see header file */
+void
+autocrypt_db_dialog_run(const gchar *date_string, GtkWindow *parent)
+{
+ GtkWidget *dialog;
+ GtkWidget *vbox;
+ GtkWidget *label;
+ GtkWidget *scrolled_window;
+ GtkWidget *tree_view;
+ GtkListStore *model;
+ GtkTreeSelection *selection;
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ GList *keys = NULL;
+ int sqlite_res;
+
+ dialog = gtk_dialog_new_with_buttons(_("Autocrypt database"), parent,
+ GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(), _("_Close"), GTK_RESPONSE_CLOSE,
NULL);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
+ gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), vbox);
+ gtk_widget_set_vexpand (vbox, TRUE);
+ label = gtk_label_new(_("Double-click key to show details"));
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, TRUE, 0);
+
+ scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled_window), GTK_SHADOW_ETCHED_IN);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC);
+ gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0);
+
+ model = gtk_list_store_new(AC_DB_VIEW_COLUMNS, G_TYPE_STRING, /* address */
+ G_TYPE_STRING,
/* formatted last seen timestamp */
+ G_TYPE_STRING,
/* formatted last Autocrypt message timestamp */
+ G_TYPE_BOOLEAN,
/* user prefers encrypted messages */
+ G_TYPE_POINTER);
/* key */
+
+ tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
+ g_signal_connect(tree_view, "button_press_event", G_CALLBACK(key_button_event_press_cb), dialog);
+ gtk_container_add(GTK_CONTAINER(scrolled_window), tree_view);
+ selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
+ gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
+
+ /* add the keys */
+ sqlite_res = sqlite3_step(query[5]);
+ while (sqlite_res == SQLITE_ROW) {
+ gchar *last_seen_buf;
+ gchar *last_ac_buf;
+ GBytes *key;
+ GtkTreeIter iter;
+
+ last_seen_buf = libbalsa_date_to_utf8(sqlite3_column_int64(query[5], 1), date_string);
+ last_ac_buf = libbalsa_date_to_utf8(sqlite3_column_int64(query[5], 2), date_string);
+ key = g_bytes_new(sqlite3_column_blob(query[5], 4), sqlite3_column_bytes(query[5], 4));
+ keys = g_list_prepend(keys, key);
+
+ gtk_list_store_append(model, &iter);
+ gtk_list_store_set(model, &iter,
+ AC_ADDRESS_COLUMN, sqlite3_column_text(query[5], 0),
+ AC_LAST_SEEN_COLUMN, last_seen_buf,
+ AC_TIMESTAMP_COLUMN, last_ac_buf,
+ AC_PREFER_ENCRYPT_COLUMN, sqlite3_column_int(query[5], 3),
+ AC_KEY_PTR_COLUMN, key,
+ -1);
+ g_free(last_seen_buf);
+ g_free(last_ac_buf);
+
+ sqlite_res = sqlite3_step(query[5]);
+ }
+ sqlite3_reset(query[5]);
+
+ /* set up the tree view */
+ g_object_unref(G_OBJECT(model));
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Mailbox"), renderer, "text", 0, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Last seen"), renderer, "text", 1, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Last Autocrypt message"), renderer, "text", 2,
NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+
+ renderer = gtk_cell_renderer_toggle_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Prefer encryption"), renderer, "active", 3,
NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_widget_show_all(vbox);
+
+ (void) gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ g_list_free_full(keys, (GDestroyNotify) g_bytes_unref);
+}
+
+
+static AutocryptRecommend
+autocrypt_check_ia_list(gpgme_ctx_t gpgme_ctx,
+ InternetAddressList *recipients,
+ time_t ref_time,
+ GList **missing_keys,
+ GError **error)
+{
+ AutocryptRecommend result = AUTOCRYPT_ENCR_AVAIL_MUTUAL;
+ gint i;
+
+ for (i = 0; (result > AUTOCRYPT_ENCR_DISABLE) && (i < internet_address_list_length(recipients)); i++)
{
+ InternetAddress *ia = internet_address_list_get_address(recipients, i);
+
+ /* check all entries in the list, handle groups recursively */
+ if (INTERNET_ADDRESS_IS_GROUP(ia)) {
+ result = autocrypt_check_ia_list(gpgme_ctx, INTERNET_ADDRESS_GROUP(ia)->members, ref_time,
missing_keys, error);
+ } else {
+ AutocryptData *autocrypt_user;
+ const gchar *mailbox;
+
+ mailbox = INTERNET_ADDRESS_MAILBOX(ia)->addr;
+ autocrypt_user = autocrypt_user_info(mailbox, NULL);
+ if (autocrypt_user == NULL) {
+ GList *keys = NULL;
+
+ /* check if we have a public key, keep the state if we found one, disable if not */
+ if (libbalsa_gpgme_list_keys(gpgme_ctx, &keys, NULL, mailbox, FALSE, FALSE, FALSE,
error)) {
+ if (keys != NULL) {
+ g_list_free_full(keys, (GDestroyNotify) gpgme_key_unref);
+ g_debug("'%s': found in public key ring, overall status %d", mailbox,
result);
+ } else {
+ result = AUTOCRYPT_ENCR_DISABLE;
+ g_debug("'%s': not in Autocrypt db or public key ring, overall status
%d", mailbox, result);
+ }
+ } else {
+ result = AUTOCRYPT_ENCR_ERROR;
+ }
+ } else {
+ /* we found Autocrypt data for this user */
+ if ((autocrypt_user->expires > 0) && (autocrypt_user->expires <= ref_time)) {
+ result = AUTOCRYPT_ENCR_DISABLE; /* key has expired */
+ } else if (autocrypt_user->ac_timestamp < (autocrypt_user->last_seen - (35 * 24 * 60
* 60))) {
+ result = MIN(result, AUTOCRYPT_ENCR_DISCOURAGE); /* Autocrypt
timestamp > 35 days older than last seen */
+ } else if (autocrypt_user->prefer_encrypt) {
+ result = MIN(result, AUTOCRYPT_ENCR_AVAIL_MUTUAL); /* user requested
"prefer-encrypt=mutual" */
+ } else {
+ result = MIN(result, AUTOCRYPT_ENCR_AVAIL); /* user did
not request "prefer-encrypt=mutual" */
+ }
+
+ /* check if the Autocrypt key is already in the key ring, add it to the list of
missing ones otherwise */
+ if (missing_keys != NULL) {
+ GList *keys = NULL;
+
+ if (libbalsa_gpgme_list_keys(gpgme_ctx, &keys, NULL, autocrypt_user->fingerprint,
FALSE, FALSE, FALSE, error)) {
+ if (keys != NULL) {
+ g_list_free_full(keys, (GDestroyNotify) gpgme_key_unref);
+ } else {
+ *missing_keys = g_list_prepend(*missing_keys,
g_bytes_ref(autocrypt_user->keydata));
+ }
+ } else {
+ result = AUTOCRYPT_ENCR_ERROR;
+ }
+ }
+ autocrypt_free(autocrypt_user);
+ g_debug("'%s': found in Autocrypt db, overall status %d", mailbox, result);
+ }
+ }
+ }
+
+ return result;
+}
+
+
+static void
+autocrypt_free(AutocryptData *data)
+{
+ if (data != NULL) {
+ g_free(data->addr);
+ g_free(data->fingerprint);
+ if (data->keydata) {
+ g_bytes_unref(data->keydata);
+ }
+ g_free(data);
+ }
+}
+
+
+static void
+autocrypt_close(void)
+{
+ guint n;
+
+ g_debug("closing Autocrypt database");
+ G_LOCK(db_mutex);
+ for (n = 0U; n < NUM_QUERIES; n++) {
+ sqlite3_finalize(query[n]);
+ query[n] = NULL;
+ }
+ sqlite3_close(autocrypt_db);
+ autocrypt_db = NULL;
+ G_UNLOCK(db_mutex);
+}
+
+
+static AutocryptData *
+parse_autocrypt_header(const gchar *value)
+{
+ gchar **attributes;
+ gboolean attr_seen[3] = { FALSE, FALSE, FALSE };
+ AutocryptData *new_data;
+ gint n;
+ gboolean broken;
+
+ new_data = g_new0(AutocryptData, 1U);
+ attributes = g_strsplit(value, ";", -1);
+ if (attributes == NULL) {
+ g_info("empty Autocrypt header");
+ broken = TRUE;
+ } else {
+ broken = FALSE;
+ }
+
+ for (n = 0; !broken && (attributes[n] != NULL); n++) {
+ gchar **items;
+
+ items = g_strsplit(attributes[n], "=", 2);
+ if ((items == NULL) || (items[0] == NULL) || (items[1] == NULL)) {
+ g_info("bad Autocrypt header attribute");
+ broken = TRUE;
+ } else {
+ broken = !eval_autocrypt_attr(g_strstrip(items[0]), g_strstrip(items[1]), attr_seen,
new_data);
+ }
+ g_strfreev(items);
+ }
+ g_strfreev(attributes);
+
+ if (!broken) {
+ if (!attr_seen[0] || !attr_seen[2]) {
+ g_info("missing mandatory Autocrypt header attribute");
+ broken = TRUE;
+ }
+ }
+
+ /* try to import the key into a temporary context */
+ if (!broken) {
+ gboolean success = FALSE;
+ gpgme_ctx_t ctx;
+
+ ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, NULL);
+ if (ctx != NULL) {
+ gchar *temp_dir = NULL;
+
+ if (!libbalsa_mktempdir(&temp_dir)) {
+ g_warning("Failed to create a temporary folder");
+ } else {
+ GList *keys = NULL;
+ GError *error = NULL;
+
+ success = libbalsa_gpgme_ctx_set_home(ctx, temp_dir, &error) &&
+ libbalsa_gpgme_import_bin_key(ctx, new_data->keydata, NULL, &error) &&
+ libbalsa_gpgme_list_keys(ctx, &keys, NULL, NULL, FALSE, FALSE, FALSE,
&error);
+ if (success && (keys != NULL) && (keys->next == NULL)) {
+ gpgme_key_t key = (gpgme_key_t) keys->data;
+
+ if ((key != NULL) && (key->subkeys != NULL)) {
+ new_data->fingerprint = g_strdup(key->subkeys->fpr);
+ new_data->expires = key->subkeys->expires;
+ }
+ } else {
+ g_warning("Failed to import key data: %s", (error != NULL) ?
error->message : "unknown");
+ }
+ g_clear_error(&error);
+
+ g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+ libbalsa_delete_directory_contents(temp_dir);
+ g_rmdir(temp_dir);
+ }
+
+ gpgme_release(ctx);
+ }
+ }
+
+ /* check if a broken header has been detected, or if importing the key failed */
+ if (broken || (new_data->fingerprint == NULL)) {
+ autocrypt_free(new_data);
+ new_data = NULL;
+ } else {
+ g_debug("valid Autocrypt header for '%s', prefer encrypt %d, key fingerprint %s",
new_data->addr, new_data->prefer_encrypt,
+ new_data->fingerprint);
+ }
+
+ return new_data;
+}
+
+
+static gboolean
+eval_autocrypt_attr(const gchar *attr, const gchar *value, gboolean *seen, AutocryptData *target)
+{
+ gboolean result = FALSE;
+
+ if (seen[2]) {
+ g_info("broken Autocrypt header, extra attribute after keydata");
+ } else if (strcmp(attr, "addr") == 0) {
+ if (seen[0]) {
+ g_info("duplicated Autocrypt header attribute 'addr'");
+ } else {
+ seen[0] = TRUE;
+ /* note: not exactly the canonicalisation as required by the Autocrypt standard, but
should work in all practical use
+ * cases... */
+ target->addr = g_ascii_strdown(value, -1);
+ result = TRUE;
+ }
+ } else if (strcmp(attr, "prefer-encrypt") == 0) {
+ if (seen[1]) {
+ g_info("duplicated Autocrypt header attribute 'addr'");
+ } else {
+ seen[1] = TRUE;
+ if (strcmp(value, "mutual") == 0) {
+ target->prefer_encrypt = TRUE;
+ result = TRUE;
+ } else {
+ g_info("bad value '%s' for Autocrypt header attribute 'prefer-encrypt'",
value);
+ }
+ }
+ } else if (strcmp(attr, "keydata") == 0) {
+ guchar *data;
+ gsize len;
+
+ seen[2] = TRUE;
+ data = g_base64_decode(value, &len);
+ if (data == NULL) {
+ g_info("invalid keydata in Autocrypt header");
+ } else {
+ target->keydata = g_bytes_new_take(data, len);
+ result = TRUE;
+ }
+ } else if (attr[0] == '_') {
+ g_debug("ignoring non-critical Autocrypt header attribute '%s'", attr);
+ result = TRUE; /* note that this is no error */
+ } else {
+ g_info("unexpected Autocrypt header attribute '%s'", attr);
+ }
+
+ return result;
+}
+
+
+static AutocryptData *
+autocrypt_user_info(const gchar *mailbox, GError **error)
+{
+ int sqlite_res;
+ AutocryptData *user_info = NULL;
+
+ g_return_val_if_fail((mailbox != NULL) && (autocrypt_db != NULL), NULL);
+
+ sqlite_res = sqlite3_bind_text(query[0], 1, mailbox, -1, SQLITE_STATIC);
+ if (sqlite_res == SQLITE_OK) {
+ sqlite_res = sqlite3_step(query[0]);
+ if (sqlite_res == SQLITE_ROW) {
+ user_info = g_new0(AutocryptData, 1U);
+ user_info->addr = g_strdup((const gchar *) sqlite3_column_text(query[0], 0));
+ user_info->last_seen = sqlite3_column_int64(query[0], 1);
+ user_info->ac_timestamp = sqlite3_column_int64(query[0], 2);
+ user_info->keydata = g_bytes_new(sqlite3_column_blob(query[0], 3),
sqlite3_column_bytes(query[0], 3));
+ user_info->fingerprint = g_strdup((const gchar *) sqlite3_column_text(query[0], 4));
+ user_info->expires = sqlite3_column_int64(query[0], 5);
+ user_info->prefer_encrypt = (sqlite3_column_int(query[0], 6) != 0);
+ sqlite_res = sqlite3_step(query[0]);
+ }
+
+ if (sqlite_res != SQLITE_DONE) {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data
for “%s”: %s"), mailbox,
+ sqlite3_errmsg(autocrypt_db));
+ autocrypt_free(user_info);
+ user_info = NULL;
+ }
+ } else {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, sqlite_res, _("error reading Autocrypt data for
“%s”: %s"), mailbox,
+ sqlite3_errmsg(autocrypt_db));
+ }
+ sqlite3_reset(query[0]);
+
+ return user_info;
+}
+
+
+static void
+add_or_update_user_info(const AutocryptData *user_info, time_t date_header, gboolean update, GError **error)
+{
+ guint query_idx;
+ gconstpointer keyvalue;
+ gsize keysize;
+
+ query_idx = update ? 2 : 1;
+ keyvalue = g_bytes_get_data(user_info->keydata, &keysize);
+ if ((sqlite3_bind_text(query[query_idx], 1, user_info->addr, -1, SQLITE_STATIC) != SQLITE_OK) ||
+ (sqlite3_bind_int64(query[query_idx], 2, date_header) != SQLITE_OK) ||
+ (sqlite3_bind_blob(query[query_idx], 3, keyvalue, keysize, SQLITE_STATIC) != SQLITE_OK) ||
+ (sqlite3_bind_text(query[query_idx], 4, user_info->fingerprint, -1, SQLITE_STATIC) !=
SQLITE_OK) ||
+ (sqlite3_bind_int64(query[query_idx], 5, user_info->expires) != SQLITE_OK) ||
+ (sqlite3_bind_int(query[query_idx], 6, user_info->prefer_encrypt) != SQLITE_OK) ||
+ (sqlite3_step(query[query_idx]) != SQLITE_DONE)) {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, -1, _("%s user “%s” failed: %s"), update ?
_("update") : _("insert"),
+ user_info->addr, sqlite3_errmsg(autocrypt_db));
+ } else {
+ g_debug("%s user '%s'", update ? "updated" : "inserted", user_info->addr);
+ }
+ sqlite3_reset(query[query_idx]);
+}
+
+
+static void
+update_last_seen(const gchar *addr, time_t date_header, GError **error)
+{
+ if ((sqlite3_bind_text(query[3], 1, addr, -1, SQLITE_STATIC) != SQLITE_OK) ||
+ (sqlite3_bind_int64(query[3], 2, date_header) != SQLITE_OK) ||
+ (sqlite3_step(query[3]) != SQLITE_DONE)) {
+ g_set_error(error, AUTOCRYPT_ERROR_QUARK, -1, _("update user “%s” failed: %s"), addr,
sqlite3_errmsg(autocrypt_db));
+ } else {
+ g_debug("updated last_seen for '%s'", addr);
+ }
+ sqlite3_reset(query[3]);
+}
+
+
+static gboolean
+key_button_event_press_cb(GtkWidget *widget,
+ GdkEventButton *event,
+ gpointer data)
+{
+ GtkTreeView *tree_view = GTK_TREE_VIEW(widget);
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(tree_view);
+ GtkTreePath *path;
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+
+ g_return_val_if_fail(event != NULL, FALSE);
+ if ((event->type != GDK_2BUTTON_PRESS) || event->window != gtk_tree_view_get_bin_window(tree_view)) {
+ return FALSE;
+ }
+
+ if (gtk_tree_view_get_path_at_pos(tree_view, event->x, event->y, &path, NULL, NULL, NULL)) {
+ if (!gtk_tree_selection_path_is_selected(selection, path)) {
+ gtk_tree_view_set_cursor(tree_view, path, NULL, FALSE);
+ gtk_tree_view_scroll_to_cell(tree_view, path, NULL, FALSE, 0, 0);
+ }
+ gtk_tree_path_free(path);
+ }
+
+ /* note: silently ignore all errors below... */
+ if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+ gpgme_ctx_t ctx;
+
+ ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, NULL);
+ if (ctx != NULL) {
+ gchar *temp_dir = NULL;
+
+ if (libbalsa_mktempdir(&temp_dir)) {
+ GBytes *key;
+ GList *keys = NULL;
+ gboolean success;
+
+ gtk_tree_model_get(model, &iter, AC_KEY_PTR_COLUMN, &key, -1);
+ success = libbalsa_gpgme_ctx_set_home(ctx, temp_dir, NULL) &&
+ libbalsa_gpgme_import_bin_key(ctx, key, NULL, NULL) &&
+ libbalsa_gpgme_list_keys(ctx, &keys, NULL, NULL, FALSE, FALSE, TRUE,
NULL);
+ if (success && (keys != NULL)) {
+ GtkWidget *dialog;
+
+ dialog = libbalsa_key_dialog(GTK_WINDOW(data), GTK_BUTTONS_CLOSE,
(gpgme_key_t) keys->data, GPG_SUBKEY_CAP_ALL,
+ NULL, NULL);
+ (void) gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ g_list_free_full(keys, (GDestroyNotify) gpgme_key_release);
+ }
+ libbalsa_delete_directory_contents(temp_dir);
+ g_rmdir(temp_dir);
+ }
+
+ gpgme_release(ctx);
+ }
+ }
+
+ return TRUE;
+}
+
+#endif /* ENABLE_AUTOCRYPT */
diff --git a/libbalsa/autocrypt.h b/libbalsa/autocrypt.h
new file mode 100644
index 000000000..11f9dd21f
--- /dev/null
+++ b/libbalsa/autocrypt.h
@@ -0,0 +1,148 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2018 Stuart Parmenter and others,
+ * See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Note: see https://autocrypt.org/level1.html for the Autocrypt specs
+ */
+
+#ifndef LIBBALSA_AUTOCRYPT_H_
+#define LIBBALSA_AUTOCRYPT_H_
+
+#ifndef BALSA_VERSION
+# error "Include config.h before this file."
+#endif
+
+#ifdef ENABLE_AUTOCRYPT
+
+#include "libbalsa.h"
+
+
+#define AUTOCRYPT_ERROR_QUARK (g_quark_from_static_string("autocrypt"))
+
+
+enum _AutocryptMode {
+ AUTOCRYPT_DISABLE, /**< Disable Autocrypt support. */
+ AUTOCRYPT_NOPREFERENCE, /**< Enable Autocrypt support, but do not request
"prefer-encrypt=mutual". */
+ AUTOCRYPT_PREFER_ENCRYPT /**< Enable Autocrypt support and request
"prefer-encrypt=mutual". */
+};
+
+typedef enum _AutocryptMode AutocryptMode;
+
+
+enum _AutocryptRecommend {
+ AUTOCRYPT_ENCR_ERROR, /**< An error occurred when calculating the
recommendation for encryption. */
+ AUTOCRYPT_ENCR_DISABLE, /**< Encryption is not possible due to a missing
usable key. */
+ AUTOCRYPT_ENCR_DISCOURAGE, /**< Encryption is possible but discouraged by
Autocrypt. */
+ AUTOCRYPT_ENCR_AVAIL, /**< Encryption is possible, but at least one
recipient does not request
+ * "prefer-encrypt=mutual". */
+ AUTOCRYPT_ENCR_AVAIL_MUTUAL /**< Encryption is possible, and all recipients
request "prefer-encrypt=mutual". */
+};
+
+typedef enum _AutocryptRecommend AutocryptRecommend;
+
+
+/** \brief Initialise the Autocrypt subsystem
+ *
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, FALSE if any error coourred
+ *
+ * Open and if necessary initialise the Autocrypt SQLite3 database <tt>autocrypt.db</tt> in the user's Balsa
folder.
+ */
+gboolean autocrypt_init(GError **error);
+
+/** \brief Update the Autocrypt database from a received message
+ *
+ * \param message Balsa message
+ * \param error filled with error information on error, may be NULL
+ *
+ * Scan the headers of the passed message and update the Autocrypt database according to the Autocrypt
specifications, section 2.3
+ * <em>Updating Autocrypt Peer State</em>.
+ *
+ * \todo Spam messages should be ignored, but how can we detect them?
+ */
+void autocrypt_from_message(LibBalsaMessage *message,
+ GError **error);
+
+/** \brief Create an Autocrypt header value
+ *
+ * \param identity the identity for which the Autocrypt header shall be created
+ * \param error filled with error information on error, may be NULL
+ * \return a newly allocated string containing the properly folded Autocrypt header
+ *
+ * Create a an Autocrypt header value according to the Autocrypt specifications. Note that the included key
data may or may not be
+ * minimalistic, depending upon the export capabilities of the gpg backend being used. It is an error to
call this function if the
+ * Autocrypt mode of the passed identity is AUTOCRYPT_DISABLE.
+ */
+gchar *autocrypt_header(const LibBalsaIdentity *identity,
+ GError **error)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+/** \brief Check if a media type shall be ignored for Autocrypt
+ *
+ * \param content_type message content type
+ * \return TRUE if the media type shall be ignored
+ *
+ * The standard requests that multipart/report shall be ignored. This function also blacklists
text/calendar which is not required
+ * by the standard (see https://lists.mayfirst.org/pipermail/autocrypt/2018-November/000441.html for a
discussion).
+ */
+gboolean autocrypt_ignore(GMimeContentType *content_type);
+
+/** \brief Get a key from the Autocrypt database
+ *
+ * \param fingerprint key fingerprint
+ * \param error filled with error information on error, may be NULL
+ * \return a new object containing the raw key data on success, or NULL if the key is not in the Autocrypt
database
+ *
+ * If available, returns the key whose fingerprint ends in the passed value from the Autocrypt database.
+ */
+GBytes *autocrypt_get_key(const gchar *fingerprint,
+ GError **error)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+/** \brief Get the recommendation for encryption
+ *
+ * \param recipients message recipients
+ * \param missing_keys filled with a list of GBytes *, containing all Autocrypt keys missing in the key
ring, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return the result of the recommendation check
+ *
+ * Calculate the Autocrypt recommendation for encryption, according to sect. 2.4 of the standard. Note that
all recipients which
+ * are not listed in the Autocrypt database, but for which a valid key exists in the GnuPG key ring, are
treated as if they
+ * requested "prefer-encrypt=mutual".
+ *
+ * \sa https://autocrypt.org/level1.html#provide-a-recommendation-for-message-encryption
+ */
+AutocryptRecommend autocrypt_recommendation(InternetAddressList *recipients,
+ GList
**missing_keys,
+ GError
**error);
+
+/** \brief Show the Autocrypt database
+ *
+ * \param date_string time stamp formatting template
+ * \param parent parent window
+ *
+ * Display a modal dialog with the contents of the Autocrypt database.
+ */
+void autocrypt_db_dialog_run(const gchar *date_string,
+ GtkWindow *parent);
+
+
+#endif /* ENABLE_AUTOCRYPT */
+
+
+#endif /* LIBBALSA_AUTOCRYPT_H_ */
diff --git a/libbalsa/identity.c b/libbalsa/identity.c
index 6b2d73ecd..215dbc1a1 100644
--- a/libbalsa/identity.c
+++ b/libbalsa/identity.c
@@ -117,6 +117,9 @@ libbalsa_identity_init(LibBalsaIdentity* ident)
ident->crypt_protocol = LIBBALSA_PROTECT_OPENPGP;
ident->force_gpg_key_id = NULL;
ident->force_smime_key_id = NULL;
+#ifdef ENABLE_AUTOCRYPT
+ ident->autocrypt_mode = AUTOCRYPT_DISABLE;
+#endif
ident->request_mdn = FALSE;
ident->request_dsn = FALSE;
/*
@@ -635,6 +638,16 @@ static void ident_dialog_add_gpg_menu(GtkWidget * grid, gint row,
GtkDialog * dialog,
const gchar * label_name,
const gchar * menu_key);
+#ifdef ENABLE_AUTOCRYPT
+static void ident_dialog_add_autocrypt_menu(GtkWidget *grid,
+ gint row,
+ GtkDialog *dialog,
+ const gchar
*label_name,
+ const gchar
*menu_key);
+static void display_frame_set_autocrypt_mode(GObject *dialog,
+ const gchar *key,
+ AutocryptMode
*value);
+#endif
static void add_show_menu(const char *label, gpointer data,
GtkWidget * menu);
static void ident_dialog_free_values(GPtrArray * values);
@@ -1067,6 +1080,11 @@ setup_ident_frame(GtkDialog * dialog, gboolean createp, gpointer tree,
_("use certificate with this id for signing S/MIME messages\n"
"(leave empty for automatic selection)"),
"identity-keyid-sm");
+#ifdef ENABLE_AUTOCRYPT
+ ident_dialog_add_autocrypt_menu(grid, row++, dialog,
+ _("Autocrypt mode"),
+ "identity-autocrypt");
+#endif
#ifndef HAVE_GPGME
gtk_widget_set_sensitive(grid, FALSE);
#endif
@@ -1546,6 +1564,9 @@ ident_dialog_update(GObject * dlg)
id->force_gpg_key_id = g_strstrip(ident_dialog_get_text(dlg, "identity-keyid"));
g_free(id->force_smime_key_id);
id->force_smime_key_id = g_strstrip(ident_dialog_get_text(dlg, "identity-keyid-sm"));
+#ifdef ENABLE_AUTOCRYPT
+ id->autocrypt_mode = GPOINTER_TO_INT(ident_dialog_get_value(dlg, "identity-autocrypt"));
+#endif
return TRUE;
}
@@ -1931,6 +1952,9 @@ display_frame_update(GObject * dialog, LibBalsaIdentity* ident)
&ident->crypt_protocol);
display_frame_set_field(dialog, "identity-keyid", ident->force_gpg_key_id);
display_frame_set_field(dialog, "identity-keyid-sm", ident->force_smime_key_id);
+#ifdef ENABLE_AUTOCRYPT
+ display_frame_set_autocrypt_mode(dialog, "identity-autocrypt", &ident->autocrypt_mode);
+#endif
}
@@ -2033,6 +2057,9 @@ libbalsa_identity_new_config(const gchar* name)
ident->crypt_protocol = libbalsa_conf_get_int("CryptProtocol=16");
ident->force_gpg_key_id = libbalsa_conf_get_string("ForceKeyID");
ident->force_smime_key_id = libbalsa_conf_get_string("ForceKeyIDSMime");
+#ifdef ENABLE_AUTOCRYPT
+ ident->autocrypt_mode = libbalsa_conf_get_int("Autocrypt=0");
+#endif
return ident;
}
@@ -2079,6 +2106,9 @@ libbalsa_identity_save(LibBalsaIdentity* ident, const gchar* group)
libbalsa_conf_set_int("CryptProtocol", ident->crypt_protocol);
libbalsa_conf_set_string("ForceKeyID", ident->force_gpg_key_id);
libbalsa_conf_set_string("ForceKeyIDSMime", ident->force_smime_key_id);
+#ifdef ENABLE_AUTOCRYPT
+ libbalsa_conf_set_int("Autocrypt", ident->autocrypt_mode);
+#endif
libbalsa_conf_pop_group();
}
@@ -2131,6 +2161,54 @@ display_frame_set_gpg_mode(GObject * dialog, const gchar* key, gint * value)
}
}
+#ifdef ENABLE_AUTOCRYPT
+static void
+display_frame_set_autocrypt_mode(GObject * dialog, const gchar* key, AutocryptMode * value)
+{
+ GtkComboBox *opt_menu = g_object_get_data(G_OBJECT(dialog), key);
+
+ switch (*value)
+ {
+ case AUTOCRYPT_NOPREFERENCE:
+ gtk_combo_box_set_active(opt_menu, 1);
+ break;
+ case AUTOCRYPT_PREFER_ENCRYPT:
+ gtk_combo_box_set_active(opt_menu, 2);
+ break;
+ default:
+ gtk_combo_box_set_active(opt_menu, 0);
+ *value = AUTOCRYPT_DISABLE;
+ }
+}
+
+static void
+ident_dialog_add_autocrypt_menu(GtkWidget * grid, gint row, GtkDialog * dialog,
+ const gchar * label_name, const gchar * menu_key)
+{
+ GtkWidget *label;
+ GtkWidget *opt_menu;
+ GPtrArray *values;
+
+ label = gtk_label_new_with_mnemonic(label_name);
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
+
+ opt_menu = gtk_combo_box_text_new();
+ values = g_ptr_array_sized_new(3);
+ g_object_set_data_full(G_OBJECT(opt_menu), "identity-value", values,
+ (GDestroyNotify) ident_dialog_free_values);
+ gtk_grid_attach(GTK_GRID(grid), opt_menu, 1, row, 1, 1);
+ g_object_set_data(G_OBJECT(dialog), menu_key, opt_menu);
+
+ add_show_menu(_("disabled"),
+ GINT_TO_POINTER(AUTOCRYPT_DISABLE), opt_menu);
+ add_show_menu(_("enabled, no preference"),
+ GINT_TO_POINTER(AUTOCRYPT_NOPREFERENCE), opt_menu);
+ add_show_menu(_("enabled, prefer encryption"),
+ GINT_TO_POINTER(AUTOCRYPT_PREFER_ENCRYPT), opt_menu);
+}
+#endif
+
/*
* Add an option menu to the given dialog with a label next to it
* explaining the contents. A reference to the entry is stored as
diff --git a/libbalsa/identity.h b/libbalsa/identity.h
index d8768d27c..d3f3d93e9 100644
--- a/libbalsa/identity.h
+++ b/libbalsa/identity.h
@@ -28,6 +28,7 @@
#include <gtk/gtk.h>
#include <gmime/internet-address.h>
+#include "autocrypt.h"
#include "libbalsa.h"
@@ -85,6 +86,9 @@ G_BEGIN_DECLS
gint crypt_protocol;
gchar *force_gpg_key_id;
gchar *force_smime_key_id;
+#ifdef ENABLE_AUTOCRYPT
+ AutocryptMode autocrypt_mode;
+#endif
LibBalsaSmtpServer *smtp_server;
};
diff --git a/libbalsa/libbalsa-gpgme-keys.c b/libbalsa/libbalsa-gpgme-keys.c
index 57f5fbff9..085c71bb7 100644
--- a/libbalsa/libbalsa-gpgme-keys.c
+++ b/libbalsa/libbalsa-gpgme-keys.c
@@ -41,6 +41,11 @@ typedef struct _keyserver_op_t {
} keyserver_op_t;
+static gboolean import_key_real(gpgme_ctx_t ctx,
+ gconstpointer key_buf,
+ gsize buf_len,
+ gchar **import_info,
+ GError **error);
static inline gboolean check_key(const gpgme_key_t key,
gboolean secret,
gboolean on_keyserver,
@@ -265,26 +270,134 @@ libbalsa_gpgme_export_key(gpgme_ctx_t ctx,
}
+/* documentation: see header file */
+gchar *
+libbalsa_gpgme_export_autocrypt_key(const gchar *fingerprint, const gchar *mailbox, GError **error)
+{
+ gchar *export_args[10] = { "", "--export", "--export-options", "export-minimal,no-export-attributes",
+ NULL, NULL, NULL, NULL, NULL, NULL };
+ gpgme_ctx_t ctx;
+ gchar *result = NULL;
+
+ g_return_val_if_fail((fingerprint != NULL) && (mailbox != NULL), NULL);
+
+ ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_SPAWN, NULL, NULL, error);
+ if (ctx != NULL) {
+ gpgme_data_t keybuf;
+ gpgme_error_t gpgme_err;
+
+ gpgme_err = gpgme_data_new(&keybuf);
+ if (gpgme_err != GPG_ERR_NO_ERROR) {
+ libbalsa_gpgme_set_error(error, gpgme_err, _("cannot create data buffer"));
+ } else {
+ const gpg_capabilities *gpg_capas;
+ guint param_idx;
+
+ gpg_capas = libbalsa_gpgme_gpg_capabilities();
+ g_assert(gpg_capas != NULL); /* paranoid - we're called for OpenPGP, so
the info /should/ be there... */
+ param_idx = 4U;
+ if (gpg_capas->export_filter_subkey) {
+ export_args[param_idx++] = g_strdup("--export-filter");
+ export_args[param_idx++] = g_strdup("drop-subkey=usage!~e && usage!~s");
+
+ }
+ if (gpg_capas->export_filter_uid) {
+ export_args[param_idx++] = g_strdup("--export-filter");
+ export_args[param_idx++] = g_strdup_printf("keep-uid=mbox=%s", mailbox);
+ }
+ export_args[param_idx] = g_strdup(fingerprint);
+
+ /* run... */
+ gpgme_err = gpgme_op_spawn(ctx, gpg_capas->gpg_path, (const gchar **) export_args,
NULL, keybuf, NULL, 0);
+ for (param_idx = 4U; export_args[param_idx] != NULL; param_idx++) {
+ g_free(export_args[param_idx]);
+ }
+ if (gpgme_err != GPG_ERR_NO_ERROR) {
+ libbalsa_gpgme_set_error(error, gpgme_err, _("cannot export minimal key for
“%s”"), mailbox);
+ gpgme_data_release(keybuf);
+ } else {
+ size_t keysize;
+ void *keydata;
+
+ keydata = gpgme_data_release_and_get_mem(keybuf, &keysize);
+ if ((keydata == NULL) || (keysize == 0U)) {
+ g_set_error(error, GPGME_ERROR_QUARK, -1, _("cannot export minimal
key for “%s”"), mailbox);
+ } else {
+ result = g_base64_encode(keydata, keysize);
+ }
+ gpgme_free(keydata);
+ }
+ }
+
+ gpgme_release(ctx);
+ }
+
+ return result;
+}
+
+
/* documentation: see header file */
gboolean
libbalsa_gpgme_import_ascii_key(gpgme_ctx_t ctx,
const gchar *key_buf,
gchar **import_info,
GError **error)
+{
+ g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+
+ return import_key_real(ctx, key_buf, strlen(key_buf), import_info, error);
+}
+
+
+/* documentation: see header file */
+gboolean
+libbalsa_gpgme_import_bin_key(gpgme_ctx_t ctx,
+ GBytes *key_buf,
+ gchar **import_info,
+ GError **error)
+{
+ gconstpointer key_data;
+ gsize key_len;
+
+ g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+ key_data = g_bytes_get_data(key_buf, &key_len);
+ return import_key_real(ctx, key_data, key_len, import_info, error);
+}
+
+
+/* ---- local functions ------------------------------------------------------ */
+
+/** \brief Import a binary or ASCII-armoured key
+ *
+ * \param ctx GpgME context
+ * \param key_buf ASCII or binary GnuPG key buffer
+ * \param buf_len number of bytes in the GnuPG key buffer
+ * \param import_info filled with human-readable information about the import, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, or FALSE on error
+ *
+ * Import an ASCII-armoured or binary GnuPG key into the key ring.
+ */
+static gboolean
+import_key_real(gpgme_ctx_t ctx,
+ gconstpointer key_buf,
+ gsize buf_len,
+ gchar **import_info,
+ GError **error)
{
gpgme_data_t buffer;
gpgme_error_t gpgme_err;
gboolean result = FALSE;
- g_return_val_if_fail((ctx != NULL) && (key_buf != NULL), FALSE);
+ g_return_val_if_fail(buf_len > 0, FALSE);
- gpgme_err = gpgme_data_new_from_mem(&buffer, key_buf, strlen(key_buf), 1);
+ gpgme_err = gpgme_data_new_from_mem(&buffer, key_buf, buf_len, 0);
if (gpgme_err != GPG_ERR_NO_ERROR) {
libbalsa_gpgme_set_error(error, gpgme_err, _("cannot create data buffer"));
} else {
gpgme_err = gpgme_op_import(ctx, buffer);
if (gpgme_err != GPG_ERR_NO_ERROR) {
- libbalsa_gpgme_set_error(error, gpgme_err, _("importing ASCII-armored key data
failed"));
+ libbalsa_gpgme_set_error(error, gpgme_err, _("importing key data failed"));
} else {
result = TRUE;
if (import_info != NULL) {
@@ -298,8 +411,6 @@ libbalsa_gpgme_import_ascii_key(gpgme_ctx_t ctx,
}
-/* ---- local functions ------------------------------------------------------ */
-
/** \brief Check if a key is usable
*
* \param key GpgME key
@@ -363,11 +474,10 @@ gpgme_keyserver_run(gpointer user_data)
GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(), GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
_("Cannot find a key with fingerprint %s on the key server."),
keyserver_op->fingerprint);
} else if (keys->next != NULL) {
+ /* more than one key found */
dialog = gtk_message_dialog_new(keyserver_op->parent,
GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(),
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE,
- ngettext("Found %u key with fingerprint %s on the key server. Please check
and import the proper key manually.",
- "Found %u keys with fingerprint %s on the key server. Please check
and import the proper key manually.",
- g_list_length(keys)),
+ _("Found %u keys with fingerprint %s on the key server. Please check and
import the proper key manually."),
g_list_length(keys), keyserver_op->fingerprint);
} else {
dialog = gpgme_keyserver_do_import(keyserver_op, (gpgme_key_t) keys->data);
diff --git a/libbalsa/libbalsa-gpgme-keys.h b/libbalsa/libbalsa-gpgme-keys.h
index 0468f833e..9141132a7 100644
--- a/libbalsa/libbalsa-gpgme-keys.h
+++ b/libbalsa/libbalsa-gpgme-keys.h
@@ -115,6 +115,22 @@ gchar *libbalsa_gpgme_export_key(gpgme_ctx_t ctx,
GError **error)
G_GNUC_WARN_UNUSED_RESULT;
+/** \brief Export a key for Autocrypt
+ *
+ * \param fingerprint key fingerprint, may be NULL
+ * \param mailbox key uid
+ * \param error filled with error information on error
+ * \return a newly allocated string containing the BASE64-encoded key on success, NULL on error
+ *
+ * Export the minimal key for using it in a Autocrypt: header. If specified, the key is selected by the
passed fingerprint,
+ * otherwise the first key matching the passed mailbox is used. Depending on the gpg backend version, all
other uid's and all
+ * subkeys which are not required are stripped.
+ */
+gchar *libbalsa_gpgme_export_autocrypt_key(const gchar *fingerprint,
+ const gchar *mailbox,
+ GError **error)
+ G_GNUC_WARN_UNUSED_RESULT;
+
/** \brief Import an ASCII-armoured key
*
* \param ctx GpgME context
@@ -130,6 +146,20 @@ gboolean libbalsa_gpgme_import_ascii_key(gpgme_ctx_t ctx,
gchar **import_info,
GError **error);
+/** \brief Import a binary key
+ *
+ * \param ctx GpgME context
+ * \param key_buf binary GnuPG key buffer
+ * \param import_info filled with human-readable information about the import, may be NULL
+ * \param error filled with error information on error, may be NULL
+ * \return TRUE on success, or FALSE on error
+ *
+ * Import a binary GnuPG key into the key ring.
+ */
+gboolean libbalsa_gpgme_import_bin_key(gpgme_ctx_t ctx,
+ GBytes *key_buf,
+ gchar **import_info,
+ GError **error);
G_END_DECLS
diff --git a/libbalsa/libbalsa-gpgme.c b/libbalsa/libbalsa-gpgme.c
index 98d797696..41dfe560f 100644
--- a/libbalsa/libbalsa-gpgme.c
+++ b/libbalsa/libbalsa-gpgme.c
@@ -78,9 +78,13 @@ static gchar *utf8_valid_str(const char *gpgme_str)
static const gchar *get_utf8_locale(int category);
#endif
+static void gpg_check_capas(const gchar *gpg_path,
+ const gchar *version);
+
static gboolean has_proto_openpgp = FALSE;
static gboolean has_proto_cms = FALSE;
+static gpg_capabilities gpg_capas;
static gpgme_passphrase_cb_t gpgme_passphrase_cb = NULL;
static lbgpgme_select_key_cb select_key_cb = NULL;
@@ -126,6 +130,9 @@ libbalsa_gpgme_init(gpgme_passphrase_cb_t get_passphrase,
g_debug("protocol %s: engine %s (home %s, version %s)",
gpgme_get_protocol_name(e->protocol),
e->file_name, e->home_dir, e->version);
+ if (e->protocol == GPGME_PROTOCOL_OpenPGP) {
+ gpg_check_capas(e->file_name, e->version);
+ }
e = e->next;
}
}
@@ -184,6 +191,24 @@ libbalsa_gpgme_check_crypto_engine(gpgme_protocol_t protocol)
}
+/** \brief Get capabilities of the gpg engine
+ *
+ * \return a pointer to the capabilities of the GnuPG engine, or NULL if it is not supported
+ *
+ * If an engine for the OpenPGP protocol is available, return a structure containing the path of the
executable, and information if
+ * some \em export-filter options are available. This information is needed to export a minimal Autocrypt
key, but unfortunately
+ * cannot be determined from the engine version.
+ *
+ * \sa libbalsa_gpgme_export_autocrypt_key(), gpg_check_capas()
+ * \todo Actually, gpgme should provide a minimalistic key export.
+ */
+const gpg_capabilities *
+libbalsa_gpgme_gpg_capabilities(void)
+{
+ return has_proto_openpgp ? &gpg_capas : NULL;
+}
+
+
/** \brief Create a new GpgME context for a protocol
*
* \param protocol requested protocol
@@ -1132,3 +1157,34 @@ get_utf8_locale(int category)
return localebuf;
}
#endif
+
+/*
+ * Note: this function is a hack to detect if the gpg engine in use support the '--export-filter' options
'keep-uid=...' and
+ * 'drop-subkey=...' (since 2.2.9) needed for exporting a minimal Autocrypt key.
+ */
+static void
+gpg_check_capas(const gchar *gpg_path, const gchar *version)
+{
+ gchar *gpg_args[] = { (gchar *) gpg_path, "--export", "--export-filter", "keep-uid=primary=1",
"0000000000000000", NULL };
+ gint exit_status;
+ guint major;
+ guint minor;
+ guint release;
+
+ gpg_capas.gpg_path = g_strdup(gpg_path);
+
+ /* check for the "--export-filter keep-uid=..." option */
+ if (g_spawn_sync(NULL, gpg_args, NULL, G_SPAWN_STDOUT_TO_DEV_NULL + G_SPAWN_STDERR_TO_DEV_NULL, NULL,
NULL, NULL, NULL,
+ &exit_status, NULL)) {
+ gpg_capas.export_filter_uid = g_spawn_check_exit_status(exit_status, NULL);
+ }
+ g_debug("%s supports '--export-filter keep-uid=...': %d", gpg_path, gpg_capas.export_filter_uid);
+
+ /* check for the "--export-filter drop-subkey=usage!~e && usage!~s" option */
+ if (sscanf(version, "%u.%u.%u", &major, &minor, &release) == 3) {
+ gpg_capas.export_filter_subkey = (major > 2U) ||
+ ((major == 2U) && (minor > 2U)) ||
+ ((major == 2U) && (minor == 2U) && (release >= 9U));
+ }
+ g_debug("%s supports '--export-filter drop-subkey=...': %d", gpg_path,
gpg_capas.export_filter_subkey);
+}
diff --git a/libbalsa/libbalsa-gpgme.h b/libbalsa/libbalsa-gpgme.h
index 8a52164c8..7339e378d 100644
--- a/libbalsa/libbalsa-gpgme.h
+++ b/libbalsa/libbalsa-gpgme.h
@@ -43,6 +43,15 @@ G_BEGIN_DECLS
#define GPGME_ERROR_QUARK (g_quark_from_static_string("gmime-gpgme"))
+struct _gpg_capabilities {
+ const gchar *gpg_path; /**< OpenPGP engine path */
+ gboolean export_filter_uid; /**< OpenPGP engine supports the 'keep-uid=...'
export-filter option. */
+ gboolean export_filter_subkey; /**< OpenPGP engine supports the 'drop-subkey=...'
export-filter option. */
+};
+
+typedef struct _gpg_capabilities gpg_capabilities;
+
+
/** Callback to select a key from a list
* Parameters:
* - user name
@@ -75,6 +84,7 @@ void libbalsa_gpgme_init(gpgme_passphrase_cb_t get_passphrase,
lbgpgme_select_key_cb select_key_cb,
lbgpgme_accept_low_trust_cb accept_low_trust);
gboolean libbalsa_gpgme_check_crypto_engine(gpgme_protocol_t protocol);
+const gpg_capabilities *libbalsa_gpgme_gpg_capabilities(void);
gpgme_ctx_t libbalsa_gpgme_new_with_proto(gpgme_protocol_t protocol,
gpgme_passphrase_cb_t
callback,
GtkWindow
*parent,
diff --git a/libbalsa/meson.build b/libbalsa/meson.build
index dc5685269..d0d8911b1 100644
--- a/libbalsa/meson.build
+++ b/libbalsa/meson.build
@@ -2,6 +2,8 @@
if gpgmecfg == 'true'
libbalsa_gpgme_extra = [
+ 'autocrypt.h',
+ 'autocrypt.c',
'libbalsa-gpgme.h',
'libbalsa-gpgme.c',
'libbalsa-gpgme-cb.h',
diff --git a/libbalsa/send.c b/libbalsa/send.c
index f4e747ada..33ed67fed 100644
--- a/libbalsa/send.c
+++ b/libbalsa/send.c
@@ -1509,6 +1509,20 @@ libbalsa_message_create_mime_message(LibBalsaMessage *message,
g_mime_object_append_header(GMIME_OBJECT(mime_message), "X-Mailer", tmp);
g_free(tmp);
+#ifdef ENABLE_AUTOCRYPT
+ /* add Autocrypt header if requested */
+ if ((message->ident != NULL) && (message->ident->autocrypt_mode != AUTOCRYPT_DISABLE) &&
+ !autocrypt_ignore(g_mime_object_get_content_type(mime_root))) {
+ tmp = autocrypt_header(message->ident, NULL);
+ if (tmp == NULL) {
+ g_object_unref(G_OBJECT(mime_message));
+ return LIBBALSA_MESSAGE_CREATE_ERROR;
+ }
+ g_mime_object_append_header(GMIME_OBJECT(mime_message), "Autocrypt", tmp);
+ g_free(tmp);
+ }
+#endif
+
message->mime_msg = mime_message;
return LIBBALSA_MESSAGE_CREATE_OK;
diff --git a/meson.build b/meson.build
index ad9f7a9c3..a623bbafa 100644
--- a/meson.build
+++ b/meson.build
@@ -47,6 +47,7 @@ endif
gnome_desktop = get_option('gnome-desktop')
gpgmecfg = get_option('gpgme')
+autocryptcfg = get_option('autocrypt')
canberra = get_option('canberra')
compface = get_option('compface')
gss = get_option('gss')
@@ -242,6 +243,19 @@ if gpgmecfg != 'false'
endif
endif
+# Autocrypt
+if gpgmecfg != 'false'
+ if autocryptcfg == 'true'
+ autocrypt_dep = dependency('sqlite3', required : true)
+ if autocrypt_dep.found()
+ conf.set('ENABLE_AUTOCRYPT', 1, description : 'If defined, enable Autocrypt support.')
+ else
+ error('*** You enabled Autocrypt but sqlite3 library is not found.')
+ endif
+ balsa_deps += autocrypt_dep
+ endif
+endif
+
# OpenLDAP configuration.
#
if ldap != 'false'
@@ -616,6 +630,7 @@ summary = [
' Use GNOME: @0@'.format(gnome_desktop),
' Use Canberra: @0@'.format(canberra),
' Use GPGME: @0@'.format(gpgmecfg),
+ ' Use Autocrypt: @0@'.format(autocryptcfg),
' Use LDAP: @0@'.format(ldap),
' Use GSS: @0@'.format(gss),
' Use SQLite: @0@'.format(sqlite),
diff --git a/meson_options.txt b/meson_options.txt
index 76ea2c3a7..d72c558a6 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -18,6 +18,11 @@ option('gpgme',
value : 'false',
description : 'build with gpgme/GnuPG support (true|false|path to gpgme-config, default=false)')
+option('autocrypt',
+ type : 'string',
+ value : 'false',
+ description : 'build with Autocrypt support (see https://autocrypt.org/), default=no, requires gpgme and
sqlite3')
+
option('canberra',
type : 'boolean',
value : false,
diff --git a/src/balsa-message.c b/src/balsa-message.c
index 1ad4ec1dc..b95d3cde0 100644
--- a/src/balsa-message.c
+++ b/src/balsa-message.c
@@ -39,6 +39,7 @@
#include "balsa-mime-widget-message.h"
#include "balsa-mime-widget-image.h"
#include "balsa-mime-widget-crypto.h"
+#include "autocrypt.h"
#include <gdk/gdkkeysyms.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
@@ -162,6 +163,9 @@ static GdkPixbuf * get_crypto_content_icon(LibBalsaMessageBody * body,
static void message_recheck_crypto_cb(GtkWidget * button, BalsaMessage * bm);
#endif /* HAVE_GPGME */
+#ifdef ENABLE_AUTOCRYPT
+static inline gboolean autocrypt_in_use(void);
+#endif
static void
balsa_part_info_class_init(BalsaPartInfoClass *klass)
@@ -1216,8 +1220,22 @@ balsa_message_set(BalsaMessage * bm, LibBalsaMailbox * mailbox, guint msgno)
* present.
*
*/
- if (is_new && message->headers->dispnotify_to)
+ if (is_new && message->headers->dispnotify_to) {
handle_mdn_request (balsa_get_parent_window(GTK_WIDGET(bm)), message);
+ }
+
+#ifdef ENABLE_AUTOCRYPT
+ /* check for Autocrypt information if the message is new only */
+ if (is_new && autocrypt_in_use()) {
+ GError *error = NULL;
+
+ autocrypt_from_message(message, &error);
+ if (error != NULL) {
+ libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Autocrypt error: %s"), error->message);
+ }
+ g_clear_error(&error);
+ }
+#endif
if (!gtk_tree_model_get_iter_first (gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
&iter))
@@ -3320,3 +3338,17 @@ balsa_message_find_in_message(BalsaMessage * bm)
gtk_widget_grab_focus(bm->find_entry);
}
}
+
+#ifdef ENABLE_AUTOCRYPT
+static inline gboolean
+autocrypt_in_use(void)
+{
+ gboolean result = FALSE;
+ GList *ident;
+
+ for (ident = balsa_app.identities; !result && (ident != NULL); ident = ident->next) {
+ result = LIBBALSA_IDENTITY(ident->data)->autocrypt_mode != AUTOCRYPT_DISABLE;
+ }
+ return result;
+}
+#endif
diff --git a/src/balsa-mime-widget-crypto.c b/src/balsa-mime-widget-crypto.c
index 49abdae80..d2c6121f4 100644
--- a/src/balsa-mime-widget-crypto.c
+++ b/src/balsa-mime-widget-crypto.c
@@ -120,17 +120,33 @@ balsa_mime_widget_signature_widget(LibBalsaMessageBody * mime_body,
gtk_widget_set_halign(label, GTK_ALIGN_START);
gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
if (mime_body->sig_info->protocol == GPGME_PROTOCOL_OpenPGP) {
+ GtkWidget *hbox;
GtkWidget *button;
+ hbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_EXPAND);
+ gtk_box_set_spacing(GTK_BOX(hbox), BMW_HBOX_SPACE);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
if (mime_body->sig_info->status == GPG_ERR_NO_PUBKEY) {
+#ifdef ENABLE_AUTOCRYPT
+ GBytes *autocrypt_key;
+
+ autocrypt_key = autocrypt_get_key(mime_body->sig_info->fingerprint, NULL);
+ if (autocrypt_key != NULL) {
+ button = gtk_button_new_with_mnemonic(_("_Import Autocrypt key"));
+ g_object_set_data_full(G_OBJECT(button), "autocrypt_key", autocrypt_key,
(GDestroyNotify) g_bytes_unref);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(on_key_import_button), NULL);
+ gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
+ }
+#endif
button = gtk_button_new_with_mnemonic(_("_Search key server for this key"));
} else {
button = gtk_button_new_with_mnemonic(_("_Search key server for updates of this key"));
}
g_signal_connect(G_OBJECT(button), "clicked",
G_CALLBACK(on_gpg_key_button),
- (gpointer)mime_body->sig_info->fingerprint);
- gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0);
+ (gpointer) mime_body->sig_info->fingerprint);
+ gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
}
/* Hack alert: if we omit the box below and use the expander as signature widget
@@ -241,17 +257,27 @@ on_key_import_button(GtkButton *button,
gpointer user_data)
{
gpgme_ctx_t ctx;
- gboolean success;
+ gboolean success = FALSE;
GError *error = NULL;
gchar *import_info = NULL;
GtkWidget *dialog;
ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, &error);
if (ctx != NULL) {
- success = libbalsa_gpgme_import_ascii_key(ctx, g_object_get_data(G_OBJECT(button),
"keydata"), &import_info, &error);
+ const gchar *keydata;
+
+ keydata = g_object_get_data(G_OBJECT(button), "keydata");
+ if (keydata != NULL) {
+ success = libbalsa_gpgme_import_ascii_key(ctx, keydata, &import_info, &error);
+ } else {
+ GBytes *key_buf;
+
+ key_buf = (GBytes *) g_object_get_data(G_OBJECT(button), "autocrypt_key");
+ if (key_buf != NULL) {
+ success = libbalsa_gpgme_import_bin_key(ctx, key_buf, &import_info, &error);
+ }
+ }
gpgme_release(ctx);
- } else {
- success = FALSE;
}
if (success) {
@@ -266,7 +292,7 @@ on_key_import_button(GtkButton *button,
GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(),
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
- _("Error importing key data: %s"), error->message);
+ _("Error importing key data: %s"), (error != NULL) ? error->message : _("unknown
error"));
g_clear_error(&error);
}
g_free(import_info);
@@ -328,6 +354,7 @@ create_import_keys_widget(GtkBox *box, const gchar *key_buf, GError **error)
libbalsa_delete_directory_contents(temp_dir);
g_rmdir(temp_dir);
+ g_free(temp_dir);
}
gpgme_release(ctx);
diff --git a/src/main-window.c b/src/main-window.c
index 10b64d0b0..9529e1154 100644
--- a/src/main-window.c
+++ b/src/main-window.c
@@ -896,6 +896,16 @@ address_book_activated(GSimpleAction * action,
gtk_widget_show(GTK_WIDGET(ab));
}
+#ifdef ENABLE_AUTOCRYPT
+static void
+autocrypt_db_activated(GSimpleAction G_GNUC_UNUSED *action,
+ GVariant G_GNUC_UNUSED *parameter,
+ gpointer user_data)
+{
+ autocrypt_db_dialog_run(balsa_app.date_string, GTK_WINDOW(user_data));
+}
+#endif
+
static void
prefs_activated(GSimpleAction * action,
GVariant * parameter,
@@ -1890,6 +1900,9 @@ bw_add_app_action_entries(GActionMap * action_map, gpointer user_data)
{"toolbars", toolbars_activated},
{"identities", identities_activated},
{"address-book", address_book_activated},
+#ifdef ENABLE_AUTOCRYPT
+ {"autocrypt-db", autocrypt_db_activated},
+#endif
{"prefs", prefs_activated},
{"help", help_activated},
{"about", about_activated},
@@ -1912,6 +1925,9 @@ bw_add_win_action_entries(GActionMap * action_map)
{"page-setup", page_setup_activated},
{"print", print_activated},
{"address-book", address_book_activated},
+#ifdef ENABLE_AUTOCRYPT
+ {"autocrypt-db", autocrypt_db_activated},
+#endif
{"quit", quit_activated},
{"copy", copy_activated},
{"select-all", select_all_activated},
diff --git a/src/main.c b/src/main.c
index 24243f731..343b280a4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -47,6 +47,7 @@
#include "information.h"
#include "imap-server.h"
#include "libbalsa-conf.h"
+#include "autocrypt.h"
#include "libinit_balsa/assistant_init.h"
@@ -503,6 +504,9 @@ balsa_startup_cb(GApplication *application,
gpointer user_data)
{
gchar *default_icon;
+#ifdef ENABLE_AUTOCRYPT
+ GError *error = NULL;
+#endif
#ifdef ENABLE_NLS
/* Initialize the i18n stuff */
@@ -537,6 +541,13 @@ balsa_startup_cb(GApplication *application,
libbalsa_mailbox_date_format = &balsa_app.date_string;
+#ifdef ENABLE_AUTOCRYPT
+ if (!autocrypt_init(&error)) {
+ libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Autocrypt error: %s"), error->message);
+ g_error_free(error);
+ }
+#endif
+
/* checking for valid config files */
config_init(cmd_get_stats);
diff --git a/src/sendmsg-window.c b/src/sendmsg-window.c
index 5e39b2715..d47b709c4 100644
--- a/src/sendmsg-window.c
+++ b/src/sendmsg-window.c
@@ -79,6 +79,11 @@
#if HAVE_GTKSOURCEVIEW
#include <gtksourceview/gtksource.h>
#endif /* HAVE_GTKSOURCEVIEW */
+#ifdef ENABLE_AUTOCRYPT
+#include "autocrypt.h"
+#include "libbalsa-gpgme.h"
+#include "libbalsa-gpgme-keys.h"
+#endif /* ENABLE_AUTOCRYPT */
typedef struct {
pid_t pid_editor;
@@ -5045,6 +5050,58 @@ subject_not_empty(BalsaSendmsg * bsmsg)
}
#ifdef HAVE_GPGME
+
+static void
+config_dlg_button(GtkDialog *dialog, gint response_id, const gchar *icon_id)
+{
+ GtkWidget *button;
+
+ button = gtk_dialog_get_widget_for_response(dialog, response_id);
+ if (button != NULL) {
+ GtkWidget *image;
+
+ image = gtk_image_new_from_icon_name(icon_id, GTK_ICON_SIZE_BUTTON);
+ gtk_button_set_image(GTK_BUTTON(button), image);
+ }
+}
+
+static gboolean
+run_check_encrypt_dialog(BalsaSendmsg *bsmsg, const gchar *secondary_msg, gint default_button)
+{
+ GtkWidget *dialog;
+ gboolean result = TRUE;
+ gint choice;
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(bsmsg->window),
+ GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL | libbalsa_dialog_flags(),
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_NONE,
+ _("Message could be encrypted"));
+ gtk_message_dialog_format_secondary_markup(GTK_MESSAGE_DIALOG(dialog), "%s", secondary_msg);
+ gtk_dialog_add_buttons(GTK_DIALOG(dialog),
+ _("Send _encrypted"), GTK_RESPONSE_YES,
+ _("Send _unencrypted"), GTK_RESPONSE_NO,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), default_button);
+
+ /* add button images */
+ config_dlg_button(GTK_DIALOG(dialog), GTK_RESPONSE_YES, balsa_icon_id(BALSA_PIXMAP_GPG_ENCRYPT));
+ config_dlg_button(GTK_DIALOG(dialog), GTK_RESPONSE_NO, balsa_icon_id(BALSA_PIXMAP_SEND));
+
+ choice = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (choice == GTK_RESPONSE_YES) {
+ bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_ENCRYPT);
+ } else if ((choice == GTK_RESPONSE_CANCEL) || (choice == GTK_RESPONSE_DELETE_EVENT)) {
+ result = FALSE;
+ } else {
+ /* nothing to do */
+ }
+
+ return result;
+}
+
static gboolean
check_suggest_encryption(BalsaSendmsg * bsmsg)
{
@@ -5052,9 +5109,10 @@ check_suggest_encryption(BalsaSendmsg * bsmsg)
gboolean can_encrypt;
gpgme_protocol_t protocol;
gint len;
+ gboolean result = TRUE;
/* check if the user wants to see the message */
- if (!bsmsg->ident->warn_send_plain)
+ if ((bsmsg->ident == NULL) || !bsmsg->ident->warn_send_plain)
return TRUE;
/* nothing to do if encryption is already enabled */
@@ -5087,77 +5145,168 @@ check_suggest_encryption(BalsaSendmsg * bsmsg)
g_object_unref(ia_list);
}
- /* ask the user if we could encrypt this message */
+ /* ask the user if we should encrypt this message */
if (can_encrypt) {
- GtkWidget *dialog;
- gint choice;
- GtkWidget *button;
- GtkWidget *hbox;
- GtkWidget *image;
- GtkWidget *label;
+ gchar *message;
- dialog = gtk_message_dialog_new
- (GTK_WINDOW(bsmsg->window),
- GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
- GTK_MESSAGE_QUESTION,
- GTK_BUTTONS_NONE,
- _("You did not select encryption for this message, although "
- "%s public keys are available for all recipients. In order "
- "to protect your privacy, the message could be %s encrypted."),
- gpgme_get_protocol_name(protocol),
- gpgme_get_protocol_name(protocol));
-#if HAVE_MACOSX_DESKTOP
- libbalsa_macosx_menu_for_parent(dialog, GTK_WINDOW(bsmsg->window));
-#endif
+ message = g_markup_printf_escaped(_("You did not select encryption for this message, although "
+ "%s public keys are available for all recipients. In order "
+ "to protect your privacy, the message could be %s encrypted."),
+ gpgme_get_protocol_name(protocol), gpgme_get_protocol_name(protocol));
+ result = run_check_encrypt_dialog(bsmsg, message, GTK_RESPONSE_YES);
+ g_free(message);
+ }
+ return result;
+}
+#ifdef ENABLE_AUTOCRYPT
+static gboolean
+import_autocrypt_keys(GList *missing_keys, GError **error)
+{
+ gpgme_ctx_t ctx;
+ gboolean result;
- button = gtk_button_new();
- gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_YES);
- gtk_widget_set_can_default(button, TRUE);
- gtk_widget_grab_focus(button);
-
- hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
- gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(hbox, GTK_ALIGN_CENTER);
- gtk_container_add(GTK_CONTAINER(button), hbox);
- image = gtk_image_new_from_icon_name(balsa_icon_id(BALSA_PIXMAP_GPG_ENCRYPT),
- GTK_ICON_SIZE_BUTTON);
- gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
- label = gtk_label_new_with_mnemonic(_("Send _encrypted"));
- gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
- gtk_widget_show_all(button);
-
- button = gtk_button_new();
- gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_NO);
- gtk_widget_set_can_default(button, TRUE);
-
- hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
- gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(hbox, GTK_ALIGN_CENTER);
- gtk_container_add(GTK_CONTAINER(button), hbox);
- image = gtk_image_new_from_icon_name(balsa_icon_id(BALSA_PIXMAP_SEND),
- GTK_ICON_SIZE_BUTTON);
- gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
- label = gtk_label_new_with_mnemonic(_("Send _unencrypted"));
- gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
- gtk_widget_show_all(button);
-
- button = gtk_button_new_with_mnemonic(_("_Cancel"));
- gtk_widget_show(button);
- gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button, GTK_RESPONSE_CANCEL);
- gtk_widget_set_can_default(button, TRUE);
+ ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_OpenPGP, NULL, NULL, error);
+ if (ctx != NULL) {
+ GList *key;
- choice = gtk_dialog_run(GTK_DIALOG(dialog));
- gtk_widget_destroy(dialog);
- if (choice == GTK_RESPONSE_YES)
- bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_ENCRYPT);
- else if (choice == GTK_RESPONSE_CANCEL || choice == GTK_RESPONSE_DELETE_EVENT)
- return FALSE;
+ result = TRUE;
+ for (key = missing_keys; result && (key != NULL); key = key->next) {
+ result = libbalsa_gpgme_import_bin_key(ctx, (GBytes *) key->data, NULL, error);
+ }
+ gpgme_release(ctx);
+ } else {
+ result = FALSE;
+ }
+
+ return result;
+}
+
+static gboolean
+check_autocrypt_recommendation(BalsaSendmsg *bsmsg)
+{
+ InternetAddressList *check_list;
+ InternetAddressList *tmp_list;
+ gint len;
+ AutocryptRecommend autocrypt_mode;
+ GList *missing_keys = NULL;
+ GError *error = NULL;
+ gboolean result;
+
+ /* check if autocrypt is enabled, use the non-Autocrypt approach if not */
+ if ((bsmsg->ident == NULL) || (bsmsg->ident->autocrypt_mode == AUTOCRYPT_DISABLE)) {
+ return check_suggest_encryption(bsmsg);
+ }
+
+ /* nothing to do if encryption is already enabled or if S/MIME mode is selected */
+ if ((bsmsg->gpg_mode & (LIBBALSA_PROTECT_ENCRYPT | LIBBALSA_PROTECT_SMIMEV3)) != 0) {
+ return TRUE;
}
- return TRUE;
+ /* we can not encrypt if we have bcc recipients */
+ tmp_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "BCC:");
+ len = internet_address_list_length(tmp_list);
+ g_object_unref(tmp_list);
+ if (len > 0) {
+ return TRUE;
+ }
+
+ /* get the Autocrypt recommendation for all To: and Cc: addresses */
+ check_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "To:");
+ tmp_list = libbalsa_address_view_get_list(bsmsg->recipient_view, "CC:");
+ internet_address_list_append(check_list, tmp_list);
+ g_object_unref(tmp_list);
+ internet_address_list_add(check_list, bsmsg->ident->ia); /* validates that we have a key for
the current identity */
+ autocrypt_mode = autocrypt_recommendation(check_list, &missing_keys, &error);
+ g_object_unref(check_list);
+
+ /* eject on error or disabled */
+ if (autocrypt_mode <= AUTOCRYPT_ENCR_DISABLE) {
+ if (autocrypt_mode == AUTOCRYPT_ENCR_ERROR) {
+ libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("error checking Autocrypt keys: %s"),
+ (error != NULL) ? error->message : _("unknown"));
+ g_clear_error(&error);
+ result = FALSE;
+ } else {
+ result = TRUE;
+ }
+ } else {
+ gchar *message;
+ const gchar *protoname;
+ gint default_choice;
+
+ protoname = gpgme_get_protocol_name(GPGME_PROTOCOL_OpenPGP);
+ message = g_markup_printf_escaped(_("You did not select encryption for this message, although
"
+ "%s public keys are available for all recipients. In order "
+ "to protect your privacy, the message could be %s encrypted."),
+ protoname, protoname);
+
+ /* default to encryption if all participants have prefer-encrypt=mutual, or if we reply to an
encrypted message */
+ if (((autocrypt_mode == AUTOCRYPT_ENCR_AVAIL_MUTUAL) && (bsmsg->ident->autocrypt_mode ==
AUTOCRYPT_PREFER_ENCRYPT)) ||
+ ((bsmsg->parent_message != NULL) && (bsmsg->parent_message->prot_state ==
LIBBALSA_MSG_PROTECT_CRYPT))) {
+ default_choice = GTK_RESPONSE_YES;
+ } else if (autocrypt_mode == AUTOCRYPT_ENCR_AVAIL) {
+ default_choice = GTK_RESPONSE_NO;
+ } else { /* autocrypt_mode == AUTOCRYPT_ENCR_DISCOURAGE */
+ gchar *tmp_msg;
+
+ default_choice = GTK_RESPONSE_NO;
+ tmp_msg = g_strconcat(message,
+ _("\nHowever, encryption is discouraged as the Autocrypt status indicates that "
+ "some recipients <i>might</i> no be able to read the message."), NULL);
+ g_free(message);
+ message = tmp_msg;
+ }
+
+ /* add a note if keys are imported into the key ring */
+ if (missing_keys != NULL) {
+ guint key_count;
+ gchar *key_msg;
+ gchar *tmp_msg;
+
+ key_count = g_list_length(missing_keys);
+ key_msg = g_strdup_printf(ngettext("<i>Note:</i> choosing encryption will import %u key from "
+ "the Autocrypt database
into the GnuPG key ring.",
+ "<i>Note:</i>
choosing encryption will import %u keys from "
+ "the Autocrypt database
into the GnuPG key ring.", key_count), key_count);
+ tmp_msg = g_strconcat(message, "\n", key_msg, NULL);
+ g_free(message);
+ g_free(key_msg);
+ message = tmp_msg;
+ }
+
+ /* run the dialog */
+ result = run_check_encrypt_dialog(bsmsg, message, default_choice);
+
+ if (result && ((bsmsg->gpg_mode & LIBBALSA_PROTECT_ENCRYPT) != 0)) {
+ /* make sure the message is also signed as required by the Autocrypt standard, and that a
protocol is selected */
+ if ((bsmsg->gpg_mode & LIBBALSA_PROTECT_PROTOCOL) == 0) {
+ bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | (LIBBALSA_PROTECT_RFC3156 +
LIBBALSA_PROTECT_SIGN));
+ } else {
+ bsmsg_setup_gpg_ui_by_mode(bsmsg, bsmsg->gpg_mode | LIBBALSA_PROTECT_SIGN);
+ }
+
+ /* import any missing keys */
+ if (missing_keys != NULL) {
+ result = import_autocrypt_keys(missing_keys, &error);
+ if (!result) {
+ libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("Cannot import Autocrypt
keys: %s"), error->message);
+ g_clear_error(&error);
+ }
+ }
+ }
+ }
+
+ /* clean up the missing keys list */
+ if (missing_keys != NULL) {
+ g_list_free_full(missing_keys, (GDestroyNotify) g_bytes_unref);
+ }
+
+ return result;
}
-#endif
+#endif /* ENABLE_AUTOCRYPT */
+#endif /* HAVE_GPGME */
+
/* "send message" menu and toolbar callback.
*/
@@ -5179,8 +5328,15 @@ send_message_handler(BalsaSendmsg * bsmsg, gboolean queue_only)
return FALSE;
#ifdef HAVE_GPGME
- if (!check_suggest_encryption(bsmsg))
- return FALSE;
+#ifdef ENABLE_AUTOCRYPT
+ if (!check_autocrypt_recommendation(bsmsg)) {
+ return FALSE;
+ }
+#else
+ if (!check_suggest_encryption(bsmsg)) {
+ return FALSE;
+ }
+#endif /* ENABLE_AUTOCRYPT */
if ((bsmsg->gpg_mode & LIBBALSA_PROTECT_OPENPGP) != 0) {
gboolean warn_mp;
diff --git a/ui/main-window.ui b/ui/main-window.ui
index 548da3366..4fdd3ef80 100644
--- a/ui/main-window.ui
+++ b/ui/main-window.ui
@@ -59,6 +59,11 @@
translatable='yes'>_Address Book</attribute>
<attribute name='action'>app.address-book</attribute>
</item>
+ <item>
+ <attribute name='label'
+ translatable='yes'>A_utocrypt Database</attribute>
+ <attribute name='action'>app.autocrypt-db</attribute>
+ </item>
</section>
<section>
<item>
@@ -139,6 +144,11 @@
<attribute name='action'>win.address-book</attribute>
<attribute name='accel'>b</attribute>
</item>
+ <item>
+ <attribute name='label'
+ translatable='yes'>A_utocrypt Database</attribute>
+ <attribute name='action'>win.autocrypt-db</attribute>
+ </item>
</section>
<section>
<item>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]