diff --git a/benches/benchmark_decrypting.rs b/benches/benchmark_decrypting.rs index 47162e52a..f46cf7a7e 100644 --- a/benches/benchmark_decrypting.rs +++ b/benches/benchmark_decrypting.rs @@ -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), diff --git a/src/chat.rs b/src/chat.rs index 682c0e6a8..933dcee1b 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -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?; diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index f6681e573..0f1b07cd4 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -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; diff --git a/src/decrypt.rs b/src/decrypt.rs index 3da5217d8..48f8cd4a2 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -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>> { + shared_secrets: &[String], +) -> Result, Option)>> { 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)) } diff --git a/src/message.rs b/src/message.rs index cf4b84304..839cfab86 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1385,6 +1385,18 @@ impl Message { pub fn error(&self) -> Option { 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. diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 2384e5910..f03ef7c70 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -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 = 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> { let file_name = msg.get_filename().context("msg has no file")?; let blob = msg diff --git a/src/mimeparser.rs b/src/mimeparser.rs index a5feffa3d..c904d7bfc 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -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 = context + let mut secrets: Vec = 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 { diff --git a/src/param.rs b/src/param.rs index d6b866663..21cffdd7d 100644 --- a/src/param.rs +++ b/src/param.rs @@ -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`], diff --git a/src/pgp.rs b/src/pgp.rs index 6b328063d..a9240e979 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -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, private_keys_for_decryption: &[SignedSecretKey], - symmetric_secrets: &[String], -) -> Result> { + shared_secrets: &[String], +) -> Result<(pgp::composed::Message<'static>, Option)> { 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 = symmetric_secrets + let message_password: Vec = 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, Vec, )> { - 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(()) } diff --git a/src/securejoin.rs b/src/securejoin.rs index 4132382b7..431be8e90 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -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 diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index a8dee9fd3..fc07579f2 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -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 }, } } diff --git a/src/securejoin/qrinvite.rs b/src/securejoin/qrinvite.rs index 5413b1fbd..20d20baaa 100644 --- a/src/securejoin/qrinvite.rs +++ b/src/securejoin/qrinvite.rs @@ -18,7 +18,7 @@ pub enum QrInvite { Contact { contact_id: ContactId, fingerprint: Fingerprint, - invitenumber: String, + invitenumber: Option, authcode: String, }, Group { @@ -26,7 +26,7 @@ pub enum QrInvite { fingerprint: Fingerprint, name: String, grpid: String, - invitenumber: String, + invitenumber: Option, 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 for QrInvite { @@ -92,7 +105,7 @@ impl TryFrom for QrInvite { } => Ok(QrInvite::Contact { contact_id, fingerprint, - invitenumber, + invitenumber: Some(invitenumber), authcode, }), Qr::AskVerifyGroup { @@ -107,7 +120,7 @@ impl TryFrom for QrInvite { fingerprint, name: grpname, grpid, - invitenumber, + invitenumber: Some(invitenumber), authcode, }), Qr::AskJoinBroadcast { diff --git a/src/token.rs b/src/token.rs index a5bdfc068..70b11e48d 100644 --- a/src/token.rs +++ b/src/token.rs @@ -61,6 +61,21 @@ pub async fn lookup( .await } +pub async fn lookup_all(context: &Context, namespace: Namespace) -> Result> { + context + .sql + .query_map( + "SELECT token FROM tokens WHERE namespc=? ORDER BY timestamp DESC LIMIT 1", + (namespace,), + |row| row.get(0), + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await +} + pub async fn lookup_or_new( context: &Context, namespace: Namespace, diff --git a/test-data/golden/test_broadcast_joining_golden_bob b/test-data/golden/test_broadcast_joining_golden_bob index aa32d1c1c..2dec03942 100644 --- a/test-data/golden/test_broadcast_joining_golden_bob +++ b/test-data/golden/test_broadcast_joining_golden_bob @@ -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] --------------------------------------------------------------------------------