diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 562ab23f5..f2eb6508e 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -2205,5 +2205,290 @@ fn b_encode(value: &str) -> String { ) } +pub(crate) async fn render_symm_encrypted_securejoin_message( + context: &Context, + step: &str, + rfc724_mid: &str, + attach_self_pubkey: bool, + auth: &str, +) -> Result { + info!(context, "Sending secure-join message {step:?}."); + + let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); + + let from_addr = context.get_primary_self_addr().await?; + let from = new_address_with_name("", from_addr); + + let mut to: Vec> = Vec::new(); + to.push(hidden_recipients()); + + headers.push(("From", from.into())); + + headers.push(( + "To", + mail_builder::headers::address::Address::new_list(to.clone()).into(), + )); + + // TODO not sure if we even need a timestamp + let timestamp = create_smeared_timestamp(context); + 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(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(), + )); + } else if step != "vc-request" { + headers.push(( + "Auto-Submitted", + mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(), + )); + } + + 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(), + )); + + headers.push(( + "Secure-Join-Auth", + mail_builder::headers::text::Text::new(auth.to_string()).into(), + )); + + let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); + + // Split headers based on header confidentiality policy. + + // Headers that must go into IMF header section. + // + // These are standard headers such as Date, In-Reply-To, References, which cannot be placed + // anywhere else according to the standard. Placing headers here also allows them to be fetched + // individually over IMAP without downloading the message body. This is why Chat-Version is + // placed here. + let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); + + // Headers that MUST NOT (only) go into IMF header section: + // - Large headers which may hit the header section size limit on the server, such as + // Chat-User-Avatar with a base64-encoded image inside. + // - Headers duplicated here that servers mess up with in the IMF header section, like + // Message-ID. + // - Nonstandard headers that should be DKIM-protected because e.g. OpenDKIM only signs + // known headers. + // + // The header should be hidden from MTA + // by moving it either into protected part + // in case of encrypted mails + // or unprotected MIME preamble in case of unencrypted mails. + let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); + + // Opportunistically protected headers. + // + // These headers are placed into encrypted part *if* the message is encrypted. Place headers + // which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the + // message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here. + // + // If the message is not encrypted, these headers are placed into IMF header section, so make + // sure that the message will be encrypted if you place any sensitive information here. + 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" { + protected_headers.push(header.clone()); + + unprotected_headers.push(header.clone()); + } else if header_name == "to" { + unprotected_headers.push(("To", hidden_recipients().into())); + } else if 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 { + protected_headers.push(header.clone()); + + match header_name.as_str() { + "subject" => { + unprotected_headers.push(( + "Subject", + mail_builder::headers::raw::Raw::new("[...]").into(), + )); + } + "in-reply-to" + | "references" + | "auto-submitted" + | "chat-version" + | "autocrypt-setup-message" => { + unprotected_headers.push(header.clone()); + } + _ => { + // Other headers are removed from unprotected part. + } + } + } + } + + let outer_message = { + // 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 + .into_iter() + .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" + && let mail_builder::headers::HeaderType::ContentType(ct) = v + { + let mut ct_new = ct.clone(); + ct_new = ct_new.attribute("protected-headers", "v1"); + ct_new = ct_new.attribute("hp", "cipher"); + *ct = ct_new; + break; + } + } + + // Disable compression for SecureJoin to ensure + // there are no compression side channels + // leaking information about the tokens. + let compress = false; + + if context.get_config_bool(Config::TestHooks).await? + && let Some(hook) = &*context.pre_encrypt_mime_hook.lock() + { + message = hook(context, message); + } + + let encrypted = encrypt_helper + .encrypt_symmetrically(context, auth, message, compress) + .await?; + + // XXX: additional newline is needed + // to pass filtermail at + // : + let encrypted = encrypted + "\n"; + + // Set the appropriate Content-Type for the outer message + MimePart::new( + "multipart/encrypted; protocol=\"application/pgp-encrypted\"", + vec![ + // Autocrypt part 1 + MimePart::new("application/pgp-encrypted", "Version: 1\r\n").header( + "Content-Description", + mail_builder::headers::raw::Raw::new("PGP/MIME version identification"), + ), + // Autocrypt part 2 + MimePart::new( + "application/octet-stream; name=\"encrypted.asc\"", + encrypted, + ) + .header( + "Content-Description", + mail_builder::headers::raw::Raw::new("OpenPGP encrypted message"), + ) + .header( + "Content-Disposition", + mail_builder::headers::raw::Raw::new("inline; filename=\"encrypted.asc\";"), + ), + ], + ) + }; + + // Store the unprotected headers on the outer message. + 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(); + let message = String::from_utf8_lossy(&buffer).to_string(); + + Ok(message) +} + #[cfg(test)] mod mimefactory_tests; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index fd9298562..e754f8ddb 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -1,13 +1,12 @@ //! Bob's side of SecureJoin handling, the joiner-side. -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, bail}; use super::HandshakeMessage; use super::qrinvite::QrInvite; use crate::chat::{self, ChatId, is_contact_in_chat}; -use crate::chatlist_events; use crate::constants::{Blocked, Chattype}; -use crate::contact::Origin; +use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::events::EventType; use crate::key::self_fingerprint; @@ -18,7 +17,8 @@ use crate::param::{Param, Params}; use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint}; use crate::stock_str; use crate::sync::Sync::*; -use crate::tools::{smeared_time, time}; +use crate::tools::{create_outgoing_rfc724_mid, smeared_time, time}; +use crate::{chatlist_events, mimefactory}; /// Starts the securejoin protocol with the QR `invite`. /// @@ -299,47 +299,145 @@ pub(crate) async fn send_handshake_message( chat_id: ChatId, step: BobHandshakeMsg, ) -> Result<()> { - let mut msg = Message { - viewtype: Viewtype::Text, - text: step.body_text(invite), - hidden: true, - ..Default::default() - }; - msg.param.set_cmd(SystemMessage::SecurejoinMessage); + if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) { + // Send a minimal symmetrically-encrypted vc-request message - // Sends the step in Secure-Join header. - msg.param.set(Param::Arg, step.securejoin_header(invite)); + // TODO: Either add a message to the database, or make sure that smtp.rs gets along with a 0 or NULL msg_id + /* + msg.state = MessageState::OutPending; + msg.timestamp_sort = create_smeared_timestamp(context); + msg.rfc724_mid = create_outgoing_rfc724_mid(); + let is_bot = context.get_config_bool(Config::Bot).await?; + msg.param + .set_optional(Param::Bot, Some("1").filter(|_| is_bot)); - match step { - BobHandshakeMsg::Request => { - // Sends the Secure-Join-Invitenumber header in mimefactory.rs. - msg.param.set(Param::Arg2, invite.invitenumber()); - msg.force_plaintext(); - } - BobHandshakeMsg::RequestWithAuth => { - // Sends the Secure-Join-Auth header in mimefactory.rs. - msg.param.set(Param::Arg2, invite.authcode()); - msg.param.set_int(Param::GuaranteeE2ee, 1); + let raw_id = context + .sql + .insert( + "INSERT INTO msgs ( + rfc724_mid, + chat_id, + from_id, + to_id, + timestamp, + type, + state, + txt, + txt_normalized, + subject, + param, + hidden, + mime_in_reply_to, + mime_references, + mime_modified, + mime_headers, + mime_compressed, + location_id, + ephemeral_timer, + ephemeral_timestamp) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);", + params_slice![ + msg.rfc724_mid, + msg.chat_id, + msg.from_id, + to_id, + msg.timestamp_sort, + msg.viewtype, + msg.state, + msg_text, + normalize_text(&msg_text), + &msg.subject, + msg.param.to_string(), + msg.hidden, + msg.in_reply_to.as_deref().unwrap_or_default(), + new_references, + new_mime_headers.is_some(), + new_mime_headers.unwrap_or_default(), + location_id as i32, + ephemeral_timer, + ephemeral_timestamp + ], + ) + .await?; + context.new_msgs_notify.notify_one(); + msg.id = MsgId::new(u32::try_from(raw_id)?); + */ - // Sends our own fingerprint in the Secure-Join-Fingerprint header. - let bob_fp = self_fingerprint(context).await?; - msg.param.set(Param::Arg3, bob_fp); + let row_ids = create_send_msg_jobs(context, msg) + .await + .context("Failed to create send jobs")?; - // Sends the grpid in the Secure-Join-Group header. - // - // `Secure-Join-Group` header is deprecated, - // but old Delta Chat core requires that Alice receives it. - // - // Previous Delta Chat core also sent `Secure-Join-Group` header - // in `vg-request` messages, - // but it was not used on the receiver. - if let QrInvite::Group { grpid, .. } = invite { - msg.param.set(Param::Arg4, grpid); + let rfc724_mid = create_outgoing_rfc724_mid(); + let contact = Contact::get_by_id(context, invite.contact_id()).await?; + let recipient = contact.get_addr(); + + let rendered_message = mimefactory::render_symm_encrypted_securejoin_message( + context, + recipient, + rfc724_mid, + invite.authcode(), + ) + .await?; + + context + .sql + .execute( + "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) + VALUES (?1, ?2, ?3, ?4)", + ( + &rfc724_mid, + &recipient, + &rendered_message, + 0, // TODO + ), + ) + .await?; + + context.scheduler.interrupt_smtp().await; + } else { + let mut msg = Message { + viewtype: Viewtype::Text, + text: step.body_text(invite), + hidden: true, + ..Default::default() + }; + + msg.param.set_cmd(SystemMessage::SecurejoinMessage); + + // Sends the step in Secure-Join header. + msg.param.set(Param::Arg, step.securejoin_header(invite)); + + match step { + BobHandshakeMsg::Request => { + // Sends the Secure-Join-Invitenumber header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.invitenumber()); + msg.force_plaintext(); } - } - }; + BobHandshakeMsg::RequestWithAuth => { + // Sends the Secure-Join-Auth header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.authcode()); + msg.param.set_int(Param::GuaranteeE2ee, 1); - chat::send_msg(context, chat_id, &mut msg).await?; + // Sends our own fingerprint in the Secure-Join-Fingerprint header. + let bob_fp = self_fingerprint(context).await?; + msg.param.set(Param::Arg3, bob_fp); + + // Sends the grpid in the Secure-Join-Group header. + // + // `Secure-Join-Group` header is deprecated, + // but old Delta Chat core requires that Alice receives it. + // + // Previous Delta Chat core also sent `Secure-Join-Group` header + // in `vg-request` messages, + // but it was not used on the receiver. + if let QrInvite::Group { grpid, .. } = invite { + msg.param.set(Param::Arg4, grpid); + } + } + }; + + chat::send_msg(context, chat_id, &mut msg).await?; + } Ok(()) } diff --git a/src/securejoin/qrinvite.rs b/src/securejoin/qrinvite.rs index 4bb3b71e1..26c4cb436 100644 --- a/src/securejoin/qrinvite.rs +++ b/src/securejoin/qrinvite.rs @@ -12,7 +12,7 @@ use crate::qr::Qr; /// Represents the data from a QR-code scan. /// -/// There are methods to conveniently access fields present in both variants. +/// There are methods to conveniently access fields present in all three variants. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum QrInvite { Contact { @@ -20,6 +20,7 @@ pub enum QrInvite { fingerprint: Fingerprint, invitenumber: String, authcode: String, + is_v3: bool, }, Group { contact_id: ContactId, @@ -28,6 +29,7 @@ pub enum QrInvite { grpid: String, invitenumber: String, authcode: String, + is_v3: bool, }, Broadcast { contact_id: ContactId, @@ -36,6 +38,7 @@ pub enum QrInvite { grpid: String, invitenumber: String, authcode: String, + is_v3: bool, }, } @@ -78,6 +81,14 @@ impl QrInvite { | Self::Broadcast { authcode, .. } => authcode, } } + + pub fn is_v3(&self) -> bool { + match self { + QrInvite::Contact { is_v3, .. } => is_v3, + QrInvite::Group { is_v3, .. } => is_v3, + QrInvite::Broadcast { is_v3, .. } => is_v3, + } + } } impl TryFrom for QrInvite {