Compare commits

...

7 Commits

13 changed files with 400 additions and 81 deletions

View File

@@ -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]

View File

@@ -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<ChatId> {
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<ChatId> {
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<String>),
@@ -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,

View File

@@ -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");

View File

@@ -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))
}

View File

@@ -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> {

View File

@@ -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,

View File

@@ -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
// <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(
@@ -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();

View File

@@ -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 (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();
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);
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)
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(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)
}
};
(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

View File

@@ -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',

View File

@@ -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<u8>,
private_keys_for_decryption: &[SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<pgp::composed::Message<'static>> {
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<Password> = 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<u8>) -> Result<String> {
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<u8>) -> Result<String> {
.await?
}
/// Symmetric encryption.
pub async fn encrypt_for_broadcast(
plain: Vec<u8>,
passphrase: &str,
private_key_for_signing: SignedSecretKey,
compress: bool,
) -> Result<String> {
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<T: BufRead + std::fmt::Debug + 'static + Send>(
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<Fingerprint>,
Vec<u8>,
)> {
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(())
}
}

View File

@@ -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<GroupChangesInfo> {
// TODO code duplication with apply_in_broadcast_changes()
ensure!(chat.typ == Chattype::OutBroadcast);
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?;
let mut send_event_chat_modified = false;
let mut better_msg = None;
return Ok(GroupChangesInfo {
better_msg: Some("".to_string()),
added_removed_id: None,
silent: true,
extra_msgs: vec![],
});
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) {
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);

View File

@@ -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?

View File

@@ -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.