From ef841b1aa33488eb19cc46988a3dfce2316a903c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 24 Dec 2021 19:29:38 +0100 Subject: [PATCH] Securejoin: store bobstate in database instead of context The state bob needs to maintain during a secure-join process when exchanging messages used to be stored on the context. This means if the process was killed this state was lost and the securejoin process would fail. Moving this state into the database should help this. This still only allows a single securejoin process at a time, this may be relaxed in the future. For now any previous securejoin process that was running is killed if a new one is started (this was already the case). This can remove some of the complexity around BobState handling: since the state is in the database we can already make state interactions transactional and correct. We no longer need the mutex around the state handling. This means the BobStateHandle construct that was handling the interactions between always having a valid state and handling the mutex is no longer needed, resulting in some nice simplifications. Part of #2777. --- CHANGELOG.md | 2 +- src/chat.rs | 2 +- src/context.rs | 3 - src/key.rs | 2 +- src/securejoin.rs | 396 ++++++++++++--------------------- src/securejoin/bob.rs | 257 +++++++++++++++++++++ src/securejoin/bobstate.rs | 444 ++++++++++++++++++++----------------- src/securejoin/qrinvite.rs | 21 +- src/sql/migrations.rs | 13 ++ src/sql/tables.sql | 8 + src/stock_str.rs | 22 +- src/test_utils.rs | 67 ++++-- 12 files changed, 742 insertions(+), 495 deletions(-) create mode 100644 src/securejoin/bob.rs 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() }