diff --git a/src/e2ee.rs b/src/e2ee.rs index 2665b1142..5db3bbf41 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -209,29 +209,58 @@ pub async fn try_decrypt( Ok((out_mail, signatures)) } -/// Returns a reference to the encrypted payload and validates the autocrypt structure. -fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> { - ensure!( - mail.ctype.mimetype == "multipart/encrypted", - "Not a multipart/encrypted message: {}", - mail.ctype.mimetype - ); +/// Returns a reference to the encrypted payload of a valid PGP/MIME message. +/// +/// Returns `None` if the message is not a valid PGP/MIME message. +fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { + if mail.ctype.mimetype != "multipart/encrypted" { + return None; + } if let [first_part, second_part] = &mail.subparts[..] { - ensure!( - first_part.ctype.mimetype == "application/pgp-encrypted", - "Invalid Autocrypt Level 1 version part: {:?}", - first_part.ctype, - ); - - ensure!( - second_part.ctype.mimetype == "application/octet-stream", - "Invalid Autocrypt Level 1 encrypted part: {:?}", - second_part.ctype - ); - - Ok(second_part) + if first_part.ctype.mimetype == "application/pgp-encrypted" + && second_part.ctype.mimetype == "application/octet-stream" + { + Some(second_part) + } else { + None + } } else { - bail!("Invalid Autocrypt Level 1 Mime Parts") + None + } +} + +/// Returns a reference to the encrypted payload of a ["Mixed +/// Up"][pgpmime-message-mangling] message. +/// +/// According to [RFC 3156] encrypted messages should have +/// `multipart/encrypted` MIME type and two parts, but Microsoft +/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this +/// structure by changing the type to `multipart/mixed` and prepending +/// an empty part at the start. +/// +/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying +/// "Empty Message", so we don't check its contents at all, checking +/// only for `text/plain` type. +/// +/// Returns `None` if the message is not a "Mixed Up" message. +/// +/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156 +/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html +fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> { + if mail.ctype.mimetype != "multipart/mixed" { + return None; + } + if let [first_part, second_part, third_part] = &mail.subparts[..] { + if first_part.ctype.mimetype == "text/plain" + && second_part.ctype.mimetype == "application/pgp-encrypted" + && third_part.ctype.mimetype == "application/octet-stream" + { + Some(third_part) + } else { + None + } + } else { + None } } @@ -242,12 +271,12 @@ async fn decrypt_if_autocrypt_message( public_keyring_for_validate: Keyring, ret_valid_signatures: &mut HashSet, ) -> Result>> { - let encrypted_data_part = match get_autocrypt_mime(mail) { - Err(_) => { + let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) { + None => { // not an autocrypt mime message, abort and ignore return Ok(None); } - Ok(res) => res, + Some(res) => res, }; info!(context, "Detected Autocrypt-mime message"); @@ -547,4 +576,27 @@ Sent with my Delta Chat Messenger: https://delta.chat"; assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err()); assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap()); } + + #[test] + fn test_mixed_up_mime() -> Result<()> { + // "Mixed Up" mail as received when sending an encrypted + // message using Delta Chat Desktop via ProtonMail IMAP/SMTP + // Bridge. + let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml"); + let mail = mailparse::parse_mail(mixed_up_mime)?; + assert!(get_autocrypt_mime(&mail).is_none()); + assert!(get_mixed_up_mime(&mail).is_some()); + + // Same "Mixed Up" mail repaired by Thunderbird 78.9.0. + // + // It added `X-Enigmail-Info: Fixed broken PGP/MIME message` + // header although the repairing is done by the built-in + // OpenPGP support, not Enigmail. + let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml"); + let mail = mailparse::parse_mail(repaired_mime)?; + assert!(get_autocrypt_mime(&mail).is_some()); + assert!(get_mixed_up_mime(&mail).is_none()); + + Ok(()) + } } diff --git a/standards.md b/standards.md index 86156afba..562330e6b 100644 --- a/standards.md +++ b/standards.md @@ -11,7 +11,7 @@ Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf. Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154)) Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177)) Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749)) -End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)) and Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) +End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html) Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx) Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-over-email-specification) Detect mailing list | List-Id ([RFC 2919](https://tools.ietf.org/html/rfc2919)) and Precedence ([RFC 3834](https://tools.ietf.org/html/rfc3834)) diff --git a/test-data/message/protonmail-mixed-up.eml b/test-data/message/protonmail-mixed-up.eml new file mode 100644 index 000000000..1a0105d7d --- /dev/null +++ b/test-data/message/protonmail-mixed-up.eml @@ -0,0 +1,36 @@ +Return-Path: +Delivered-To: bob@example.org +Date: Mon, 29 Mar 2021 11:30:57 +0000 +To: Bob +From: Alice +Reply-To: Alice +Subject: ... +Message-ID: +In-Reply-To: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0" + +This is a multi-part message in MIME format. + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +Empty Message +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0 +Content-Type: application/pgp-encrypted; name=attachment.pgp +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=attachment.pgp + +VmVyc2lvbjogMQ0KDQo= + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0 +Content-Type: application/octet-stream; name=encrypted.asc +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=encrypted.asc + +UEdQIFBBWUxPQUQgV0FTIEhFUkUK + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0-- + diff --git a/test-data/message/protonmail-repaired.eml b/test-data/message/protonmail-repaired.eml new file mode 100644 index 000000000..eec98022b --- /dev/null +++ b/test-data/message/protonmail-repaired.eml @@ -0,0 +1,31 @@ +Return-Path: +Delivered-To: bob@example.org +Date: Mon, 29 Mar 2021 11:30:57 +0000 +To: Bob +From: Alice +Reply-To: Alice +Subject: ... +Message-ID: +In-Reply-To: +MIME-Version: 1.0 +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0" +X-Enigmail-Info: Fixed broken PGP/MIME message + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0 +Content-Type: application/pgp-encrypted; name=attachment.pgp +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=attachment.pgp + +VmVyc2lvbjogMQ0KDQo= + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0 +Content-Type: application/octet-stream; name=encrypted.asc +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=encrypted.asc + +UEdQIFBBWUxPQUQgV0FTIEhFUkUK + +--b1_01FB8kHjERilpSep0FbmgBMNYR3TvWQ30jPthW5L0-- +