From 3389e938203c7a596b9ac3017cd4a706728d54d5 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 21 Jul 2025 17:37:48 +0200 Subject: [PATCH] feat: Add broadcast QR type (todo: documentation) --- deltachat-ffi/src/lot.rs | 6 ++++ deltachat-jsonrpc/src/api/types/qr.rs | 32 +++++++++++++++++++ src/qr.rs | 45 ++++++++++++++++++++++++++- src/securejoin/bob.rs | 2 ++ src/securejoin/qrinvite.rs | 20 ++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index e77483ef7..392a2b4ba 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -45,6 +45,7 @@ impl Lot { Self::Qr(qr) => match qr { Qr::AskVerifyContact { .. } => None, Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)), + Qr::AskJoinBroadcast { broadcast_name, .. } => Some(Cow::Borrowed(broadcast_name)), Qr::FprOk { .. } => None, Qr::FprMismatch { .. } => None, Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)), @@ -99,6 +100,7 @@ impl Lot { Self::Qr(qr) => match qr { Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact, Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup, + Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast, Qr::FprOk { .. } => LotState::QrFprOk, Qr::FprMismatch { .. } => LotState::QrFprMismatch, Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr, @@ -126,6 +128,7 @@ impl Lot { Self::Qr(qr) => match qr { Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(), Qr::AskVerifyGroup { .. } => Default::default(), + Qr::AskJoinBroadcast { .. } => Default::default(), Qr::FprOk { contact_id } => contact_id.to_u32(), Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(), Qr::FprWithoutAddr { .. } => Default::default(), @@ -169,6 +172,9 @@ pub enum LotState { /// text1=groupname QrAskVerifyGroup = 202, + /// text1=broadcast_name + QrAskJoinBroadcast = 204, + /// id=contact QrFprOk = 210, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index 61d8141f7..b7c177b09 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -34,6 +34,21 @@ pub enum QrObject { /// Authentication code. authcode: String, }, + /// Ask the user whether to join the broadcast channel. + AskJoinBroadcast { + /// Chat name. + broadcast_name: String, + /// Group ID. + grpid: String, + /// ID of the contact. + contact_id: u32, + /// Fingerprint of the contact key as scanned from the QR code. + fingerprint: String, + + /// The secret shared between all members, + /// used to symmetrically encrypt&decrypt messages. + shared_secret: String, + }, /// Contact fingerprint is verified. /// /// Ask the user if they want to start chatting. @@ -207,6 +222,23 @@ impl From for QrObject { authcode, } } + Qr::AskJoinBroadcast { + broadcast_name, + grpid, + contact_id, + fingerprint, + shared_secret, + } => { + let contact_id = contact_id.to_u32(); + let fingerprint = fingerprint.to_string(); + QrObject::AskJoinBroadcast { + broadcast_name, + grpid, + contact_id, + fingerprint, + shared_secret, + } + } Qr::FprOk { contact_id } => { let contact_id = contact_id.to_u32(); QrObject::FprOk { contact_id } diff --git a/src/qr.rs b/src/qr.rs index 2076837dd..2cdd8de0b 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -84,6 +84,21 @@ pub enum Qr { authcode: String, }, + /// Ask whether to join the broadcast channel. + AskJoinBroadcast { + // TODO document + broadcast_name: String, + + // TODO not sure wheter it makes sense to call this grpid just because it's called like this in the db + grpid: String, + + contact_id: ContactId, + + fingerprint: Fingerprint, + + shared_secret: String, + }, + /// Contact fingerprint is verified. /// /// Ask the user if they want to start chatting. @@ -381,7 +396,7 @@ pub fn format_backup(qr: &Qr) -> Result { /// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH` -/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&c=CHANNELNAME&x=CHANNELID&s=SHAREDSECRET` +/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&b=BROADCAST_SHARED_SECRET` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR` async fn decode_openpgp(context: &Context, qr: &str) -> Result { let payload = qr @@ -440,6 +455,10 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { .get("x") .filter(|&s| validate_id(s)) .map(|s| s.to_string()); + let broadcast_shared_secret = param + .get("b") + .filter(|&s| validate_id(s)) + .map(|s| s.to_string()); let grpname = if grpid.is_some() { if let Some(encoded_name) = param.get("g") { @@ -526,6 +545,30 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { authcode, }) } + } else if let (Some(addr), Some(broadcast_name), Some(grpid), Some(shared_secret)) = + (&addr, grpname, grpid, broadcast_shared_secret) + { + // This is a broadcast channel invite link. + // TODO code duplication with the previous block + // TODO at some point, we can mark this person as verified + let addr = ContactAddress::new(addr)?; + let (contact_id, _) = Contact::add_or_lookup_ex( + context, + &name, + &addr, + &fingerprint.hex(), + Origin::UnhandledSecurejoinQrScan, + ) + .await + .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?; + + Ok(Qr::AskJoinBroadcast { + broadcast_name, + grpid, + contact_id, + fingerprint, + shared_secret, + }) } else if let Some(addr) = addr { let fingerprint = fingerprint.hex(); let (contact_id, _) = diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 5392f9469..8173b312f 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -47,6 +47,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul let hidden = match invite { QrInvite::Contact { .. } => Blocked::Not, QrInvite::Group { .. } => Blocked::Yes, + QrInvite::Broadcast { .. } => Blocked::Yes, }; let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden) .await @@ -113,6 +114,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul chat::add_info_msg(context, group_chat_id, &msg, time()).await?; Ok(group_chat_id) } + QrInvite::Broadcast { .. } => {} QrInvite::Contact { .. } => { // For setup-contact the BobState already ensured the 1:1 chat exists because it // uses it to send the handshake messages. diff --git a/src/securejoin/qrinvite.rs b/src/securejoin/qrinvite.rs index 023d6875b..c472ac3cf 100644 --- a/src/securejoin/qrinvite.rs +++ b/src/securejoin/qrinvite.rs @@ -29,6 +29,13 @@ pub enum QrInvite { invitenumber: String, authcode: String, }, + Broadcast { + broadcast_name: String, + grpid: String, + contact_id: ContactId, + fingerprint: Fingerprint, + shared_secret: String, + }, } impl QrInvite { @@ -95,6 +102,19 @@ impl TryFrom for QrInvite { invitenumber, authcode, }), + Qr::AskJoinBroadcast { + broadcast_name, + grpid, + contact_id, + fingerprint, + shared_secret, + } => Ok(QrInvite::Broadcast { + broadcast_name, + grpid, + contact_id, + fingerprint, + shared_secret, + }), _ => bail!("Unsupported QR type"), } }