diff --git a/libbalsa/autocrypt.c b/libbalsa/autocrypt.c index 6c6c5f5a7..3cc95bbcf 100644 --- a/libbalsa/autocrypt.c +++ b/libbalsa/autocrypt.c @@ -92,7 +92,11 @@ enum { static void autocrypt_close(void); -static AutocryptData *parse_autocrypt_header(const gchar *value); +static AutocryptData *scan_autocrypt_headers(GList * const header_list, + const gchar *from_addr) + G_GNUC_WARN_UNUSED_RESULT; +static AutocryptData *parse_autocrypt_header(const gchar *value) + G_GNUC_WARN_UNUSED_RESULT; static gboolean eval_autocrypt_attr(const gchar *attr, const gchar *value, gboolean *seen, @@ -196,16 +200,16 @@ autocrypt_from_message(LibBalsaMessage *message, GError **error) { const gchar *from_addr; - GMimeHeaderList *headers; - GMimeHeaderIter iter; - AutocryptData *autocrypt = NULL; + AutocryptData *autocrypt; 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)); + (message->headers->content_type != NULL) && (autocrypt_db != NULL)); // FIXME - we should ignore spam - how can we detect it? - /* check for content types which shall be ignored */ + /* check for content types which shall be ignored + * Note: see Autocrypt Level 1 standard, section 2.3 (https://autocrypt.org/level1.html#updating-autocrypt-peer-state) for + * details about this and the following checks which may result in completely ignoring the message. */ 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)); @@ -231,40 +235,7 @@ autocrypt_from_message(LibBalsaMessage *message, 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; - } - } + autocrypt = scan_autocrypt_headers(message->headers->user_hdrs, from_addr); /* update the database */ G_LOCK(db_mutex); @@ -643,6 +614,58 @@ autocrypt_close(void) } +/** \brief Extract Autocrypt data from message headers + * + * \param header_list list of headers pointing to gchar** (name, value) pairs + * \param from_addr sender mailbox extracted from the From: header + * \return the data extracted from the Autocrypt header, or NULL if no valid data is present + * + * The following rules apply according to the Autocrypt Level 1 standard: + * - invalid Autocrypt headers are just discarded, but checking for more Autocrypt headers continues (see section 2.1 The + * Autocrypt Header, https://autocrypt.org/level1.html#the-autocrypt-header); + * - if the \em addr attribute of an otherwise valid Autocrypt header does not match the mailbox extracted from the From: message + * header, the Autocrypt header shall be treated as being invalid and discarded (see section 2.1); + * - if more than one valid Autocrypt header is present, \em all Autocrypt headers shall be discarded (see section 2.3 Updating + * Autocrypt Peer State, https://autocrypt.org/level1.html#updating-autocrypt-peer-state); + * + * Thus, this function returns a newly allocated Autocrypt data structure iff the passed headers list contains exactly \em one valid + * Autocrypt header. + */ +static AutocryptData * +scan_autocrypt_headers(GList * const header_list, const gchar *from_addr) +{ + GList *header; + AutocryptData *result = NULL; + + for (header = header_list; header != NULL; header = header->next) { + const gchar **header_parts = (const gchar **) header->data; + + if ((g_ascii_strcasecmp(header_parts[0], "Autocrypt") == 0) && (header_parts[1] != NULL)) { + AutocryptData *new_data; + + new_data = parse_autocrypt_header(header_parts[1]); + if (new_data != NULL) { + if (result == NULL) { + if (g_ascii_strcasecmp(new_data->addr, from_addr) != 0) { + g_info("Autocrypt header for '%s' in message from '%s', ignore header", new_data->addr, from_addr); + autocrypt_free(new_data); + } else { + result = new_data; + } + } else { + g_info("more than one valid Autocrypt header"); + autocrypt_free(result); + autocrypt_free(new_data); + return NULL; + } + } + } + } + + return result; +} + + static AutocryptData * parse_autocrypt_header(const gchar *value) { @@ -696,10 +719,11 @@ parse_autocrypt_header(const gchar *value) } else { GList *keys = NULL; GError *error = NULL; + guint bad_keys = 0U; 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); + libbalsa_gpgme_list_keys(ctx, &keys, &bad_keys, NULL, FALSE, FALSE, FALSE, &error); if (success && (keys != NULL) && (keys->next == NULL)) { gpgme_key_t key = (gpgme_key_t) keys->data; @@ -708,7 +732,8 @@ parse_autocrypt_header(const gchar *value) new_data->expires = key->subkeys->expires; } } else { - g_warning("Failed to import key data: %s", (error != NULL) ? error->message : "unknown"); + g_warning("Failed to import or list key data for '%s': %s (%u keys, %u bad)", new_data->addr, + (error != NULL) ? error->message : "unknown", (keys != NULL) ? g_list_length(keys) : 0U, bad_keys); } g_clear_error(&error);