diff --git a/src/e2ee.rs b/src/e2ee.rs index 9968c2245..4ca0cd1a9 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -59,6 +59,26 @@ impl EncryptHelper { Ok(ctext) } + /// TODO documentation + pub async fn encrypt_for_broadcast( + self, + context: &Context, + passphrase: &str, + mail_to_encrypt: MimePart<'static>, + compress: bool, + ) -> Result { + let sign_key = load_self_secret_key(context).await?; + + let mut raw_message = Vec::new(); + let cursor = Cursor::new(&mut raw_message); + mail_to_encrypt.clone().write_part(cursor).ok(); + + let ctext = + pgp::encrypt_for_broadcast(raw_message, passphrase, Some(sign_key), compress).await?; + + 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 { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 606d11631..304bfd223 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1117,18 +1117,42 @@ impl MimeFactory { Loaded::Mdn { .. } => true, }; - // Encrypt to self unconditionally, - // even for a single-device setup. - let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; - encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone())); + let symmetric_key = match &self.loaded { + Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => { + // If there is no symmetric key yet + // (because this is an old broadcast channel, + // created before we had symmetric encryption), + // we just encrypt asymmetrically. + // Symmetric encryption exists since 2025-08; + // some time after that, we can think about requiring everyone + // to switch to symmetrically-encrypted broadcast lists. + chat.param.get(Param::SymmetricKey) + } + _ => None, + }; + + let encrypted = if let Some(symmetric_key) = symmetric_key { + encrypt_helper + .encrypt_for_broadcast(context, symmetric_key, message, compress) + .await? + } else { + // Asymmetric encryption + + // Encrypt to self unconditionally, + // even for a single-device setup. + let mut encryption_keyring = vec![encrypt_helper.public_key.clone()]; + encryption_keyring + .extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone())); + + encrypt_helper + .encrypt(context, encryption_keyring, message, compress) + .await? + }; // XXX: additional newline is needed // to pass filtermail at - // - let encrypted = encrypt_helper - .encrypt(context, encryption_keyring, message, compress) - .await? - + "\n"; + // : + let encrypted = encrypted + "\n"; // Set the appropriate Content-Type for the outer message MimePart::new( diff --git a/src/param.rs b/src/param.rs index 9e0433a25..5d32c23da 100644 --- a/src/param.rs +++ b/src/param.rs @@ -169,6 +169,11 @@ pub enum Param { /// post something to the mailing list. ListPost = b'p', + /// For Chats of type [`Chattype::OutBroadcast`] and [`Chattype::InBroadcast`]: + /// The symmetric key shared among all chat participants, + /// used to encrypt and decrypt messages. + SymmetricKey = b'z', + /// For Contacts: If this is the List-Post address of a mailing list, contains /// the List-Id of the mailing list (which is also used as the group id of the chat). ListId = b's', diff --git a/src/pgp.rs b/src/pgp.rs index e00a41310..735b6a210 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -12,6 +12,7 @@ use pgp::composed::{ SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder, TheRing, }; +use pgp::crypto::aead::{AeadAlgorithm, ChunkSize}; use pgp::crypto::ecc_curve::ECCCurve; use pgp::crypto::hash::HashAlgorithm; use pgp::crypto::sym::SymmetricKeyAlgorithm; @@ -322,6 +323,41 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec) -> Result { .await? } +/// Symmetric encryption. +pub async fn encrypt_for_broadcast( + plain: Vec, + passphrase: &str, + private_key_for_signing: Option, + compress: bool, +) -> Result { + let passphrase = Password::from(passphrase.to_string()); + + tokio::task::spawn_blocking(move || { + let mut rng = thread_rng(); + let s2k = StringToKey::new_default(&mut rng); + let msg = MessageBuilder::from_bytes("", plain); + let mut msg = msg.seipd_v2( + &mut rng, + SymmetricKeyAlgorithm::AES128, + AeadAlgorithm::Ocb, + ChunkSize::C8KiB, + ); + msg.encrypt_with_password(&mut rng, s2k, &passphrase)?; + + if let Some(ref skey) = private_key_for_signing { + msg.sign(&**skey, Password::empty(), HASH_ALGORITHM); + if compress { + msg.compression(CompressionAlgorithm::ZLIB); + } + } + + let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?; + + Ok(encoded_msg) + }) + .await? +} + /// Symmetric decryption. pub async fn symm_decrypt( passphrase: &str,