diff --git a/Cargo.toml b/Cargo.toml index 4ff5119260..f62a952e00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/src/chat.rs b/src/chat.rs index bee6ec0223..7539b5bc55 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -42,9 +42,9 @@ use crate::smtp::send_msg_to_smtp; use crate::stock_str; use crate::sync::{self, Sync::*, SyncData}; use crate::tools::{ - IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid, - create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset, - smeared_time, time, truncate_msg_text, + IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id, + create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path, + gm2local_offset, smeared_time, time, truncate_msg_text, }; use crate::webxdc::StatusUpdateSerial; use crate::{chatlist_events, imap}; @@ -3685,7 +3685,8 @@ pub async fn create_group_chat( /// Returns the created chat's id. pub async fn create_broadcast(context: &Context, chat_name: String) -> Result { let grpid = create_id(); - create_broadcast_ex(context, Sync, grpid, chat_name).await + let secret = create_broadcast_shared_secret(); + create_broadcast_ex(context, Sync, grpid, chat_name, secret).await } pub(crate) async fn create_broadcast_ex( @@ -3693,6 +3694,7 @@ pub(crate) async fn create_broadcast_ex( sync: sync::Sync, grpid: String, chat_name: String, + secret: String, ) -> Result { let row_id = { let chat_name = &chat_name; @@ -3710,17 +3712,28 @@ pub(crate) async fn create_broadcast_ex( }, )?); } + let mut param = Params::new(); + // param.set(Param::Unpromoted, 1); // TODO broadcasts will just never be unpromoted for now + param.set(Param::SymmetricKey, &secret); t.execute( "INSERT INTO chats \ (type, name, grpid, param, created_timestamp) \ - VALUES(?, ?, ?, \'U=1\', ?);", + VALUES(?, ?, ?, ?, ?);", ( Chattype::OutBroadcast, &chat_name, &grpid, + param.to_string(), create_smeared_timestamp(context), ), )?; + let chat_id = t.last_insert_rowid(); + // TODO code duplication of `INSERT INTO broadcasts_shared_secrets` + t.execute( + "INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?) + ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id", + (chat_id, &secret), + )?; Ok(t.last_insert_rowid().try_into()?) }; context.sql.transaction(trans_fn).await? @@ -3732,7 +3745,7 @@ pub(crate) async fn create_broadcast_ex( if sync.into() { let id = SyncId::Grpid(grpid); - let action = SyncAction::CreateBroadcast(chat_name); + let action = SyncAction::CreateBroadcast { chat_name, secret }; self::sync(context, id, action).await.log_err(context).ok(); } @@ -3918,7 +3931,7 @@ pub(crate) async fn add_contact_to_chat_ex( } add_to_chat_contacts_table(context, time(), chat_id, &[contact_id]).await?; } - if chat.typ == Chattype::Group && chat.is_promoted() { + if chat.is_promoted() { msg.viewtype = Viewtype::Text; let contact_addr = contact.get_addr().to_lowercase(); @@ -4948,7 +4961,10 @@ pub(crate) enum SyncAction { SetVisibility(ChatVisibility), SetMuted(MuteDuration), /// Create broadcast channel with the given name. - CreateBroadcast(String), + CreateBroadcast { + chat_name: String, + secret: String, + }, Rename(String), /// Set chat contacts by their addresses. SetContacts(Vec), @@ -5011,8 +5027,15 @@ impl Context { .id } SyncId::Grpid(grpid) => { - if let SyncAction::CreateBroadcast(name) = action { - create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?; + if let SyncAction::CreateBroadcast { chat_name, secret } = action { + create_broadcast_ex( + self, + Nosync, + grpid.clone(), + chat_name.clone(), + secret.to_string(), + ) + .await?; return Ok(()); } get_chat_id_by_grpid(self, grpid) @@ -5035,7 +5058,7 @@ impl Context { SyncAction::Accept => chat_id.accept_ex(self, Nosync).await, SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await, SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await, - SyncAction::CreateBroadcast(_) => { + SyncAction::CreateBroadcast { .. } => { Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request.")) } SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await, diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index b693934f76..2a15f20c63 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3036,6 +3036,45 @@ async fn test_leave_broadcast_multidevice() -> 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; @@ -3746,7 +3785,9 @@ async fn test_sync_broadcast() -> Result<()> { assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name()); assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty()); add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?; - sync(alice0, alice1).await; + let sent = alice0.pop_sent_msg().await; + let rcvd = alice1.recv_msg(&sent).await; + dbg!(rcvd); // TODO // This also imports Bob's key from the vCard. // Otherwise it is possible that second device @@ -3788,12 +3829,25 @@ async fn test_sync_name() -> Result<()> { let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?; sync(alice0, alice1).await; let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?; + set_chat_name(alice0, a0_broadcast_id, "Broadcast channel 42").await?; - sync(alice0, alice1).await; + //sync(alice0, alice1).await; // crash + + let sent = alice0.pop_sent_msg().await; + let rcvd = alice1.recv_msg(&sent).await; + assert_eq!(rcvd.from_id, ContactId::SELF); + assert_eq!(rcvd.to_id, ContactId::SELF); + assert_eq!( + rcvd.text, + "You changed group name from \"Channel\" to \"Broadcast channel 42\"." + ); + assert_eq!(rcvd.param.get_cmd(), SystemMessage::GroupNameChanged); let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid) .await? .unwrap() .0; + assert_eq!(rcvd.chat_id, a1_broadcast_id); + let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?; assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast); assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42"); diff --git a/src/decrypt.rs b/src/decrypt.rs index 8e242a57ea..47d4372196 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -14,13 +14,14 @@ use crate::pgp; pub fn try_decrypt<'a>( mail: &'a ParsedMail<'a>, private_keyring: &'a [SignedSecretKey], + symmetric_secrets: &[String], ) -> Result>> { 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)) } diff --git a/src/e2ee.rs b/src/e2ee.rs index 9968c22457..c1d1d77d2e 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -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 { + 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 { diff --git a/src/headerdef.rs b/src/headerdef.rs index 330a4d9ba0..fd385c3762 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -93,6 +93,10 @@ pub enum HeaderDef { /// This message obsoletes the text of the message defined here by rfc724_mid. ChatEdit, + /// The secret shared amongst all recipients of this broadcast channel. + /// This secret. + ChatBroadcastSecret, + /// [Autocrypt](https://autocrypt.org/) header. Autocrypt, AutocryptGossip, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 606d116313..7980d3b205 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -789,7 +789,7 @@ impl MimeFactory { } } - if let Loaded::Message { chat, .. } = &self.loaded { + if let Loaded::Message { msg, chat } = &self.loaded { if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast { headers.push(( "List-ID", @@ -799,6 +799,15 @@ impl MimeFactory { )) .into(), )); + + if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup { + if let Some(secret) = chat.param.get(Param::SymmetricKey) { + headers.push(( + "Chat-Broadcast-Secret", + mail_builder::headers::text::Text::new(secret.to_string()).into(), + )); + } + } } } @@ -979,6 +988,15 @@ impl MimeFactory { } else { unprotected_headers.push(header.clone()); } + } else if header_name == "chat-broadcast-secret" { + if is_encrypted { + protected_headers.push(header.clone()); + } else { + warn!( + context, + "Message is unnecrypted, not including broadcast secret" + ); + } } else if is_encrypted { protected_headers.push(header.clone()); @@ -1117,18 +1135,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 - // - 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( @@ -1361,7 +1404,6 @@ impl MimeFactory { } } SystemMessage::MemberAddedToGroup => { - ensure!(chat.typ != Chattype::OutBroadcast); // TODO: lookup the contact by ID rather than email address. // We are adding key-contacts, the cannot be looked up by address. let email_to_add = msg.param.get(Param::Arg).unwrap_or_default(); diff --git a/src/mimeparser.rs b/src/mimeparser.rs index bc6b38cd2f..be38ab59d1 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -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 = context + .sql + .query_map( + "SELECT secret FROM broadcasts_shared_secrets", + (), + |row| row.get(0), + |rows| { + rows.collect::, _>>() + .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 diff --git a/src/param.rs b/src/param.rs index 9e0433a256..1e1302bfb8 100644 --- a/src/param.rs +++ b/src/param.rs @@ -169,6 +169,13 @@ pub enum Param { /// post something to the mailing list. ListPost = b'p', + /// For Chats and Messages: + /// For chats of type [`Chattype::OutBroadcast`] and [`Chattype::InBroadcast`] // TODO (or just OutBroadcast) + /// and for messages adding members to such a chat. + /// The symmetric key shared among all chat participants, + /// used to encrypt and decrypt messages. + SymmetricKey = b'z', // TODO remove this again + /// 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 e00a41310b..69beddded9 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; @@ -235,9 +236,10 @@ pub fn pk_calc_signature( /// /// Receiver private keys are provided in /// `private_keys_for_decryption`. -pub fn pk_decrypt( +pub fn decrypt( ctext: Vec, private_keys_for_decryption: &[SignedSecretKey], + symmetric_secrets: &[String], ) -> Result> { let cursor = Cursor::new(ctext); let (msg, _headers) = Message::from_armor(cursor)?; @@ -245,10 +247,17 @@ pub fn pk_decrypt( let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect(); let empty_pw = Password::empty(); + // TODO it may degrade performance that we always try out all passwords here + let message_password: Vec = symmetric_secrets + .iter() + .map(|p| Password::from(p.as_str())) + .collect(); + let message_password: Vec<&Password> = message_password.iter().collect(); + let ring = TheRing { secret_keys: skeys, key_passwords: vec![&empty_pw], - message_password: vec![], + message_password, session_keys: vec![], allow_legacy: false, }; @@ -311,7 +320,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec) -> Result { tokio::task::spawn_blocking(move || { let mut rng = thread_rng(); let s2k = StringToKey::new_default(&mut rng); - let builder = MessageBuilder::from_bytes("", plain); + let builder: MessageBuilder<'_> = MessageBuilder::from_bytes("", plain); let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM); builder.encrypt_with_password(s2k, &passphrase)?; @@ -322,6 +331,39 @@ 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: SignedSecretKey, + 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)?; + + msg.sign(&*private_key_for_signing, 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, @@ -345,7 +387,10 @@ mod tests { use tokio::sync::OnceCell; use super::*; - use crate::test_utils::{alice_keypair, bob_keypair}; + use crate::{ + key::load_self_secret_key, + test_utils::{TestContextManager, alice_keypair, bob_keypair}, + }; fn pk_decrypt_and_validate<'a>( ctext: &'a [u8], @@ -356,7 +401,7 @@ mod tests { HashSet, Vec, )> { - let mut msg = pk_decrypt(ctext.to_vec(), private_keys_for_decryption)?; + let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?; let content = msg.as_data_vec()?; let ret_signature_fingerprints = valid_signature_fingerprints(&msg, public_keys_for_validation)?; @@ -542,4 +587,32 @@ mod tests { assert_eq!(content, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_encrypt_decrypt_broadcast() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let plain = Vec::from(b"this is the secret message"); + let shared_secret = "shared secret"; + let ctext = encrypt_for_broadcast( + plain.clone(), + shared_secret, + load_self_secret_key(alice).await?, + true, + ) + .await?; + + let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?; + let mut decrypted = decrypt( + ctext.into(), + &bob_private_keyring, + &[shared_secret.to_string()], + )?; + + assert_eq!(decrypted.as_data_vec()?, plain); + + Ok(()) + } } diff --git a/src/receive_imf.rs b/src/receive_imf.rs index fe980d52a0..6e29a81df4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1540,10 +1540,25 @@ async fn do_chat_assignment( if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? { id - } else { + } else if let Some(secret) = + mime_parser.get_header(HeaderDef::ChatBroadcastSecret) + { let name = compute_mailinglist_name(mailinglist_header, &listid, mime_parser); - chat::create_broadcast_ex(context, Nosync, listid, name).await? + chat::create_broadcast_ex( + context, + Nosync, + listid, + name, + secret.to_string(), + ) + .await? + } else { + warn!( + context, + "Unknown shared secret for outgoing broadcast (TRASH)" + ); + DC_CHAT_ID_TRASH }, ); } @@ -3450,21 +3465,46 @@ async fn apply_out_broadcast_changes( chat: &mut Chat, from_id: ContactId, ) -> Result { + // TODO code duplication with apply_in_broadcast_changes() ensure!(chat.typ == Chattype::OutBroadcast); + let mut send_event_chat_modified = false; + let mut better_msg = None; + + apply_chat_name_and_avatar_changes( + context, + mime_parser, + from_id, + chat, + &mut send_event_chat_modified, + &mut better_msg, + ) + .await?; + if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { - // The sender of the message left the broadcast channel - remove_from_chat_contacts_table(context, chat.id, from_id).await?; - - return Ok(GroupChangesInfo { - better_msg: Some("".to_string()), - added_removed_id: None, - silent: true, - extra_msgs: vec![], - }); + if from_id != ContactId::SELF { + // The sender of the message left the broadcast channel + remove_from_chat_contacts_table(context, chat.id, from_id).await?; + + return Ok(GroupChangesInfo { + better_msg: Some("".to_string()), + added_removed_id: None, + silent: true, + extra_msgs: vec![], + }); + } } - Ok(GroupChangesInfo::default()) + if send_event_chat_modified { + context.emit_event(EventType::ChatModified(chat.id)); + chatlist_events::emit_chatlist_item_changed(context, chat.id); + } + Ok(GroupChangesInfo { + better_msg, + added_removed_id: None, + silent: false, + extra_msgs: vec![], + }) } async fn apply_in_broadcast_changes( @@ -3497,6 +3537,17 @@ async fn apply_in_broadcast_changes( } } + if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) { + context + .sql + .execute( + "INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?) + ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id", + (chat.id, secret), + ) + .await?; + } + if send_event_chat_modified { context.emit_event(EventType::ChatModified(chat.id)); chatlist_events::emit_chatlist_item_changed(context, chat.id); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 653d74d09a..18735a42c2 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1251,6 +1251,18 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint); .await?; } + inc_and_check(&mut migration_version, 133)?; + if dbversion < migration_version { + sql.execute_migration( + "CREATE TABLE broadcasts_shared_secrets( + chat_id INTEGER PRIMARY KEY NOT NULL, -- TODO we don't actually need the chat_id + secret TEXT NOT NULL + ) STRICT", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? diff --git a/src/tools.rs b/src/tools.rs index cb37bdeee8..2122e02a1b 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -300,6 +300,26 @@ pub(crate) fn create_id() -> String { base64::engine::general_purpose::URL_SAFE.encode(arr) } +/// Generate a shared secret for a broadcast channel, consisting of 64 characters.. +/// +/// The string generated by this function has 384 bits of entropy +/// and is returned as 64 Base64 characters, each containing 6 bits of entropy. +/// 384 is chosen because it is sufficiently secure +/// (larger than AES-128 keys used for message encryption) +/// TODO: Is it still true that we use AES-128? This info is taken from create_id() comment above. +/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character). +// TODO ask someone what a good size would be here +pub(crate) fn create_broadcast_shared_secret() -> String { + // ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure. + let mut rng = thread_rng(); + + // Generate 384 random bits. + let mut arr = [0u8; 48]; + rng.fill(&mut arr[..]); + + base64::engine::general_purpose::URL_SAFE.encode(arr) +} + /// Returns true if given string is a valid ID. /// /// All IDs generated with `create_id()` should be considered valid.