From 5ab1fdca2e829f08b24e70d9825bf46ae6624d3c Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 23 Mar 2026 23:52:00 +0100 Subject: [PATCH] feat: use SEIPDv2 if all recipients support it --- src/mimefactory.rs | 13 ++--- src/mimefactory/mimefactory_tests.rs | 87 +++++++++++++++++++++++++++- src/pgp.rs | 29 ++++++++++ 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index ca615b470..4ed7fa58f 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -32,7 +32,7 @@ use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::{SystemMessage, is_hidden}; use crate::param::Param; use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg}; -use crate::pgp::{SeipdVersion, addresses_from_public_key}; +use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2}; use crate::simplify::escape_message_footer_marks; use crate::stock_str; use crate::tools::{ @@ -1176,14 +1176,13 @@ impl MimeFactory { } else { // Asymmetric encryption - let seipd_version = if encryption_pubkeys.is_empty() { - // If message is sent only to self, - // use v2 SEIPD. + // Use SEIPDv2 if all recipients support it. + let seipd_version = if encryption_pubkeys + .iter() + .all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey)) + { SeipdVersion::V2 } else { - // If message is sent to others, - // they may not support v2 SEIPD yet, - // so use v1 SEIPD. SeipdVersion::V1 }; diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index bd79d92c5..fe03953a9 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -1,6 +1,9 @@ use deltachat_contact_tools::ContactAddress; use mail_builder::headers::Header; use mailparse::{MailHeaderMap, addrparse_header}; +use pgp::armor; +use pgp::packet::{Packet, PacketParser}; +use std::io::BufReader; use std::str; use std::time::Duration; @@ -11,7 +14,7 @@ use crate::chat::{ }; use crate::chatlist::Chatlist; use crate::constants; -use crate::contact::Origin; +use crate::contact::{Origin, import_vcard}; use crate::headerdef::HeaderDef; use crate::message; use crate::mimeparser::MimeMessage; @@ -877,3 +880,85 @@ async fn test_no_empty_to_header() -> Result<()> { Ok(()) } + +/// Parses ASCII-armored message and checks that it only has PKESK and SEIPD packets. +/// +/// Panics if SEIPD packets are not of expected version. +fn assert_seipd_version(payload: &str, version: usize) { + let cursor = Cursor::new(payload); + let dearmor = armor::Dearmor::new(cursor); + let packet_parser = PacketParser::new(BufReader::new(dearmor)); + for packet in packet_parser { + match packet.unwrap() { + Packet::PublicKeyEncryptedSessionKey(_pkesk) => {} + Packet::SymEncryptedProtectedData(seipd) => { + assert_eq!(seipd.version(), version); + } + packet => { + panic!("Unexpected packet {:?}", packet); + } + } + } +} + +/// Tests that messages between two test accounts use SEIPDv2 and not SEIPDv1. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_use_seipdv2() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat_id = alice.create_chat_id(bob).await; + let sent = alice.send_text(alice_chat_id, "Hello!").await; + assert_seipd_version(&sent.payload, 2); + + Ok(()) +} + +/// Tests that messages to keys that don't advertise SEIPDv2 support +/// are sent using SEIPDv1. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_fallback_to_seipdv1() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let charlie = &tcm.charlie().await; + + // vCard of Alice with no SEIPDv2 feature advertised in the key. + let alice_vcard = "BEGIN:VCARD +VERSION:4.0 +EMAIL:alice@example.org +FN:Alice +KEY:data:application/pgp-keys;base64,mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFggAOBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGSwj2Gp7ZRDE3oA/i4MCyDMTsjWqDZoQwX/A/GoTO2/V0wKPhjJJy/8m2pMAPkBjOnGOtx2SZpQvJGTa9h804RY6iDrRuI8A/8tEEXAA7g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeIeAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8GnsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp3jsMAg== +REV:20250412T195751Z +END:VCARD"; + let contact_ids = import_vcard(bob, alice_vcard).await.unwrap(); + let alice_contact_id = contact_ids[0]; + let chat_id = ChatId::create_for_contact(bob, alice_contact_id) + .await + .unwrap(); + + // Bob sends a message to Alice with SEIPDv1 packet. + let sent = bob.send_text(chat_id, "Hello!").await; + assert_seipd_version(&sent.payload, 1); + + // Bob creates a group with Alice and Charlie. + // Sending a message there should also use SEIPDv1 + // because for Bob it looks like Alice does not support SEIPDv2. + let charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await; + let group_id = create_group(bob, "groupname").await.unwrap(); + chat::add_contact_to_chat(bob, group_id, alice_contact_id).await?; + chat::add_contact_to_chat(bob, group_id, charlie_contact_id).await?; + + let sent = bob.send_text(group_id, "Hello!").await; + assert_seipd_version(&sent.payload, 1); + + // Bob gets a new key of Alice via new vCard + // and learns that Alice supports SEIPDv2. + assert_eq!(bob.add_or_lookup_contact_id(alice).await, alice_contact_id); + + let sent = bob.send_text(group_id, "Hello again with SEIPDv2!").await; + assert_seipd_version(&sent.payload, 2); + + Ok(()) +} diff --git a/src/pgp.rs b/src/pgp.rs index eba26f98f..2b10da1a3 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -495,6 +495,35 @@ pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option< None } +/// Returns true if public key advertises SEIPDv2 feature. +pub(crate) fn pubkey_supports_seipdv2(public_key: &SignedPublicKey) -> bool { + // If any Direct Key Signature or any User ID signature has SEIPDv2 feature, + // assume that recipient can handle SEIPDv2. + // + // Third-party User ID signatures are dropped during certificate merging. + // We don't check if the User ID is primary User ID. + // Primary User ID is preferred during merging + // and if some key has only non-primary User ID + // it is acceptable. It is anyway unlikely that SEIPDv2 + // is advertised in a key without DKS or primary User ID. + public_key + .details + .direct_signatures + .iter() + .chain( + public_key + .details + .users + .iter() + .flat_map(|user| user.signatures.iter()), + ) + .any(|signature| { + signature + .features() + .is_some_and(|features| features.seipd_v2()) + }) +} + #[cfg(test)] mod tests { use std::sync::LazyLock;