mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 01:46:34 +03:00
Broadcast-securejoin is working!!
This commit is contained in:
@@ -45,6 +45,8 @@ pub enum QrObject {
|
||||
/// Fingerprint of the contact key as scanned from the QR code.
|
||||
fingerprint: String,
|
||||
|
||||
authcode: String,
|
||||
|
||||
/// The secret shared between all members,
|
||||
/// used to symmetrically encrypt&decrypt messages.
|
||||
shared_secret: String,
|
||||
@@ -227,6 +229,7 @@ impl From<Qr> for QrObject {
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
shared_secret,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
@@ -236,6 +239,7 @@ impl From<Qr> for QrObject {
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
shared_secret,
|
||||
}
|
||||
}
|
||||
|
||||
60
src/chat.rs
60
src/chat.rs
@@ -43,9 +43,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_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,
|
||||
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,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
use crate::{chatlist_events, imap};
|
||||
@@ -1646,6 +1646,18 @@ impl Chat {
|
||||
self.typ == Chattype::Mailinglist
|
||||
}
|
||||
|
||||
/// Returns true if chat is an outgoing broadcast channel.
|
||||
pub fn is_out_broadcast(&self) -> bool {
|
||||
self.typ == Chattype::OutBroadcast
|
||||
}
|
||||
|
||||
/// Returns true if the chat is a broadcast channel,
|
||||
/// regardless of whether self is on the sending
|
||||
/// or receiving side.
|
||||
pub fn is_any_broadcast(&self) -> bool {
|
||||
matches!(self.typ, Chattype::OutBroadcast | Chattype::InBroadcast)
|
||||
}
|
||||
|
||||
/// Returns None if user can send messages to this chat.
|
||||
///
|
||||
/// Otherwise returns a reason useful for logging.
|
||||
@@ -1726,7 +1738,7 @@ impl Chat {
|
||||
match self.typ {
|
||||
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
|
||||
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
||||
Chattype::InBroadcast => Ok(false),
|
||||
Chattype::InBroadcast => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2916,13 +2928,18 @@ async fn prepare_send_msg(
|
||||
CantSendReason::ContactRequest => {
|
||||
// Allow securejoin messages, they are supposed to repair the verification.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
}
|
||||
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
||||
// Necessary checks should be made anyway before removing contact
|
||||
// from the chat.
|
||||
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
|
||||
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
|
||||
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||
CantSendReason::InBroadcast => {
|
||||
matches!(
|
||||
msg.param.get_cmd(),
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||
)
|
||||
}
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
@@ -3777,7 +3794,7 @@ pub async fn create_group_ex(
|
||||
/// Returns the created chat's id.
|
||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||
let grpid = create_id();
|
||||
let secret = create_broadcast_shared_secret();
|
||||
let secret = create_id();
|
||||
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
|
||||
}
|
||||
|
||||
@@ -3840,6 +3857,35 @@ pub(crate) async fn create_broadcast_ex(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn load_broadcast_shared_secret(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
) -> Result<Option<String>> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn save_broadcast_shared_secret(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
shared_secret: &str,
|
||||
) -> Result<()> {
|
||||
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, shared_secret),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set chat contacts in the `chats_contacts` table.
|
||||
pub(crate) async fn update_chat_contacts_table(
|
||||
context: &Context,
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
@@ -2929,11 +2930,13 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
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 qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
let mut sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||
|
||||
assert!(!sent.payload.contains("List-ID"));
|
||||
|
||||
@@ -15,7 +15,7 @@ use tokio::fs;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::chat::{self, Chat, load_broadcast_shared_secret};
|
||||
use crate::config::Config;
|
||||
use crate::constants::ASM_SUBJECT;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
@@ -231,6 +231,9 @@ impl MimeFactory {
|
||||
|
||||
// Do not encrypt messages to mailing lists.
|
||||
encryption_keys = None;
|
||||
} else if chat.is_out_broadcast() {
|
||||
// Encrypt, but only symmetrically, not with the public keys.
|
||||
encryption_keys = Some(Vec::new());
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
msg.param.get(Param::Arg)
|
||||
@@ -563,8 +566,10 @@ impl MimeFactory {
|
||||
// messages are auto-sent unlike usual unencrypted messages.
|
||||
step == "vg-request-with-auth"
|
||||
|| step == "vc-request-with-auth"
|
||||
|| step == "vb-request-with-auth"
|
||||
|| step == "vg-member-added"
|
||||
|| step == "vc-contact-confirm"
|
||||
// TODO possibly add vb-member-added here
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1144,7 +1149,7 @@ impl MimeFactory {
|
||||
};
|
||||
|
||||
let symmetric_key: Option<String> = match &self.loaded {
|
||||
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
|
||||
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
|
||||
// If there is no symmetric key yet
|
||||
// (because this is an old broadcast channel,
|
||||
// created before we had symmetric encryption),
|
||||
@@ -1152,13 +1157,7 @@ impl MimeFactory {
|
||||
// Symmetric encryption exists since 2025-08;
|
||||
// some time after that, we can think about requiring everyone
|
||||
// to switch to symmetrically-encrypted broadcast lists.
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
|
||||
(chat.id,),
|
||||
)
|
||||
.await?
|
||||
load_broadcast_shared_secret(context, chat.id).await?
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -1515,7 +1514,10 @@ impl MimeFactory {
|
||||
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
if !param2.is_empty() {
|
||||
headers.push((
|
||||
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
||||
if step == "vg-request-with-auth"
|
||||
|| step == "vc-request-with-auth"
|
||||
|| step == "vb-request-with-auth"
|
||||
{
|
||||
"Secure-Join-Auth"
|
||||
} else {
|
||||
"Secure-Join-Invitenumber"
|
||||
|
||||
18
src/qr.rs
18
src/qr.rs
@@ -96,6 +96,8 @@ pub enum Qr {
|
||||
|
||||
fingerprint: Fingerprint,
|
||||
|
||||
authcode: String,
|
||||
|
||||
shared_secret: String,
|
||||
},
|
||||
|
||||
@@ -396,7 +398,7 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
|
||||
|
||||
/// 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&g=BROADCAST_NAME&x=BROADCAST_ID&b=BROADCAST_SHARED_SECRET`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&s=AUTH&b=BROADCAST_SHARED_SECRET`
|
||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
|
||||
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
@@ -474,7 +476,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
None
|
||||
};
|
||||
|
||||
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
|
||||
if let (Some(addr), Some(invitenumber), Some(authcode)) =
|
||||
(&addr, invitenumber, authcode.clone())
|
||||
{
|
||||
let addr = ContactAddress::new(addr)?;
|
||||
let (contact_id, _) = Contact::add_or_lookup_ex(
|
||||
context,
|
||||
@@ -545,8 +549,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
authcode,
|
||||
})
|
||||
}
|
||||
} else if let (Some(addr), Some(broadcast_name), Some(grpid), Some(shared_secret)) =
|
||||
(&addr, grpname, grpid, broadcast_shared_secret)
|
||||
} else if let (
|
||||
Some(addr),
|
||||
Some(broadcast_name),
|
||||
Some(grpid),
|
||||
Some(authcode),
|
||||
Some(shared_secret),
|
||||
) = (&addr, grpname, grpid, authcode, broadcast_shared_secret)
|
||||
{
|
||||
// This is a broadcast channel invite link.
|
||||
// TODO code duplication with the previous block
|
||||
@@ -567,6 +576,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
shared_secret,
|
||||
})
|
||||
} else if let Some(addr) = addr {
|
||||
|
||||
@@ -44,7 +44,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
|
||||
use crate::simplify;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress, create_broadcast_shared_secret, remove_subject_prefix};
|
||||
use crate::tools::{self, buf_compress, create_id, remove_subject_prefix};
|
||||
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
|
||||
use crate::{contact, imap};
|
||||
|
||||
@@ -1566,7 +1566,7 @@ async fn do_chat_assignment(
|
||||
} else {
|
||||
let name =
|
||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||
let secret = create_broadcast_shared_secret();
|
||||
let secret = create_id();
|
||||
chat::create_broadcast_ex(context, Nosync, listid, name, secret).await?
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,10 @@ use anyhow::{Context as _, Error, Result, bail, ensure};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
|
||||
use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid,
|
||||
load_broadcast_shared_secret,
|
||||
};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
||||
@@ -46,9 +49,9 @@ fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
||||
|
||||
/// Generates a Secure Join QR code.
|
||||
///
|
||||
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
|
||||
/// [`ChatId`] generates a join-group QR code for the given chat.
|
||||
pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
|
||||
/// With `chat` set to `None` this generates a setup-contact QR code, with `chat` set to a
|
||||
/// [`ChatId`] generates a join-group/join-broadcast-channel QR code for the given chat.
|
||||
pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
|
||||
/*=======================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 1 in "Setup verified contact" protocol ====
|
||||
@@ -56,12 +59,13 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
|
||||
ensure_secret_key_exists(context).await.ok();
|
||||
|
||||
let chat = match group {
|
||||
let chat = match chat {
|
||||
Some(id) => {
|
||||
let chat = Chat::load_from_db(context, id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group,
|
||||
"Can't generate SecureJoin QR code for 1:1 chat {id}"
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"Can't generate SecureJoin QR code for chat {id} of type {}",
|
||||
chat.typ
|
||||
);
|
||||
if chat.grpid.is_empty() {
|
||||
let err = format!("Can't generate QR code, chat {id} is a email thread");
|
||||
@@ -94,24 +98,44 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
||||
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
|
||||
let qr = if let Some(chat) = chat {
|
||||
// parameters used: a=g=x=i=s=
|
||||
let group_name = chat.get_name();
|
||||
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||
if sync_token {
|
||||
context
|
||||
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
if chat.typ == Chattype::OutBroadcast {
|
||||
let broadcast_name = chat.get_name();
|
||||
let broadcast_name_urlencoded =
|
||||
utf8_percent_encode(broadcast_name, NON_ALPHANUMERIC).to_string();
|
||||
let broadcast_secret = load_broadcast_shared_secret(context, chat.id)
|
||||
.await?
|
||||
.context("Could not find broadcast secret")?;
|
||||
|
||||
format!(
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&s={}&b={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&broadcast_name_urlencoded,
|
||||
&chat.grpid,
|
||||
&auth,
|
||||
broadcast_secret
|
||||
)
|
||||
} else {
|
||||
// parameters used: a=g=x=i=s=
|
||||
let group_name = chat.get_name();
|
||||
let group_name_urlencoded =
|
||||
utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||
if sync_token {
|
||||
context
|
||||
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
}
|
||||
format!(
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
&chat.grpid,
|
||||
&invitenumber,
|
||||
&auth,
|
||||
)
|
||||
}
|
||||
format!(
|
||||
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint.hex(),
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
&chat.grpid,
|
||||
&invitenumber,
|
||||
&auth,
|
||||
)
|
||||
} else {
|
||||
// parameters used: a=n=i=s=
|
||||
if sync_token {
|
||||
@@ -266,9 +290,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
info!(context, "Received secure-join message {step:?}.");
|
||||
|
||||
let join_vg = step.starts_with("vg-");
|
||||
|
||||
if !matches!(step, "vg-request" | "vc-request") {
|
||||
// TODO talk with link2xt about whether we need to protect against this identity-misbinding attack,
|
||||
// and if so, how
|
||||
if !matches!(step, "vg-request" | "vc-request" | "vb-request-with-auth") {
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
for (addr, key) in &mime_message.gossiped_keys {
|
||||
@@ -338,7 +362,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
========================================================*/
|
||||
bob::handle_auth_required(context, mime_message).await
|
||||
}
|
||||
"vg-request-with-auth" | "vc-request-with-auth" => {
|
||||
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-with-auth" => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||
@@ -399,7 +423,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
if !join_vg {
|
||||
if step.starts_with("vc-") {
|
||||
ChatId::create_for_contact(context, contact_id).await?;
|
||||
}
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
@@ -500,6 +524,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
/// we know that we are Alice (inviter-observer)
|
||||
/// that just marked peer (Bob) as verified
|
||||
/// in response to correct vc-request-with-auth message.
|
||||
// TODO here I may be able to fix some multi-device things
|
||||
pub(crate) async fn observe_securejoin_on_other_device(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
|
||||
@@ -4,7 +4,9 @@ use anyhow::{Context as _, Result};
|
||||
|
||||
use super::HandshakeMessage;
|
||||
use super::qrinvite::QrInvite;
|
||||
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
|
||||
use crate::chat::{
|
||||
self, ChatId, ProtectionStatus, is_contact_in_chat, save_broadcast_shared_secret,
|
||||
};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
@@ -56,8 +58,43 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
// Now start the protocol and initialise the state.
|
||||
{
|
||||
if let QrInvite::Broadcast { shared_secret, .. } = &invite {
|
||||
// TODO this causes some performance penalty because joining_chat_id is used again below,
|
||||
// but maybe it's fine
|
||||
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
// TODO save the secret to the second device
|
||||
save_broadcast_shared_secret(context, broadcast_chat_id, shared_secret).await?;
|
||||
|
||||
if verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await? {
|
||||
info!(context, "Using fast securejoin with symmetric encryption");
|
||||
|
||||
// The message has to be sent into the broadcast chat, rather than the 1:1 chat,
|
||||
// so that it will be symmetrically encrypted
|
||||
send_handshake_message(
|
||||
context,
|
||||
&invite,
|
||||
broadcast_chat_id,
|
||||
BobHandshakeMsg::RequestWithAuth,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mark 1:1 chat as verified already.
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(invite.contact_id()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Start the original (non-broadcast) protocol and initialise the state.
|
||||
let has_key = context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -115,22 +152,22 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
Ok(group_chat_id)
|
||||
}
|
||||
QrInvite::Broadcast { .. } => {
|
||||
// For a secure-join we need to create the group and add the contact. The group will
|
||||
// only become usable once the protocol is finished.
|
||||
let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
// TODO code duplication with previous block
|
||||
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
if !is_contact_in_chat(context, broadcast_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
group_chat_id,
|
||||
broadcast_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO this message should be translatable:
|
||||
let msg = "You were invited to join this channel. Waiting for the channel owner's device to reply…";
|
||||
chat::add_info_msg(context, group_chat_id, msg, time()).await?;
|
||||
Ok(group_chat_id)
|
||||
chat::add_info_msg(context, broadcast_chat_id, msg, time()).await?;
|
||||
Ok(broadcast_chat_id)
|
||||
}
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
@@ -318,7 +355,7 @@ pub(crate) async fn send_handshake_message(
|
||||
pub(crate) enum BobHandshakeMsg {
|
||||
/// vc-request or vg-request
|
||||
Request,
|
||||
/// vc-request-with-auth or vg-request-with-auth
|
||||
/// vc-request-with-auth, vg-request-with-auth, or vb-request-with-auth
|
||||
RequestWithAuth,
|
||||
}
|
||||
|
||||
@@ -342,14 +379,14 @@ impl BobHandshakeMsg {
|
||||
Self::Request => match invite {
|
||||
QrInvite::Contact { .. } => "vc-request",
|
||||
QrInvite::Group { .. } => "vg-request",
|
||||
QrInvite::Broadcast { .. } => "broadcast-request",
|
||||
QrInvite::Broadcast { .. } => {
|
||||
panic!("There is no request-with-auth for broadcasts")
|
||||
} // TODO remove panic
|
||||
},
|
||||
Self::RequestWithAuth => match invite {
|
||||
QrInvite::Contact { .. } => "vc-request-with-auth",
|
||||
QrInvite::Group { .. } => "vg-request-with-auth",
|
||||
QrInvite::Broadcast { .. } => {
|
||||
panic!("There is no request-with-auth for broadcasts")
|
||||
} // TODO remove panic
|
||||
QrInvite::Broadcast { .. } => "vb-request-with-auth",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,11 @@ pub enum QrInvite {
|
||||
authcode: String,
|
||||
},
|
||||
Broadcast {
|
||||
broadcast_name: String,
|
||||
grpid: String,
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
broadcast_name: String,
|
||||
grpid: String,
|
||||
authcode: String,
|
||||
shared_secret: String,
|
||||
},
|
||||
}
|
||||
@@ -71,8 +72,9 @@ impl QrInvite {
|
||||
/// The `AUTH` code of the setup-contact/secure-join protocol.
|
||||
pub fn authcode(&self) -> &str {
|
||||
match self {
|
||||
Self::Contact { authcode, .. } | Self::Group { authcode, .. } => authcode,
|
||||
Self::Broadcast { .. } => panic!("broadcast invite has no authcode"), // TODO panic
|
||||
Self::Contact { authcode, .. }
|
||||
| Self::Group { authcode, .. }
|
||||
| Self::Broadcast { authcode, .. } => authcode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,15 +115,17 @@ impl TryFrom<Qr> for QrInvite {
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
shared_secret,
|
||||
} => Ok(QrInvite::Broadcast {
|
||||
broadcast_name,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
authcode,
|
||||
shared_secret,
|
||||
}),
|
||||
_ => bail!("Unsupported QR type"),
|
||||
_ => bail!("Unsupported QR type: {qr:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,15 +229,15 @@ impl TestContextManager {
|
||||
pub async fn exec_securejoin_qr(
|
||||
&self,
|
||||
scanner: &TestContext,
|
||||
scanned: &TestContext,
|
||||
inviter: &TestContext,
|
||||
qr: &str,
|
||||
) -> ChatId {
|
||||
let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
scanned.recv_msg_opt(&sent).await;
|
||||
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
inviter.recv_msg_opt(&sent).await;
|
||||
} else if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
scanner.recv_msg_opt(&sent).await;
|
||||
} else {
|
||||
break;
|
||||
|
||||
19
src/tools.rs
19
src/tools.rs
@@ -300,25 +300,6 @@ 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)
|
||||
/// 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 - also, not sure whether the AES-128 thing is true
|
||||
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