diff --git a/src/config.rs b/src/config.rs index 305e8765c9..9bc0d775c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -407,9 +407,6 @@ pub enum Config { #[strum(props(default = "1"))] SyncMsgs, - /// Make all outgoing messages with Autocrypt header "multipart/signed". - SignUnencrypted, - /// Let the core save all events to the database. /// This value is used internally to remember the MsgId of the logging xdc #[strum(props(default = "0"))] @@ -710,7 +707,6 @@ impl Context { | Config::Bot | Config::NotifyAboutWrongPw | Config::SyncMsgs - | Config::SignUnencrypted | Config::DisableIdle => { ensure!( matches!(value, None | Some("0") | Some("1")), diff --git a/src/context.rs b/src/context.rs index 801dfbce8d..95ce3c7774 100644 --- a/src/context.rs +++ b/src/context.rs @@ -991,12 +991,6 @@ impl Context { .await? .to_string(), ); - res.insert( - "sign_unencrypted", - self.get_config_int(Config::SignUnencrypted) - .await? - .to_string(), - ); res.insert( "debug_logging", self.get_config_int(Config::DebugLogging).await?.to_string(), diff --git a/src/e2ee.rs b/src/e2ee.rs index e7de115856..9a1f639bfd 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -79,16 +79,6 @@ impl EncryptHelper { Ok(ctext) } - - /// Signs the passed-in `mail` using the private key from `context`. - /// Returns the payload and the signature. - pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result { - let sign_key = load_self_secret_key(context).await?; - let mut buffer = Vec::new(); - mail.clone().write_part(&mut buffer)?; - let signature = pgp::pk_calc_signature(buffer, &sign_key)?; - Ok(signature) - } } /// Ensures a private key exists for the configured user. diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3a07a750e4..5619c3b76b 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1227,53 +1227,18 @@ impl MimeFactory { message.header(header, value) }); let message = MimePart::new("multipart/mixed", vec![message]); - let mut message = protected_headers + let message = protected_headers .iter() .fold(message, |message, (header, value)| { message.header(*header, value.clone()) }); - if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? { - // Deduplicate unprotected headers that also are in the protected headers: - let protected: HashSet<&str> = - HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header)); - unprotected_headers.retain(|(header, _value)| !protected.contains(header)); + // Deduplicate unprotected headers that also are in the protected headers: + let protected: HashSet<&str> = + HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header)); + unprotected_headers.retain(|(header, _value)| !protected.contains(header)); - message - } else { - 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"); - if use_std_header_protection { - ct_new = ct_new.attribute("hp", "clear"); - } - *ct = ct_new; - break; - } - } - - let signature = encrypt_helper.sign(context, &message).await?; - MimePart::new( - "multipart/signed; protocol=\"application/pgp-signature\"; protected", - vec![ - message, - MimePart::new( - "application/pgp-signature; name=\"signature.asc\"", - signature, - ) - .header( - "Content-Description", - mail_builder::headers::raw::Raw::<'static>::new( - "OpenPGP digital signature", - ), - ) - .attachment("signature"), - ], - ) - } + message }; let MimeFactory { @@ -2192,10 +2157,6 @@ fn group_headers_by_confidentiality( } } } else { - // Copy the header to the protected headers - // in case of signed-only message. - // If the message is not signed, this value will not be used. - protected_headers.push(header.clone()); unprotected_headers.push(header.clone()) } } diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index 90de2d27a6..f47dfc0861 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -601,70 +601,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_selfavatar_unencrypted_signed() { - // create chat with bob, set selfavatar - let t = TestContext::new_alice().await; - t.set_config(Config::SignUnencrypted, Some("1")) - .await - .unwrap(); - let chat = t.create_chat_with_contact("bob", "bob@example.org").await; - - let file = t.dir.path().join("avatar.png"); - let bytes = include_bytes!("../../test-data/image/avatar64x64.png"); - tokio::fs::write(&file, bytes).await.unwrap(); - t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) - .await - .unwrap(); - - // send message to bob: that should get multipart/signed. - // `Subject:` is protected by copying it. - // make sure, `Subject:` stays in the outer header (imf header) - let mut msg = Message::new_text("this is the text!".to_string()); - - let sent_msg = t.send_msg(chat.id, &mut msg).await; - let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n"); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("multipart/signed").count(), 1); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!( - part.match_indices("multipart/mixed; protected-headers=\"v1\"") - .count(), - 1 - ); - assert_eq!(part.match_indices("From:").count(), 1); - assert_eq!(part.match_indices("Message-ID:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 1); - assert_eq!(part.match_indices("Autocrypt:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - - let part = payload.next().unwrap(); - assert_eq!(part.match_indices("text/plain").count(), 1); - assert_eq!(part.match_indices("From:").count(), 0); - assert_eq!(part.match_indices("Message-ID:").count(), 1); - assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0); - assert_eq!(part.match_indices("Subject:").count(), 0); - - let body = payload.next().unwrap(); - assert_eq!(body.match_indices("this is the text!").count(), 1); - - let bob = TestContext::new_bob().await; - bob.recv_msg(&sent_msg).await; - let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) - .await - .unwrap() - .unwrap(); - let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap(); - assert_eq!(alice_contact.is_key_contact(), false); -} - /// Test that removed member address does not go into the `To:` field. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_remove_member_bcc() -> Result<()> { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 04e618a855..d906a058d5 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -304,37 +304,9 @@ impl MimeMessage { // Parse hidden headers. let mimetype = mail.ctype.mimetype.parse::()?; - let (part, mimetype) = - if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" { - if let Some(part) = mail.subparts.first() { - // We don't remove "subject" from `headers` because currently just signed - // messages are shown as unencrypted anyway. - - timestamp_sent = - Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd); - MimeMessage::merge_headers( - context, - &mut headers, - &mut headers_removed, - &mut recipients, - &mut past_members, - &mut from, - &mut list_post, - &mut chat_disposition_notification_to, - part, - ); - (part, part.ctype.mimetype.parse::()?) - } else { - // Not a valid signed message, handle it as plaintext. - (&mail, mimetype) - } - } else { - // Currently we do not sign unencrypted messages by default. - (&mail, mimetype) - }; if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" - && let Some(part) = part.subparts.first() + && let Some(part) = mail.subparts.first() { for field in &part.headers { let key = field.get_key().to_lowercase(); @@ -358,8 +330,7 @@ impl MimeMessage { ); } - // Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave - // them in signed-only emails, but has no value currently. + // Remove headers that are allowed _only_ in the encrypted+signed part let encrypted = false; Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted); @@ -2217,9 +2188,6 @@ pub(crate) fn parse_message_id(ids: &str) -> Result { /// Returns whether the outer header value must be ignored if the message contains a signed (and /// optionally encrypted) part. This is independent from the modern Header Protection defined in /// . -/// -/// NB: There are known cases when Subject and List-ID only appear in the outer headers of -/// signed-only messages. Such messages are shown as unencrypted anyway. fn is_protected(key: &str) -> bool { key.starts_with("chat-") || matches!( diff --git a/src/mimeparser/mimeparser_tests.rs b/src/mimeparser/mimeparser_tests.rs index b3f2e6bc97..ee67f48ebe 100644 --- a/src/mimeparser/mimeparser_tests.rs +++ b/src/mimeparser/mimeparser_tests.rs @@ -7,6 +7,7 @@ use crate::{ chat, chatlist::Chatlist, constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS}, + contact::Contact, key, message::{MessageState, MessengerMessage}, receive_imf::receive_imf, @@ -2041,32 +2042,24 @@ async fn test_multiple_autocrypt_hdrs() -> Result<()> { Ok(()) } -/// Tests that timestamp of signed but not encrypted message is protected. +/// Tests receiving a simple signed-unencrypted message +/// that was generated by an old version of Core that supported sending such messages. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_protected_date() -> Result<()> { +async fn test_receive_signed_only() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = &tcm.alice().await; let bob = &tcm.bob().await; - alice.set_config(Config::SignUnencrypted, Some("1")).await?; + let imf_raw = include_bytes!("../../test-data/message/unencrypted_signed_simple.eml"); + let msg = receive_imf(bob, imf_raw, false).await?.unwrap(); + assert_eq!(msg.msg_ids.len(), 1); + let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?; + assert_eq!(msg.get_text(), "Hello!"); + assert_eq!(msg.viewtype, Viewtype::Text); + assert_eq!(msg.get_timestamp(), 1615987853); - let alice_chat = alice.create_email_chat(bob).await; - let alice_msg_id = chat::send_text_msg(alice, alice_chat.id, "Hello!".to_string()).await?; - let alice_msg = Message::load_from_db(alice, alice_msg_id).await?; - assert_eq!(alice_msg.get_showpadlock(), false); - - let mut sent_msg = alice.pop_sent_msg().await; - sent_msg.payload = sent_msg.payload.replacen( - "Date:", - "Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)\r\nX-Not-Date:", - 1, - ); - let bob_msg = bob.recv_msg(&sent_msg).await; - assert_eq!(alice_msg.get_text(), bob_msg.get_text()); + let alice_contact = Contact::get_by_id(bob, msg.from_id).await.unwrap(); + assert_eq!(alice_contact.is_key_contact(), false); - // Timestamp that the sender has put into the message - // should always be displayed as is on the receiver. - assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp()); Ok(()) } diff --git a/src/pgp.rs b/src/pgp.rs index 2b10da1a35..1417369aa2 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -6,15 +6,15 @@ use std::io::Cursor; use anyhow::{Context as _, Result, ensure}; use deltachat_contact_tools::{EmailAddress, may_be_valid_addr}; use pgp::composed::{ - ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType, - MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey, - SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig, + Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType, MessageBuilder, + SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey, SignedSecretKey, + SubkeyParamsBuilder, SubpacketConfig, }; use pgp::crypto::aead::{AeadAlgorithm, ChunkSize}; use pgp::crypto::ecc_curve::ECCCurve; use pgp::crypto::hash::HashAlgorithm; use pgp::crypto::sym::SymmetricKeyAlgorithm; -use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData}; +use pgp::packet::{Signature, Subpacket, SubpacketData}; use pgp::types::{ CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _, StringToKey, @@ -202,47 +202,6 @@ pub async fn pk_encrypt( .await? } -/// Produces a detached signature for `plain` text using `private_key_for_signing`. -pub fn pk_calc_signature( - plain: Vec, - private_key_for_signing: &SignedSecretKey, -) -> Result { - let rng = thread_rng(); - - let mut config = SignatureConfig::from_key( - rng, - &private_key_for_signing.primary_key, - SignatureType::Binary, - )?; - - config.hashed_subpackets = vec![ - Subpacket::regular(SubpacketData::IssuerFingerprint( - private_key_for_signing.fingerprint(), - ))?, - Subpacket::critical(SubpacketData::SignatureCreationTime( - pgp::types::Timestamp::now(), - ))?, - ]; - config.unhashed_subpackets = vec![]; - if private_key_for_signing.version() <= KeyVersion::V4 { - config - .unhashed_subpackets - .push(Subpacket::regular(SubpacketData::IssuerKeyId( - private_key_for_signing.legacy_key_id(), - ))?); - } - - let signature = config.sign( - &private_key_for_signing.primary_key, - &Password::empty(), - plain.as_slice(), - )?; - - let sig = DetachedSignature::new(signature); - - Ok(sig.to_armored_string(ArmorOptions::default())?) -} - /// Returns fingerprints /// of all keys from the `public_keys_for_validation` keyring that /// have valid signatures in `msg` and corresponding intended recipient fingerprints diff --git a/test-data/message/unencrypted_signed_simple.eml b/test-data/message/unencrypted_signed_simple.eml new file mode 100644 index 0000000000..f3aba4b849 --- /dev/null +++ b/test-data/message/unencrypted_signed_simple.eml @@ -0,0 +1,70 @@ +Content-Type: multipart/signed; protocol="application/pgp-signature"; protected; + boundary="18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0" +MIME-Version: 1.0 +From: +To: +Subject: Message from alice@example.org +Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET) +X-Not-Date: Tue, 28 Apr 2026 20:20:34 +0000 +Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost> +References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost> +Chat-Version: 1.0 +Chat-Disposition-Notification-To: alice@example.org +Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5 + DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB + AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL + DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM + Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL + CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+ + +XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr + g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI + eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G + nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp + 3jsMAg== + + +--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0 +Content-Type: multipart/mixed; protected-headers="v1"; hp="clear"; + boundary="18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0" +From: +To: +Subject: Message from alice@example.org +Date: Tue, 28 Apr 2026 20:20:34 +0000 +References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost> +Chat-Version: 1.0 +Chat-Disposition-Notification-To: alice@example.org +Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5 + DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB + AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL + DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM + Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL + CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+ + +XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr + g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI + eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G + nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp + 3jsMAg== + + +--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0 +Content-Type: text/plain; charset="utf-8" +Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost> +Content-Transfer-Encoding: 7bit + +Hello! +--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0-- + +--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0 +Content-Type: application/pgp-signature; name="signature.asc"; + charset="utf-8" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="signature" +Content-Transfer-Encoding: quoted-printable + +-----BEGIN PGP SIGNATURE-----=0A=0AwnUEABYIAB0WIQQub6LLI7Uy1yhjS1hksI9hqe2UQ= +wWCafEWkQAKCRBksI9hqe2U=0AQ4qaAQCFSLVDANIjaXswP8V5zIwUSvGnUwsMD+ruozO0mG2AqA= +D9EqpWeD6cc+is=0Av9/nvp6uHi35pUmDX0s1XKu3xbSTWg8=3D=0A=3Dr9hO=0A-----END PGP= + SIGNATURE-----=0A +--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0-- + +