diff --git a/src/chat.rs b/src/chat.rs index 7c2a855a6b..0ad2fd49d4 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -35,11 +35,13 @@ use crate::download::{ use crate::ensure_and_debug_assert_eq; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; +use crate::key; use crate::key::{Fingerprint, self_fingerprint}; use crate::location; use crate::log::{LogExt, warn}; use crate::logged_debug_assert; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; +use crate::mimefactory; use crate::mimefactory::{MimeFactory, RenderedEmail}; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; @@ -2757,6 +2759,11 @@ async fn render_mime_message_and_pre_message( msg: &mut Message, mimefactory: MimeFactory, ) -> Result<(Option, RenderedEmail)> { + let from_addr = context.get_primary_self_addr().await?; + let public_key = key::load_self_public_key(context).await?; + let secret_key = key::load_self_secret_key(context).await?; + let timestamp = msg.timestamp_sort; + let needs_pre_message = msg.viewtype.has_file() && mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages && msg @@ -2773,15 +2780,32 @@ async fn render_mime_message_and_pre_message( let mut mimefactory_post_msg = mimefactory.clone(); mimefactory_post_msg.set_as_post_message(); - let rendered_msg = Box::pin(mimefactory_post_msg.render(context)) + let (queued_msg, side_effects) = Box::pin(mimefactory_post_msg.pre_render(context)) .await .context("Failed to render post-message")?; + let rendered_msg = mimefactory::render_queued_mail( + queued_msg, + &public_key, + &secret_key, + from_addr.clone(), + timestamp, + side_effects, + )?; + let mut mimefactory_pre_msg = mimefactory; mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg); - let rendered_pre_msg = Box::pin(mimefactory_pre_msg.render(context)) + let (queued_pre_msg, pre_side_effects) = Box::pin(mimefactory_pre_msg.pre_render(context)) .await .context("pre-message failed to render")?; + let rendered_pre_msg = mimefactory::render_queued_mail( + queued_pre_msg, + &public_key, + &secret_key, + from_addr, + timestamp, + pre_side_effects, + )?; if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD { warn!( @@ -2794,7 +2818,17 @@ async fn render_mime_message_and_pre_message( Ok((Some(rendered_pre_msg), rendered_msg)) } else { - Ok((None, Box::pin(mimefactory.render(context)).await?)) + let (queued_msg, side_effects) = Box::pin(mimefactory.pre_render(context)).await?; + let rendered_msg = mimefactory::render_queued_mail( + queued_msg, + &public_key, + &secret_key, + from_addr, + timestamp, + side_effects, + )?; + + Ok((None, rendered_msg)) } } @@ -2839,7 +2873,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Err(err); } }; - let attach_selfavatar = mimefactory.attach_selfavatar; let mut recipients = mimefactory.recipients(); let from = context.get_primary_self_addr().await?; @@ -2926,14 +2959,22 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - let now = time(); - if let Some(last_added_location_timestamp) = rendered_msg.last_added_location_timestamp { + if let Some(last_added_location_timestamp) = + rendered_msg.side_effects.last_added_location_timestamp + { location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp) .await?; } - if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await + if rendered_msg.side_effects.avatar_is_attached + || rendered_pre_msg + .as_ref() + .is_some_and(|msg| msg.side_effects.avatar_is_attached) { - error!(context, "Failed to set selfavatar timestamp: {err:#}."); + msg.chat_id + .set_selfavatar_timestamp(context, now) + .await + .context("Failed to set selfavatar timestamp")?; } if rendered_msg.is_encrypted { @@ -2941,7 +2982,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - } else { msg.param.remove(Param::GuaranteeE2ee); } - msg.subject.clone_from(&rendered_msg.subject); + msg.subject.clone_from(&rendered_msg.side_effects.subject); // Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`. context .sql @@ -2974,7 +3015,7 @@ WHERE id=? let trans_fn = |t: &mut rusqlite::Transaction| { let mut row_ids = Vec::::new(); - if let Some(sync_ids) = rendered_msg.sync_ids_to_delete { + if let Some(sync_ids) = rendered_msg.side_effects.sync_ids_to_delete { t.execute( &format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"), (), diff --git a/src/download.rs b/src/download.rs index b5f9ec0c1d..1a8b0cf4b9 100644 --- a/src/download.rs +++ b/src/download.rs @@ -209,7 +209,7 @@ impl Session { let (sender, receiver) = async_channel::unbounded(); { let _fetch_msgs_lock_guard = context.fetch_msgs_mutex.lock().await; - self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender) + Box::pin(self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, sender)) .await?; } if receiver.recv().await.is_err() { diff --git a/src/e2ee.rs b/src/e2ee.rs index 77606419c2..8e25272ff6 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -1,105 +1,9 @@ //! End-to-end encryption support. -use std::io::Cursor; - -use anyhow::Result; -use mail_builder::mime::MimePart; - -use crate::aheader::{Aheader, EncryptPreference}; -use crate::context::Context; -use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key}; -use crate::pgp::{self, SeipdVersion}; - -#[derive(Debug)] -pub struct EncryptHelper { - pub addr: String, - pub public_key: SignedPublicKey, -} - -impl EncryptHelper { - pub async fn new(context: &Context) -> Result { - let addr = context.get_primary_self_addr().await?; - let public_key = load_self_public_key(context).await?; - - Ok(EncryptHelper { addr, public_key }) - } - - pub fn get_aheader(&self) -> Aheader { - Aheader { - addr: self.addr.clone(), - public_key: self.public_key.clone(), - prefer_encrypt: EncryptPreference::Mutual, - verified: false, - } - } - - /// Tries to encrypt the passed in `mail`. - pub async fn encrypt( - self, - context: &Context, - keyring: Vec, - mail_to_encrypt: MimePart<'static>, - compress: bool, - seipd_version: SeipdVersion, - ) -> Result { - let mut raw_message = Vec::new(); - let cursor = Cursor::new(&mut raw_message); - mail_to_encrypt.clone().write_part(cursor).ok(); - - let ctext = self - .encrypt_raw(context, keyring, raw_message, compress, seipd_version) - .await?; - Ok(ctext) - } - - pub async fn encrypt_raw( - self, - context: &Context, - keyring: Vec, - raw_message: Vec, - compress: bool, - seipd_version: SeipdVersion, - ) -> Result { - let sign_key = load_self_secret_key(context).await?; - let ctext = - pgp::pk_encrypt(raw_message, keyring, sign_key, compress, seipd_version).await?; - - Ok(ctext) - } - - /// Symmetrically encrypt the message. This is used for broadcast channels. - /// `shared secret` is the secret that will be used for symmetric encryption. - pub async fn encrypt_symmetrically( - self, - context: &Context, - shared_secret: &str, - mail_to_encrypt: MimePart<'static>, - compress: bool, - sign: bool, - ) -> Result { - let sign_key = if sign { - Some(load_self_secret_key(context).await?) - } else { - None - }; - - let shared_secret = shared_secret.to_string(); - let mut raw_message = Vec::new(); - let cursor = Cursor::new(&mut raw_message); - mail_to_encrypt.clone().write_part(cursor).ok(); - - let ctext = tokio::task::spawn_blocking(move || { - pgp::symm_encrypt_message(raw_message, sign_key, shared_secret, compress) - }) - .await??; - - Ok(ctext) - } -} - #[cfg(test)] mod tests { - use super::*; + use anyhow::Result; + use crate::chat; use crate::chat::send_text_msg; use crate::config::Config; diff --git a/src/imap.rs b/src/imap.rs index efc6ee0483..6343752354 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -753,8 +753,7 @@ impl Imap { }; let actually_download_messages_future = async { - session - .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender) + Box::pin(session.fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)) .await .context("fetch_many_msgs") }; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3ee7f35920..fa4bebe02e 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -8,6 +8,7 @@ use base64::Engine as _; use data_encoding::BASE32_NOPAD; use deltachat_contact_tools::sanitize_bidi_characters; use iroh_gossip::proto::TopicId; +use mail_builder::headers::Header as _; use mail_builder::headers::HeaderType; use mail_builder::headers::address::Address; use mail_builder::mime::MimePart; @@ -21,11 +22,11 @@ use crate::constants::{BROADCAST_INCOMPATIBILITY_MSG, Chattype, DC_FROM_HANDSHAK use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::download::PostMsgMetadata; -use crate::e2ee::EncryptHelper; use crate::ensure_and_debug_assert; use crate::ephemeral::Timer as EphemeralTimer; use crate::headerdef::HeaderDef; -use crate::key::{DcKey, SignedPublicKey, self_fingerprint}; +use crate::key; +use crate::key::{DcKey, SignedPublicKey, SignedSecretKey, self_fingerprint}; use crate::location; use crate::log::warn; use crate::message::{Message, MsgId, Viewtype}; @@ -142,43 +143,340 @@ pub struct MimeFactory { /// using `Chat-Disposition-Notification-To` header. req_mdn: bool, + /// True if the avatar should be attached. + attach_selfavatar: bool, + + /// This field is used to sustain the topic id of webxdcs needed for peer channels. + webxdc_topic: Option, + + /// Pre-message / post-message / atomic message. + pre_message_mode: PreMessageMode, +} + +/// Result of rendering non-MDN message. +pub struct RenderedMessage { + main_part: MimePart<'static>, + + parts: Vec>, + /// Largest timestamp of the location sent in `location.kml` in this message. last_added_location_timestamp: Option, + /// True if the avatar is attached to the message. + avatar_is_attached: bool, + /// If the created mime-structure contains sync-items, /// the IDs of these items are listed here. /// The IDs are returned via `RenderedEmail` /// and must be deleted if the message is actually queued for sending. sync_ids_to_delete: Option, +} - /// True if the avatar should be attached. - pub attach_selfavatar: bool, +/// Email message queued, but not sent yet. +/// +/// It is stored unencrypted to +/// make it possible to change protected headers +/// like the From address, Autocrypt header +/// and the Date later. +#[derive(Debug, Clone)] +pub(crate) struct QueuedMail { + /// Unencrypted queued message. + /// + /// This message has both the headers and the body, + /// but without the From, Autocrypt and Date headers. + /// + /// For encrypted messages this is the OpenPGP payload. + raw_message: Vec, - /// This field is used to sustain the topic id of webxdcs needed for peer channels. - webxdc_topic: Option, + /// Display name to put in the `From:` field. + /// + /// Email address is not determined yet here. + display_name: String, - /// Pre-message / post-message / atomic message. - pre_message_mode: PreMessageMode, -} + /// Message-ID. + rfc724_mid: String, -/// Result of rendering a message, ready to be submitted to a send job. -#[derive(Debug, Clone)] -pub struct RenderedEmail { - pub message: String, - pub is_encrypted: bool, + /// Public keys to which the message should be encrypted. + encryption_pubkeys: Option>, + + /// Shared secret if the message should be encrypted symmetrically. + shared_secret: Option, + + /// If true, Autocrypt header should be added before sending. + should_attach_pubkey: bool, + /// If true, OpenPGP compression may be used. + should_compress: bool, + + /// If true, encrypted message should be signed as well. + should_sign: bool, +} + +/// Side effects that should be applied at the same time +/// as the message is persisted in the queue. +#[derive(Debug, Clone, Default)] +pub struct RenderSideEffects { /// Largest timestamp of the location sent in `location.kml` in this message. pub last_added_location_timestamp: Option, + /// True if the message has the avatar attached. + /// + /// Timestamp of the last time avatar was gossiped should be updated. + pub avatar_is_attached: bool, + /// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted /// from `multi_device_sync` once the message is actually queued for sending. pub sync_ids_to_delete: Option, + /// Subject that was rendered into the message. + /// + /// Used to update the subject on the sent message object. + pub subject: String, +} + +/// Renders [`QueuedMail`]. +/// +/// Adds headers: +/// - `From` +/// - `Date` +/// - `Autocrypt` +/// - `Message-ID` +/// +/// Encrypts and signs the message if necessary. +pub(crate) fn render_queued_mail( + queued_mail: QueuedMail, + public_key: &SignedPublicKey, + secret_key: &SignedSecretKey, + from_addr: String, + timestamp: i64, + side_effects: RenderSideEffects, +) -> Result { + let QueuedMail { + rfc724_mid, + display_name, + mut raw_message, + encryption_pubkeys, + shared_secret, + should_attach_pubkey, + should_compress, + should_sign, + } = queued_mail; + + let mut inner_headers: Vec = Vec::new(); + let mut outer_headers: Vec = Vec::new(); + + let is_encrypted = encryption_pubkeys.is_some(); + + let from_header = new_address_with_name(&display_name, from_addr.clone()); + inner_headers.extend_from_slice(b"From: "); + from_header.write_header(&mut inner_headers, 6)?; + + if is_encrypted { + let unencrypted_from = Address::new_address(None::<&'static str>, from_addr.to_string()); + outer_headers.extend_from_slice(b"From: "); + unencrypted_from.write_header(&mut outer_headers, 6)?; + + inner_headers.extend_from_slice(b"HP-Outer: From: "); + unencrypted_from.write_header(&mut inner_headers, 16)?; + } else { + outer_headers.extend_from_slice(b"From: "); + from_header.write_header(&mut outer_headers, 6)?; + } + + let date = chrono::DateTime::::from_timestamp(timestamp, 0) + .unwrap() + .to_rfc2822(); + inner_headers.extend_from_slice(b"Date: "); + inner_headers.extend_from_slice(date.as_bytes()); + inner_headers.extend_from_slice(b"\r\n"); + + if is_encrypted { + inner_headers.extend_from_slice(b"HP-Outer: Date: "); + inner_headers.extend_from_slice(date.as_bytes()); + inner_headers.extend_from_slice(b"\r\n"); + + // Randomized date goes to unprotected header. + // + // We cannot just send "Thu, 01 Jan 1970 00:00:00 +0000" + // or omit the header because GMX then fails with + // + // host mx00.emig.gmx.net[212.227.15.9] said: + // 554-Transaction failed + // 554-Reject due to policy restrictions. + // 554 For explanation visit https://postmaster.gmx.net/en/case?... + // (in reply to end of DATA command) + // + // and the explanation page says + // "The time information deviates too much from the actual time". + // + // We also limit the range to 6 days (518400 seconds) + // because with a larger range we got + // error "500 Date header far in the past/future" + // which apparently originates from Symantec Messaging Gateway + // and means the message has a Date that is more + // than 7 days in the past: + // + let timestamp_offset = rand::random_range(0..518400); + let protected_timestamp = timestamp.saturating_sub(timestamp_offset); + let unprotected_date = + chrono::DateTime::::from_timestamp(protected_timestamp, 0) + .unwrap() + .to_rfc2822(); + outer_headers.extend_from_slice(b"Date: "); + outer_headers.extend_from_slice(unprotected_date.as_bytes()); + outer_headers.extend_from_slice(b"\r\n"); + } else { + outer_headers.extend_from_slice(b"Date: "); + outer_headers.extend_from_slice(date.as_bytes()); + outer_headers.extend_from_slice(b"\r\n"); + } + + inner_headers.extend_from_slice(b"Message-ID: <"); + inner_headers.extend_from_slice(rfc724_mid.as_bytes()); + inner_headers.extend_from_slice(b">\r\n"); + outer_headers.extend_from_slice(b"Message-ID: <"); + outer_headers.extend_from_slice(rfc724_mid.as_bytes()); + outer_headers.extend_from_slice(b">\r\n"); + if is_encrypted { + inner_headers.extend_from_slice(b"HP-Outer: Message-ID: <"); + inner_headers.extend_from_slice(rfc724_mid.as_bytes()); + inner_headers.extend_from_slice(b">\r\n"); + } + + // MIME header . + outer_headers.extend_from_slice(b"MIME-Version: 1.0\r\n"); + + if should_attach_pubkey { + let aheader = Aheader { + addr: from_addr.clone(), + public_key: public_key.clone(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false, + }; + let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string()); + if is_encrypted { + inner_headers.extend_from_slice(b"Autocrypt: "); + autocrypt_header.write_header(&mut inner_headers, 11)?; + } else { + outer_headers.extend_from_slice(b"Autocrypt: "); + autocrypt_header.write_header(&mut outer_headers, 11)?; + } + } + + if is_encrypted { + // Copy not protected headers to outer headers. + let (parsed_headers, _index) = mailparse::parse_headers(&raw_message)?; + for parsed_header in parsed_headers { + let original_header_name = parsed_header.get_key(); + let header_name = original_header_name.to_lowercase(); + + if header_name == "mime-version" + || header_name == "content-type" + || header_name == "content-transfer-encoding" + || header_name == "content-disposition" + { + // Structural headers shouldn't be added as "HP-Outer". They are defined in + // . + continue; + } + let header_value = if header_name == "mime-version" + || header_name == "chat-version" + || header_name == "chat-is-post-message" + { + parsed_header.get_value_raw() + } else if header_name == "subject" { + &b"[...]"[..] + } else if header_name == "to" { + &b"To: \"hidden-recipients\""[..] + } else { + continue; + }; + + outer_headers.extend_from_slice(original_header_name.as_bytes()); + outer_headers.extend_from_slice(b": "); + outer_headers.extend_from_slice(header_value); + outer_headers.extend_from_slice(b"\r\n"); + + inner_headers.extend_from_slice(b"HP-Outer: "); + inner_headers.extend_from_slice(original_header_name.as_bytes()); + inner_headers.extend_from_slice(b": "); + inner_headers.extend_from_slice(header_value); + inner_headers.extend_from_slice(b"\r\n"); + } + } + + let mut message = if let Some(encryption_pubkeys) = encryption_pubkeys { + let mut full_raw_message = inner_headers.clone(); + full_raw_message.append(&mut raw_message); + + let encrypted = if let Some(shared_secret) = shared_secret { + let sign_key = if should_sign { + Some(secret_key.clone()) + } else { + None + }; + + crate::pgp::symm_encrypt_message( + full_raw_message, + sign_key, + shared_secret, + should_compress, + )? + } else { + // Asymmetric encryption + + // Use SEIPDv2 if all recipients support it. + let seipd_version = if encryption_pubkeys + .iter() + .all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey)) + { + SeipdVersion::V2 + } else { + SeipdVersion::V1 + }; + + // Encrypt to self unconditionally, + // even for a single-device setup. + let mut encryption_keyring = vec![public_key.clone()]; + encryption_keyring.extend(encryption_pubkeys.iter().map(|(_addr, key)| (*key).clone())); + + crate::pgp::pk_encrypt( + full_raw_message, + encryption_keyring, + secret_key.clone(), + should_compress, + seipd_version, + )? + }; + + let message = wrap_encrypted_part(encrypted); + + part_to_vec(message) + } else { + raw_message + }; + + let mut full_message = outer_headers; + full_message.append(&mut message); + Ok(RenderedEmail { + message: String::from_utf8_lossy(&full_message).to_string(), + is_encrypted, + rfc724_mid, + side_effects, + }) +} + +/// Result of rendering a message, ready to be submitted to a send job. +#[derive(Debug, Clone)] +pub struct RenderedEmail { + pub message: String, + + pub is_encrypted: bool, + /// Message ID (Message in the sense of Email) pub rfc724_mid: String, - /// Message subject. - pub subject: String, + pub side_effects: RenderSideEffects, } fn new_address_with_name(name: &str, address: String) -> Address<'static> { @@ -558,8 +856,6 @@ impl MimeFactory { in_reply_to, references, req_mdn, - last_added_location_timestamp: None, - sync_ids_to_delete: None, attach_selfavatar, webxdc_topic, pre_message_mode: PreMessageMode::None, @@ -609,8 +905,6 @@ impl MimeFactory { in_reply_to: String::default(), references: Vec::new(), req_mdn: false, - last_added_location_timestamp: None, - sync_ids_to_delete: None, attach_selfavatar: false, webxdc_topic: None, pre_message_mode: PreMessageMode::None, @@ -737,53 +1031,40 @@ impl MimeFactory { self.recipients.clone() } - /// Consumes a `MimeFactory` and renders it into a message which is then stored in - /// `smtp`-table to be used by the SMTP loop - #[expect(clippy::arithmetic_side_effects)] - pub async fn render(mut self, context: &Context) -> Result { - let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); - - let from = new_address_with_name(&self.from_displayname, self.from_addr.clone()); - - let mut to: Vec> = Vec::new(); - for (name, addr) in &self.to { - to.push(Address::new_address( - if name.is_empty() { - None - } else { - Some(name.to_string()) - }, - addr.clone(), - )); - } - - let mut past_members: Vec> = Vec::new(); // Contents of `Chat-Group-Past-Members` header. - for (name, addr) in &self.past_members { - past_members.push(Address::new_address( - if name.is_empty() { - None - } else { - Some(name.to_string()) - }, - addr.clone(), - )); - } - + async fn render_headers( + &mut self, + context: &Context, + subject_str: &str, + ) -> Result)>> { ensure_and_debug_assert!( self.member_timestamps.is_empty() - || to.len() + past_members.len() == self.member_timestamps.len(), - "to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})", - to.len(), - past_members.len(), + || self.to.len().checked_add(self.past_members.len()) + == Some(self.member_timestamps.len()), + "self.to.len() ({}) + self.past_members.len() ({}) != self.member_timestamps.len() ({})", + self.to.len(), + self.past_members.len(), self.member_timestamps.len(), ); - if to.is_empty() { - to.push(hidden_recipients()); - } - // Start with Internet Message Format headers in the order of the standard example - // . - headers.push(("From", from.into())); + let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); + + let to: Vec> = if self.to.is_empty() { + vec![hidden_recipients()] + } else { + self.to + .iter() + .map(|(name, addr)| { + Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + ) + }) + .collect() + }; if let Some(sender_displayname) = &self.sender_displayname { let sender = new_address_with_name(sender_displayname, self.from_addr.clone()); @@ -793,10 +1074,25 @@ impl MimeFactory { "To", mail_builder::headers::address::Address::new_list(to.clone()).into(), )); - if !past_members.is_empty() { + + if !self.past_members.is_empty() { + let past_members: Vec> = self + .past_members + .iter() + .map(|(name, addr)| { + Address::new_address( + if name.is_empty() { + None + } else { + Some(name.to_string()) + }, + addr.clone(), + ) + }) + .collect(); headers.push(( "Chat-Group-Past-Members", - mail_builder::headers::address::Address::new_list(past_members.clone()).into(), + mail_builder::headers::address::Address::new_list(past_members).into(), )); } @@ -832,35 +1128,11 @@ impl MimeFactory { } } - let subject_str = self.subject_str(context).await?; headers.push(( "Subject", mail_builder::headers::text::Text::new(subject_str.to_string()).into(), )); - let date = chrono::DateTime::::from_timestamp(self.timestamp, 0) - .unwrap() - .to_rfc2822(); - headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); - - let rfc724_mid = match &self.loaded { - Loaded::Message { msg, .. } => match &self.pre_message_mode { - PreMessageMode::Pre { .. } => { - if msg.pre_rfc724_mid.is_empty() { - create_outgoing_rfc724_mid() - } else { - msg.pre_rfc724_mid.clone() - } - } - _ => msg.rfc724_mid.clone(), - }, - Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), - }; - headers.push(( - "Message-ID", - mail_builder::headers::message_id::MessageId::new(rfc724_mid.clone()).into(), - )); - // Reply headers as in . if !self.in_reply_to.is_empty() { headers.push(( @@ -928,7 +1200,6 @@ impl MimeFactory { } } - // Non-standard headers. headers.push(( "Chat-Version", mail_builder::headers::raw::Raw::new("1.0").into(), @@ -944,19 +1215,6 @@ impl MimeFactory { )); } - let grpimage = self.grpimage(); - let skip_autocrypt = self.should_skip_autocrypt(); - let encrypt_helper = EncryptHelper::new(context).await?; - - if !skip_autocrypt { - // unless determined otherwise we add the Autocrypt header - let aheader = encrypt_helper.get_aheader().to_string(); - headers.push(( - "Autocrypt", - mail_builder::headers::raw::Raw::new(aheader).into(), - )); - } - if self.pre_message_mode == PreMessageMode::Post { headers.push(( "Chat-Is-Post-Message", @@ -973,8 +1231,6 @@ impl MimeFactory { )); } - let is_encrypted = self.will_be_encrypted(); - // Add ephemeral timer for non-MDN messages. // For MDNs it does not matter because they are not visible // and ignored by the receiver. @@ -988,18 +1244,76 @@ impl MimeFactory { } } - let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { - msg.param.get_cmd() == SystemMessage::SecurejoinMessage - } else { - false + Ok(headers) + } + + /// Helper function render the messages that are not queued. + /// + /// Used for MDNs because their payload is rendered right before sending. + pub async fn render(self, context: &Context) -> Result { + let timestamp = self.timestamp; + let from_addr = context.get_primary_self_addr().await?; + let public_key = key::load_self_public_key(context).await?; + let secret_key = key::load_self_secret_key(context).await?; + let (queued_mail, side_effects) = Box::pin(self.pre_render(context)).await?; + let rendered_mail = render_queued_mail( + queued_mail, + &public_key, + &secret_key, + from_addr, + timestamp, + side_effects, + )?; + Ok(rendered_mail) + } + + /// Consumes a `MimeFactory` and renders it into a message which is then stored in + /// `smtp`-table to be used by the SMTP loop + #[expect(clippy::arithmetic_side_effects)] + pub(crate) async fn pre_render( + mut self, + context: &Context, + ) -> Result<(QueuedMail, RenderSideEffects)> { + let rfc724_mid = match &self.loaded { + Loaded::Message { msg, .. } => match &self.pre_message_mode { + PreMessageMode::Pre { .. } => { + if msg.pre_rfc724_mid.is_empty() { + create_outgoing_rfc724_mid() + } else { + msg.pre_rfc724_mid.clone() + } + } + _ => msg.rfc724_mid.clone(), + }, + Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), }; + let subject_str = self.subject_str(context).await?; + let mut headers = self.render_headers(context, &subject_str).await?; + + let grpimage = self.grpimage(); + + let is_encrypted = self.will_be_encrypted(); + + let last_added_location_timestamp; + let avatar_is_attached; + let sync_ids_to_delete; + let message: MimePart<'static> = match &self.loaded { Loaded::Message { msg, .. } => { let msg = msg.clone(); - let (main_part, mut parts) = self + let RenderedMessage { + main_part, + mut parts, + last_added_location_timestamp: tmp_last_added_location_timestamp, + avatar_is_attached: tmp_avatar_is_attached, + sync_ids_to_delete: tmp_sync_ids_to_delete, + } = self .render_message(context, &mut headers, &grpimage, is_encrypted) .await?; + last_added_location_timestamp = tmp_last_added_location_timestamp; + avatar_is_attached = tmp_avatar_is_attached; + sync_ids_to_delete = tmp_sync_ids_to_delete; if parts.is_empty() { // Single part, render as regular message. main_part @@ -1016,29 +1330,49 @@ impl MimeFactory { } } } - Loaded::Mdn { .. } => self.render_mdn()?, + Loaded::Mdn { .. } => { + last_added_location_timestamp = None; + avatar_is_attached = false; + sync_ids_to_delete = None; + self.render_mdn()? + } }; - let HeadersByConfidentiality { - mut unprotected_headers, - hidden_headers, - protected_headers, - } = group_headers_by_confidentiality( - headers, - &self.from_addr, - self.timestamp, - is_encrypted, - is_securejoin_message, - ); + let should_attach_pubkey = !self.should_skip_autocrypt(); + let is_post_message = self.pre_message_mode == PreMessageMode::Post; + let side_effects = RenderSideEffects { + avatar_is_attached, + sync_ids_to_delete, + last_added_location_timestamp, + subject: subject_str, + }; - let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys { - let mut message = add_headers_to_encrypted_part( - message, - &unprotected_headers, - hidden_headers, - protected_headers, - ); + // Disable compression for SecureJoin to ensure + // there are no compression side channels + // leaking information about the tokens. + let should_compress = match &self.loaded { + Loaded::Message { msg, .. } => msg.param.get_cmd() != SystemMessage::SecurejoinMessage, + Loaded::Mdn { .. } => true, + }; + + let shared_secret: Option = match &self.loaded { + Loaded::Message { chat, msg } if should_encrypt_with_broadcast_secret(msg, chat) => { + let secret = load_broadcast_secret(context, chat.id).await?; + if secret.is_none() { + // If there is no shared secret yet + // because this is an old broadcast channel, + // created before we had symmetric encryption, + // we show an error message. + let text = BROADCAST_INCOMPATIBILITY_MSG; + chat::add_info_msg(context, chat.id, text).await?; + bail!(text); + } + secret + } + _ => None, + }; + if let Some(ref encryption_pubkeys) = self.encryption_pubkeys { // Add gossip headers in chats with multiple recipients let multiple_recipients = encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?; @@ -1049,10 +1383,10 @@ impl MimeFactory { match &self.loaded { Loaded::Message { chat, msg } => { if !should_hide_recipients(msg, chat) { - for (addr, key) in &encryption_pubkeys { + for (addr, key) in encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); - if self.pre_message_mode == PreMessageMode::Post { + if is_post_message { continue; } @@ -1105,10 +1439,10 @@ impl MimeFactory { } .to_string(); - message = message.header( + headers.push(( "Autocrypt-Gossip", - mail_builder::headers::raw::Raw::new(header), - ); + mail_builder::headers::raw::Raw::new(header).into(), + )); context .sql @@ -1127,73 +1461,34 @@ impl MimeFactory { // Never gossip in MDNs. } } + } - // Disable compression for SecureJoin to ensure - // there are no compression side channels - // leaking information about the tokens. - let compress = match &self.loaded { - Loaded::Message { msg, .. } => { - msg.param.get_cmd() != SystemMessage::SecurejoinMessage - } - Loaded::Mdn { .. } => true, - }; - - let shared_secret: Option = match &self.loaded { - Loaded::Message { chat, msg } - if should_encrypt_with_broadcast_secret(msg, chat) => - { - let secret = load_broadcast_secret(context, chat.id).await?; - if secret.is_none() { - // If there is no shared secret yet - // because this is an old broadcast channel, - // created before we had symmetric encryption, - // we show an error message. - let text = BROADCAST_INCOMPATIBILITY_MSG; - chat::add_info_msg(context, chat.id, text).await?; - bail!(text); - } - secret - } - _ => None, - }; - - let encrypted = if let Some(shared_secret) = shared_secret { - let sign = true; - encrypt_helper - .encrypt_symmetrically(context, &shared_secret, message, compress, sign) - .await? - } else { - // Asymmetric encryption + let is_encrypted = self.will_be_encrypted(); + let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { + msg.param.get_cmd() == SystemMessage::SecurejoinMessage + } else { + false + }; - // Use SEIPDv2 if all recipients support it. - let seipd_version = if encryption_pubkeys - .iter() - .all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey)) - { - SeipdVersion::V2 - } else { - SeipdVersion::V1 - }; + let display_name = if is_securejoin_message && !is_encrypted { + // Unencrypted securejoin messages should _not_ include the display name. + "".to_string() + } else { + self.from_displayname.clone() + }; - // Encrypt to self unconditionally, - // even for a single-device setup. - let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; - encryption_keyring - .extend(encryption_pubkeys.iter().map(|(_addr, key)| (*key).clone())); + let is_mdn = matches!(self.loaded, Loaded::Mdn { .. }); + let should_sign = true; - encrypt_helper - .encrypt( - context, - encryption_keyring, - message, - compress, - seipd_version, - ) - .await? - }; + let HeadersByConfidentiality { + mut unprotected_headers, + hidden_headers, + protected_headers, + } = group_headers_by_confidentiality(headers); - wrap_encrypted_part(encrypted) - } else if matches!(self.loaded, Loaded::Mdn { .. }) { + let message = if self.encryption_pubkeys.is_some() { + add_headers_to_encrypted_part(message, hidden_headers, protected_headers) + } else if is_mdn { // Never add outer multipart/mixed wrapper to MDN // as multipart/report Content-Type is used to recognize MDNs // by Delta Chat receiver and Chatmail servers @@ -1209,6 +1504,10 @@ impl MimeFactory { .fold(message, |message, (header, value)| { message.header(header, value) }); + let message = message.header( + "Message-ID", + mail_builder::headers::message_id::MessageId::new(rfc724_mid.clone()), + ); let message = MimePart::new("multipart/mixed", vec![message]); let message = protected_headers .iter() @@ -1221,24 +1520,26 @@ impl MimeFactory { HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header)); unprotected_headers.retain(|(header, _value)| !protected.contains(header)); - message + unprotected_headers + .iter() + .cloned() + .fold(message, |message, (header, value)| { + message.header(header, value) + }) }; + let raw_message = part_to_vec(message); - let MimeFactory { - last_added_location_timestamp, - .. - } = self; - - let message = render_outer_message(unprotected_headers, outer_message); - - Ok(RenderedEmail { - message, - is_encrypted, - last_added_location_timestamp, - sync_ids_to_delete: self.sync_ids_to_delete, + let queued_email = QueuedMail { + raw_message, rfc724_mid, - subject: subject_str, - }) + display_name, + encryption_pubkeys: self.encryption_pubkeys.clone(), + shared_secret, + should_attach_pubkey, + should_sign, + should_compress, + }; + Ok((queued_email, side_effects)) } /// Returns MIME part with a `message.kml` attachment. @@ -1278,12 +1579,12 @@ impl MimeFactory { } async fn render_message( - &mut self, + &self, context: &Context, headers: &mut Vec<(&'static str, HeaderType<'static>)>, grpimage: &Option, is_encrypted: bool, - ) -> Result<(MimePart<'static>, Vec>)> { + ) -> Result { let Loaded::Message { chat, msg } = &self.loaded else { bail!("Attempt to render MDN as a message"); }; @@ -1775,21 +2076,25 @@ impl MimeFactory { parts.push(msg_kml_part); } - if !matches!(self.pre_message_mode, PreMessageMode::Pre { .. }) - && location::is_sending_to_chat(context, msg.chat_id).await? - && let Some((part, timestamp)) = self.get_location_kml_part(context).await? - { - parts.push(part); - self.last_added_location_timestamp = Some(timestamp); - } + let last_added_location_timestamp = + if !matches!(self.pre_message_mode, PreMessageMode::Pre { .. }) + && location::is_sending_to_chat(context, msg.chat_id).await? + && let Some((part, timestamp)) = self.get_location_kml_part(context).await? + { + parts.push(part); + Some(timestamp) + } else { + None + }; + let mut sync_ids_to_delete = None; // we do not piggyback sync-files to other self-sent-messages // to not risk files becoming too larger and being skipped by download-on-demand. if command == SystemMessage::MultiDeviceSync { let json = msg.param.get(Param::Arg).unwrap_or_default(); let ids = msg.param.get(Param::Arg2).unwrap_or_default(); parts.push(context.build_sync_part(json.to_string())); - self.sync_ids_to_delete = Some(ids.to_string()); + sync_ids_to_delete = Some(ids.to_string()); } else if command == SystemMessage::WebxdcStatusUpdate { let json = msg.param.get(Param::Arg).unwrap_or_default(); parts.push(context.build_status_update_part(json)); @@ -1816,9 +2121,9 @@ impl MimeFactory { } } - self.attach_selfavatar = + let avatar_is_attached = self.attach_selfavatar && self.pre_message_mode != PreMessageMode::Post; - if self.attach_selfavatar { + if avatar_is_attached { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { Ok(avatar) => headers.push(( @@ -1834,7 +2139,13 @@ impl MimeFactory { } } - Ok((main_part, parts)) + Ok(RenderedMessage { + main_part, + parts, + last_added_location_timestamp, + avatar_is_attached, + sync_ids_to_delete, + }) } /// Render an MDN @@ -1903,23 +2214,6 @@ impl MimeFactory { } } -/// Stores the unprotected headers on the outer message, and renders it. -pub(crate) fn render_outer_message( - unprotected_headers: Vec<(&'static str, HeaderType<'static>)>, - outer_message: MimePart<'static>, -) -> String { - let outer_message = unprotected_headers - .into_iter() - .fold(outer_message, |message, (header, value)| { - message.header(header, value) - }); - - let mut buffer = Vec::new(); - let cursor = Cursor::new(&mut buffer); - outer_message.clone().write_part(cursor).ok(); - String::from_utf8_lossy(&buffer).to_string() -} - /// Takes the encrypted part, wraps it in a MimePart, /// and sets the appropriate Content-Type for the outer message pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> { @@ -1936,38 +2230,18 @@ pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> { fn add_headers_to_encrypted_part( message: MimePart<'static>, - unprotected_headers: &[(&'static str, HeaderType<'static>)], hidden_headers: Vec<(&'static str, HeaderType<'static>)>, protected_headers: Vec<(&'static str, HeaderType<'static>)>, ) -> MimePart<'static> { // Store protected headers in the inner message. - let message = protected_headers - .into_iter() - .fold(message, |message, (header, value)| { - message.header(header, value) - }); - // Add hidden headers to encrypted payload. - let mut message: MimePart<'static> = hidden_headers + let mut message: MimePart<'static> = protected_headers .into_iter() + .chain(hidden_headers) .fold(message, |message, (header, value)| { message.header(header, value) }); - message = unprotected_headers - .iter() - // Structural headers shouldn't be added as "HP-Outer". They are defined in - // . - .filter(|(name, _)| { - !(name.eq_ignore_ascii_case("mime-version") - || name.eq_ignore_ascii_case("content-type") - || name.eq_ignore_ascii_case("content-transfer-encoding") - || name.eq_ignore_ascii_case("content-disposition")) - }) - .fold(message, |message, (name, value)| { - message.header(format!("HP-Outer: {name}"), value.clone()) - }); - // Set the appropriate Content-Type for the inner message for (h, v) in &mut message.headers { if h == "Content-Type" @@ -2022,84 +2296,26 @@ struct HeadersByConfidentiality { /// See [`HeadersByConfidentiality`] for more info. fn group_headers_by_confidentiality( headers: Vec<(&'static str, HeaderType<'static>)>, - from_addr: &str, - timestamp: i64, - is_encrypted: bool, - is_securejoin_message: bool, ) -> HeadersByConfidentiality { let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); let mut protected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); - // MIME header . - unprotected_headers.push(( - "MIME-Version", - mail_builder::headers::raw::Raw::new("1.0").into(), - )); - for header @ (original_header_name, _header_value) in &headers { let header_name = original_header_name.to_lowercase(); - if header_name == "message-id" { - unprotected_headers.push(header.clone()); - hidden_headers.push(header.clone()); - } else if is_hidden(&header_name) { - hidden_headers.push(header.clone()); - } else if header_name == "from" { - // Unencrypted securejoin messages should _not_ include the display name: - if is_encrypted || !is_securejoin_message { - protected_headers.push(header.clone()); - } + debug_assert_ne!(header_name, "from"); + debug_assert_ne!(header_name, "message-id"); + debug_assert_ne!(header_name, "autocrypt"); + debug_assert_ne!(header_name, "date"); - unprotected_headers.push(( - original_header_name, - Address::new_address(None::<&'static str>, from_addr.to_string()).into(), - )); + if is_hidden(&header_name) { + hidden_headers.push(header.clone()); } else if header_name == "to" { protected_headers.push(header.clone()); - if is_encrypted { - unprotected_headers.push(("To", hidden_recipients().into())); - } else { - unprotected_headers.push(header.clone()); - } + unprotected_headers.push(("To", hidden_recipients().into())); } else if header_name == "chat-broadcast-secret" { - if is_encrypted { - protected_headers.push(header.clone()); - } - } else if is_encrypted && header_name == "date" { protected_headers.push(header.clone()); - - // Randomized date goes to unprotected header. - // - // We cannot just send "Thu, 01 Jan 1970 00:00:00 +0000" - // or omit the header because GMX then fails with - // - // host mx00.emig.gmx.net[212.227.15.9] said: - // 554-Transaction failed - // 554-Reject due to policy restrictions. - // 554 For explanation visit https://postmaster.gmx.net/en/case?... - // (in reply to end of DATA command) - // - // and the explanation page says - // "The time information deviates too much from the actual time". - // - // We also limit the range to 6 days (518400 seconds) - // because with a larger range we got - // error "500 Date header far in the past/future" - // which apparently originates from Symantec Messaging Gateway - // and means the message has a Date that is more - // than 7 days in the past: - // - let timestamp_offset = rand::random_range(0..518400); - let protected_timestamp = timestamp.saturating_sub(timestamp_offset); - let unprotected_date = - chrono::DateTime::::from_timestamp(protected_timestamp, 0) - .unwrap() - .to_rfc2822(); - unprotected_headers.push(( - "Date", - mail_builder::headers::raw::Raw::new(unprotected_date).into(), - )); - } else if is_encrypted { + } else { protected_headers.push(header.clone()); match header_name.as_str() { @@ -2116,8 +2332,6 @@ fn group_headers_by_confidentiality( // Other headers are removed from unprotected part. } } - } else { - unprotected_headers.push(header.clone()) } } HeadersByConfidentiality { @@ -2238,17 +2452,17 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( context: &Context, step: &str, rfc724_mid: &str, - attach_self_pubkey: bool, + should_attach_pubkey: bool, auth: &str, shared_secret: &str, ) -> Result { info!(context, "Sending secure-join message {step:?}."); - let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); + let timestamp = time(); - let from_addr = context.get_primary_self_addr().await?; - let from = new_address_with_name("", from_addr.to_string()); - headers.push(("From", from.into())); + let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); + + let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); let to: Vec> = vec![hidden_recipients()]; headers.push(( @@ -2261,17 +2475,6 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( mail_builder::headers::text::Text::new("Secure-Join".to_string()).into(), )); - let timestamp = time(); - let date = chrono::DateTime::::from_timestamp(timestamp, 0) - .unwrap() - .to_rfc2822(); - headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); - - headers.push(( - "Message-ID", - mail_builder::headers::message_id::MessageId::new(rfc724_mid.to_string()).into(), - )); - // Automatic Response headers if context.get_config_bool(Config::Bot).await? { headers.push(( @@ -2280,16 +2483,6 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( )); } - let encrypt_helper = EncryptHelper::new(context).await?; - - if attach_self_pubkey { - let aheader = encrypt_helper.get_aheader().to_string(); - headers.push(( - "Autocrypt", - mail_builder::headers::raw::Raw::new(aheader).into(), - )); - } - headers.push(( "Secure-Join", mail_builder::headers::raw::Raw::new(step.to_string()).into(), @@ -2300,46 +2493,63 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( mail_builder::headers::text::Text::new(auth.to_string()).into(), )); - let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); - - let is_encrypted = true; - let is_securejoin_message = true; let HeadersByConfidentiality { - unprotected_headers, hidden_headers, protected_headers, - } = group_headers_by_confidentiality( - headers, - &from_addr, - timestamp, - is_encrypted, - is_securejoin_message, - ); + .. + } = group_headers_by_confidentiality(headers); + + let message = add_headers_to_encrypted_part(message, hidden_headers, protected_headers); + + // Disable compression for SecureJoin to ensure + // there are no compression side channels + // leaking information about the tokens. + let should_compress = false; + + // Only sign the message if we attach the pubkey. + let should_sign = should_attach_pubkey; + + let raw_message = part_to_vec(message); + + let queued_mail = QueuedMail { + raw_message, + display_name: String::new(), + rfc724_mid: rfc724_mid.to_string(), + encryption_pubkeys: Some(vec![]), + shared_secret: Some(shared_secret.to_string()), + should_attach_pubkey, + should_sign, + should_compress, + }; - let outer_message = { - let message = add_headers_to_encrypted_part( - message, - &unprotected_headers, - hidden_headers, - protected_headers, - ); + let public_key = key::load_self_public_key(context).await?; + let secret_key = key::load_self_secret_key(context).await?; - // Disable compression for SecureJoin to ensure - // there are no compression side channels - // leaking information about the tokens. - let compress = false; - // Only sign the message if we attach the pubkey. - let sign = attach_self_pubkey; - let encrypted = encrypt_helper - .encrypt_symmetrically(context, shared_secret, message, compress, sign) - .await?; + let side_effects = RenderSideEffects { + subject: "Secure-Join".to_string(), - wrap_encrypted_part(encrypted) + ..Default::default() }; - let message = render_outer_message(unprotected_headers, outer_message); + let from_addr = context.get_primary_self_addr().await?; + let rendered_mail = render_queued_mail( + queued_mail, + &public_key, + &secret_key, + from_addr, + timestamp, + side_effects, + )?; + + Ok(rendered_mail.message) +} - Ok(message) +/// Renders MIME part into a vector. +pub(crate) fn part_to_vec(message: MimePart<'static>) -> Vec { + let mut raw_message = Vec::new(); + let cursor = Cursor::new(&mut raw_message); + message.write_part(cursor).ok(); + raw_message } #[cfg(test)] diff --git a/src/pgp.rs b/src/pgp.rs index 8821d36105..29fd31307f 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -21,7 +21,6 @@ use pgp::types::{ }; use rand_old::{Rng as _, thread_rng}; use sha2::Sha256; -use tokio::runtime::Handle; use crate::key::{DcKey, Fingerprint}; @@ -109,97 +108,93 @@ pub enum SeipdVersion { /// Encrypts `plain` text using `public_keys_for_encryption` /// and signs it using `private_key_for_signing`. #[expect(clippy::arithmetic_side_effects)] -pub async fn pk_encrypt( +pub fn pk_encrypt( plain: Vec, public_keys_for_encryption: Vec, private_key_for_signing: SignedSecretKey, compress: bool, seipd_version: SeipdVersion, ) -> Result { - Handle::current() - .spawn_blocking(move || { - let mut rng = thread_rng(); + let mut rng = thread_rng(); - let pkeys = public_keys_for_encryption - .iter() - .filter_map(select_pk_for_encryption); - let subpkts = { - let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1); - hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime( - pgp::types::Timestamp::now(), - ))?); - for key in &public_keys_for_encryption { - let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint()); - let subpkt = match private_key_for_signing.version() < KeyVersion::V6 { - true => Subpacket::regular(data)?, - false => Subpacket::critical(data)?, - }; - hashed.push(subpkt); - } - hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint( - private_key_for_signing.fingerprint(), - ))?); - let mut unhashed = vec![]; - if private_key_for_signing.version() <= KeyVersion::V4 { - unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId( - private_key_for_signing.legacy_key_id(), - ))?); - } - SubpacketConfig::UserDefined { hashed, unhashed } + let pkeys = public_keys_for_encryption + .iter() + .filter_map(select_pk_for_encryption); + let subpkts = { + let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1); + hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime( + pgp::types::Timestamp::now(), + ))?); + for key in &public_keys_for_encryption { + let data = SubpacketData::IntendedRecipientFingerprint(key.fingerprint()); + let subpkt = match private_key_for_signing.version() < KeyVersion::V6 { + true => Subpacket::regular(data)?, + false => Subpacket::critical(data)?, }; + hashed.push(subpkt); + } + hashed.push(Subpacket::regular(SubpacketData::IssuerFingerprint( + private_key_for_signing.fingerprint(), + ))?); + let mut unhashed = vec![]; + if private_key_for_signing.version() <= KeyVersion::V4 { + unhashed.push(Subpacket::regular(SubpacketData::IssuerKeyId( + private_key_for_signing.legacy_key_id(), + ))?); + } + SubpacketConfig::UserDefined { hashed, unhashed } + }; - let msg = MessageBuilder::from_bytes("", plain); - let encoded_msg = match seipd_version { - SeipdVersion::V1 => { - let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM); - - for pkey in pkeys { - msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; - } + let msg = MessageBuilder::from_bytes("", plain); + let encoded_msg = match seipd_version { + SeipdVersion::V1 => { + let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM); - let hash_algorithm = private_key_for_signing.hash_alg(); - msg.sign_with_subpackets( - &*private_key_for_signing, - Password::empty(), - hash_algorithm, - subpkts, - ); - if compress { - msg.compression(CompressionAlgorithm::ZLIB); - } + for pkey in pkeys { + msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; + } - msg.to_armored_string(&mut rng, Default::default())? - } - SeipdVersion::V2 => { - let mut msg = msg.seipd_v2( - &mut rng, - SYMMETRIC_KEY_ALGORITHM, - AeadAlgorithm::Ocb, - ChunkSize::C8KiB, - ); + let hash_algorithm = private_key_for_signing.hash_alg(); + msg.sign_with_subpackets( + &*private_key_for_signing, + Password::empty(), + hash_algorithm, + subpkts, + ); + if compress { + msg.compression(CompressionAlgorithm::ZLIB); + } - for pkey in pkeys { - msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; - } + msg.to_armored_string(&mut rng, Default::default())? + } + SeipdVersion::V2 => { + let mut msg = msg.seipd_v2( + &mut rng, + SYMMETRIC_KEY_ALGORITHM, + AeadAlgorithm::Ocb, + ChunkSize::C8KiB, + ); + + for pkey in pkeys { + msg.encrypt_to_key_anonymous(&mut rng, &pkey)?; + } - let hash_algorithm = private_key_for_signing.hash_alg(); - msg.sign_with_subpackets( - &*private_key_for_signing, - Password::empty(), - hash_algorithm, - subpkts, - ); - if compress { - msg.compression(CompressionAlgorithm::ZLIB); - } + let hash_algorithm = private_key_for_signing.hash_alg(); + msg.sign_with_subpackets( + &*private_key_for_signing, + Password::empty(), + hash_algorithm, + subpkts, + ); + if compress { + msg.compression(CompressionAlgorithm::ZLIB); + } - msg.to_armored_string(&mut rng, Default::default())? - } - }; + msg.to_armored_string(&mut rng, Default::default())? + } + }; - Ok(encoded_msg) - }) - .await? + Ok(encoded_msg) } /// Returns fingerprints @@ -485,7 +480,7 @@ mod tests { config::Config, decrypt, key::{load_self_public_key, self_fingerprint, store_self_keypair}, - mimefactory::{render_outer_message, wrap_encrypted_part}, + mimefactory::{part_to_vec, wrap_encrypted_part}, test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair}, token, }; @@ -511,8 +506,8 @@ mod tests { store_self_keypair(t, secret_key).await?; let mime_message = wrap_encrypted_part(bytes.try_into().unwrap()); - let rendered = render_outer_message(vec![], mime_message); - let parsed = mailparse::parse_mail(rendered.as_bytes())?; + let rendered = part_to_vec(mime_message); + let parsed = mailparse::parse_mail(&rendered)?; let (decrypted, _fp) = decrypt::decrypt(t, &parsed).await?.unwrap(); Ok(decrypted) } @@ -585,7 +580,6 @@ mod tests { compress, SeipdVersion::V2, ) - .await .unwrap() }) .await @@ -776,8 +770,7 @@ mod tests { KEYS.alice_secret.clone(), compress, SeipdVersion::V2, - ) - .await?; + )?; // Trying to decrypt it should fail with an OK error message: let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?; diff --git a/src/reaction.rs b/src/reaction.rs index 279c1afa1f..3ba72c5183 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -967,8 +967,7 @@ Content-Transfer-Encoding: base64\r alice_secret_key, compress, SeipdVersion::V2, - ) - .await?; + )?; let boundary = "boundary123"; let rcvd_mail = format!( diff --git a/src/test_utils.rs b/src/test_utils.rs index f5f245c8a3..a72b3187c9 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -22,6 +22,7 @@ use tokio::runtime::Handle; use tokio::{fs, task}; use uuid::Uuid; +use crate::aheader::{Aheader, EncryptPreference}; use crate::chat::{ self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table, create_group, }; @@ -33,7 +34,6 @@ use crate::contact::{ Contact, ContactId, Modifier, Origin, import_vcard, make_vcard, mark_contact_id_as_verified, }; use crate::context::Context; -use crate::e2ee::EncryptHelper; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::key::{self, DcKey, self_fingerprint}; use crate::log::warn; @@ -1240,8 +1240,15 @@ pub async fn encrypt_raw_message( receivers: &[&TestContext], payload: &[u8], ) -> Result { - let encryption_helper = EncryptHelper::new(context).await?; - let mut encryption_keyring = vec![encryption_helper.public_key.clone()]; + let public_key = key::load_self_public_key(context).await?; + let aheader = Aheader { + addr: context.get_primary_self_addr().await?, + public_key: public_key.clone(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false, + }; + + let mut encryption_keyring = vec![public_key.clone()]; for receiver in receivers { encryption_keyring.push(key::load_self_public_key(receiver).await?); @@ -1250,18 +1257,17 @@ pub async fn encrypt_raw_message( let from = context.get_primary_self_addr().await?; let compress = false; - let mut cleartext = format!("Autocrypt: {}", encryption_helper.get_aheader()).into_bytes(); + let mut cleartext = format!("Autocrypt: {aheader}").into_bytes(); cleartext.extend_from_slice(b"\r\n"); cleartext.extend_from_slice(payload); - let encrypted_payload = encryption_helper - .encrypt_raw( - context, - encryption_keyring, - cleartext, - compress, - SeipdVersion::V2, - ) - .await?; + let sign_key = key::load_self_secret_key(context).await?; + let encrypted_payload = crate::pgp::pk_encrypt( + cleartext, + encryption_keyring, + sign_key, + compress, + SeipdVersion::V2, + )?; let boundary = Uuid::new_v4(); let res = format!(