diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c12aaced..7471b51db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Changes - add more SMTP logging #3093 - place common headers like `From:` before the large `Autocrypt:` header #3079 - +- keep track of securejoin joiner status in database to survive restarts #2920 ## 1.76.0 diff --git a/src/chat.rs b/src/chat.rs index dd35e82ee..d92c800f1 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -175,7 +175,7 @@ impl ChatId { /// Returns the unblocked 1:1 chat with `contact_id`. /// /// This should be used when **a user action** creates a chat 1:1, it ensures the chat - /// exists and is unblocked and scales the [`Contact`]'s origin. + /// exists, is unblocked and scales the [`Contact`]'s origin. pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result { ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await } diff --git a/src/context.rs b/src/context.rs index 93262e564..b8a70bfb0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -24,7 +24,6 @@ use crate::login_param::LoginParam; use crate::message::{self, MessageState, MsgId}; use crate::quota::QuotaInfo; use crate::scheduler::Scheduler; -use crate::securejoin::Bob; use crate::sql::Sql; #[derive(Clone, Debug)] @@ -45,7 +44,6 @@ pub struct InnerContext { /// Blob directory path pub(crate) blobdir: PathBuf, pub(crate) sql: Sql, - pub(crate) bob: Bob, pub(crate) last_smeared_timestamp: RwLock, pub(crate) running_state: RwLock, /// Mutex to avoid generating the key for the user more than once. @@ -171,7 +169,6 @@ impl Context { blobdir, running_state: RwLock::new(Default::default()), sql: Sql::new(dbfile), - bob: Default::default(), last_smeared_timestamp: RwLock::new(0), generating_key_mutex: Mutex::new(()), oauth2_mutex: Mutex::new(()), diff --git a/src/key.rs b/src/key.rs index 688030fc3..ee1aa298f 100644 --- a/src/key.rs +++ b/src/key.rs @@ -318,7 +318,7 @@ pub async fn store_self_keypair( } /// A key fingerprint -#[derive(Clone, Eq, PartialEq, Hash)] +#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub struct Fingerprint(Vec); impl Fingerprint { diff --git a/src/securejoin.rs b/src/securejoin.rs index c0f2d2883..a4d76dbeb 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -3,13 +3,12 @@ use std::convert::TryFrom; 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, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; +use crate::chat::{self, Chat, ChatId, ChatIdBlocked}; use crate::config::Config; -use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL}; +use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL}; use crate::contact::{Contact, Origin, VerifiedStatus}; use crate::context::Context; use crate::dc_tools::time; @@ -25,28 +24,16 @@ use crate::qr::check_qr; use crate::stock_str; use crate::token; +mod bob; mod bobstate; mod qrinvite; use crate::token::Namespace; -use bobstate::{BobHandshakeStage, BobState, BobStateHandle}; +use bobstate::BobState; use qrinvite::QrInvite; pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.'); -macro_rules! joiner_progress { - ($context:tt, $contact_id:expr, $progress:expr) => { - assert!( - $progress >= 0 && $progress <= 1000, - "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" - ); - $context.emit_event($crate::events::EventType::SecurejoinJoinerProgress { - contact_id: $contact_id, - progress: $progress, - }); - }; -} - macro_rules! inviter_progress { ($context:tt, $contact_id:expr, $progress:expr) => { assert!( @@ -60,100 +47,6 @@ macro_rules! inviter_progress { }; } -/// State for setup-contact/secure-join protocol joiner's side, aka Bob's side. -/// -/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the -/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the -/// database. For Bob this state is only carried live on the [`Context`] in this struct. -#[derive(Debug, Default)] -pub(crate) struct Bob { - inner: Mutex>, -} - -/// Return value for [`Bob::start_protocol`]. -/// -/// This indicates which protocol variant was started and provides the required information -/// about it. -enum StartedProtocolVariant { - /// The setup-contact protocol, to verify a contact. - SetupContact, - /// The secure-join protocol, to join a group. - SecureJoin { - group_id: String, - group_name: String, - }, -} - -impl Bob { - /// Starts the securejoin protocol with the QR `invite`. - /// - /// This will try to start the securejoin protocol for the given QR `invite`. If it - /// succeeded the protocol state will be tracked in `self`. - /// - /// This function takes care of starting the "ongoing" mechanism if required and - /// handling errors while starting the protocol. - /// - /// # Returns - /// - /// If the started protocol is joining a group the returned struct contains information - /// about the group and ongoing process. - async fn start_protocol( - &self, - context: &Context, - invite: QrInvite, - ) -> Result { - let mut guard = self.inner.lock().await; - if guard.is_some() { - warn!(context, "The new securejoin will replace the ongoing one."); - *guard = None; - } - let variant = match invite { - 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 { - Ok((state, stage)) => { - if matches!(stage, BobHandshakeStage::RequestWithAuthSent) { - joiner_progress!(context, state.invite().contact_id(), 400); - } - *guard = Some(state); - Ok(variant) - } - Err(err) => { - if let StartedProtocolVariant::SecureJoin { .. } = variant { - context.free_ongoing().await; - } - Err(err) - } - } - } - - /// Returns a handle to the [`BobState`] of the handshake. - /// - /// If there currently isn't a handshake running this will return `None`. Otherwise - /// this will return a handle to the current [`BobState`]. This handle allows - /// processing an incoming message and allows terminating the handshake. - /// - /// The handle contains an exclusive lock, which is held until the handle is dropped. - /// This guarantees all state and state changes are correct and allows safely - /// terminating the handshake without worrying about concurrency. - async fn state(&self, context: &Context) -> Option> { - let guard = self.inner.lock().await; - let ret = BobStateHandle::from_guard(guard); - if ret.is_none() { - info!(context, "No active BobState found for securejoin handshake"); - } - ret - } -} - /// Generates a Secure Join QR code. /// /// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a @@ -280,7 +173,7 @@ pub enum JoinError { pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result { securejoin(context, qr).await.map_err(|err| { warn!(context, "Fatal joiner error: {:#}", err); - // This is a modal operation, the user has context on what failed. + // The user just scanned this QR code so has context on what failed. error!(context, "QR process failed"); err }) @@ -297,47 +190,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result { let invite = QrInvite::try_from(qr_scan)?; - match context.bob.start_protocol(context, invite.clone()).await? { - StartedProtocolVariant::SetupContact => { - // for a one-to-one-chat, the chat is already known, return the chat-id, - // the verification runs in background - let chat_id = ChatId::create_for_contact(context, invite.contact_id()) - .await - .map_err(JoinError::UnknownContact)?; - Ok(chat_id) - } - StartedProtocolVariant::SecureJoin { - group_id, - group_name, - } => { - // 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 - None, - ) - .await? - }; - 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) - } - } + bob::start_protocol(context, invite).await } /// Error when failing to send a protocol handshake message. @@ -442,9 +295,8 @@ pub(crate) enum HandshakeMessage { /// Handle incoming secure-join handshake. /// -/// This function will update the securejoin state in [`InnerContext::bob`] and also -/// terminate the ongoing process using [`Context::stop_ongoing`] as required by the -/// protocol. +/// This function will update the securejoin state in the database as the protocol +/// progresses. /// /// A message which results in [`Err`] will be hidden from the user but not deleted, it may /// be a valid message for something else we are not aware off. E.g. it could be part of a @@ -452,8 +304,6 @@ pub(crate) enum HandshakeMessage { /// /// When `handle_securejoin_handshake()` is called, the message is not yet filed in the /// database; this is done by `receive_imf()` later on as needed. -/// -/// [`InnerContext::bob`]: crate::context::InnerContext::bob #[allow(clippy::indexing_slicing)] pub(crate) async fn handle_securejoin_handshake( context: &Context, @@ -521,38 +371,7 @@ pub(crate) async fn handle_securejoin_handshake( ==== Bob - the joiner's side ===== ==== Step 4 in "Setup verified contact" protocol ===== ========================================================*/ - 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, - contact_id, - bobstate.chat_id(context).await?, - why, - ) - .await?; - Ok(HandshakeMessage::Done) - } - Some(_stage) => { - if join_vg { - // the message reads "Alice replied, waiting for being added to the group…"; - // show it only on secure-join and not on setup-contact therefore. - 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) - } - None => Ok(HandshakeMessage::Ignore), - }, - None => Ok(HandshakeMessage::Ignore), - } + bob::handle_auth_required(context, mime_message).await } "vg-request-with-auth" | "vc-request-with-auth" => { /*========================================================== @@ -683,44 +502,14 @@ pub(crate) async fn handle_securejoin_handshake( ==== Bob - the joiner's side ==== ==== Step 7 in "Setup verified contact" protocol ==== =======================================================*/ - info!(context, "matched vc-contact-confirm step"); - let retval = if join_vg { - HandshakeMessage::Propagate - } else { - HandshakeMessage::Ignore - }; - 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, - contact_id, - bobstate.chat_id(context).await?, - why, - ) - .await?; - Ok(HandshakeMessage::Done) - } - Some(BobHandshakeStage::Completed) => { - // Can only be BobHandshakeStage::Completed - secure_connection_established( - context, - contact_id, - bobstate.chat_id(context).await?, - ) - .await?; - Ok(retval) - } - Some(_) => { - warn!( - context, - "Impossible state returned from handling handshake message" - ); - Ok(retval) - } - None => Ok(retval), + match BobState::from_db(&context.sql).await? { + Some(bobstate) => { + bob::handle_contact_confirm(context, bobstate, mime_message).await + } + None => match join_vg { + true => Ok(HandshakeMessage::Propagate), + false => Ok(HandshakeMessage::Ignore), }, - None => Ok(retval), } } "vg-member-added-received" | "vc-contact-confirm-received" => { @@ -851,7 +640,7 @@ async fn secure_connection_established( chat_id: ChatId, ) -> Result<(), Error> { let contact = Contact::get_by_id(context, contact_id).await?; - let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await; + let msg = stock_str::contact_verified(context, &contact).await; chat::add_info_msg(context, chat_id, &msg, time()).await?; context.emit_event(EventType::ChatModified(chat_id)); Ok(()) @@ -863,16 +652,8 @@ async fn could_not_establish_secure_connection( chat_id: ChatId, details: &str, ) -> Result<(), Error> { - let contact = Contact::get_by_id(context, contact_id).await; - let msg = stock_str::contact_not_verified( - context, - if let Ok(ref contact) = contact { - contact.get_addr() - } else { - "?" - }, - ) - .await; + let contact = Contact::get_by_id(context, contact_id).await?; + let msg = stock_str::contact_not_verified(context, &contact).await; chat::add_info_msg(context, chat_id, &msg, time()).await?; warn!( context, @@ -945,19 +726,31 @@ mod tests { use crate::test_utils::{TestContext, TestContextManager}; #[async_std::test] - async fn test_setup_contact() -> Result<()> { + async fn test_setup_contact() { let mut tcm = TestContextManager::new().await; let alice = tcm.alice().await; let bob = tcm.bob().await; - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0); - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 0 + ); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 0 + ); // Step 1: Generate QR-code, ChatId(0) indicates setup-contact - let qr = dc_get_securejoin_qr(&alice.ctx, None).await?; + let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap(); // Step 2: Bob scans QR-code, sends vc-request - dc_join_securejoin(&bob.ctx, &qr).await?; - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); + dc_join_securejoin(&bob.ctx, &qr).await.unwrap(); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 1 + ); let sent = bob.pop_sent_msg().await; assert!(!bob.ctx.has_ongoing().await); @@ -969,7 +762,13 @@ mod tests { // Step 3: Alice receives vc-request, sends vc-auth-required alice.recv_msg(&sent).await; - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1); + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 1 + ); let sent = alice.pop_sent_msg().await; let msg = bob.parse_msg(&sent).await; @@ -1031,21 +830,30 @@ mod tests { .await .unwrap(); assert_eq!( - contact_bob.is_verified(&alice.ctx).await?, + contact_bob.is_verified(&alice.ctx).await.unwrap(), VerifiedStatus::Unverified ); // Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm alice.recv_msg(&sent).await; assert_eq!( - contact_bob.is_verified(&alice.ctx).await?, + contact_bob.is_verified(&alice.ctx).await.unwrap(), VerifiedStatus::BidirectVerified ); // exactly one one-to-one chat should be visible for both now // (check this before calling alice.create_chat() explicitly below) - assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1); - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1); + assert_eq!( + Chatlist::try_load(&alice, 0, None, None) + .await + .unwrap() + .len(), + 1 + ); + assert_eq!( + Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(), + 1 + ); // Check Alice got the verified message in her 1:1 chat. { @@ -1085,14 +893,14 @@ mod tests { .await .unwrap(); assert_eq!( - contact_bob.is_verified(&bob.ctx).await?, + contact_bob.is_verified(&bob.ctx).await.unwrap(), VerifiedStatus::Unverified ); // Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received bob.recv_msg(&sent).await; assert_eq!( - contact_alice.is_verified(&bob.ctx).await?, + contact_alice.is_verified(&bob.ctx).await.unwrap(), VerifiedStatus::BidirectVerified ); @@ -1123,7 +931,6 @@ mod tests { msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-contact-confirm-received" ); - Ok(()) } #[async_std::test] @@ -1295,14 +1102,15 @@ mod tests { let alice = tcm.alice().await; let bob = tcm.bob().await; + // We start with empty chatlists. assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0); assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - let chatid = + let alice_chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?; // Step 1: Generate QR-code, secure-join implied by chatid - let qr = dc_get_securejoin_qr(&alice.ctx, Some(chatid)) + let qr = dc_get_securejoin_qr(&alice.ctx, Some(alice_chatid)) .await .unwrap(); @@ -1393,6 +1201,35 @@ mod tests { "vg-member-added" ); + { + // Now Alice's chat with Bob should still be hidden, the verified message should + // appear in the group chat. + + let chat = alice + .get_chat(&bob) + .await + .expect("Alice has no 1:1 chat with bob"); + assert_eq!( + chat.blocked, + Blocked::Yes, + "Alice's 1:1 chat with Bob is not hidden" + ); + let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, 0x1, None) + .await + .unwrap() + .into_iter() + .filter_map(|item| match item { + chat::ChatItem::Message { msg_id } => Some(msg_id), + _ => None, + }) + .min() + .expect("No messages in Alice's group chat"); + let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap(); + assert!(msg.is_info()); + let text = msg.get_text().unwrap(); + assert!(text.contains("bob@example.net verified")); + } + // Bob should not yet have Alice verified let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown) @@ -1407,10 +1244,53 @@ mod tests { // Step 7: Bob receives vg-member-added, sends vg-member-added-received bob.recv_msg(&sent).await; - assert_eq!( - contact_alice.is_verified(&bob.ctx).await?, - VerifiedStatus::BidirectVerified - ); + { + // Bob has Alice verified, message shows up in the group chat. + assert_eq!( + contact_alice.is_verified(&bob.ctx).await?, + VerifiedStatus::BidirectVerified + ); + let chat = bob + .get_chat(&alice) + .await + .expect("Bob has no 1:1 chat with Alice"); + assert_eq!( + chat.blocked, + Blocked::Yes, + "Bob's 1:1 chat with Alice is not hidden" + ); + for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None) + .await + .unwrap() + { + if let chat::ChatItem::Message { msg_id } = item { + let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); + let text = msg.get_text().unwrap(); + println!("msg {} text: {}", msg_id, text); + } + } + let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None) + .await + .unwrap() + .into_iter(); + loop { + match msg_iter.next() { + Some(chat::ChatItem::Message { msg_id }) => { + let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); + let text = msg.get_text().unwrap(); + match text.contains("alice@example.org verified") { + true => { + assert!(msg.is_info()); + break; + } + false => continue, + } + } + Some(_) => continue, + None => panic!("Verified message not found in Bob's group chat"), + } + } + } let sent = bob.pop_sent_msg().await; let msg = alice.parse_msg(&sent).await; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs new file mode 100644 index 000000000..110981886 --- /dev/null +++ b/src/securejoin/bob.rs @@ -0,0 +1,257 @@ +//! Bob's side of SecureJoin handling. +//! +//! This are some helper functions around [`BobState`] which augment the state changes with +//! the required user interactions. + +use anyhow::Result; + +use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus}; +use crate::constants::{Blocked, Chattype}; +use crate::contact::Contact; +use crate::context::Context; +use crate::dc_tools::time; +use crate::events::EventType; +use crate::mimeparser::MimeMessage; +use crate::{chat, stock_str}; + +use super::bobstate::{BobHandshakeStage, BobState}; +use super::qrinvite::QrInvite; +use super::{HandshakeMessage, JoinError}; + +/// Starts the securejoin protocol with the QR `invite`. +/// +/// This will try to start the securejoin protocol for the given QR `invite`. If it +/// succeeded the protocol state will be tracked in `self`. +/// +/// This function takes care of handling multiple concurrent joins and handling errors while +/// starting the protocol. +/// +/// # Returns +/// +/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1 +/// chat with Alice, for a SecureJoin QR this is the group chat. +pub(super) async fn start_protocol( + context: &Context, + invite: QrInvite, +) -> Result { + // A 1:1 chat is needed to send messages to Alice. When joining a group this chat is + // hidden, if a user starts sending messages in it it will be unhidden in + // dc_receive_imf. + let hidden = match invite { + QrInvite::Contact { .. } => Blocked::Not, + QrInvite::Group { .. } => Blocked::Yes, + }; + let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden) + .await + .map_err(JoinError::UnknownContact)?; + + // Now start the protocol and initialise the state + let (state, stage, aborted_states) = + BobState::start_protocol(context, invite.clone(), chat_id).await?; + for state in aborted_states { + error!(context, "Aborting previously unfinished QR Join process."); + state.notify_aborted(context, "new QR scanned").await?; + state.emit_progress(context, JoinerProgress::Error); + } + if matches!(stage, BobHandshakeStage::RequestWithAuthSent) { + state.emit_progress(context, JoinerProgress::RequestWithAuthSent); + } + match invite { + 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. + // TODO: how does this group become usable? + let group_chat_id = state.joining_chat_id(context).await?; + if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? { + chat::add_to_chat_contacts_table(context, group_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) + } + QrInvite::Contact { .. } => { + // For setup-contact the BobState already ensured the 1:1 chat exists because it + // uses it to send the handshake messages. + Ok(state.alice_chat()) + } + } +} + +/// Handles `vc-auth-required` and `vg-auth-required` handshake messages. +/// +/// # Bob - the joiner's side +/// ## Step 4 in the "Setup Contact protocol" +pub(super) async fn handle_auth_required( + context: &Context, + message: &MimeMessage, +) -> Result { + match BobState::from_db(&context.sql).await? { + Some(mut bobstate) => match bobstate.handle_message(context, message).await? { + Some(BobHandshakeStage::Terminated(why)) => { + bobstate.notify_aborted(context, why).await?; + Ok(HandshakeMessage::Done) + } + Some(_stage) => { + if bobstate.is_join_group() { + // The message reads "Alice replied, waiting to be added to the group…", + // so only show it on secure-join and not on setup-contact. + let contact_id = bobstate.invite().contact_id(); + let msg = stock_str::secure_join_replies(context, contact_id).await; + let chat_id = bobstate.joining_chat_id(context).await?; + chat::add_info_msg(context, chat_id, &msg, time()).await?; + } + bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent); + Ok(HandshakeMessage::Done) + } + None => Ok(HandshakeMessage::Ignore), + }, + None => Ok(HandshakeMessage::Ignore), + } +} + +/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages. +/// +/// # Bob - the joiner's side +/// ## Step 4 in the "Setup Contact protocol" +pub(super) async fn handle_contact_confirm( + context: &Context, + mut bobstate: BobState, + message: &MimeMessage, +) -> Result { + let retval = if bobstate.is_join_group() { + HandshakeMessage::Propagate + } else { + HandshakeMessage::Ignore + }; + match bobstate.handle_message(context, message).await? { + Some(BobHandshakeStage::Terminated(why)) => { + bobstate.notify_aborted(context, why).await?; + Ok(HandshakeMessage::Done) + } + Some(BobHandshakeStage::Completed) => { + // Note this goes to the 1:1 chat, as when joining a group we implicitly also + // verify both contacts (this could be a bug/security issue, see + // e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177). + bobstate.notify_peer_verified(context).await?; + Ok(retval) + } + Some(_) => { + warn!( + context, + "Impossible state returned from handling handshake message" + ); + Ok(retval) + } + None => Ok(retval), + } +} + +/// Private implementations for user interactions about this [`BobState`]. +impl BobState { + fn is_join_group(&self) -> bool { + match self.invite() { + QrInvite::Contact { .. } => false, + QrInvite::Group { .. } => true, + } + } + + fn emit_progress(&self, context: &Context, progress: JoinerProgress) { + let contact_id = self.invite().contact_id(); + context.emit_event(EventType::SecurejoinJoinerProgress { + contact_id, + progress: progress.into(), + }); + } + + /// Returns the [`ChatId`] of the chat being joined. + /// + /// This is the chat in which you want to notify the user as well. + /// + /// When joining a group this is the [`ChatId`] of the group chat, when verifying a + /// contact this is the [`ChatId`] of the 1:1 chat. The 1:1 chat is assumed to exist + /// because a [`BobState`] can not exist without, the group chat will be created if it + /// does not yet exist. + async fn joining_chat_id(&self, context: &Context) -> Result { + match self.invite() { + QrInvite::Contact { .. } => Ok(self.alice_chat()), + QrInvite::Group { + ref grpid, + ref name, + .. + } => { + let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? { + Some((chat_id, _protected, _blocked)) => { + chat_id.unblock(context).await?; + chat_id + } + None => { + ChatId::create_multiuser_record( + context, + Chattype::Group, + grpid, + name, + Blocked::Not, + ProtectionStatus::Unprotected, // protection is added later as needed + None, + ) + .await? + } + }; + Ok(group_chat_id) + } + } + } + + /// Notifies the user that the SecureJoin was aborted. + /// + /// This creates an info message in the chat being joined. + async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> { + let contact = Contact::get_by_id(context, self.invite().contact_id()).await?; + let msg = stock_str::contact_not_verified(context, &contact).await; + let chat_id = self.joining_chat_id(context).await?; + chat::add_info_msg(context, chat_id, &msg, time()).await?; + warn!( + context, + "StockMessage::ContactNotVerified posted to joining chat ({})", why + ); + Ok(()) + } + + /// Notifies the user that the SecureJoin peer is verified. + /// + /// This creates an info message in the chat being joined. + async fn notify_peer_verified(&self, context: &Context) -> Result<()> { + let contact = Contact::get_by_id(context, self.invite().contact_id()).await?; + let msg = stock_str::contact_verified(context, &contact).await; + let chat_id = self.joining_chat_id(context).await?; + chat::add_info_msg(context, chat_id, &msg, time()).await?; + context.emit_event(EventType::ChatModified(chat_id)); + Ok(()) + } +} + +/// Progress updates for [`EventType::SecurejoinJoinerProgress`]. +/// +/// This has an `From for usize` impl yielding numbers between 0 and a 1000 +/// which can be shown as a progress bar. +enum JoinerProgress { + /// An error occurred. + Error, + /// vg-vc-request-with-auth sent. + /// + /// Typically shows as "alice@addr verified, introducing myself." + RequestWithAuthSent, + // /// Completed securejoin. + // Succeeded, +} + +impl From for usize { + fn from(progress: JoinerProgress) -> Self { + match progress { + JoinerProgress::Error => 0, + JoinerProgress::RequestWithAuthSent => 400, + // JoinerProgress::Succeeded => 1000, + } + } +} diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index d22215d67..fe0038f8d 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -5,14 +5,13 @@ //! provides all the information to its driver so it can perform the correct interactions. //! //! The [`BobState`] is only directly used to initially create it when starting the -//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be -//! used to work with the state. +//! protocol. -use anyhow::{bail, Error, Result}; -use async_std::sync::MutexGuard; +use anyhow::{Error, Result}; +use rusqlite::Connection; use crate::chat::{self, ChatId}; -use crate::constants::{Blocked, Viewtype}; +use crate::constants::Viewtype; use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::events::EventType; @@ -21,6 +20,7 @@ use crate::key::{DcKey, SignedPublicKey}; use crate::message::Message; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; +use crate::sql::Sql; use super::qrinvite::QrInvite; use super::{ @@ -46,123 +46,14 @@ pub enum BobHandshakeStage { Terminated(&'static str), } -/// A handle to work with the [`BobState`] of Bob's securejoin protocol. +/// The securejoin state kept while Bob is joining. /// -/// This handle can only be created for when an underlying [`BobState`] exists. It keeps -/// open a lock which guarantees unique access to the state and this struct must be dropped -/// to return the lock. -pub struct BobStateHandle<'a> { - guard: MutexGuard<'a, Option>, - bobstate: BobState, - clear_state_on_drop: bool, -} - -impl<'a> BobStateHandle<'a> { - /// Creates a new instance, upholding the guarantee that [`BobState`] must exist. - pub fn from_guard(mut guard: MutexGuard<'a, Option>) -> Option { - guard.take().map(|bobstate| Self { - guard, - bobstate, - clear_state_on_drop: false, - }) - } - - /// 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. - pub fn invite(&self) -> &QrInvite { - &self.bobstate.invite - } - - /// Handles the given message for the securejoin handshake for Bob. - /// - /// This proxies to [`BobState::handle_message`] and makes sure to clear the state when - /// the protocol state is terminal. It returns `Some` if the message successfully - /// advanced the state of the protocol state machine, `None` otherwise. - pub async fn handle_message( - &mut self, - context: &Context, - mime_message: &MimeMessage, - ) -> Option { - info!(context, "Handling securejoin message for BobStateHandle"); - match self.bobstate.handle_message(context, mime_message).await { - Ok(Some(stage)) => { - if matches!( - stage, - BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_) - ) { - self.finish_protocol(context).await; - } - Some(stage) - } - Ok(None) => None, - Err(err) => { - warn!( - context, - "Error handling handshake message, aborting handshake: {}", err - ); - self.finish_protocol(context).await; - None - } - } - } - - /// Marks the bob handshake as finished. - /// - /// This will clear the state on [`InnerContext::bob`] once this handle is dropped, - /// allowing a new handshake to be started from [`Bob`]. - /// - /// Note that the state is only cleared on Drop since otherwise the invariant that the - /// state is always consistent is violated. However the "ongoing" process is released - /// here a little bit earlier as this requires access to the Context, which we do not - /// have on Drop (Drop can not run asynchronous code). Stopping the "ongoing" process - /// will release [`securejoin`](super::securejoin) which in turn will finally free the - /// ongoing process using [`Context::free_ongoing`]. - /// - /// [`InnerContext::bob`]: crate::context::InnerContext::bob - /// [`Bob`]: super::Bob - async fn finish_protocol(&mut self, context: &Context) { - info!(context, "Finishing securejoin handshake protocol for Bob"); - self.clear_state_on_drop = true; - if let QrInvite::Group { .. } = self.bobstate.invite { - context.stop_ongoing().await; - } - } -} - -impl<'a> Drop for BobStateHandle<'a> { - fn drop(&mut self) { - if self.clear_state_on_drop { - // The Option should already be empty because we take it out in the ctor, - // however the typesystem doesn't guarantee this so do it again anyway. - self.guard.take(); - } else { - // Make sure to put back the BobState into the Option of the Mutex, it was taken - // out by the constructor. - self.guard.replace(self.bobstate.clone()); - } - } -} - -/// The securejoin state kept in-memory while Bob is joining. +/// This is stored in the database and loaded from there using [`BobState::from_db`]. To +/// create a new one use [`BobState::start_protocol`]. /// -/// This is currently stored in [`Bob`] which is stored on the [`Context`], thus Bob can -/// only run one securejoin joiner protocol at a time. -/// -/// This purposefully has nothing optional, the state is always fully valid. See -/// [`Bob::state`] to get access to this state. +/// This purposefully has nothing optional, the state is always fully valid. However once a +/// terminal state is reached in [`BobState::next`] the entry in the database will already +/// have been deleted. /// /// # Conducting the securejoin handshake /// @@ -177,6 +68,8 @@ impl<'a> Drop for BobStateHandle<'a> { /// [`Bob::state`]: super::Bob::state #[derive(Debug, Clone)] pub struct BobState { + /// Database primary key. + id: i64, /// The QR Invite code. invite: QrInvite, /// The next expected message from Alice. @@ -188,39 +81,120 @@ pub struct BobState { impl BobState { /// Starts the securejoin protocol and creates a new [`BobState`]. /// + /// The `chat_id` needs to be the ID of the 1:1 chat with Alice, this chat will be used + /// to exchange the SecureJoin handshake messages as well as for showing error messages. + /// /// # Bob - the joiner's side /// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 + /// + /// This currently aborts any other securejoin process if any did not yet complete. The + /// ChatIds of the relevant 1:1 chat of any aborted handshakes are returned so that you + /// can report the aboreted handshake in the chat. (Yes, there can only ever be one + /// ChatId in that Vec, the database doesn't care though.) pub async fn start_protocol( context: &Context, invite: QrInvite, - ) -> Result<(Self, BobHandshakeStage), JoinError> { - 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 { - invite, - next: SecureJoinStep::ContactConfirm, - chat_id, + chat_id: ChatId, + ) -> Result<(Self, BobHandshakeStage, Vec), JoinError> { + let (stage, next) = + 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"); + send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth) + .await?; + ( + BobHandshakeStage::RequestWithAuthSent, + SecureJoinStep::ContactConfirm, + ) + } else { + send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?; + (BobHandshakeStage::RequestSent, SecureJoinStep::AuthRequired) }; - state - .send_handshake_message(context, BobHandshakeMsg::RequestWithAuth) - .await?; - Ok((state, BobHandshakeStage::RequestWithAuthSent)) - } else { - let state = Self { - invite, - next: SecureJoinStep::AuthRequired, - chat_id, - }; - state - .send_handshake_message(context, BobHandshakeMsg::Request) - .await?; - Ok((state, BobHandshakeStage::RequestSent)) - } + let (id, aborted_states) = + Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?; + let state = Self { + id, + invite, + next, + chat_id, + }; + Ok((state, stage, aborted_states)) + } + + /// Inserts a new entry in the bobstate table, deleting all previous entries. + /// + /// Returns the ID of the newly inserted entry and all the aborted states. + async fn insert_new_db_entry( + context: &Context, + next: SecureJoinStep, + invite: QrInvite, + chat_id: ChatId, + ) -> Result<(i64, Vec)> { + context + .sql + .transaction(move |transaction| { + // We need to start a write transaction right away, so that we have the + // database locked and no one else can write to this table while we read the + // rows that we will delete. So start with a dummy UPDATE. + transaction.execute( + r#"UPDATE bobstate SET next_step=?;"#, + params![SecureJoinStep::Terminated], + )?; + let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?; + let mut aborted = Vec::new(); + for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? { + let id = id?; + let state = BobState::from_db_id(transaction, id)?; + aborted.push(state); + } + + // Finally delete everything and insert new row. + transaction.execute("DELETE FROM bobstate;", params![])?; + transaction.execute( + "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);", + params![invite, next, chat_id], + )?; + let id = transaction.last_insert_rowid(); + Ok((id, aborted)) + }) + .await + } + + /// Load [`BobState`] from the database. + pub async fn from_db(sql: &Sql) -> Result> { + // Because of how Self::start_protocol() updates the database we are currently + // guaranteed to only have one row. + sql.query_row_optional( + "SELECT id, invite, next_step, chat_id FROM bobstate;", + paramsv![], + |row| { + let s = BobState { + id: row.get(0)?, + invite: row.get(1)?, + next: row.get(2)?, + chat_id: row.get(3)?, + }; + Ok(s) + }, + ) + .await + } + + fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result { + connection.query_row( + "SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;", + params![id], + |row| { + let s = BobState { + id, + invite: row.get(0)?, + next: row.get(1)?, + chat_id: row.get(2)?, + }; + Ok(s) + }, + ) } /// Returns the [`QrInvite`] used to create this [`BobState`]. @@ -228,20 +202,45 @@ impl BobState { &self.invite } + /// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice). + pub fn alice_chat(&self) -> ChatId { + self.chat_id + } + + /// Updates the [`BobState::next`] field in memory and the database. + /// + /// If the next state is a terminal state it will remove this [`BobState`] from the + /// database. + /// + /// If a user scanned a new QR code after this [`BobState`] was loaded this update will + /// fail currently because starting a new joiner process currently kills any previously + /// running processes. This is a limitation which will go away in the future. + async fn update_next(&mut self, sql: &Sql, next: SecureJoinStep) -> Result<()> { + // TODO: write test verifying how this would fail. + match next { + SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => { + sql.execute( + "UPDATE bobstate SET next_step=? WHERE id=?;", + paramsv![next, self.id], + ) + .await?; + } + SecureJoinStep::Terminated | SecureJoinStep::Completed => { + sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id)) + .await?; + } + } + self.next = next; + Ok(()) + } + /// Handles the given message for the securejoin handshake for Bob. /// /// If the message was not used for this handshake `None` is returned, otherwise the new /// stage is returned. Once [`BobHandshakeStage::Completed`] or /// [`BobHandshakeStage::Terminated`] are reached this [`BobState`] should be destroyed, /// further calling it will just result in the messages being unused by this handshake. - /// - /// # Errors - /// - /// Under normal operation this should never return an error, regardless of what kind of - /// message it is called with. Any errors therefore should be treated as fatal internal - /// errors and this entire [`BobState`] should be thrown away as the state machine can - /// no longer be considered consistent. - async fn handle_message( + pub async fn handle_message( &mut self, context: &Context, mime_message: &MimeMessage, @@ -304,17 +303,20 @@ impl BobState { } else { "Required encryption missing" }; - self.next = SecureJoinStep::Terminated; + self.update_next(&context.sql, SecureJoinStep::Terminated) + .await?; return Ok(Some(BobHandshakeStage::Terminated(reason))); } if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id()) .await? { - self.next = SecureJoinStep::Terminated; + self.update_next(&context.sql, SecureJoinStep::Terminated) + .await?; return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); } info!(context, "Fingerprint verified.",); - self.next = SecureJoinStep::ContactConfirm; + self.update_next(&context.sql, SecureJoinStep::ContactConfirm) + .await?; self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth) .await?; Ok(Some(BobHandshakeStage::RequestWithAuthSent)) @@ -362,7 +364,8 @@ impl BobState { if vg_expect_encrypted && !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) { - self.next = SecureJoinStep::Terminated; + self.update_next(&context.sql, SecureJoinStep::Terminated) + .await?; return Ok(Some(BobHandshakeStage::Terminated( "Contact confirm message not encrypted", ))); @@ -394,7 +397,8 @@ impl BobState { // This is not an error affecting the protocol outcome. .ok(); - self.next = SecureJoinStep::Completed; + self.update_next(&context.sql, SecureJoinStep::Completed) + .await?; Ok(Some(BobHandshakeStage::Completed)) } @@ -406,48 +410,60 @@ impl BobState { context: &Context, step: BobHandshakeMsg, ) -> Result<(), SendMsgError> { - let mut msg = Message { - viewtype: Viewtype::Text, - text: Some(step.body_text(&self.invite)), - hidden: true, - ..Default::default() - }; - msg.param.set_cmd(SystemMessage::SecurejoinMessage); - - // Sends the step in Secure-Join header. - msg.param - .set(Param::Arg, step.securejoin_header(&self.invite)); - - match step { - BobHandshakeMsg::Request => { - // Sends the Secure-Join-Invitenumber header in mimefactory.rs. - msg.param.set(Param::Arg2, self.invite.invitenumber()); - msg.force_plaintext(); - } - BobHandshakeMsg::RequestWithAuth => { - // Sends the Secure-Join-Auth header in mimefactory.rs. - msg.param.set(Param::Arg2, self.invite.authcode()); - msg.param.set_int(Param::GuaranteeE2ee, 1); - } - BobHandshakeMsg::ContactConfirmReceived => { - msg.param.set_int(Param::GuaranteeE2ee, 1); - } - }; - - // Sends our own fingerprint in the Secure-Join-Fingerprint header. - let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint(); - msg.param.set(Param::Arg3, bob_fp.hex()); - - // Sends the grpid in the Secure-Join-Group header. - if let QrInvite::Group { ref grpid, .. } = self.invite { - msg.param.set(Param::Arg4, grpid); - } - - chat::send_msg(context, self.chat_id, &mut msg).await?; - Ok(()) + send_handshake_message(context, &self.invite, self.chat_id, step).await } } +/// Sends the requested handshake message to Alice. +/// +/// Same as [`BobState::send_handshake_message`] but this variation allows us to send this +/// message before we create the state in [`BobState::start_protocol`]. +async fn send_handshake_message( + context: &Context, + invite: &QrInvite, + chat_id: ChatId, + step: BobHandshakeMsg, +) -> Result<(), SendMsgError> { + let mut msg = Message { + viewtype: Viewtype::Text, + text: Some(step.body_text(invite)), + hidden: true, + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::SecurejoinMessage); + + // Sends the step in Secure-Join header. + msg.param.set(Param::Arg, step.securejoin_header(invite)); + + match step { + BobHandshakeMsg::Request => { + // Sends the Secure-Join-Invitenumber header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.invitenumber()); + msg.force_plaintext(); + } + BobHandshakeMsg::RequestWithAuth => { + // Sends the Secure-Join-Auth header in mimefactory.rs. + msg.param.set(Param::Arg2, invite.authcode()); + msg.param.set_int(Param::GuaranteeE2ee, 1); + } + BobHandshakeMsg::ContactConfirmReceived => { + msg.param.set_int(Param::GuaranteeE2ee, 1); + } + }; + + // Sends our own fingerprint in the Secure-Join-Fingerprint header. + let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint(); + msg.param.set(Param::Arg3, bob_fp.hex()); + + // Sends the grpid in the Secure-Join-Group header. + if let QrInvite::Group { ref grpid, .. } = invite { + msg.param.set(Param::Arg4, grpid); + } + + chat::send_msg(context, chat_id, &mut msg).await?; + Ok(()) +} + /// Identifies the SecureJoin handshake messages Bob can send. enum BobHandshakeMsg { /// vc-request or vg-request @@ -492,8 +508,8 @@ impl BobHandshakeMsg { } /// The next message expected by [`BobState`] in the setup-contact/secure-join protocol. -#[derive(Debug, Clone, PartialEq)] -enum SecureJoinStep { +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SecureJoinStep { /// Expecting the auth-required message. /// /// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d. @@ -533,3 +549,29 @@ impl SecureJoinStep { } } } + +impl rusqlite::types::ToSql for SecureJoinStep { + fn to_sql(&self) -> rusqlite::Result> { + let num = match &self { + SecureJoinStep::AuthRequired => 0, + SecureJoinStep::ContactConfirm => 1, + SecureJoinStep::Terminated => 2, + SecureJoinStep::Completed => 3, + }; + let val = rusqlite::types::Value::Integer(num); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +impl rusqlite::types::FromSql for SecureJoinStep { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + i64::column_result(value).and_then(|val| match val { + 0 => Ok(SecureJoinStep::AuthRequired), + 1 => Ok(SecureJoinStep::ContactConfirm), + 2 => Ok(SecureJoinStep::Terminated), + 3 => Ok(SecureJoinStep::Completed), + _ => Err(rusqlite::types::FromSqlError::OutOfRange(val)), + }) + } +} diff --git a/src/securejoin/qrinvite.rs b/src/securejoin/qrinvite.rs index 392969541..9e25a6159 100644 --- a/src/securejoin/qrinvite.rs +++ b/src/securejoin/qrinvite.rs @@ -14,7 +14,7 @@ use crate::qr::Qr; /// Represents the data from a QR-code scan. /// /// There are methods to conveniently access fields present in both variants. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum QrInvite { Contact { contact_id: u32, @@ -100,3 +100,22 @@ impl TryFrom for QrInvite { } } } + +impl rusqlite::types::ToSql for QrInvite { + fn to_sql(&self) -> rusqlite::Result> { + let json = serde_json::to_string(self) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; + let val = rusqlite::types::Value::Text(json); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +impl rusqlite::types::FromSql for QrInvite { + fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { + String::column_result(value).and_then(|val| { + serde_json::from_str(&val) + .map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err))) + }) + } +} diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index f9fc471ba..aa7500708 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -579,6 +579,19 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); ) .await?; } + if dbversion < 86 { + info!(context, "[migration] v86"); + sql.execute_migration( + r#"CREATE TABLE bobstate ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + invite TEXT NOT NULL, + next_step INTEGER NOT NULL, + chat_id INTEGER NOT NULL + );"#, + 86, + ) + .await?; + } Ok(( recalc_fingerprints, diff --git a/src/sql/tables.sql b/src/sql/tables.sql index b7ac112d7..c52c29823 100644 --- a/src/sql/tables.sql +++ b/src/sql/tables.sql @@ -168,6 +168,14 @@ CREATE TABLE tokens ( timestamp INTEGER DEFAULT 0 ); +-- The currently running securejoin protocols, joiner-side. +-- CREATE TABLE bobstate ( +-- id INTEGER PRIMARY KEY AUTOINCREMENT, +-- invite TEXT NOT NULL, +-- next_step INTEGER NOT NULL, +-- chat_id INTEGER NOT NULL +-- ); + CREATE TABLE locations ( id INTEGER PRIMARY KEY AUTOINCREMENT, latitude REAL DEFAULT 0.0, diff --git a/src/stock_str.rs b/src/stock_str.rs index aeb9f8086..eabdc0260 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -636,20 +636,19 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C } /// Stock string: `%1$s verified.`. -pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef) -> String { +pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String { + let addr = contact.get_name_n_addr(); translated(context, StockMessage::ContactVerified) .await - .replace1(contact_addr) + .replace1(addr) } /// Stock string: `Cannot verify %1$s`. -pub(crate) async fn contact_not_verified( - context: &Context, - contact_addr: impl AsRef, -) -> String { +pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String { + let addr = contact.get_name_n_addr(); translated(context, StockMessage::ContactNotVerified) .await - .replace1(contact_addr) + .replace1(addr) } /// Stock string: `Changed setup for %1$s`. @@ -1197,8 +1196,15 @@ mod tests { #[async_std::test] async fn test_stock_string_repl_str() { let t = TestContext::new().await; + let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org") + .await + .unwrap(); + let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap(); // uses %1$s substitution - assert_eq!(contact_verified(&t, "Foo").await, "Foo verified."); + assert_eq!( + contact_verified(&t, &contact).await, + "Someone (someone@example.org) verified." + ); // We have no string using %1$d to test... } diff --git a/src/test_utils.rs b/src/test_utils.rs index 01b6a8d05..110a211da 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -23,7 +23,7 @@ use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1}; -use crate::contact::{Contact, Origin}; +use crate::contact::{Contact, Modifier, Origin}; use crate::context::Context; use crate::dc_receive_imf::dc_receive_imf; use crate::dc_tools::EmailAddress; @@ -399,31 +399,56 @@ impl TestContext { .expect("failed to load msg") } + /// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary. + pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact { + let name = other + .ctx + .get_config(Config::Displayname) + .await + .unwrap_or_default() + .unwrap_or_default(); + let addr = other + .ctx + .get_config(Config::ConfiguredAddr) + .await + .unwrap() + .unwrap(); + // MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the + // origin when creating this contact. + let (contact_id, modified) = + Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress) + .await + .unwrap(); + match modified { + Modifier::None => (), + Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr), + Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr), + } + Contact::load_from_db(&self.ctx, contact_id).await.unwrap() + } + + /// Returns 1:1 [`Chat`] with another account, if it exists. + /// + /// This first creates a contact using the configured details on the other account, then + /// creates a 1:1 chat with this contact. + pub async fn get_chat(&self, other: &TestContext) -> Option { + let contact = self.add_or_lookup_contact(other).await; + match ChatId::lookup_by_contact(&self.ctx, contact.id) + .await + .unwrap() + { + Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()), + None => None, + } + } + /// Creates or returns an existing 1:1 [`Chat`] with another account. /// /// This first creates a contact using the configured details on the other account, then /// creates a 1:1 chat with this contact. pub async fn create_chat(&self, other: &TestContext) -> Chat { - let (contact_id, _modified) = Contact::add_or_lookup( - self, - &other - .ctx - .get_config(Config::Displayname) - .await - .unwrap_or_default() - .unwrap_or_default(), - &other - .ctx - .get_config(Config::ConfiguredAddr) - .await - .unwrap() - .unwrap(), - Origin::ManuallyCreated, - ) - .await - .unwrap(); - - let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap(); + let contact = self.add_or_lookup_contact(other).await; + let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap(); Chat::load_from_db(self, chat_id).await.unwrap() }