feat: Transfer the broadcast secret in an encrypted message rather than directly in the QR code

This commit is contained in:
Hocuri
2025-08-07 11:31:29 +02:00
parent e1abaebeb5
commit 738f6c1799
14 changed files with 265 additions and 134 deletions

View File

@@ -71,7 +71,7 @@ fn criterion_benchmark(c: &mut Criterion) {
});
b.iter(|| {
let mut msg =
let (mut msg, _) =
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
let decrypted = msg.as_data_vec().unwrap();
@@ -101,7 +101,7 @@ fn criterion_benchmark(c: &mut Criterion) {
});
b.iter(|| {
let mut msg = decrypt(
let (mut msg, _) = decrypt(
encrypted.clone().into_bytes(),
&[key_pair.secret.clone()],
black_box(&secrets),

View File

@@ -2941,10 +2941,13 @@ async fn prepare_send_msg(
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
)
}
CantSendReason::MissingKey => msg
.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default(),
CantSendReason::MissingKey => {
msg.param
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
// V2 securejoin messages are symmetrically encrypted, no need for the public key:
|| msg.securejoin_step() == Some("vb-request-v2")
}
_ => false,
};
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
@@ -3878,11 +3881,13 @@ pub(crate) async fn save_broadcast_shared_secret(
chat_id: ChatId,
secret: &str,
) -> Result<()> {
info!(context, "Saving broadcast secret for chat {chat_id}");
info!(context, "dbg the new secret for chat {chat_id} is {secret}");
context
.sql
.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.secret",
(chat_id, secret),
)
.await?;

View File

@@ -7,7 +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::securejoin::{get_securejoin_qr, join_securejoin};
use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
TimeShiftFalsePositiveNote, sync,
@@ -3094,8 +3094,12 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
tcm.section("Alice creates broadcast channel with Bob.");
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
tcm.exec_securejoin_qr(bob0, alice, &qr).await;
sync(bob0, bob1).await;
join_securejoin(bob0, &qr).await.unwrap();
let request = bob0.pop_sent_msg().await;
alice.recv_msg(&request).await;
let answer = alice.pop_sent_msg().await;
bob0.recv_msg(&answer).await;
bob1.recv_msg(&answer).await;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;

View File

@@ -10,18 +10,22 @@ use crate::pgp;
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
///
/// If successful and the message is encrypted, returns decrypted body.
/// If successful and the message is encrypted, returns a tuple of:
///
/// - The decrypted and decompressed message
/// - If the message was symmetrically encrypted:
/// The index in `shared_secrets` of the secret used to decrypt the message.
pub fn try_decrypt<'a>(
mail: &'a ParsedMail<'a>,
private_keyring: &'a [SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<Option<::pgp::composed::Message<'static>>> {
shared_secrets: &[String],
) -> Result<Option<(::pgp::composed::Message<'static>, Option<usize>)>> {
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
return Ok(None);
};
let data = encrypted_data_part.get_body_raw()?;
let msg = pgp::decrypt(data, private_keyring, symmetric_secrets)?;
let msg = pgp::decrypt(data, private_keyring, shared_secrets)?;
Ok(Some(msg))
}

View File

@@ -1385,6 +1385,18 @@ impl Message {
pub fn error(&self) -> Option<String> {
self.error.clone()
}
// TODO this function could be used a lot more
/// If this is a secure-join message,
/// returns the current step,
/// which is put into the `Secure-Join` header.
pub(crate) fn securejoin_step(&self) -> Option<&str> {
if self.param.get_cmd() == SystemMessage::SecurejoinMessage {
self.param.get(Param::Arg)
} else {
None
}
}
}
/// State of the message.

View File

@@ -329,7 +329,7 @@ impl MimeFactory {
if let Some(public_key) = public_key_opt {
keys.push((addr.clone(), public_key))
} else if id != ContactId::SELF && !chat.is_any_broadcast() {
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
missing_key_addresses.insert(addr.clone());
if is_encrypted {
warn!(context, "Missing key for {addr}");
@@ -350,7 +350,7 @@ impl MimeFactory {
if let Some(public_key) = public_key_opt {
keys.push((addr.clone(), public_key))
} else if id != ContactId::SELF && !chat.is_any_broadcast() {
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
missing_key_addresses.insert(addr.clone());
if is_encrypted {
warn!(context, "Missing key for {addr}");
@@ -430,7 +430,7 @@ impl MimeFactory {
encryption_keys = if !is_encrypted {
None
} else if chat.is_out_broadcast() {
} else if should_encrypt_symmetrically(&msg, &chat) {
// Encrypt, but only symmetrically, not with the public keys.
Some(Vec::new())
} else {
@@ -579,7 +579,7 @@ 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 == "vb-request-v2"
|| step == "vg-member-added"
|| step == "vb-member-added"
|| step == "vc-contact-confirm"
@@ -825,7 +825,7 @@ impl MimeFactory {
} else if let Loaded::Message { msg, .. } = &self.loaded {
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
if step != "vg-request" && step != "vc-request" && step != "vb-request-v2" {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
@@ -1181,7 +1181,13 @@ impl MimeFactory {
};
let symmetric_key: Option<String> = match &self.loaded {
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
Loaded::Message { msg, .. } if should_encrypt_with_auth_token(msg) => {
// TODO rather than setting Arg2, bob.rs could set a param `Param::SharedSecretForEncryption` or similar
msg.param.get(Param::Arg2).map(|s| s.to_string())
}
Loaded::Message { chat, msg }
if should_encrypt_with_broadcast_secret(msg, chat) =>
{
// If there is no symmetric key yet
// (because this is an old broadcast channel,
// created before we had symmetric encryption),
@@ -1196,6 +1202,7 @@ impl MimeFactory {
let encrypted = if let Some(symmetric_key) = symmetric_key {
info!(context, "Symmetrically encrypting for broadcast channel.");
info!(context, "secret: {symmetric_key}"); // TODO
encrypt_helper
.encrypt_for_broadcast(context, &symmetric_key, message, compress)
.await?
@@ -1547,7 +1554,7 @@ impl MimeFactory {
headers.push((
if step == "vg-request-with-auth"
|| step == "vc-request-with-auth"
|| step == "vb-request-with-auth"
|| step == "vb-request-v2"
{
"Secure-Join-Auth"
} else {
@@ -1898,6 +1905,22 @@ fn hidden_recipients() -> Address<'static> {
Address::new_group(Some("hidden-recipients".to_string()), Vec::new())
}
fn should_encrypt_with_auth_token(msg: &Message) -> bool {
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
&& msg.param.get(Param::Arg).unwrap_or_default() == "vb-request-v2"
}
fn should_encrypt_with_broadcast_secret(msg: &Message, chat: &Chat) -> bool {
chat.is_any_broadcast()
&& msg.param.get_cmd() != SystemMessage::SecurejoinMessage
// The member-added message in a broadcast must be asymmetrirally encrypted:
&& msg.param.get_cmd() != SystemMessage::MemberAddedToGroup
}
fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
should_encrypt_with_auth_token(msg) || should_encrypt_with_broadcast_secret(msg, chat)
}
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
let file_name = msg.get_filename().context("msg has no file")?;
let blob = msg

View File

@@ -18,7 +18,6 @@ use crate::authres::handle_authres;
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants;
use crate::contact::ContactId;
use crate::context::Context;
use crate::decrypt::{try_decrypt, validate_detached_signature};
@@ -35,6 +34,7 @@ use crate::tools::{
get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id,
};
use crate::{chatlist_events, location, stock_str, tools};
use crate::{constants, token};
/// A parsed MIME message.
///
@@ -136,6 +136,10 @@ pub(crate) struct MimeMessage {
/// Sender timestamp in secs since epoch. Allowed to be in the future due to unsynchronized
/// clocks, but not too much.
pub(crate) timestamp_sent: i64,
/// How the message was encrypted (and now successfully decrypted):
/// The asymmetric key, an AUTH token, or a broadcast's shared secret.
pub(crate) was_encrypted_with: EncryptedWith,
}
#[derive(Debug, PartialEq)]
@@ -234,6 +238,25 @@ pub enum SystemMessage {
CallEnded = 67,
}
#[derive(Debug)]
pub(crate) enum EncryptedWith {
AsymmetricKey,
BroadcastSecret(String),
AuthToken(String),
None,
}
impl EncryptedWith {
pub(crate) fn auth_token(&self) -> Option<&str> {
match self {
EncryptedWith::AsymmetricKey => None,
EncryptedWith::BroadcastSecret(_) => None,
EncryptedWith::AuthToken(token) => Some(token),
EncryptedWith::None => None,
}
}
}
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
@@ -354,7 +377,7 @@ 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
let mut secrets: Vec<String> = context
.sql
.query_map(
"SELECT secret FROM broadcasts_shared_secrets",
@@ -366,11 +389,13 @@ impl MimeMessage {
},
)
.await?;
let num_broadcast_secrets = secrets.len();
secrets.extend(token::lookup_all(context, token::Namespace::Auth).await?);
let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
let (mail, is_encrypted, decrypted_with) = match tokio::task::block_in_place(|| {
try_decrypt(&mail, &private_keyring, &secrets)
}) {
Ok(Some(mut msg)) => {
Ok(Some((mut msg, index_of_secret))) => {
mail_raw = msg.as_data_vec().unwrap_or_default();
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
@@ -397,18 +422,29 @@ impl MimeMessage {
aheader_value = Some(protected_aheader_value);
}
(Ok(decrypted_mail), true)
let decrypted_with = if let Some(index_of_secret) = index_of_secret {
let used_secret = secrets.into_iter().nth(index_of_secret).unwrap_or_default();
if index_of_secret < num_broadcast_secrets {
EncryptedWith::BroadcastSecret(used_secret)
} else {
EncryptedWith::AuthToken(used_secret)
}
} else {
EncryptedWith::AsymmetricKey
};
(Ok(decrypted_mail), true, decrypted_with)
}
Ok(None) => {
mail_raw = Vec::new();
decrypted_msg = None;
(Ok(mail), false)
(Ok(mail), false, EncryptedWith::None)
}
Err(err) => {
mail_raw = Vec::new();
decrypted_msg = None;
warn!(context, "decryption failed: {:#}", err);
(Err(err), false)
(Err(err), false, EncryptedWith::None)
}
};
@@ -620,6 +656,7 @@ impl MimeMessage {
is_bot: None,
timestamp_rcvd,
timestamp_sent,
was_encrypted_with: decrypted_with,
};
match partial {

View File

@@ -103,6 +103,9 @@ pub enum Param {
/// removed from the group.
///
/// For "MemberAddedToGroup" this is the email address added to the group.
///
/// For securejoin messages, this is the step,
/// which is put into the `Secure-Join` header.
Arg = b'E',
/// For Messages
@@ -111,6 +114,9 @@ pub enum Param {
///
/// For `BobHandshakeMsg::RequestWithAuth`, this is the `Secure-Join-Auth` header.
///
/// For version two of the securejoin protocol (`vb-request-v2`),
/// this is the Auth token used to encrypt the message.
///
/// For [`SystemMessage::MultiDeviceSync`], this contains the ids that are synced.
///
/// For [`SystemMessage::MemberAddedToGroup`],

View File

@@ -8,7 +8,7 @@ use chrono::SubsecRound;
use deltachat_contact_tools::EmailAddress;
use pgp::armor::BlockType;
use pgp::composed::{
ArmorOptions, Deserializable, KeyType as PgpKeyType, Message, MessageBuilder,
ArmorOptions, Deserializable, InnerRingResult, KeyType as PgpKeyType, Message, MessageBuilder,
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
StandaloneSignature, SubkeyParamsBuilder, TheRing,
};
@@ -239,13 +239,19 @@ pub fn pk_calc_signature(
/// Decrypts the message with keys from the private key keyring.
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
/// Receiver private keys are passed in `private_keys_for_decryption`,
/// shared secrets used for symmetric encryption
/// are passed in `shared_secrets`.
///
/// Returns a tuple of:
/// - The decrypted and decompressed message
/// - If the message was symmetrically encrypted:
/// The index in `shared_secrets` of the secret used to decrypt the message.
pub fn decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
symmetric_secrets: &[String],
) -> Result<pgp::composed::Message<'static>> {
shared_secrets: &[String],
) -> Result<(pgp::composed::Message<'static>, Option<usize>)> {
let cursor = Cursor::new(ctext);
let (msg, _headers) = Message::from_armor(cursor)?;
@@ -253,7 +259,7 @@ pub fn decrypt(
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
let message_password: Vec<Password> = shared_secrets
.iter()
.map(|p| Password::from(p.as_str()))
.collect();
@@ -266,12 +272,17 @@ pub fn decrypt(
session_keys: vec![],
allow_legacy: false,
};
let (msg, _ring_result) = msg.decrypt_the_ring(ring, true)?;
let (msg, ring_result) = msg.decrypt_the_ring(ring, true)?;
// remove one layer of compression
let msg = msg.decompress()?;
Ok(msg)
let decrypted_with_secret = ring_result
.message_password
.iter()
.position(|&p| p == InnerRingResult::Ok);
Ok((msg, decrypted_with_secret))
}
/// Returns fingerprints
@@ -407,7 +418,7 @@ mod tests {
HashSet<Fingerprint>,
Vec<u8>,
)> {
let mut msg = 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);
@@ -611,13 +622,14 @@ mod tests {
.await?;
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
let mut decrypted = decrypt(
let (mut decrypted, index_of_secret) = decrypt(
ctext.into(),
&bob_private_keyring,
&[shared_secret.to_string()],
)?;
assert_eq!(decrypted.as_data_vec()?, plain);
assert_eq!(index_of_secret, Some(0));
Ok(())
}

View File

@@ -296,7 +296,7 @@ pub(crate) async fn handle_securejoin_handshake(
// -> or just ignore the problem for now - we will need to solve it for all messages anyways: https://github.com/chatmail/core/issues/7057
if !matches!(
step,
"vg-request" | "vc-request" | "vb-request-with-auth" | "vb-member-added"
"vg-request" | "vc-request" | "vb-request-v2" | "vb-member-added"
) {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
@@ -367,7 +367,7 @@ pub(crate) async fn handle_securejoin_handshake(
========================================================*/
bob::handle_auth_required(context, mime_message).await
}
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-with-auth" => {
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-v2" => {
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
@@ -390,8 +390,12 @@ pub(crate) async fn handle_securejoin_handshake(
);
return Ok(HandshakeMessage::Ignore);
}
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code,
// or that the message was encrypted with the secret written to the QR code.
let auth = mime_message
.get_header(HeaderDef::SecureJoinAuth)
.or_else(|| mime_message.was_encrypted_with.auth_token());
let Some(auth) = auth else {
warn!(
context,
"Ignoring {step} message because of missing auth code."
@@ -447,9 +451,16 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
// IMAP-delete the message to avoid handling it by another device and adding the
// member twice. Another device will know the member's key from Autocrypt-Gossip.
Ok(HandshakeMessage::Done)
if step.starts_with("vb-") {
// For broadcasts, we don't want to delete the message,
// because the other device should also internally add the member
// and see the key (because it won't see the member via autocrypt-gossip).
Ok(HandshakeMessage::Propagate)
} else {
// IMAP-delete the message to avoid handling it by another device and adding the
// member twice. Another device will know the member's key from Autocrypt-Gossip.
Ok(HandshakeMessage::Done)
}
} else {
// Setup verified contact.
secure_connection_established(
@@ -594,7 +605,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
inviter_progress(context, contact_id, 1000);
}
// TODO not sure if I should ad vb-request-with-auth here
// TODO not sure if I should add vb-request-v2 here
// Actually, I'm not even sure why vg-request-with-auth is here - why do we create a 1:1 chat??
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
// This actually reflects what happens on the first device (which does the secure

View File

@@ -1,18 +1,16 @@
//! Bob's side of SecureJoin handling, the joiner-side.
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, bail};
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{
self, ChatId, ProtectionStatus, is_contact_in_chat, save_broadcast_shared_secret,
};
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
use crate::constants::{Blocked, Chattype};
use crate::contact::Origin;
use crate::context::Context;
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::log::{LogExt as _, info};
use crate::log::info;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -51,63 +49,48 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Group { .. } => Blocked::Yes,
QrInvite::Broadcast { .. } => Blocked::Yes,
};
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
// The 1:1 chat with the inviter
let private_chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
// The chat id of the 1:1 chat, group or broadcast that is being joined
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
context.emit_event(EventType::ContactsChanged(None));
if let QrInvite::Broadcast {
shared_secret,
grpid,
broadcast_name,
..
} = &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?;
save_broadcast_shared_secret(context, broadcast_chat_id, shared_secret).await?;
let id = chat::SyncId::Grpid(grpid.to_string());
let action = chat::SyncAction::CreateInBroadcast {
chat_name: broadcast_name.to_string(),
shared_secret: shared_secret.to_string(),
};
chat::sync(context, id, action).await.log_err(context).ok();
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(),
});
if invite.is_v2() {
if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
{
bail!("V2 protocol failed because of fingerprint mismatch");
}
info!(context, "Using fast securejoin with symmetric encryption");
let mut msg = Message {
viewtype: Viewtype::Text,
text: "Secure-Join: vb-request-v2".to_string(),
hidden: true,
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
msg.param.set(Param::Arg, "vb-request-v2");
msg.param.set(Param::Arg2, invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
let bob_fp = self_fingerprint(context).await?;
msg.param.set(Param::Arg3, bob_fp);
chat::send_msg(context, private_chat_id, &mut msg).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.
// Start the version 1 protocol and initialise the state.
let has_key = context
.sql
.exists(
@@ -122,11 +105,16 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
{
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
.await?;
send_handshake_message(
context,
&invite,
private_chat_id,
BobHandshakeMsg::RequestWithAuth,
)
.await?;
// Mark 1:1 chat as verified already.
chat_id
private_chat_id
.set_protection(
context,
ProtectionStatus::Protected,
@@ -140,9 +128,10 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
});
} else {
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
.await?;
insert_new_db_entry(context, invite.clone(), chat_id).await?;
insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
}
}
@@ -150,28 +139,26 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Group { .. } => {
// 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? {
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(
context,
time(),
group_chat_id,
joining_chat_id,
&[invite.contact_id()],
)
.await?;
}
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
Ok(group_chat_id)
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
Ok(joining_chat_id)
}
QrInvite::Broadcast { .. } => {
// 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? {
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(
context,
time(),
broadcast_chat_id,
joining_chat_id,
&[invite.contact_id()],
)
.await?;
@@ -179,8 +166,8 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// 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, broadcast_chat_id, msg, time()).await?;
Ok(broadcast_chat_id)
chat::add_info_msg(context, joining_chat_id, msg, time()).await?;
Ok(joining_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
@@ -189,14 +176,14 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
// race with its change, we don't add our message below the protection message.
let sort_to_bottom = true;
let (received, incoming) = (false, false);
let ts_sort = chat_id
let ts_sort = private_chat_id
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
.await?;
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
if private_chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
let ts_start = time();
chat::add_info_msg_with_cmd(
context,
chat_id,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
ts_sort,
@@ -207,7 +194,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
)
.await?;
}
Ok(chat_id)
Ok(private_chat_id)
}
}
}
@@ -334,7 +321,7 @@ pub(crate) async fn send_handshake_message(
match step {
BobHandshakeMsg::Request => {
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
msg.param.set(Param::Arg2, invite.invitenumber());
msg.param.set_optional(Param::Arg2, invite.invitenumber());
msg.force_plaintext();
}
BobHandshakeMsg::RequestWithAuth => {
@@ -368,7 +355,7 @@ pub(crate) async fn send_handshake_message(
pub(crate) enum BobHandshakeMsg {
/// vc-request or vg-request
Request,
/// vc-request-with-auth, vg-request-with-auth, or vb-request-with-auth
/// vc-request-with-auth, vg-request-with-auth, or vb-request-v2
RequestWithAuth,
}
@@ -399,7 +386,9 @@ impl BobHandshakeMsg {
Self::RequestWithAuth => match invite {
QrInvite::Contact { .. } => "vc-request-with-auth",
QrInvite::Group { .. } => "vg-request-with-auth",
QrInvite::Broadcast { .. } => "vb-request-with-auth",
QrInvite::Broadcast { .. } => {
panic!("There is no request-with-auth for broadcasts")
} // TODO remove panic
},
}
}

View File

@@ -18,7 +18,7 @@ pub enum QrInvite {
Contact {
contact_id: ContactId,
fingerprint: Fingerprint,
invitenumber: String,
invitenumber: Option<String>,
authcode: String,
},
Group {
@@ -26,7 +26,7 @@ pub enum QrInvite {
fingerprint: Fingerprint,
name: String,
grpid: String,
invitenumber: String,
invitenumber: Option<String>,
authcode: String,
},
Broadcast {
@@ -62,10 +62,12 @@ impl QrInvite {
}
/// The `INVITENUMBER` of the setup-contact/secure-join protocol.
pub fn invitenumber(&self) -> &str {
pub fn invitenumber(&self) -> Option<&str> {
match self {
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => invitenumber,
Self::Broadcast { .. } => panic!("broadcast invite has no invite number"), // TODO panic
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => {
invitenumber.as_deref()
}
Self::Broadcast { .. } => None,
}
}
@@ -77,6 +79,17 @@ impl QrInvite {
| Self::Broadcast { authcode, .. } => authcode,
}
}
/// Whether this QR code uses the faster "version 2" protocol,
/// where the first message from Bob to Alice is symmetrically encrypted
/// with the AUTH code.
/// We may decide in the future to backwards-compatibly mark QR codes as V2,
/// but for now, everything without an invite number
/// is definitely V2,
/// because the invite number is needed for V1.
pub(crate) fn is_v2(&self) -> bool {
self.invitenumber().is_none()
}
}
impl TryFrom<Qr> for QrInvite {
@@ -92,7 +105,7 @@ impl TryFrom<Qr> for QrInvite {
} => Ok(QrInvite::Contact {
contact_id,
fingerprint,
invitenumber,
invitenumber: Some(invitenumber),
authcode,
}),
Qr::AskVerifyGroup {
@@ -107,7 +120,7 @@ impl TryFrom<Qr> for QrInvite {
fingerprint,
name: grpname,
grpid,
invitenumber,
invitenumber: Some(invitenumber),
authcode,
}),
Qr::AskJoinBroadcast {

View File

@@ -61,6 +61,21 @@ pub async fn lookup(
.await
}
pub async fn lookup_all(context: &Context, namespace: Namespace) -> Result<Vec<String>> {
context
.sql
.query_map(
"SELECT token FROM tokens WHERE namespc=? ORDER BY timestamp DESC LIMIT 1",
(namespace,),
|row| row.get(0),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await
}
pub async fn lookup_or_new(
context: &Context,
namespace: Namespace,

View File

@@ -1,5 +1,5 @@
InBroadcast#Chat#11: My Channel [1 member(s)] Icon: e9b6c7a78aa2e4f415644f55a553e73.png
--------------------------------------------------------------------------------
Msg#12: info (Contact#Contact#Info): You were invited to join this channel. Waiting for the channel owner's device to reply… [NOTICED][INFO]
Msg#13🔒: (Contact#Contact#10): Member Me added by Alice. [FRESH][INFO]
Msg#11: info (Contact#Contact#Info): You were invited to join this channel. Waiting for the channel owner's device to reply… [NOTICED][INFO]
Msg#12🔒: (Contact#Contact#10): Member Me added by Alice. [FRESH][INFO]
--------------------------------------------------------------------------------