diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index a5da32c53..bdd8838ae 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2249,21 +2249,12 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch * This function is typically called when dc_check_qr() returns * lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP. * - * Depending on the given QR code, - * this function may takes some time and sends and receives several messages. - * Therefore, you should call it always in a separate thread; - * if you want to abort it, you should call dc_stop_ongoing_process(). + * The function returns immediately and the handshake runs in background, + * sending and receiving several messages. + * During the handshake, info messages are added to the chat, + * showing progress, success or errors. * - * - If the given QR code starts the Setup-Contact protocol, - * the function typically returns immediately - * and the handshake runs in background. - * Subsequent calls of dc_join_securejoin() will abort unfinished tasks. - * The returned chat is the one-to-one opportunistic chat. - * When the protocol has finished, an info-message is added to that chat. - * - If the given QR code starts the Verified-Group-Invite protocol, - * the function waits until the protocol has finished. - * This is because the protected group is not opportunistic - * and can be created only when the contacts have verified each other. + * Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes. * * See https://countermitm.readthedocs.io/en/latest/new.html * for details about both protocols. @@ -2273,10 +2264,8 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch * @param qr The text of the scanned QR code. Typically, the same string as given * to dc_check_qr(). * @return Chat-id of the joined chat, the UI may redirect to the this chat. - * If the out-of-band verification failed or was aborted, 0 is returned. + * On errors, 0 is returned, however, most errors will happen during handshake later on. * A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified. - * If needed, this be checked with dc_chat_is_protected() and dc_contact_is_verified(), - * however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally. */ uint32_t dc_join_securejoin (dc_context_t* context, const char* qr); diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 8e4ba82ca..1af8e0afb 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -425,6 +425,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu getqr []\n\ getbadqr\n\ checkqr \n\ + joinqr \n\ setqr \n\ providerinfo \n\ event \n\ diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 433e4a1e0..452ee65c1 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -224,10 +224,11 @@ const CONTACT_COMMANDS: [&str; 9] = [ "unblock", "listblocked", ]; -const MISC_COMMANDS: [&str; 10] = [ +const MISC_COMMANDS: [&str; 11] = [ "getqr", "getbadqr", "checkqr", + "joinqr", "event", "fileinfo", "clear", diff --git a/src/chat.rs b/src/chat.rs index d42448ba6..5779db426 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -175,8 +175,8 @@ impl ChatId { } /// Same as `create_for_contact()` with an additional `create_blocked` parameter - /// that is used in case the chat does not exist. - /// If the chat exists already, `create_blocked` is ignored. + /// that is used in case the chat does not exist or to unblock existing chats. + /// `create_blocked` won't block already unblocked chats again. pub(crate) async fn create_for_contact_with_blocked( context: &Context, contact_id: u32, @@ -184,7 +184,7 @@ impl ChatId { ) -> Result { let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? { Some(chat) => { - if chat.blocked != Blocked::Not { + if create_blocked == Blocked::Not && chat.blocked != Blocked::Not { chat.id.unblock(context).await?; } chat.id @@ -304,7 +304,7 @@ impl ChatId { self.delete(context).await?; } Chattype::Mailinglist => { - if self.set_blocked(context, Blocked::Manually).await? { + if self.set_blocked(context, Blocked::Yes).await? { context.emit_event(EventType::ChatModified(self)); } } @@ -3891,7 +3891,7 @@ mod tests { // create contact, then blocked chat let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); - let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Manually) + let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes) .await .unwrap() .id; @@ -3900,7 +3900,7 @@ mod tests { .unwrap() .unwrap(); assert_eq!(chat_id, chat2.id); - assert_eq!(chat2.blocked, Blocked::Manually); + assert_eq!(chat2.blocked, Blocked::Yes); // test nonexistent contact let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap(); @@ -4410,4 +4410,38 @@ mod tests { Ok(()) } + + #[async_std::test] + async fn test_create_for_contact_with_blocked() -> Result<()> { + let t = TestContext::new().await; + let (contact_id, _) = + Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?; + + // create a blocked chat + let chat_id_orig = + ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert!(!chat_id_orig.is_special()); + let chat = Chat::load_from_db(&t, chat_id_orig).await?; + assert_eq!(chat.blocked, Blocked::Yes); + + // repeating the call, the same chat must still be blocked + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Yes); + + // already created chats are unblocked if requested + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Not); + + // however, already created chats are not re-blocked + let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?; + assert_eq!(chat_id, chat_id_orig); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.blocked, Blocked::Not); + + Ok(()) + } } diff --git a/src/chatlist.rs b/src/chatlist.rs index a8cd888e7..60a3575e3 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -363,7 +363,7 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result { .sql .count( "SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;", - paramsv![Blocked::Manually, ChatVisibility::Archived], + paramsv![Blocked::Yes, ChatVisibility::Archived], ) .await?; Ok(count) diff --git a/src/constants.rs b/src/constants.rs index 108351cc0..124ca5d3c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -24,7 +24,7 @@ pub static DC_VERSION_STR: Lazy = Lazy::new(|| env!("CARGO_PKG_VERSION") #[repr(i8)] pub enum Blocked { Not = 0, - Manually = 1, + Yes = 1, Request = 2, } @@ -385,7 +385,7 @@ mod tests { // values may be written to disk and must not change assert_eq!(Blocked::Not, Blocked::default()); assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap()); - assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap()); + assert_eq!(Blocked::Yes, Blocked::from_i32(1).unwrap()); assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap()); } diff --git a/src/contact.rs b/src/contact.rs index 2e86001a9..3d467a395 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -711,7 +711,7 @@ impl Contact { .sql .query_map( "SELECT name, grpid FROM chats WHERE type=? AND blocked=?;", - paramsv![Chattype::Mailinglist, Blocked::Manually], + paramsv![Chattype::Mailinglist, Blocked::Yes], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), |rows| { rows.collect::, _>>() diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index bdaf5e026..7ce6eb67f 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -633,7 +633,9 @@ async fn add_parts( if chat_id.is_none() { // try to create a normal chat - let create_blocked = if from_id == to_id { + let create_blocked = if *hidden { + Blocked::Yes + } else if from_id == DC_CONTACT_ID_SELF { Blocked::Not } else { Blocked::Request @@ -798,7 +800,7 @@ async fn add_parts( if chat_id.is_none() && allow_creation { let create_blocked = if !Contact::is_blocked_load(context, to_id).await? { if self_sent && *hidden { - Blocked::Manually + Blocked::Yes } else { Blocked::Not } @@ -1543,6 +1545,17 @@ async fn create_or_lookup_group( } set_better_msg(mime_parser, &better_msg); + let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { + if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { + warn!(context, "verification problem: {}", err); + let s = format!("{}. See 'Info' for more details", err); + mime_parser.repl_msg_by_error(&s); + } + ProtectionStatus::Protected + } else { + ProtectionStatus::Unprotected + }; + // check if the group does not exist but should be created let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid) .await @@ -1563,18 +1576,6 @@ async fn create_or_lookup_group( || X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap())) { // group does not exist but should be created - let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { - if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await - { - warn!(context, "verification problem: {}", err); - let s = format!("{}. See 'Info' for more details", err); - mime_parser.repl_msg_by_error(&s); - } - ProtectionStatus::Protected - } else { - ProtectionStatus::Unprotected - }; - if !allow_creation { info!(context, "creating group forbidden by caller"); return Ok(None); @@ -1615,6 +1616,16 @@ async fn create_or_lookup_group( // .add_protection_msg(context, ProtectionStatus::Protected, false, 0) // .await?; //} + } else if let Some(chat_id) = chat_id { + if create_protected == ProtectionStatus::Protected { + let chat = Chat::load_from_db(context, chat_id).await?; + if !chat.is_protected() { + chat_id + .inner_set_protection(context, ProtectionStatus::Protected) + .await?; + recreate_member_list = true; + } + } } // again, check chat_id diff --git a/src/securejoin.rs b/src/securejoin.rs index 522dc7282..f8d7049ae 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,17 +1,15 @@ //! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol). use std::convert::TryFrom; -use std::time::{Duration, Instant}; -use anyhow::{anyhow, bail, Context as _, Error, Result}; -use async_std::channel::Receiver; +use anyhow::{bail, Context as _, Error, Result}; use async_std::sync::Mutex; use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; -use crate::chat::{self, Chat, ChatId, ChatIdBlocked}; +use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; -use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL}; +use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL}; use crate::contact::{Contact, Origin, VerifiedStatus}; use crate::context::Context; use crate::dc_tools::time; @@ -81,8 +79,8 @@ enum StartedProtocolVariant { SetupContact, /// The secure-join protocol, to join a group. SecureJoin { - ongoing_receiver: Receiver<()>, group_id: String, + group_name: String, }, } @@ -110,16 +108,14 @@ impl Bob { *guard = None; } let variant = match invite { - QrInvite::Group { ref grpid, .. } => { - let receiver = context - .alloc_ongoing() - .await - .map_err(|_| JoinError::OngoingRunning)?; - StartedProtocolVariant::SecureJoin { - ongoing_receiver: receiver, - group_id: grpid.clone(), - } - } + QrInvite::Group { + ref grpid, + ref name, + .. + } => StartedProtocolVariant::SecureJoin { + group_id: grpid.clone(), + group_name: name.clone(), + }, _ => StartedProtocolVariant::SetupContact, }; match BobState::start_protocol(context, invite).await { @@ -280,9 +276,7 @@ pub enum JoinError { /// This is the start of the process for the joiner. See the module and ffi documentation /// for more details. /// -/// When joining a group this will start an "ongoing" process and will block until the -/// process is completed, the [`ChatId`] for the new group is not known any sooner. When -/// verifying a contact this returns immediately. +/// The function returns immediately and the handshake will run in background. pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result { securejoin(context, qr).await.map_err(|err| { warn!(context, "Fatal joiner error: {:#}", err); @@ -313,39 +307,34 @@ async fn securejoin(context: &Context, qr: &str) -> Result { Ok(chat_id) } StartedProtocolVariant::SecureJoin { - ongoing_receiver, group_id, + group_name, } => { - // for a group-join, wait until the protocol is finished and the group is created - ongoing_receiver - .recv() - .await - .map_err(|_| JoinError::OngoingSenderDropped)?; - - // handle_securejoin_handshake() calls Context::stop_ongoing before the group - // chat is created (it is created after handle_securejoin_handshake() returns by - // dc_receive_imf()). As a hack we just wait a bit for it to appear. - - // If the protocol is aborted by Bob, this timeout will also happen. - let start = Instant::now(); - let chatid = loop { - { - match chat::get_chat_id_by_grpid(context, &group_id).await? { - Some((chatid, _is_protected, _blocked)) => break chatid, - None => { - if start.elapsed() > Duration::from_secs(7) { - context.free_ongoing().await; - return Err(JoinError::Other(anyhow!( - "Ongoing sender dropped (this is a bug)" - ))); - } - } - } - } - async_std::task::sleep(Duration::from_millis(50)).await; + // for a group-join, also create the chat soon and let the verification run in background. + // however, the group will become usable only when the protocol has finished. + let contact_id = invite.contact_id(); + let chat_id = if let Some((chat_id, _protected, _blocked)) = + chat::get_chat_id_by_grpid(context, &group_id).await? + { + chat_id.unblock(context).await?; + chat_id + } else { + ChatId::create_multiuser_record( + context, + Chattype::Group, + group_id, + group_name, + Blocked::Not, + ProtectionStatus::Unprotected, // protection is added later as needed + ) + .await? }; - context.free_ongoing().await; - Ok(chatid) + if !is_contact_in_chat(context, chat_id, contact_id).await? { + chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?; + } + let msg = stock_str::secure_join_started(context, contact_id).await; + chat::add_info_msg(context, chat_id, msg, time()).await?; + Ok(chat_id) } } } @@ -358,13 +347,13 @@ async fn securejoin(context: &Context, qr: &str) -> Result { #[error("Failed sending handshake message")] pub struct SendMsgError(#[from] anyhow::Error); -async fn send_handshake_msg( +/// Send handshake message from Alice's device; +/// Bob's handshake messages are sent in `BobState::send_handshake_message()`. +async fn send_alice_handshake_msg( context: &Context, - contact_chat_id: ChatId, + contact_id: u32, step: &str, - param2: &str, fingerprint: Option, - grpid: &str, ) -> Result<(), SendMsgError> { let mut msg = Message { viewtype: Viewtype::Text, @@ -373,67 +362,55 @@ async fn send_handshake_msg( ..Default::default() }; msg.param.set_cmd(SystemMessage::SecurejoinMessage); - if step.is_empty() { - msg.param.remove(Param::Arg); - } else { - msg.param.set(Param::Arg, step); - } - if !param2.is_empty() { - msg.param.set(Param::Arg2, param2); - } + msg.param.set(Param::Arg, step); if let Some(fp) = fingerprint { msg.param.set(Param::Arg3, fp.hex()); } - if !grpid.is_empty() { - msg.param.set(Param::Arg4, grpid); - } - if step == "vg-request" || step == "vc-request" { - msg.param.set_int(Param::ForcePlaintext, 1); - } else { - msg.param.set_int(Param::GuaranteeE2ee, 1); - } - - chat::send_msg(context, contact_chat_id, &mut msg).await?; + msg.param.set_int(Param::GuaranteeE2ee, 1); + chat::send_msg( + context, + ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes) + .await? + .id, + &mut msg, + ) + .await?; Ok(()) } -async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result { - if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] { - Ok(contact_id) - } else { - Ok(0) - } +/// Get an unblocked chat that can be used for info messages. +async fn info_chat_id(context: &Context, contact_id: u32) -> Result { + let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?; + Ok(chat_id_blocked.id) } async fn fingerprint_equals_sender( context: &Context, fingerprint: &Fingerprint, - contact_chat_id: ChatId, + contact_id: u32, ) -> Result { - if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] { - if let Ok(contact) = Contact::load_from_db(context, contact_id).await { - let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await { - Ok(peerstate) => peerstate, - Err(err) => { - warn!( - context, - "Failed to sender peerstate for {}: {}", - contact.get_addr(), - err - ); - return Ok(false); - } - }; + let contact = Contact::load_from_db(context, contact_id).await?; + let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await { + Ok(peerstate) => peerstate, + Err(err) => { + warn!( + context, + "Failed to sender peerstate for {}: {}", + contact.get_addr(), + err + ); + return Ok(false); + } + }; - if let Some(peerstate) = peerstate { - if peerstate.public_key_fingerprint.is_some() - && fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap() - { - return Ok(true); - } - } + if let Some(peerstate) = peerstate { + if peerstate.public_key_fingerprint.is_some() + && fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap() + { + return Ok(true); } } + Ok(false) } @@ -494,21 +471,6 @@ pub(crate) async fn handle_securejoin_handshake( ">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step, ); - let contact_chat_id = { - let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not) - .await - .with_context(|| { - format!( - "Failed to look up or create chat for contact {}", - contact_id - ) - })?; - if chat.blocked != Blocked::Not { - chat.id.unblock(context).await?; - } - chat.id - }; - let join_vg = step.starts_with("vg-"); match step.as_str() { @@ -538,13 +500,11 @@ pub(crate) async fn handle_securejoin_handshake( inviter_progress!(context, contact_id, 300); // Alice -> Bob - send_handshake_msg( + send_alice_handshake_msg( context, - contact_chat_id, + contact_id, &format!("{}-auth-required", &step[..2]), - "", None, - "", ) .await?; Ok(HandshakeMessage::Done) @@ -557,11 +517,19 @@ pub(crate) async fn handle_securejoin_handshake( match context.bob.state(context).await { Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { - could_not_establish_secure_connection(context, bobstate.chat_id(), why) - .await?; + could_not_establish_secure_connection( + context, + contact_id, + bobstate.chat_id(context).await?, + why, + ) + .await?; Ok(HandshakeMessage::Done) } Some(_stage) => { + let msg = stock_str::secure_join_replies(context, contact_id).await; + chat::add_info_msg(context, bobstate.chat_id(context).await?, msg, time()) + .await?; joiner_progress!(context, bobstate.invite().contact_id(), 400); Ok(HandshakeMessage::Done) } @@ -584,7 +552,8 @@ pub(crate) async fn handle_securejoin_handshake( None => { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Fingerprint not provided.", ) .await?; @@ -594,16 +563,18 @@ pub(crate) async fn handle_securejoin_handshake( if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Auth not encrypted.", ) .await?; return Ok(HandshakeMessage::Ignore); } - if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? { + if !fingerprint_equals_sender(context, &fingerprint, contact_id).await? { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Fingerprint mismatch on inviter-side.", ) .await?; @@ -616,7 +587,8 @@ pub(crate) async fn handle_securejoin_handshake( None => { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Auth not provided.", ) .await?; @@ -624,14 +596,20 @@ pub(crate) async fn handle_securejoin_handshake( } }; if !token::exists(context, token::Namespace::Auth, auth_0).await { - could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.") - .await?; + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + "Auth invalid.", + ) + .await?; return Ok(HandshakeMessage::Ignore); } if mark_peer_as_verified(context, &fingerprint).await.is_err() { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Fingerprint mismatch on inviter-side.", ) .await?; @@ -639,7 +617,6 @@ pub(crate) async fn handle_securejoin_handshake( } Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?; info!(context, "Auth verified.",); - secure_connection_established(context, contact_chat_id).await?; context.emit_event(EventType::ContactsChanged(Some(contact_id))); inviter_progress!(context, contact_id, 600); if join_vg { @@ -655,6 +632,7 @@ pub(crate) async fn handle_securejoin_handshake( }; match chat::get_chat_id_by_grpid(context, field_grpid).await? { Some((group_chat_id, _, _)) => { + secure_connection_established(context, contact_id, group_chat_id).await?; if let Err(err) = chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true) .await @@ -666,13 +644,17 @@ pub(crate) async fn handle_securejoin_handshake( } } else { // Alice -> Bob - send_handshake_msg( + secure_connection_established( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, + ) + .await?; + send_alice_handshake_msg( + context, + contact_id, "vc-contact-confirm", - "", Some(fingerprint), - "", ) .await?; @@ -694,13 +676,23 @@ pub(crate) async fn handle_securejoin_handshake( match context.bob.state(context).await { Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { - could_not_establish_secure_connection(context, bobstate.chat_id(), why) - .await?; + could_not_establish_secure_connection( + context, + contact_id, + bobstate.chat_id(context).await?, + why, + ) + .await?; Ok(HandshakeMessage::Done) } Some(BobHandshakeStage::Completed) => { // Can only be BobHandshakeStage::Completed - secure_connection_established(context, bobstate.chat_id()).await?; + secure_connection_established( + context, + contact_id, + bobstate.chat_id(context).await?, + ) + .await?; Ok(retval) } Some(_) => { @@ -784,21 +776,6 @@ pub(crate) async fn observe_securejoin_on_other_device( .context("Not a Secure-Join message")?; info!(context, "observing secure-join message \'{}\'", step); - let contact_chat_id = { - let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not) - .await - .with_context(|| { - format!( - "Failed to look up or create chat for contact {}", - contact_id - ) - })?; - if chat.blocked != Blocked::Not { - chat.id.unblock(context).await?; - } - chat.id - }; - match step.as_str() { "vg-member-added" | "vc-contact-confirm" @@ -811,7 +788,8 @@ pub(crate) async fn observe_securejoin_on_other_device( ) { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Message not encrypted correctly.", ) .await?; @@ -823,7 +801,8 @@ pub(crate) async fn observe_securejoin_on_other_device( None => { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, "Fingerprint not provided, please update Delta Chat on all your devices.", ) .await?; @@ -833,7 +812,8 @@ pub(crate) async fn observe_securejoin_on_other_device( if mark_peer_as_verified(context, &fingerprint).await.is_err() { could_not_establish_secure_connection( context, - contact_chat_id, + contact_id, + info_chat_id(context, contact_id).await?, format!("Fingerprint mismatch on observing {}.", step).as_ref(), ) .await?; @@ -851,30 +831,22 @@ pub(crate) async fn observe_securejoin_on_other_device( async fn secure_connection_established( context: &Context, - contact_chat_id: ChatId, + contact_id: u32, + chat_id: ChatId, ) -> Result<(), Error> { - let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?; - let contact = Contact::get_by_id(context, contact_id).await; - - let addr = if let Ok(ref contact) = contact { - contact.get_addr() - } else { - "?" - }; - let msg = stock_str::contact_verified(context, addr).await; - chat::add_info_msg(context, contact_chat_id, msg, time()).await?; - context.emit_event(EventType::ChatModified(contact_chat_id)); - info!(context, "StockMessage::ContactVerified posted to 1:1 chat"); - + let contact = Contact::get_by_id(context, contact_id).await?; + let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await; + chat::add_info_msg(context, chat_id, msg, time()).await?; + context.emit_event(EventType::ChatModified(chat_id)); Ok(()) } async fn could_not_establish_secure_connection( context: &Context, - contact_chat_id: ChatId, + contact_id: u32, + chat_id: ChatId, details: &str, ) -> Result<(), Error> { - let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?; let contact = Contact::get_by_id(context, contact_id).await; let msg = stock_str::contact_not_verified( context, @@ -885,13 +857,11 @@ async fn could_not_establish_secure_connection( }, ) .await; - - chat::add_info_msg(context, contact_chat_id, &msg, time()).await?; + chat::add_info_msg(context, chat_id, &msg, time()).await?; error!( context, "StockMessage::ContactNotVerified posted to 1:1 chat ({})", details ); - Ok(()) } @@ -955,10 +925,12 @@ mod tests { use crate::chat; use crate::chat::ProtectionStatus; + use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::events::Event; use crate::peerstate::Peerstate; use crate::test_utils::TestContext; + use std::time::Duration; #[async_std::test] async fn test_setup_contact() -> Result<()> { @@ -1359,7 +1331,6 @@ mod tests { }; let sent = bob.pop_sent_msg().await; - assert!(bob.ctx.has_ongoing().await); assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap()); let msg = alice.parse_msg(&sent).await; assert!(!msg.was_encrypted()); @@ -1477,6 +1448,12 @@ mod tests { let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?; assert!(bob_chat.is_protected()); assert!(!bob.ctx.has_ongoing().await); + + // On this "happy path", Alice and Bob get only a group-chat where all information are added to. + // The one-to-one chats are used internally for the hidden handshake messages, + // however, should not be visible in the UIs. + assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1); + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); Ok(()) } } diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index 0ceb48f71..590d2f43a 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -8,11 +8,11 @@ //! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be //! used to work with the state. -use anyhow::{Error, Result}; +use anyhow::{bail, Error, Result}; use async_std::sync::MutexGuard; use crate::chat::{self, ChatId}; -use crate::constants::Viewtype; +use crate::constants::{Blocked, Viewtype}; use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::events::EventType; @@ -67,9 +67,18 @@ impl<'a> BobStateHandle<'a> { }) } - /// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice). - pub fn chat_id(&self) -> ChatId { - self.bobstate.chat_id + /// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice. + pub async fn chat_id(&self, context: &Context) -> Result { + match self.bobstate.invite { + QrInvite::Group { ref grpid, .. } => { + if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, &grpid).await? { + Ok(chat_id) + } else { + bail!("chat not found") + } + } + QrInvite::Contact { .. } => Ok(self.bobstate.chat_id), + } } /// Returns a reference to the [`QrInvite`] of the joiner process. @@ -185,10 +194,11 @@ impl BobState { context: &Context, invite: QrInvite, ) -> Result<(Self, BobHandshakeStage), JoinError> { - let chat_id = ChatId::create_for_contact(context, invite.contact_id()) - .await - .map_err(JoinError::UnknownContact)?; - if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? { + let chat_id = + ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes) + .await + .map_err(JoinError::UnknownContact)?; + if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? { // The scanned fingerprint matches Alice's key, we can proceed to step 4b. info!(context, "Taking securejoin protocol shortcut"); let state = Self { @@ -297,7 +307,9 @@ impl BobState { self.next = SecureJoinStep::Terminated; return Ok(Some(BobHandshakeStage::Terminated(reason))); } - if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? { + if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id()) + .await? + { self.next = SecureJoinStep::Terminated; return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); } diff --git a/src/stock_str.rs b/src/stock_str.rs index eeb019e26..8e5da95ef 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -326,6 +326,13 @@ pub enum StockMessage { #[strum(props(fallback = "%1$s of %2$s used"))] PartOfTotallUsed = 116, + + #[strum(props(fallback = "%1$s invited you to join this group.\n\n\ + Waiting for the device of %2$s to reply…"))] + SecureJoinStarted = 117, + + #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))] + SecureJoinReplies = 118, } impl StockMessage { @@ -591,6 +598,32 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String { translated(context, StockMessage::E2ePreferred).await } +/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`. +pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String { + if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await { + translated(context, StockMessage::SecureJoinStarted) + .await + .replace1(contact.get_name_n_addr()) + .replace2(contact.get_display_name()) + } else { + format!( + "secure_join_started: unknown contact {}", + inviter_contact_id + ) + } +} + +/// Stock string: `%1$s replied, waiting for being added to the group…`. +pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String { + if let Ok(contact) = Contact::get_by_id(context, contact_id).await { + translated(context, StockMessage::SecureJoinReplies) + .await + .replace1(contact.get_display_name()) + } else { + format!("secure_join_replies: unknown contact {}", contact_id) + } +} + /// Stock string: `%1$s verified.`. pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef) -> String { translated(context, StockMessage::ContactVerified) diff --git a/src/sync.rs b/src/sync.rs index 1f8f26908..cddb0fc34 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -126,12 +126,9 @@ impl Context { /// Sends out a self-sent message with items to be synchronized, if any. pub async fn send_sync_msg(&self) -> Result> { if let Some((json, ids)) = self.build_sync_json().await? { - let chat_id = ChatId::create_for_contact_with_blocked( - self, - DC_CONTACT_ID_SELF, - Blocked::Manually, - ) - .await?; + let chat_id = + ChatId::create_for_contact_with_blocked(self, DC_CONTACT_ID_SELF, Blocked::Yes) + .await?; let mut msg = Message { chat_id, viewtype: Viewtype::Text,