Skip to content

[WIP] Symmetric encryption for channels #6995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ opt-level = 1
# Make anyhow `backtrace` feature useful.
# With `debug = 0` there are no line numbers in the backtrace
# produced with RUST_BACKTRACE=1.
debug = 1
debug = 'full'
opt-level = 0

[profile.fuzz]
Expand Down
39 changes: 39 additions & 0 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2889,6 +2889,45 @@ async fn test_block_broadcast() -> Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_decrypt_broadcast_integration() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_without_secret = &tcm.bob().await;

let secret = "secret";

let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;

tcm.section("Create a broadcast channel with Bob, and send a message");
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;

let mut alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
alice_chat.param.set(Param::SymmetricKey, secret);
alice_chat.update_param(alice).await?;

// TODO the chat_id 10 is magical here:
bob.sql
.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (10, ?)",
(secret,),
)
.await?;

let sent = alice
.send_text(alice_chat_id, "Symmetrically encrypted message")
.await;
let rcvd = bob.recv_msg(&sent).await;
assert_eq!(rcvd.text, "Symmetrically encrypted message");

tcm.section("If Bob doesn't know the secret, he can't decrypt the message");
bob_without_secret.recv_msg_trash(&sent).await;

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
Expand Down
3 changes: 2 additions & 1 deletion src/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ use crate::pgp;
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<Option<::pgp::composed::Message<'static>>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};

let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::pk_decrypt(data, private_keyring)?;
let msg = pgp::decrypt(data, private_keyring, symmetric_secrets)?;

Ok(Some(msg))
}
Expand Down
19 changes: 19 additions & 0 deletions src/e2ee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ 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<String> {
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, 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<String> {
Expand Down
43 changes: 34 additions & 9 deletions src/mimefactory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1118,18 +1118,43 @@ 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 {
info!(context, "Symmetrically encrypting for broadcast channel.");
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
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
let encrypted = encrypt_helper
.encrypt(context, encryption_keyring, message, compress)
.await?
+ "\n";
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
let encrypted = encrypted + "\n";

// Set the appropriate Content-Type for the outer message
MimePart::new(
Expand Down
89 changes: 51 additions & 38 deletions src/mimeparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,50 +333,63 @@ impl MimeMessage {

let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
let symmetric_secrets: Vec<String> = context
.sql
.query_map(
"SELECT secret FROM broadcasts_shared_secrets",
(),
|row| row.get(0),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;

let (mail, is_encrypted) =
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();

let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}

decrypted_msg = Some(msg);
let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
}) {
Ok(Some(mut msg)) => {
mail_raw = msg.as_data_vec().unwrap_or_default();

timestamp_sent = Self::get_timestamp_sent(
&decrypted_mail.headers,
timestamp_sent,
timestamp_rcvd,
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
context,
"decrypted message mime-body:\n{}",
String::from_utf8_lossy(&mail_raw),
);
}

if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
decrypted_msg = Some(msg);

(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
timestamp_sent = Self::get_timestamp_sent(
&decrypted_mail.headers,
timestamp_sent,
timestamp_rcvd,
);

if let Some(protected_aheader_value) = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
}
};

(Ok(decrypted_mail), true)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
}
};

let autocrypt_header = if !incoming {
None
Expand Down
5 changes: 5 additions & 0 deletions src/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]: // TODO (or just OutBroadcast)
/// 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',
Expand Down
Loading
Loading