Skip to content

Commit e597b6f

Browse files
committed
feat: Symmetric encryption. No decryption, no sharing of the secret, not tested.
1 parent aad8f69 commit e597b6f

File tree

4 files changed

+94
-9
lines changed

4 files changed

+94
-9
lines changed

src/e2ee.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ impl EncryptHelper {
5959
Ok(ctext)
6060
}
6161

62+
/// TODO documentation
63+
pub async fn encrypt_for_broadcast(
64+
self,
65+
context: &Context,
66+
passphrase: &str,
67+
mail_to_encrypt: MimePart<'static>,
68+
compress: bool,
69+
) -> Result<String> {
70+
let sign_key = load_self_secret_key(context).await?;
71+
72+
let mut raw_message = Vec::new();
73+
let cursor = Cursor::new(&mut raw_message);
74+
mail_to_encrypt.clone().write_part(cursor).ok();
75+
76+
let ctext =
77+
pgp::encrypt_for_broadcast(raw_message, passphrase, Some(sign_key), compress).await?;
78+
79+
Ok(ctext)
80+
}
81+
6282
/// Signs the passed-in `mail` using the private key from `context`.
6383
/// Returns the payload and the signature.
6484
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {

src/mimefactory.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,18 +1118,42 @@ impl MimeFactory {
11181118
Loaded::Mdn { .. } => true,
11191119
};
11201120

1121-
// Encrypt to self unconditionally,
1122-
// even for a single-device setup.
1123-
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
1124-
encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
1121+
let symmetric_key = match &self.loaded {
1122+
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
1123+
// If there is no symmetric key yet
1124+
// (because this is an old broadcast channel,
1125+
// created before we had symmetric encryption),
1126+
// we just encrypt asymmetrically.
1127+
// Symmetric encryption exists since 2025-08;
1128+
// some time after that, we can think about requiring everyone
1129+
// to switch to symmetrically-encrypted broadcast lists.
1130+
chat.param.get(Param::SymmetricKey)
1131+
}
1132+
_ => None,
1133+
};
1134+
1135+
let encrypted = if let Some(symmetric_key) = symmetric_key {
1136+
encrypt_helper
1137+
.encrypt_for_broadcast(context, symmetric_key, message, compress)
1138+
.await?
1139+
} else {
1140+
// Asymmetric encryption
1141+
1142+
// Encrypt to self unconditionally,
1143+
// even for a single-device setup.
1144+
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
1145+
encryption_keyring
1146+
.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
1147+
1148+
encrypt_helper
1149+
.encrypt(context, encryption_keyring, message, compress)
1150+
.await?
1151+
};
11251152

11261153
// XXX: additional newline is needed
11271154
// to pass filtermail at
1128-
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
1129-
let encrypted = encrypt_helper
1130-
.encrypt(context, encryption_keyring, message, compress)
1131-
.await?
1132-
+ "\n";
1155+
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
1156+
let encrypted = encrypted + "\n";
11331157

11341158
// Set the appropriate Content-Type for the outer message
11351159
MimePart::new(

src/param.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ pub enum Param {
169169
/// post something to the mailing list.
170170
ListPost = b'p',
171171

172+
/// For Chats of type [`Chattype::OutBroadcast`] and [`Chattype::InBroadcast`]:
173+
/// The symmetric key shared among all chat participants,
174+
/// used to encrypt and decrypt messages.
175+
SymmetricKey = b'z',
176+
172177
/// For Contacts: If this is the List-Post address of a mailing list, contains
173178
/// the List-Id of the mailing list (which is also used as the group id of the chat).
174179
ListId = b's',

src/pgp.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use pgp::composed::{
1212
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
1313
StandaloneSignature, SubkeyParamsBuilder, TheRing,
1414
};
15+
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
1516
use pgp::crypto::ecc_curve::ECCCurve;
1617
use pgp::crypto::hash::HashAlgorithm;
1718
use pgp::crypto::sym::SymmetricKeyAlgorithm;
@@ -322,6 +323,41 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
322323
.await?
323324
}
324325

326+
/// Symmetric encryption.
327+
pub async fn encrypt_for_broadcast(
328+
plain: Vec<u8>,
329+
passphrase: &str,
330+
private_key_for_signing: Option<SignedSecretKey>,
331+
compress: bool,
332+
) -> Result<String> {
333+
let passphrase = Password::from(passphrase.to_string());
334+
335+
tokio::task::spawn_blocking(move || {
336+
let mut rng = thread_rng();
337+
let s2k = StringToKey::new_default(&mut rng);
338+
let msg = MessageBuilder::from_bytes("", plain);
339+
let mut msg = msg.seipd_v2(
340+
&mut rng,
341+
SymmetricKeyAlgorithm::AES128,
342+
AeadAlgorithm::Ocb,
343+
ChunkSize::C8KiB,
344+
);
345+
msg.encrypt_with_password(&mut rng, s2k, &passphrase)?;
346+
347+
if let Some(ref skey) = private_key_for_signing {
348+
msg.sign(&**skey, Password::empty(), HASH_ALGORITHM);
349+
if compress {
350+
msg.compression(CompressionAlgorithm::ZLIB);
351+
}
352+
}
353+
354+
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
355+
356+
Ok(encoded_msg)
357+
})
358+
.await?
359+
}
360+
325361
/// Symmetric decryption.
326362
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
327363
passphrase: &str,

0 commit comments

Comments
 (0)