diff --git a/CHANGELOG.md b/CHANGELOG.md index c2441250e..f345dd9cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463 ### Changes +- refactorings #3545 ### Fixes - improved error handling for account setup from qrcode #3474 diff --git a/src/decrypt.rs b/src/decrypt.rs new file mode 100644 index 000000000..22e9e94dd --- /dev/null +++ b/src/decrypt.rs @@ -0,0 +1,376 @@ +//! End-to-end decryption support. + +use std::collections::HashSet; + +use anyhow::{Context as _, Result}; +use mailparse::ParsedMail; + +use crate::aheader::Aheader; +use crate::contact::addr_cmp; +use crate::context::Context; +use crate::headerdef::HeaderDef; +use crate::headerdef::HeaderDefMap; +use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; +use crate::keyring::Keyring; +use crate::log::LogExt; +use crate::peerstate::Peerstate; +use crate::pgp; + +/// Tries to decrypt a message, but only if it is structured as an +/// Autocrypt message. +/// +/// Returns decrypted body and a set of valid signature fingerprints +/// if successful. +/// +/// If the message is wrongly signed, this will still return the decrypted +/// message but the HashSet will be empty. +pub async fn try_decrypt( + context: &Context, + mail: &ParsedMail<'_>, + decryption_info: &DecryptionInfo, +) -> Result, HashSet)>> { + // Possibly perform decryption + let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate); + + let context = context; + let encrypted_data_part = match get_autocrypt_mime(mail) + .or_else(|| get_mixed_up_mime(mail)) + .or_else(|| get_attachment_mime(mail)) + { + None => { + // not an autocrypt mime message, abort and ignore + return Ok(None); + } + Some(res) => res, + }; + info!(context, "Detected Autocrypt-mime message"); + let private_keyring: Keyring = Keyring::new_self(context) + .await + .context("failed to get own keyring")?; + + decrypt_part( + encrypted_data_part, + private_keyring, + public_keyring_for_validate, + ) + .await +} + +pub async fn create_decryption_info( + context: &Context, + mail: &ParsedMail<'_>, + message_time: i64, +) -> Result { + let from = mail + .headers + .get_header(HeaderDef::From_) + .and_then(|from_addr| mailparse::addrparse_header(from_addr).ok()) + .and_then(|from| from.extract_single_info()) + .map(|from| from.addr) + .unwrap_or_default(); + + let autocrypt_header = Aheader::from_headers(&from, &mail.headers) + .ok_or_log_msg(context, "Failed to parse Autocrypt header") + .flatten(); + + let peerstate = + get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?; + + Ok(DecryptionInfo { + from, + autocrypt_header, + peerstate, + message_time, + }) +} + +#[derive(Debug)] +pub struct DecryptionInfo { + /// The From address. This is the address from the unnencrypted, outer + /// From header. + pub from: String, + pub autocrypt_header: Option, + /// The peerstate that will be used to validate the signatures + pub peerstate: Option, + /// The timestamp when the message was sent. + /// If this is older than the peerstate's last_seen, this probably + /// means out-of-order message arrival, We don't modify the + /// peerstate in this case. + pub message_time: i64, +} + +/// 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 + } +} + +/// Returns a reference to the encrypted payload of a message turned into attachment. +/// +/// Google Workspace has an option "Append footer" which appends standard footer defined +/// by administrator to all outgoing messages. However, there is no plain text part in +/// encrypted messages sent by Delta Chat, so Google Workspace turns the message into +/// multipart/mixed MIME, where the first part is an empty plaintext part with a footer +/// and the second part is the original encrypted message. +fn get_attachment_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] = &mail.subparts[..] { + if first_part.ctype.mimetype == "text/plain" + && second_part.ctype.mimetype == "multipart/encrypted" + { + get_autocrypt_mime(second_part) + } else { + None + } + } else { + None + } +} + +/// 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[..] { + if first_part.ctype.mimetype == "application/pgp-encrypted" + && second_part.ctype.mimetype == "application/octet-stream" + { + Some(second_part) + } else { + None + } + } else { + None + } +} + +/// Returns Ok(None) if nothing encrypted was found. +async fn decrypt_part( + mail: &ParsedMail<'_>, + private_keyring: Keyring, + public_keyring_for_validate: Keyring, +) -> Result, HashSet)>> { + let data = mail.get_body_raw()?; + + if has_decrypted_pgp_armor(&data) { + let (plain, ret_valid_signatures) = + pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?; + + // Check for detached signatures. + // If decrypted part is a multipart/signed, then there is a detached signature. + let decrypted_part = mailparse::parse_mail(&plain)?; + if let Some((content, valid_detached_signatures)) = + validate_detached_signature(&decrypted_part, &public_keyring_for_validate)? + { + return Ok(Some((content, valid_detached_signatures))); + } else { + // If the message was wrongly or not signed, still return the plain text. + // The caller has to check if the signatures set is empty then. + + return Ok(Some((plain, ret_valid_signatures))); + } + } + + Ok(None) +} + +#[allow(clippy::indexing_slicing)] +fn has_decrypted_pgp_armor(input: &[u8]) -> bool { + if let Some(index) = input.iter().position(|b| *b > b' ') { + if input.len() - index > 26 { + let start = index; + let end = start + 27; + + return &input[start..end] == b"-----BEGIN PGP MESSAGE-----"; + } + } + + false +} + +/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847. +/// +/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key +/// fingerprints for which there is a valid signature. +fn validate_detached_signature( + mail: &ParsedMail<'_>, + public_keyring_for_validate: &Keyring, +) -> Result, HashSet)>> { + if mail.ctype.mimetype != "multipart/signed" { + return Ok(None); + } + + if let [first_part, second_part] = &mail.subparts[..] { + // First part is the content, second part is the signature. + let content = first_part.raw_bytes; + let signature = second_part.get_body_raw()?; + let ret_valid_signatures = + pgp::pk_validate(content, &signature, public_keyring_for_validate)?; + + Ok(Some((content.to_vec(), ret_valid_signatures))) + } else { + Ok(None) + } +} + +fn keyring_from_peerstate(peerstate: &Option) -> Keyring { + let mut public_keyring_for_validate: Keyring = Keyring::new(); + if let Some(ref peerstate) = *peerstate { + if let Some(key) = &peerstate.public_key { + public_keyring_for_validate.add(key.clone()); + } else if let Some(key) = &peerstate.gossip_key { + public_keyring_for_validate.add(key.clone()); + } + } + public_keyring_for_validate +} + +/// Applies Autocrypt header to Autocrypt peer state and saves it into the database. +/// +/// If we already know this fingerprint from another contact's peerstate, return that +/// peerstate in order to make AEAP work, but don't save it into the db yet. +/// +/// Returns updated peerstate. +pub(crate) async fn get_autocrypt_peerstate( + context: &Context, + from: &str, + autocrypt_header: Option<&Aheader>, + message_time: i64, +) -> Result> { + let mut peerstate; + + // Apply Autocrypt header + if let Some(header) = autocrypt_header { + // The "from_verified_fingerprint" part is for AEAP: + // If we know this fingerprint from another addr, + // we may want to do a transition from this other addr + // (and keep its peerstate) + // For security reasons, for now, we only do a transition + // if the fingerprint is verified. + peerstate = Peerstate::from_verified_fingerprint_or_addr( + context, + &header.public_key.fingerprint(), + from, + ) + .await?; + + if let Some(ref mut peerstate) = peerstate { + if addr_cmp(&peerstate.addr, from) { + peerstate.apply_header(header, message_time); + peerstate.save_to_db(&context.sql, false).await?; + } + // If `peerstate.addr` and `from` differ, this means that + // someone is using the same key but a different addr, probably + // because they made an AEAP transition. + // But we don't know if that's legit until we checked the + // signatures, so wait until then with writing anything + // to the database. + } else { + let p = Peerstate::from_header(header, message_time); + p.save_to_db(&context.sql, true).await?; + peerstate = Some(p); + } + } else { + peerstate = Peerstate::from_addr(context, from).await?; + } + + Ok(peerstate) +} + +#[cfg(test)] +mod tests { + use crate::receive_imf::receive_imf; + use crate::test_utils::TestContext; + + use super::*; + + #[test] + fn test_has_decrypted_pgp_armor() { + let data = b" -----BEGIN PGP MESSAGE-----"; + assert_eq!(has_decrypted_pgp_armor(data), true); + + let data = b" \n-----BEGIN PGP MESSAGE-----"; + assert_eq!(has_decrypted_pgp_armor(data), true); + + let data = b" -----BEGIN PGP MESSAGE---"; + assert_eq!(has_decrypted_pgp_armor(data), false); + + let data = b" -----BEGIN PGP MESSAGE-----"; + assert_eq!(has_decrypted_pgp_armor(data), true); + + let data = b"blas"; + assert_eq!(has_decrypted_pgp_armor(data), false); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async 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()); + assert!(get_attachment_mime(&mail).is_none()); + + // 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()); + assert!(get_attachment_mime(&mail).is_none()); + + // Another form of "Mixed Up" mail created by Google Workspace, + // where original message is turned into attachment to empty plaintext message. + let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml"); + let mail = mailparse::parse_mail(attachment_mime)?; + assert!(get_autocrypt_mime(&mail).is_none()); + assert!(get_mixed_up_mime(&mail).is_none()); + assert!(get_attachment_mime(&mail).is_some()); + + let bob = TestContext::new_bob().await; + receive_imf(&bob, attachment_mime, false).await?; + let msg = bob.get_last_msg().await; + assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!")); + + Ok(()) + } +} diff --git a/src/e2ee.rs b/src/e2ee.rs index ed7b27d11..cd92c5a3d 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -1,20 +1,13 @@ //! End-to-end encryption support. -use std::collections::HashSet; - use anyhow::{format_err, Context as _, Result}; -use mailparse::ParsedMail; use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; use crate::config::Config; -use crate::contact::addr_cmp; use crate::context::Context; -use crate::headerdef::HeaderDef; -use crate::headerdef::HeaderDefMap; -use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; +use crate::key::{DcKey, SignedPublicKey, SignedSecretKey}; use crate::keyring::Keyring; -use crate::log::LogExt; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::pgp; @@ -133,301 +126,6 @@ impl EncryptHelper { } } -/// Applies Autocrypt header to Autocrypt peer state and saves it into the database. -/// -/// If we already know this fingerprint from another contact's peerstate, return that -/// peerstate in order to make AEAP work, but don't save it into the db yet. -/// -/// Returns updated peerstate. -pub(crate) async fn get_autocrypt_peerstate( - context: &Context, - from: &str, - autocrypt_header: Option<&Aheader>, - message_time: i64, -) -> Result> { - let mut peerstate; - - // Apply Autocrypt header - if let Some(header) = autocrypt_header { - // The "from_verified_fingerprint" part is for AEAP: - // If we know this fingerprint from another addr, - // we may want to do a transition from this other addr - // (and keep its peerstate) - // For security reasons, for now, we only do a transition - // if the fingerprint is verified. - peerstate = Peerstate::from_verified_fingerprint_or_addr( - context, - &header.public_key.fingerprint(), - from, - ) - .await?; - - if let Some(ref mut peerstate) = peerstate { - if addr_cmp(&peerstate.addr, from) { - peerstate.apply_header(header, message_time); - peerstate.save_to_db(&context.sql, false).await?; - } - // If `peerstate.addr` and `from` differ, this means that - // someone is using the same key but a different addr, probably - // because they made an AEAP transition. - // But we don't know if that's legit until we checked the - // signatures, so wait until then with writing anything - // to the database. - } else { - let p = Peerstate::from_header(header, message_time); - p.save_to_db(&context.sql, true).await?; - peerstate = Some(p); - } - } else { - peerstate = Peerstate::from_addr(context, from).await?; - } - - Ok(peerstate) -} - -/// Tries to decrypt a message, but only if it is structured as an -/// Autocrypt message. -/// -/// Returns decrypted body and a set of valid signature fingerprints -/// if successful. -/// -/// If the message is wrongly signed, this will still return the decrypted -/// message but the HashSet will be empty. -pub async fn try_decrypt( - context: &Context, - mail: &ParsedMail<'_>, - decryption_info: &DecryptionInfo, -) -> Result, HashSet)>> { - // Possibly perform decryption - let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate); - - let context = context; - let encrypted_data_part = match get_autocrypt_mime(mail) - .or_else(|| get_mixed_up_mime(mail)) - .or_else(|| get_attachment_mime(mail)) - { - None => { - // not an autocrypt mime message, abort and ignore - return Ok(None); - } - Some(res) => res, - }; - info!(context, "Detected Autocrypt-mime message"); - let private_keyring: Keyring = Keyring::new_self(context) - .await - .context("failed to get own keyring")?; - - decrypt_part( - encrypted_data_part, - private_keyring, - public_keyring_for_validate, - ) - .await -} - -pub async fn create_decryption_info( - context: &Context, - mail: &ParsedMail<'_>, - message_time: i64, -) -> Result { - let from = mail - .headers - .get_header(HeaderDef::From_) - .and_then(|from_addr| mailparse::addrparse_header(from_addr).ok()) - .and_then(|from| from.extract_single_info()) - .map(|from| from.addr) - .unwrap_or_default(); - - let autocrypt_header = Aheader::from_headers(&from, &mail.headers) - .ok_or_log_msg(context, "Failed to parse Autocrypt header") - .flatten(); - - let peerstate = - get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?; - - Ok(DecryptionInfo { - from, - autocrypt_header, - peerstate, - message_time, - }) -} - -#[derive(Debug)] -pub struct DecryptionInfo { - /// The From address. This is the address from the unnencrypted, outer - /// From header. - pub from: String, - pub autocrypt_header: Option, - /// The peerstate that will be used to validate the signatures - pub peerstate: Option, - /// The timestamp when the message was sent. - /// If this is older than the peerstate's last_seen, this probably - /// means out-of-order message arrival, We don't modify the - /// peerstate in this case. - pub message_time: i64, -} - -/// 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[..] { - if first_part.ctype.mimetype == "application/pgp-encrypted" - && second_part.ctype.mimetype == "application/octet-stream" - { - Some(second_part) - } else { - None - } - } else { - 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 - } -} - -/// Returns a reference to the encrypted payload of a message turned into attachment. -/// -/// Google Workspace has an option "Append footer" which appends standard footer defined -/// by administrator to all outgoing messages. However, there is no plain text part in -/// encrypted messages sent by Delta Chat, so Google Workspace turns the message into -/// multipart/mixed MIME, where the first part is an empty plaintext part with a footer -/// and the second part is the original encrypted message. -fn get_attachment_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] = &mail.subparts[..] { - if first_part.ctype.mimetype == "text/plain" - && second_part.ctype.mimetype == "multipart/encrypted" - { - get_autocrypt_mime(second_part) - } else { - None - } - } else { - None - } -} - -fn keyring_from_peerstate(peerstate: &Option) -> Keyring { - let mut public_keyring_for_validate: Keyring = Keyring::new(); - if let Some(ref peerstate) = *peerstate { - if let Some(key) = &peerstate.public_key { - public_keyring_for_validate.add(key.clone()); - } else if let Some(key) = &peerstate.gossip_key { - public_keyring_for_validate.add(key.clone()); - } - } - public_keyring_for_validate -} - -/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847. -/// -/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key -/// fingerprints for which there is a valid signature. -fn validate_detached_signature( - mail: &ParsedMail<'_>, - public_keyring_for_validate: &Keyring, -) -> Result, HashSet)>> { - if mail.ctype.mimetype != "multipart/signed" { - return Ok(None); - } - - if let [first_part, second_part] = &mail.subparts[..] { - // First part is the content, second part is the signature. - let content = first_part.raw_bytes; - let signature = second_part.get_body_raw()?; - let ret_valid_signatures = - pgp::pk_validate(content, &signature, public_keyring_for_validate)?; - - Ok(Some((content.to_vec(), ret_valid_signatures))) - } else { - Ok(None) - } -} - -/// Returns Ok(None) if nothing encrypted was found. -async fn decrypt_part( - mail: &ParsedMail<'_>, - private_keyring: Keyring, - public_keyring_for_validate: Keyring, -) -> Result, HashSet)>> { - let data = mail.get_body_raw()?; - - if has_decrypted_pgp_armor(&data) { - let (plain, ret_valid_signatures) = - pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?; - - // Check for detached signatures. - // If decrypted part is a multipart/signed, then there is a detached signature. - let decrypted_part = mailparse::parse_mail(&plain)?; - if let Some((content, valid_detached_signatures)) = - validate_detached_signature(&decrypted_part, &public_keyring_for_validate)? - { - return Ok(Some((content, valid_detached_signatures))); - } else { - // If the message was wrongly or not signed, still return the plain text. - // The caller has to check if the signatures set is empty then. - - return Ok(Some((plain, ret_valid_signatures))); - } - } - - Ok(None) -} - -#[allow(clippy::indexing_slicing)] -fn has_decrypted_pgp_armor(input: &[u8]) -> bool { - if let Some(index) = input.iter().position(|b| *b > b' ') { - if input.len() - index > 26 { - let start = index; - let end = start + 27; - - return &input[start..end] == b"-----BEGIN PGP MESSAGE-----"; - } - } - - false -} - /// Ensures a private key exists for the configured user. /// /// Normally the private key is generated when the first message is @@ -450,7 +148,6 @@ mod tests { use crate::message::{Message, Viewtype}; use crate::param::Param; use crate::peerstate::ToSave; - use crate::receive_imf::receive_imf; use crate::test_utils::{bob_keypair, TestContext}; use super::*; @@ -498,24 +195,6 @@ Sent with my Delta Chat Messenger: https://delta.chat"; ); } - #[test] - fn test_has_decrypted_pgp_armor() { - let data = b" -----BEGIN PGP MESSAGE-----"; - assert_eq!(has_decrypted_pgp_armor(data), true); - - let data = b" \n-----BEGIN PGP MESSAGE-----"; - assert_eq!(has_decrypted_pgp_armor(data), true); - - let data = b" -----BEGIN PGP MESSAGE---"; - assert_eq!(has_decrypted_pgp_armor(data), false); - - let data = b" -----BEGIN PGP MESSAGE-----"; - assert_eq!(has_decrypted_pgp_armor(data), true); - - let data = b"blas"; - assert_eq!(has_decrypted_pgp_armor(data), false); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> { let alice = TestContext::new_alice().await; @@ -650,42 +329,4 @@ 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()); } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async 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()); - assert!(get_attachment_mime(&mail).is_none()); - - // 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()); - assert!(get_attachment_mime(&mail).is_none()); - - // Another form of "Mixed Up" mail created by Google Workspace, - // where original message is turned into attachment to empty plaintext message. - let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml"); - let mail = mailparse::parse_mail(attachment_mime)?; - assert!(get_autocrypt_mime(&mail).is_none()); - assert!(get_mixed_up_mime(&mail).is_none()); - assert!(get_attachment_mime(&mail).is_some()); - - let bob = TestContext::new_bob().await; - receive_imf(&bob, attachment_mime, false).await?; - let msg = bob.get_last_msg().await; - assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!")); - - Ok(()) - } } diff --git a/src/lib.rs b/src/lib.rs index 12459b3b5..3e99d841e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ mod configure; pub mod constants; pub mod contact; pub mod context; +mod decrypt; pub mod download; mod e2ee; pub mod ephemeral; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 11fba8f6c..1bd19dcee 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -15,8 +15,8 @@ use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; +use crate::decrypt::{create_decryption_info, try_decrypt}; use crate::dehtml::dehtml; -use crate::e2ee; use crate::events::EventType; use crate::format_flowed::unformat_flowed; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -220,12 +220,11 @@ impl MimeMessage { let mut mail_raw = Vec::new(); let mut gossiped_addr = Default::default(); let mut from_is_signed = false; - let mut decryption_info = - e2ee::create_decryption_info(context, &mail, message_time).await?; + let mut decryption_info = create_decryption_info(context, &mail, message_time).await?; // `signatures` is non-empty exactly if the message was encrypted and correctly signed. let (mail, signatures, warn_empty_signature) = - match e2ee::try_decrypt(context, &mail, &decryption_info).await { + match try_decrypt(context, &mail, &decryption_info).await { Ok(Some((raw, signatures))) => { // Encrypted, but maybe unsigned message. Only if // `signatures` set is non-empty, it is a valid diff --git a/src/peerstate.rs b/src/peerstate.rs index cf319d0f8..4865adde0 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -9,6 +9,7 @@ use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::contact::{addr_cmp, Contact, Origin}; use crate::context::Context; +use crate::decrypt::DecryptionInfo; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::Message; @@ -611,7 +612,7 @@ impl Peerstate { /// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP. pub async fn maybe_do_aeap_transition( context: &Context, - info: &mut crate::e2ee::DecryptionInfo, + info: &mut DecryptionInfo, mime_parser: &crate::mimeparser::MimeMessage, ) -> Result<()> { if let Some(peerstate) = &mut info.peerstate {