Camel: GPG message decryption can sometimes miss content
Hello!
We have been trying to chase down an issue in Chatty where decrypting PGP messages with Camel fails sometimes: World/Chatty#835
@Lihis was super helpful in figuring out we can reproduce it by only using one core: taskset 1 meson test -C _build --gdb "pgp*"
. As far as we can tell, we are doing everything right, and I have narrowed it down to a reproducible test case:
/* pgp.c
*
* Copyright (C) 1999-2008 Novell, Inc.
* 2023 Purism SPC
* 2023 Chris Talbot
*
* Author(s):
* Chris Talbot <chris@talbothome.com>
*
* Adapted from:
* https://gitlab.gnome.org/GNOME/evolution-data-server/-/blob/master/src/camel/tests/smime/
* https://gitlab.gnome.org/GNOME/evolution/-/tree/master/src/composer
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#undef NDEBUG
#undef G_DISABLE_ASSERT
#undef G_DISABLE_CHECKS
#undef G_DISABLE_CAST_CHECKS
#undef G_LOG_DOMAIN
#define PGP_TEST_DATA_DIR SOURCE_DIR "/tests/pgp"
#define PGP_TEST_BUILD_DIR BUILD_DIR "/pgp"
#include <glib.h>
#include <gio/gio.h>
#include <camel/camel.h>
#include <libedataserver/libedataserver.h>
static char *
chatty_pgp_get_data_dir (void)
{
char *chatty_data_dir = NULL;
chatty_data_dir = g_build_filename (g_get_user_data_dir (), "chatty", "pgp", NULL);
g_mkdir_with_parents (chatty_data_dir, 0700);
return chatty_data_dir;
}
static char *
chatty_pgp_get_tmp_dir (void)
{
char *chatty_tmp_dir = NULL;
chatty_tmp_dir = g_build_filename (g_get_tmp_dir (), "chatty", "pgp", NULL);
g_mkdir_with_parents (chatty_tmp_dir, 0700);
return chatty_tmp_dir;
}
static CamelSession *
chatty_pgp_create_camel_session (void)
{
g_autofree char *chatty_cache_dir = NULL;
g_autofree char *chatty_data_dir = NULL;
chatty_cache_dir = chatty_pgp_get_tmp_dir ();
chatty_data_dir = chatty_pgp_get_data_dir ();
return g_object_new (CAMEL_TYPE_SESSION,
"user-data-dir", chatty_cache_dir,
"user-cache-dir", chatty_data_dir,
NULL);
}
static CamelCipherContext *
chatty_pgp_create_camel_ctx (CamelSession *session)
{
CamelCipherContext *ctx;
ctx = camel_gpg_context_new (session);
camel_gpg_context_set_always_trust (CAMEL_GPG_CONTEXT (ctx), TRUE);
return ctx;
}
static CamelMimePart *
chatty_pgp_decrypt_mime_part (CamelMimePart *encpart)
{
CamelSession *session;
CamelCipherContext *ctx;
g_autoptr(GError) error = NULL;
CamelMimePart *outpart = NULL;
g_autoptr(CamelCipherValidity) valid = NULL;
session = chatty_pgp_create_camel_session ();
ctx = chatty_pgp_create_camel_ctx (session);
outpart = camel_mime_part_new ();
valid = camel_cipher_context_decrypt_sync (ctx, encpart, outpart, NULL, &error);
if (error != NULL) {
g_warning ("PGP decryption failed: '%s'", error->message);
g_object_unref (outpart);
encpart = NULL;
goto out;
}
out:
g_clear_object (&session);
g_clear_object (&ctx);
return outpart;
}
static CamelMimePart *
chatty_pgp_decrypt_stream (const char *data_to_check)
{
CamelStream *plaintext_stream;
CamelMimePart *conpart, *outpart;
if (!data_to_check || !*data_to_check)
return NULL;
plaintext_stream = camel_stream_mem_new ();
conpart = camel_mime_part_new ();
camel_stream_write (plaintext_stream, data_to_check, strlen(data_to_check), NULL, NULL);
g_seekable_seek (G_SEEKABLE (plaintext_stream), 0, G_SEEK_SET, NULL, NULL);
camel_data_wrapper_construct_from_stream_sync ((CamelDataWrapper *) conpart, plaintext_stream, NULL, NULL);
outpart = chatty_pgp_decrypt_mime_part (conpart);
g_clear_object (&plaintext_stream);
g_clear_object (&conpart);
return outpart;
}
static GByteArray *
chatty_pgp_get_message (CamelDataWrapper *dw)
{
GByteArray *buffer, *to_return;
CamelStream *stream;
buffer = g_byte_array_new ();
to_return = g_byte_array_new ();
stream = camel_stream_mem_new_with_byte_array (buffer);
camel_data_wrapper_write_to_stream_sync (dw, stream, NULL, NULL);
/*
* Then the stream is freed, so is the underlying data, so you have to
* Manually copy it.
*
* There will not be a `\0` in the data wrapper.
*/
g_byte_array_append (to_return, (unsigned char *) buffer->data, buffer->len);
g_object_unref (stream);
return to_return;
}
static char *
chatty_pgp_get_content (CamelMimePart *mime_part,
GList **files,
const char *directory_to_save_in)
{
GByteArray *stream_decoded = NULL;
char *message_to_return = NULL;
CamelDataWrapper *dw = NULL;
CamelContentType *content_type;
dw = camel_medium_get_content ((CamelMedium *) mime_part);
content_type = camel_mime_part_get_content_type (mime_part);
if (camel_content_type_is (content_type, "text", "*")) {
stream_decoded = chatty_pgp_get_message (dw);
message_to_return = g_strndup ((char *) stream_decoded->data, stream_decoded->len);
g_byte_array_unref (stream_decoded);
}
return message_to_return;
}
static void
test_pgp_message (void)
{
const char *stream_encrypted = "Hello, I am a test stream. I should be encrypted";
CamelMimePart *decrypt_part = NULL;
g_autofree char *signature = NULL;
g_autofree char *encrypted_msg = NULL;
g_autofree char *signed_and_encrypted_msg = NULL;
g_autofree char *recipients_to_check = NULL;
char *content_to_check = NULL;
GFile *encrypted_file = NULL;
g_autofree char *encrypted_file_path = NULL;
g_autofree char *pdu = NULL;
gsize len = 0;
g_autoptr(GError) error = NULL;
encrypted_file_path = g_build_filename (PGP_TEST_DATA_DIR, "message.txt", NULL);
g_message ("%s", encrypted_file_path);
encrypted_file = g_file_new_for_path (encrypted_file_path);
g_file_load_contents (encrypted_file, NULL, &pdu, &len, NULL, &error);
decrypt_part = chatty_pgp_decrypt_stream (pdu);
content_to_check = chatty_pgp_get_content (decrypt_part, NULL, NULL);
g_assert_cmpstr (content_to_check, ==, stream_encrypted);
}
int
main (int argc,
char *argv[])
{
int ret;
g_autofree char *gnupg_directory = NULL;
g_test_init (&argc, &argv, NULL);
gnupg_directory = g_build_filename (PGP_TEST_BUILD_DIR, ".gnupg", NULL);
if (g_mkdir_with_parents (gnupg_directory, 0700) < 0)
g_warning ("Could not create directory '%s'", gnupg_directory);
g_print ("Setting environment: 'GNUPGHOME=%s'\n", gnupg_directory);
g_setenv ("GNUPGHOME", gnupg_directory, 1);
/* You need to add the private-keys-v1.d for this to work on newer versions of gnupg */
g_free (gnupg_directory);
gnupg_directory = g_build_filename (PGP_TEST_BUILD_DIR, ".gnupg", "private-keys-v1.d", NULL);
g_mkdir_with_parents (gnupg_directory, 0700);
if ((ret = system ("gpg < /dev/null > /dev/null 2>&1")) == -1)
return 77;
g_print("Importing keys with 'gpg --import'\n");
ret = system ("gpg --import " PGP_TEST_DATA_DIR "/chatty-test.gpg.pub > /dev/null 2>&1");
if (ret < 0)
g_warning ("Inporting key error: %d", ret);
/* Trust chatty-test key sender@example.com to make signature from it valid */
ret = system ("echo 362E7448A9CEEBD2C045D9AD028782BB929585AA:6: | gpg --import-ownertrust 2>/dev/null");
if (ret < 0)
g_warning ("Setting owner trust error: %d", ret);
g_print ("GPG setup complete. Starting tests..\n");
g_test_add_func ("/pgp/message", test_pgp_message);
return g_test_run ();
}
The encrypted message is:
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="=-GBCfRMSsNQie7OH4c2sX"
--=-GBCfRMSsNQie7OH4c2sX
Content-Type: application/pgp-encrypted
Version: 1
--=-GBCfRMSsNQie7OH4c2sX
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: This is a digitally encrypted message part
-----BEGIN PGP MESSAGE-----
hIwDzBEE+bymYfgBA/91ny7qLqIcTKjsdGtnYemlLZvp/MtiWC4cSY4MgieZzlo1
kmQ3Gd9vWQRS2OeH+CYYlGBSzTYhu7pu7fuuBZ4G9jhvwHV82fQdHplsoSFOrb9R
/TJRbvMMxFuTGjb2fCGrZvmCIsL8RtKaZxXfX6vfSdHLH5Tlj7xc5ywJ0Q6PVoSM
A6QNiCSx0NgjAQP9Fl6fRGK5exfJ3Ku6y/BIgYo5Wi+mfct517vIcCtbnDfGR66V
sLoVmr8MFCiOh7m244DeV1GrLixzpFlpSLzXFMSu4aT803VdR6QAjiMZfhL25tcv
vv4FgDAdJnxZ2WWRQSEsSG+lud0uyymfH4V3Y/5Bdo/c9gZTVPw/1vxx5AiFAYwD
8hAZnpEf2GABDACV/45uYxnAoKaXeurAjgQYWKThPs8Y2NDjgYtjY+BqbbTiYf9N
pMM1pQVbj9aJlxwpgUIrHUdzSAINPYvArrMQlIXfXdxtvWdQEsLKvapnEDQltQcU
jTYdyOEAoQqxd/pfu1edBjgwQfd84C0JkNtKciC1ROb1iRWsweWi+4eQVh6BOmWO
h3HWodsnLs4IAfnafi0iEr4HV1FpuE10gRG14RCHtoYpO0Bdieos/8eNdJLKPSrG
NHZi/9QFz7h0Fn5ojk947tLoQN/jK7t0EeCt9Ib+9VL91D2eT4FQcgqfM2A3aAS/
C137u60olYqWkl8umGavHuZDqiLa9wjmPdca258a3IJZ+Ko1WeykwLQV5Uo3Aar3
qeDVYYeIwKFZzEChzRzwlTfWdEd3jk/27nCiTOZBqAq5PBgZwhny9EXeXWQ229Hd
ofS/yTZTBnoDIWgeu183UMz+lcixUPpcD8mR5PMcmxrt4qIDSU/3bYcikJ62z3jn
3zD/g1Z4PW/QJHjSpgG1j7jA0osXC64aKnKy0PSEzy7pMx1JIDDcuFHHQ48sYrzD
yGgYErPbJYQBdugsYwqyXagONpL1yyYW7lRPC/7SGL79ess0Hha8WwG0HdtB9nug
uVgNXpgJxTe/T1NEob84sLvRfrOYh1haSU3LKFYdW5r2WT4SBgpb+qlybi0V/V6p
Tl4VdOdf/cnz3ca5rhHGjTIVHUJsmQ6MUUl4KPgXzoyegzE=
=xIU0
-----END PGP MESSAGE-----
--=-GBCfRMSsNQie7OH4c2sX--
And the keys can be found here: https://gitlab.gnome.org/World/Chatty/-/tree/main/tests/pgp?ref_type=heads
The error I get is: ERROR:../tests/pgp.c:209:test_pgp_message: assertion failed (content_to_check == stream_encrypted): ("\r\n" == "Hello, I am a test stream. I should be encrypted")
(sorry, I couldn't really figure out how to narrow it down more!).