mirror of
https://github.com/chatmail/core.git
synced 2026-04-01 21:12:13 +03:00
Compare commits
7 Commits
29fbf05fe4
...
hoc/channe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba6e885ae | ||
|
|
eadbf41383 | ||
|
|
70ab41d7c2 | ||
|
|
fab39fdb97 | ||
|
|
63664396dd | ||
|
|
f10f65333b | ||
|
|
7cf42a43e7 |
@@ -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]
|
||||
|
||||
45
src/chat.rs
45
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<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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
19
src/e2ee.rs
19
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<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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
83
src/pgp.rs
83
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<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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?
|
||||
|
||||
20
src/tools.rs
20
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.
|
||||
|
||||
Reference in New Issue
Block a user