Skip to content

Commit a3a78e8

Browse files
committed
Broadcast-securejoin is working!!
1 parent 4f612fd commit a3a78e8

File tree

11 files changed

+208
-96
lines changed

11 files changed

+208
-96
lines changed

deltachat-jsonrpc/src/api/types/qr.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub enum QrObject {
4545
/// Fingerprint of the contact key as scanned from the QR code.
4646
fingerprint: String,
4747

48+
authcode: String,
49+
4850
/// The secret shared between all members,
4951
/// used to symmetrically encrypt&decrypt messages.
5052
shared_secret: String,
@@ -227,6 +229,7 @@ impl From<Qr> for QrObject {
227229
grpid,
228230
contact_id,
229231
fingerprint,
232+
authcode,
230233
shared_secret,
231234
} => {
232235
let contact_id = contact_id.to_u32();
@@ -236,6 +239,7 @@ impl From<Qr> for QrObject {
236239
grpid,
237240
contact_id,
238241
fingerprint,
242+
authcode,
239243
shared_secret,
240244
}
241245
}

src/chat.rs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ use crate::smtp::send_msg_to_smtp;
4242
use crate::stock_str;
4343
use crate::sync::{self, Sync::*, SyncData};
4444
use crate::tools::{
45-
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id,
46-
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
47-
gm2local_offset, smeared_time, time, truncate_msg_text,
45+
IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
46+
create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
47+
smeared_time, time, truncate_msg_text,
4848
};
4949
use crate::webxdc::StatusUpdateSerial;
5050
use crate::{chatlist_events, imap};
@@ -1624,6 +1624,18 @@ impl Chat {
16241624
self.typ == Chattype::Mailinglist
16251625
}
16261626

1627+
/// Returns true if chat is an outgoing broadcast channel.
1628+
pub fn is_out_broadcast(&self) -> bool {
1629+
self.typ == Chattype::OutBroadcast
1630+
}
1631+
1632+
/// Returns true if the chat is a broadcast channel,
1633+
/// regardless of whether self is on the sending
1634+
/// or receiving side.
1635+
pub fn is_any_broadcast(&self) -> bool {
1636+
matches!(self.typ, Chattype::OutBroadcast | Chattype::InBroadcast)
1637+
}
1638+
16271639
/// Returns None if user can send messages to this chat.
16281640
///
16291641
/// Otherwise returns a reason useful for logging.
@@ -1710,7 +1722,7 @@ impl Chat {
17101722
match self.typ {
17111723
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
17121724
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
1713-
Chattype::InBroadcast => Ok(false),
1725+
Chattype::InBroadcast => Ok(true),
17141726
}
17151727
}
17161728

@@ -2903,13 +2915,18 @@ async fn prepare_send_msg(
29032915
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
29042916
// Allow securejoin messages, they are supposed to repair the verification.
29052917
// If the chat is a contact request, let the user accept it later.
2918+
29062919
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
29072920
}
29082921
// Allow to send "Member removed" messages so we can leave the group/broadcast.
29092922
// Necessary checks should be made anyway before removing contact
29102923
// from the chat.
2911-
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
2912-
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
2924+
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
2925+
CantSendReason::InBroadcast => {
2926+
matches!(
2927+
msg.param.get_cmd(),
2928+
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
2929+
)
29132930
}
29142931
CantSendReason::MissingKey => msg
29152932
.param
@@ -3685,7 +3702,7 @@ pub async fn create_group_chat(
36853702
/// Returns the created chat's id.
36863703
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
36873704
let grpid = create_id();
3688-
let secret = create_broadcast_shared_secret();
3705+
let secret = create_id();
36893706
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
36903707
}
36913708

@@ -3748,6 +3765,35 @@ pub(crate) async fn create_broadcast_ex(
37483765
Ok(chat_id)
37493766
}
37503767

3768+
pub(crate) async fn load_broadcast_shared_secret(
3769+
context: &Context,
3770+
chat_id: ChatId,
3771+
) -> Result<Option<String>> {
3772+
Ok(context
3773+
.sql
3774+
.query_get_value(
3775+
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
3776+
(chat_id,),
3777+
)
3778+
.await?)
3779+
}
3780+
3781+
pub(crate) async fn save_broadcast_shared_secret(
3782+
context: &Context,
3783+
chat_id: ChatId,
3784+
shared_secret: &str,
3785+
) -> Result<()> {
3786+
context
3787+
.sql
3788+
.execute(
3789+
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
3790+
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
3791+
(chat_id, shared_secret),
3792+
)
3793+
.await?;
3794+
Ok(())
3795+
}
3796+
37513797
/// Set chat contacts in the `chats_contacts` table.
37523798
pub(crate) async fn update_chat_contacts_table(
37533799
context: &Context,

src/chat/chat_tests.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::imex::{ImexMode, has_backup, imex};
77
use crate::message::{MessengerMessage, delete_msgs};
88
use crate::mimeparser::{self, MimeMessage};
99
use crate::receive_imf::receive_imf;
10+
use crate::securejoin::get_securejoin_qr;
1011
use crate::test_utils::{
1112
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
1213
TimeShiftFalsePositiveNote, sync,
@@ -2896,11 +2897,13 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
28962897
let mut tcm = TestContextManager::new();
28972898
let alice = &tcm.alice().await;
28982899
let bob = &tcm.bob().await;
2899-
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
29002900

29012901
tcm.section("Create a broadcast channel with Bob, and send a message");
29022902
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
2903-
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
2903+
2904+
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
2905+
tcm.exec_securejoin_qr(bob, alice, &qr).await;
2906+
29042907
let mut sent = alice.send_text(alice_chat_id, "Hi somebody").await;
29052908

29062909
assert!(!sent.payload.contains("List-ID"));

src/mimefactory.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use tokio::fs;
1313

1414
use crate::aheader::{Aheader, EncryptPreference};
1515
use crate::blob::BlobObject;
16-
use crate::chat::{self, Chat};
16+
use crate::chat::{self, Chat, load_broadcast_shared_secret};
1717
use crate::config::Config;
1818
use crate::constants::ASM_SUBJECT;
1919
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
@@ -226,6 +226,9 @@ impl MimeFactory {
226226

227227
// Do not encrypt messages to mailing lists.
228228
encryption_keys = None;
229+
} else if chat.is_out_broadcast() {
230+
// Encrypt, but only symmetrically, not with the public keys.
231+
encryption_keys = Some(Vec::new());
229232
} else {
230233
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
231234
msg.param.get(Param::Arg)
@@ -541,8 +544,10 @@ impl MimeFactory {
541544
// messages are auto-sent unlike usual unencrypted messages.
542545
step == "vg-request-with-auth"
543546
|| step == "vc-request-with-auth"
547+
|| step == "vb-request-with-auth"
544548
|| step == "vg-member-added"
545549
|| step == "vc-contact-confirm"
550+
// TODO possibly add vb-member-added here
546551
}
547552
}
548553

@@ -1118,21 +1123,15 @@ impl MimeFactory {
11181123
};
11191124

11201125
let symmetric_key: Option<String> = match &self.loaded {
1121-
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
1126+
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
11221127
// If there is no symmetric key yet
11231128
// (because this is an old broadcast channel,
11241129
// created before we had symmetric encryption),
11251130
// we just encrypt asymmetrically.
11261131
// Symmetric encryption exists since 2025-08;
11271132
// some time after that, we can think about requiring everyone
11281133
// to switch to symmetrically-encrypted broadcast lists.
1129-
context
1130-
.sql
1131-
.query_get_value(
1132-
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
1133-
(chat.id,),
1134-
)
1135-
.await?
1134+
load_broadcast_shared_secret(context, chat.id).await?
11361135
}
11371136
_ => None,
11381137
};
@@ -1489,7 +1488,10 @@ impl MimeFactory {
14891488
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
14901489
if !param2.is_empty() {
14911490
headers.push((
1492-
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
1491+
if step == "vg-request-with-auth"
1492+
|| step == "vc-request-with-auth"
1493+
|| step == "vb-request-with-auth"
1494+
{
14931495
"Secure-Join-Auth"
14941496
} else {
14951497
"Secure-Join-Invitenumber"

src/qr.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ pub enum Qr {
9696

9797
fingerprint: Fingerprint,
9898

99+
authcode: String,
100+
99101
shared_secret: String,
100102
},
101103

@@ -396,7 +398,7 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
396398

397399
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
398400
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
399-
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&b=BROADCAST_SHARED_SECRET`
401+
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&s=AUTH&b=BROADCAST_SHARED_SECRET`
400402
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
401403
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
402404
let payload = qr
@@ -474,7 +476,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
474476
None
475477
};
476478

477-
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
479+
if let (Some(addr), Some(invitenumber), Some(authcode)) =
480+
(&addr, invitenumber, authcode.clone())
481+
{
478482
let addr = ContactAddress::new(addr)?;
479483
let (contact_id, _) = Contact::add_or_lookup_ex(
480484
context,
@@ -545,8 +549,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
545549
authcode,
546550
})
547551
}
548-
} else if let (Some(addr), Some(broadcast_name), Some(grpid), Some(shared_secret)) =
549-
(&addr, grpname, grpid, broadcast_shared_secret)
552+
} else if let (
553+
Some(addr),
554+
Some(broadcast_name),
555+
Some(grpid),
556+
Some(authcode),
557+
Some(shared_secret),
558+
) = (&addr, grpname, grpid, authcode, broadcast_shared_secret)
550559
{
551560
// This is a broadcast channel invite link.
552561
// TODO code duplication with the previous block
@@ -567,6 +576,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
567576
grpid,
568577
contact_id,
569578
fingerprint,
579+
authcode,
570580
shared_secret,
571581
})
572582
} else if let Some(addr) = addr {

src/receive_imf.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
4343
use crate::simplify;
4444
use crate::stock_str;
4545
use crate::sync::Sync::*;
46-
use crate::tools::{self, buf_compress, create_broadcast_shared_secret, remove_subject_prefix};
46+
use crate::tools::{self, buf_compress, create_id, remove_subject_prefix};
4747
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
4848
use crate::{contact, imap};
4949

@@ -1543,7 +1543,7 @@ async fn do_chat_assignment(
15431543
} else {
15441544
let name =
15451545
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
1546-
let secret = create_broadcast_shared_secret();
1546+
let secret = create_id();
15471547
chat::create_broadcast_ex(context, Nosync, listid, name, secret).await?
15481548
},
15491549
);

0 commit comments

Comments
 (0)