diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs new file mode 100644 index 000000000..fc266fcc2 --- /dev/null +++ b/src/securejoin/bobstate.rs @@ -0,0 +1,532 @@ +//! Secure-Join protocol state machine for Bob, the joiner-side. +//! +//! This module contains the state machine to run the Secure-Join handshake for Bob and does +//! not do any user interaction required by the protocol. Instead the state machine +//! provides all the information to the driver of them to 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. + +use anyhow::{Error, Result}; +use async_std::sync::MutexGuard; + +use crate::chat::{self, ChatId}; +use crate::constants::{Blocked, Viewtype}; +use crate::contact::{Contact, Origin}; +use crate::context::Context; +use crate::events::EventType; +use crate::headerdef::HeaderDef; +use crate::key::{DcKey, SignedPublicKey}; +use crate::message::Message; +use crate::mimeparser::{MimeMessage, SystemMessage}; +use crate::param::Param; + +use super::qrinvite::QrInvite; +use super::{ + encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified, JoinError, SendMsgError, +}; + +/// The stage of the [`BobState`] securejoin handshake protocol state machine. +/// +/// This does not concern itself with user interactions, only represents what happened to +/// the protocol state machine from handling this message. +#[derive(Clone, Copy, Debug, Display)] +pub enum BobHandshakeStage { + /// Step 2 completed: (vc|vg)-request message sent. + /// + /// Note that this is only ever returned by [`BobState::start_protocol`] and never by + /// [`BobState::handle_message`]. + RequestSent, + /// Step 4 completed: (vc|vg)-request-with-auth message sent. + RequestWithAuthSent, + /// The protocol completed successfully. + Completed, + /// The protocol prematurely terminated with given reason. + Terminated(&'static str), +} + +/// A handle to work with the [`BobState`] of Bob's securejoin protocol. +/// +/// 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>, + clear_state_on_drop: bool, +} + +impl<'a> BobStateHandle<'a> { + /// Creates a new instance, upholding the guarantee that [`BobState`] must exist. + pub fn from_guard(guard: MutexGuard<'a, Option>) -> Option { + match *guard { + Some(_) => Some(Self { + guard, + clear_state_on_drop: false, + }), + None => None, + } + } + + /// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice). + /// + /// # Safety + /// + /// [`BobStateHandle::from_guard`] guarantees this struct is only created with a valid + /// state available. + pub fn chat_id(&self) -> ChatId { + match *self.guard { + Some(ref bobstate) => bobstate.chat_id, + None => unreachable!(), + } + } + + /// Returns a reference to the [`QrInvite`] of the joiner process. + /// + /// # Safety + /// + /// [`BobStateHandle::from_guard`] guarantees this struct is only created with a valid + /// state available. + pub fn invite(&self) -> &QrInvite { + match *self.guard { + Some(ref bobstate) => &bobstate.invite, + None => unreachable!(), + } + } + + /// 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.guard { + Some(ref mut bobstate) => match 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(_) => { + self.finish_protocol(context).await; + None + } + }, + None => None, // also unreachable!() + } + } + + /// 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 cosistent is violated. However the "ongoing" prococess 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). + /// + /// [`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 Some(ref bobstate) = *self.guard { + if let QrInvite::Group { .. } = bobstate.invite { + context.stop_ongoing().await; + } + } + } +} + +impl<'a> Drop for BobStateHandle<'a> { + fn drop(&mut self) { + if self.clear_state_on_drop { + self.guard.take(); + } + } +} + +/// The securejoin state kept in-memory while Bob is joining. +/// +/// 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. +/// +/// # Conducing the securejoin handshake +/// +/// The methods on this struct allow you to interact with the state and thus conduct the +/// securejoin handshake for Bob. The methods **only concern themselves** with the protocol +/// state and explicitly avoid doing performing any user interactions required by +/// securejoin. This simplifies the concerns and logic required in both the callers and in +/// the state management. The return values can be used to understand what user +/// interactions need to happen. +/// +/// [`Bob`]: super::Bob +/// [`Bob::state`]: super::Bob::state +#[derive(Debug)] +pub struct BobState { + /// The QR Invite code. + invite: QrInvite, + /// The next expected message from Alice. + next: SecureJoinStep, + /// The [`ChatId`] of the 1:1 chat with Alice, matching [`QrInvite::contact_id`]. + chat_id: ChatId, +} + +impl BobState { + /// Starts the securejoin protocol and creates a new [`BobState`]. + /// + /// # Bob - the joiner's side + /// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 + pub async fn start_protocol( + context: &Context, + invite: QrInvite, + ) -> Result<(Self, BobHandshakeStage), JoinError> { + let chat_id = chat::create_by_contact_id(context, invite.contact_id()) + .await + .map_err(JoinError::UnknownContact)?; + if fingerprint_equals_sender(context, invite.fingerprint(), chat_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, + }; + 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)) + } + } + + /// Returns the [`QrInvite`] use to create this [`BobState`]. + pub fn invite(&self) -> &QrInvite { + &self.invite + } + + /// 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( + &mut self, + context: &Context, + mime_message: &MimeMessage, + ) -> Result> { + let step = match mime_message.get(HeaderDef::SecureJoin) { + Some(step) => step, + None => { + warn!( + context, + "Message has no Secure-Join header: {}", + mime_message.get_rfc724_mid().unwrap_or_default() + ); + return Ok(None); + } + }; + if !self.is_msg_expected(context, step.as_str()) { + info!(context, "{} message out of sync for BobState", step); + return Ok(None); + } + match step.as_str() { + "vg-auth-required" | "vc-auth-required" => { + self.step_auth_required(context, mime_message).await + } + "vg-member-added" | "vc-contact-confirm" => { + self.step_contact_confirm(context, mime_message).await + } + _ => { + warn!(context, "Invalid step for BobState: {}", step); + Ok(None) + } + } + } + + /// Returns `true` if the message is expected according to the protocol. + fn is_msg_expected(&self, context: &Context, step: &str) -> bool { + let variant_matches = match self.invite { + QrInvite::Contact { .. } => step.starts_with("vc-"), + QrInvite::Group { .. } => step.starts_with("vg-"), + }; + let step_matches = self.next.matches(context, step); + variant_matches && step_matches + } + + /// Handles a *vc-auth-required* or *vg-auth-required* message. + /// + /// # Bob - the joiner's side + /// ## Step 4 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 + async fn step_auth_required( + &mut self, + context: &Context, + mime_message: &MimeMessage, + ) -> Result> { + info!( + context, + "Bob Step 4 - handling vc-auth-require/vg-auth-required message" + ); + if !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) { + let reason = if mime_message.was_encrypted() { + "Valid signature missing" + } else { + "Required encryption missing" + }; + self.next = SecureJoinStep::Terminated; + return Ok(Some(BobHandshakeStage::Terminated(reason))); + } + if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await { + self.next = SecureJoinStep::Terminated; + return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); + } + info!(context, "Fingerprint verified.",); + self.next = SecureJoinStep::ContactConfirm; + self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth) + .await?; + Ok(Some(BobHandshakeStage::RequestWithAuthSent)) + } + + /// Handles a *vc-contact-confirm* or *vg-member-added* message. + /// + /// # Bob - the joiner's side + /// ## Step 7 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 + /// + /// This deviates from the protocol by also sending a confirmation message in response + /// to the *vc-contact-confirm* message. This has no specific value to the protocol and + /// is only done out of symmerty with *vg-member-added* handling. + async fn step_contact_confirm( + &mut self, + context: &Context, + mime_message: &MimeMessage, + ) -> Result> { + info!( + context, + "Bob Step 7 - handling vc-contact-confirm/vg-member-added message" + ); + let vg_expect_encrypted = match self.invite { + QrInvite::Contact { .. } => { + // setup-contact is always encrypted + true + } + QrInvite::Group { ref grpid, .. } => { + // This is buggy, is_verified_group will always be + // false since the group is created by receive_imf for + // the very handshake message we're handling now. But + // only after we have returned. It does not impact + // the security invariants of secure-join however. + let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, grpid) + .await + .unwrap_or((ChatId::new(0), false, Blocked::Not)); + // when joining a non-verified group + // the vg-member-added message may be unencrypted + // when not all group members have keys or prefer encryption. + // So only expect encryption if this is a verified group + is_verified_group + } + }; + if vg_expect_encrypted + && !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) + { + self.next = SecureJoinStep::Terminated; + return Ok(Some(BobHandshakeStage::Terminated( + "Contact confirm message not encrypted", + ))); + } + mark_peer_as_verified(context, self.invite.fingerprint()).await?; + Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) + .await; + emit_event!(context, EventType::ContactsChanged(None)); + + if let QrInvite::Group { .. } = self.invite { + let member_added = mime_message + .get(HeaderDef::ChatGroupMemberAdded) + .map(|s| s.as_str()) + .ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?; + if !context.is_self_addr(member_added).await? { + info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group)."); + return Ok(None); + } + } + + self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived) + .await + .map_err(|_| { + warn!( + context, + "Failed to send vc-contact-confirm-received/vg-member-added-received" + ); + }) + // This is not an error affecting the protocol outcome. + .ok(); + + self.next = SecureJoinStep::Completed; + Ok(Some(BobHandshakeStage::Completed)) + } + + /// Sends the requested handshake message to Alice. + /// + /// This takes care of adding the required headers for the step. + async fn send_handshake_message( + &self, + 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.param.set_int(Param::ForcePlaintext, 1); + } + 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(()) + } +} + +/// Identifies the SecureJoin handshake messages Bob can send. +enum BobHandshakeMsg { + /// vc-request or vg-request + Request, + /// vc-request-with-auth or vg-request-with-auth + RequestWithAuth, + /// vc-contact-confirm-received or vg-member-added-received + ContactConfirmReceived, +} + +impl BobHandshakeMsg { + /// Returns the text to send in the body of the handshake message. + /// + /// This text has no significance to the protocol, but would be visible if users see + /// this email message directly, e.g. when accessing their email without using + /// DeltaChat. + fn body_text(&self, invite: &QrInvite) -> String { + format!("Secure-Join: {}", self.securejoin_header(invite)) + } + + /// Returns the `Secure-Join` header value. + /// + /// This identifies the step this message is sending information about. Most protocol + /// steps include additional information into other headers, see + /// [`BobState::send_handshake_message`] for these. + fn securejoin_header(&self, invite: &QrInvite) -> &'static str { + match self { + Self::Request => match invite { + QrInvite::Contact { .. } => "vc-request", + QrInvite::Group { .. } => "vg-request", + }, + Self::RequestWithAuth => match invite { + QrInvite::Contact { .. } => "vc-request-with-auth", + QrInvite::Group { .. } => "vg-request-with-auth", + }, + Self::ContactConfirmReceived => match invite { + QrInvite::Contact { .. } => "vc-contact-confirm-received", + QrInvite::Group { .. } => "vg-member-added-received", + }, + } + } +} + +/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol. +#[derive(Debug, PartialEq)] +enum SecureJoinStep { + /// Expecting the auth-required message. + /// + /// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d. + AuthRequired, + /// Expecting the contact-confirm message. + /// + /// This corresponds to the `vc-contact-confirm` or `vg-member-added` message of step + /// 6b. + ContactConfirm, + /// The protocol terminated because of an error. + /// + /// The securejoin protocol terminated, this exists to ensure [`BobState`] can detect + /// when it earlier signalled that is should be terminated. It is an error to call with + /// this state. + Terminated, + /// The protocol completed. + /// + /// This exists to ensure [`BobState`] can detect when it earlier signalled that it is + /// complete. It is an error to call with this state. + Completed, +} + +impl SecureJoinStep { + /// Compares the legacy string representation of a step to a [`SecureJoinStep`] variant. + fn matches(&self, context: &Context, step: &str) -> bool { + match self { + Self::AuthRequired => step == "vc-auth-required" || step == "vg-auth-required", + Self::ContactConfirm => step == "vc-contact-confirm" || step == "vg-member-added", + SecureJoinStep::Terminated => { + warn!(context, "Terminated state for next securejoin step"); + false + } + SecureJoinStep::Completed => { + warn!(context, "Complted state for next securejoin step"); + false + } + } + } +} diff --git a/src/securejoin/mod.rs b/src/securejoin/mod.rs index 9a9d3e805..47e5af638 100644 --- a/src/securejoin/mod.rs +++ b/src/securejoin/mod.rs @@ -4,7 +4,7 @@ use std::convert::TryFrom; use std::time::{Duration, Instant}; use anyhow::{bail, Context as _, Error, Result}; -use async_std::sync::{Mutex, MutexGuard}; +use async_std::sync::Mutex; use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; @@ -26,8 +26,10 @@ use crate::sql; use crate::stock::StockMessage; use crate::token; +mod bobstate; mod qrinvite; +use bobstate::{BobHandshakeStage, BobState, BobStateHandle}; use qrinvite::{QrError, QrInvite}; pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.'); @@ -62,7 +64,7 @@ macro_rules! inviter_progress { /// /// 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. +/// database. For Bob this state is only carried live on the [`Context`] in this struct. #[derive(Debug, Default)] pub(crate) struct Bob { inner: Mutex>, @@ -91,7 +93,7 @@ impl Bob { match BobState::start_protocol(context, invite).await { Ok((state, stage)) => { if matches!(stage, BobHandshakeStage::RequestWithAuthSent) { - joiner_progress!(context, state.invite.contact_id(), 400); + joiner_progress!(context, state.invite().contact_id(), 400); } *guard = Some(state); Ok(()) @@ -124,487 +126,6 @@ impl Bob { } } -/// A handle to work with the [`BobState`] of Bob's securejoin protocol. -/// -/// 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. -struct BobStateHandle<'a> { - guard: MutexGuard<'a, Option>, - clear_state_on_drop: bool, -} - -impl<'a> BobStateHandle<'a> { - /// Creates a new instance, upholding the guarantee that [`BobState`] must exist. - fn from_guard(guard: MutexGuard<'a, Option>) -> Option { - match *guard { - Some(_) => Some(Self { - guard, - clear_state_on_drop: false, - }), - None => None, - } - } - - /// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice). - pub fn chat_id(&self) -> Result { - match *self.guard { - Some(ref bobstate) => Ok(bobstate.chat_id), - None => Err(Error::msg("Invalid BobStateHandle state")), - } - } - - /// Returns a reference to the [`QrInvite`] of the joiner process. - pub fn invite(&self) -> Result<&QrInvite> { - match *self.guard { - Some(ref bobstate) => Ok(&bobstate.invite), - None => Err(Error::msg("Invalid BobStateHandle state")), - } - } - - /// 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.guard { - Some(ref mut bobstate) => match 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(_) => { - self.finish_protocol(context).await; - None - } - }, - None => None, - } - } - - /// Marks the bob handshake as finished. - /// - /// This will clear the state on [`Context::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 cosistent is violated. However the "ongoing" prococess 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). - async fn finish_protocol(&mut self, context: &Context) { - info!(context, "Finishing securejoin handshake protocol for Bob"); - self.clear_state_on_drop = true; - if let Some(ref bobstate) = *self.guard { - if let QrInvite::Group { .. } = bobstate.invite { - context.stop_ongoing().await; - } - } - } -} - -impl<'a> Drop for BobStateHandle<'a> { - fn drop(&mut self) { - if self.clear_state_on_drop { - self.guard.take(); - } - } -} - -/// The securejoin state kept in-memory while Bob is joining. -/// -/// 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. -/// -/// # Conducing the securejoin handshake -/// -/// The methods on this struct allow you to interact with the state and thus conduct the -/// securejoin handshake for Bob. The methods **only concern themselves** with the protocol -/// state and explicitly avoid doing performing any user interactions required by -/// securejoin. This simplifies the concerns and logic required in both the callers and in -/// the state management. The return values can be used to understand what user -/// interactions need to happen. -#[derive(Debug)] -struct BobState { - /// The QR Invite code. - invite: QrInvite, - /// The next expected message from Alice. - next: SecureJoinStep, - /// The [ChatId] of the 1:1 chat with Alice, matching [QrInvite::contact]. - chat_id: ChatId, -} - -impl BobState { - /// Starts the securejoin protocol and creates a new [`BobState`]. - /// - /// # Bob - the joiner's side - /// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 - async fn start_protocol( - context: &Context, - invite: QrInvite, - ) -> Result<(Self, BobHandshakeStage), JoinError> { - let chat_id = chat::create_by_contact_id(context, invite.contact_id()) - .await - .map_err(JoinError::UnknownContact)?; - if fingerprint_equals_sender(context, invite.fingerprint(), chat_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, - }; - 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)) - } - } - - /// 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( - &mut self, - context: &Context, - mime_message: &MimeMessage, - ) -> Result> { - let step = match mime_message.get(HeaderDef::SecureJoin) { - Some(step) => step, - None => { - warn!( - context, - "Message has no Secure-Join header: {}", - mime_message.get_rfc724_mid().unwrap_or_default() - ); - return Ok(None); - } - }; - if !self.is_msg_expected(context, step.as_str()) { - info!(context, "{} message out of sync for BobState", step); - return Ok(None); - } - match step.as_str() { - "vg-auth-required" | "vc-auth-required" => { - self.step_auth_required(context, mime_message).await - } - "vg-member-added" | "vc-contact-confirm" => { - self.step_contact_confirm(context, mime_message).await - } - _ => { - warn!(context, "Invalid step for BobState: {}", step); - Ok(None) - } - } - } - - /// Returns `true` if the message is expected according to the protocol. - fn is_msg_expected(&self, context: &Context, step: &str) -> bool { - let variant_matches = match self.invite { - QrInvite::Contact { .. } => step.starts_with("vc-"), - QrInvite::Group { .. } => step.starts_with("vg-"), - }; - let step_matches = self.next.matches(context, step); - variant_matches && step_matches - } - - /// Handles a *vc-auth-required* or *vg-auth-required* message. - /// - /// # Bob - the joiner's side - /// ## Step 4 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 - async fn step_auth_required( - &mut self, - context: &Context, - mime_message: &MimeMessage, - ) -> Result> { - info!( - context, - "Bob Step 4 - handling vc-auth-require/vg-auth-required message" - ); - if !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) { - let reason = if mime_message.was_encrypted() { - "Valid signature missing" - } else { - "Required encryption missing" - }; - self.next = SecureJoinStep::Terminated; - return Ok(Some(BobHandshakeStage::Terminated(reason))); - } - if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await { - self.next = SecureJoinStep::Terminated; - return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch"))); - } - info!(context, "Fingerprint verified.",); - self.next = SecureJoinStep::ContactConfirm; - self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth) - .await?; - Ok(Some(BobHandshakeStage::RequestWithAuthSent)) - } - - /// Handles a *vc-contact-confirm* or *vg-member-added* message. - /// - /// # Bob - the joiner's side - /// ## Step 7 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0 - /// - /// This deviates from the protocol by also sending a confirmation message in response - /// to the *vc-contact-confirm* message. This has no specific value to the protocol and - /// is only done out of symmerty with *vg-member-added* handling. - async fn step_contact_confirm( - &mut self, - context: &Context, - mime_message: &MimeMessage, - ) -> Result> { - info!( - context, - "Bob Step 7 - handling vc-contact-confirm/vg-member-added message" - ); - let vg_expect_encrypted = match self.invite { - QrInvite::Contact { .. } => { - // setup-contact is always encrypted - true - } - QrInvite::Group { ref grpid, .. } => { - // This is buggy, is_verified_group will always be - // false since the group is created by receive_imf for - // the very handshake message we're handling now. But - // only after we have returned. It does not impact - // the security invariants of secure-join however. - let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, grpid) - .await - .unwrap_or((ChatId::new(0), false, Blocked::Not)); - // when joining a non-verified group - // the vg-member-added message may be unencrypted - // when not all group members have keys or prefer encryption. - // So only expect encryption if this is a verified group - is_verified_group - } - }; - if vg_expect_encrypted - && !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint())) - { - self.next = SecureJoinStep::Terminated; - return Ok(Some(BobHandshakeStage::Terminated( - "Contact confirm message not encrypted", - ))); - } - mark_peer_as_verified(context, self.invite.fingerprint()).await?; - Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) - .await; - emit_event!(context, EventType::ContactsChanged(None)); - - if let QrInvite::Group { .. } = self.invite { - let member_added = mime_message - .get(HeaderDef::ChatGroupMemberAdded) - .map(|s| s.as_str()) - .ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?; - if !context.is_self_addr(member_added).await? { - info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group)."); - return Ok(None); - } - } - - self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived) - .await - .map_err(|_| { - warn!( - context, - "Failed to send vc-contact-confirm-received/vg-member-added-received" - ); - }) - // This is not an error affecting the protocol outcome. - .ok(); - - self.next = SecureJoinStep::Completed; - Ok(Some(BobHandshakeStage::Completed)) - } - - /// Sends the requested handshake message to Alice. - /// - /// This takes care of adding the required headers for the step. - async fn send_handshake_message( - &self, - context: &Context, - step: BobHandshakeMsg, - ) -> Result<(), SendMsgError> { - let mut msg = Message::default(); - msg.viewtype = Viewtype::Text; - msg.text = Some(step.body_text(&self.invite)); - msg.hidden = true; - 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.param.set_int(Param::ForcePlaintext, 1); - } - 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(()) - } -} - -/// The stage of the [`BobState`] securejoin handshake protocol state machine. -/// -/// This does not concern itself with user interactions, only represents what happened to -/// the protocol state machine from handling this message. -#[derive(Clone, Copy, Debug, Display)] -enum BobHandshakeStage { - /// Step 2 completed: (vc|vg)-request message sent. - /// - /// Note that this is only ever returned by [`BobState::start_protocol`] and never by - /// [`BobState::handle_message`]. - RequestSent, - /// Step 4 completed: (vc|vg)-request-with-auth message sent. - RequestWithAuthSent, - /// The protocol completed successfully. - Completed, - /// The protocol prematurely terminated with given reason. - Terminated(&'static str), -} - -/// Identifies the SecureJoin handshake messages Bob can send. -enum BobHandshakeMsg { - /// vc-request or vg-request - Request, - /// vc-request-with-auth or vg-request-with-auth - RequestWithAuth, - /// vc-contact-confirm-received or vg-member-added-received - ContactConfirmReceived, -} - -impl BobHandshakeMsg { - /// Returns the text to send in the body of the handshake message. - /// - /// This text has no significance to the protocol, but would be visible if users see - /// this email message directly, e.g. when accessing their email without using - /// DeltaChat. - fn body_text(&self, invite: &QrInvite) -> String { - format!("Secure-Join: {}", self.securejoin_header(invite)) - } - - /// Returns the `Secure-Join` header value. - /// - /// This identifies the step this message is sending information about. Most protocol - /// steps include additional information into other headers, see - /// [`BobState::send_handshake_message`] for these. - fn securejoin_header(&self, invite: &QrInvite) -> &'static str { - match self { - Self::Request => match invite { - QrInvite::Contact { .. } => "vc-request", - QrInvite::Group { .. } => "vg-request", - }, - Self::RequestWithAuth => match invite { - QrInvite::Contact { .. } => "vc-request-with-auth", - QrInvite::Group { .. } => "vg-request-with-auth", - }, - Self::ContactConfirmReceived => match invite { - QrInvite::Contact { .. } => "vc-contact-confirm-received", - QrInvite::Group { .. } => "vg-member-added-received", - }, - } - } -} - -/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol. -#[derive(Debug, PartialEq)] -enum SecureJoinStep { - /// Expecting the auth-required message. - /// - /// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d. - AuthRequired, - /// Expecting the contact-confirm message. - /// - /// This corresponds to the `vc-contact-confirm` or `vg-member-added` message of step - /// 6b. - ContactConfirm, - /// The protocol terminated because of an error. - /// - /// The securejoin protocol terminated, this exists to ensure [`BobState`] can detect - /// when it earlier signalled that is should be terminated. It is an error to call with - /// this state. - Terminated, - /// The protocol completed. - /// - /// This exists to ensure [`BobState`] can detect when it earlier signalled that it is - /// complete. It is an error to call with this state. - Completed, -} - -impl SecureJoinStep { - /// Compares the legacy string representation of a step to a [`SecureJoinStep`] variant. - fn matches(&self, context: &Context, step: &str) -> bool { - match self { - Self::AuthRequired => step == "vc-auth-required" || step == "vg-auth-required", - Self::ContactConfirm => step == "vc-contact-confirm" || step == "vg-member-added", - SecureJoinStep::Terminated => { - warn!(context, "Terminated state for next securejoin step"); - false - } - SecureJoinStep::Completed => { - warn!(context, "Complted state for next securejoin step"); - false - } - } - } -} - pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option { /*======================================================= ==== Alice - the inviter side ==== @@ -874,6 +395,8 @@ async fn fingerprint_equals_sender( /// /// This status is returned to [`dc_receive_imf`] which will use it to decide what to do /// next with this incoming setup-contact/secure-join handshake message. +/// +/// [`dc_receive_imf`]: crate::dc_receive_imf::dc_receive_imf pub(crate) enum HandshakeMessage { /// The message has been fully handled and should be removed/delete. /// @@ -989,12 +512,12 @@ pub(crate) async fn handle_securejoin_handshake( match context.bob.state(context).await { Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { - could_not_establish_secure_connection(context, bobstate.chat_id()?, why) + could_not_establish_secure_connection(context, bobstate.chat_id(), why) .await; Ok(HandshakeMessage::Done) } Some(_stage) => { - joiner_progress!(context, bobstate.invite()?.contact_id(), 400); + joiner_progress!(context, bobstate.invite().contact_id(), 400); Ok(HandshakeMessage::Done) } None => Ok(HandshakeMessage::Ignore), @@ -1130,13 +653,13 @@ pub(crate) async fn handle_securejoin_handshake( match context.bob.state(context).await { Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await { Some(BobHandshakeStage::Terminated(why)) => { - could_not_establish_secure_connection(context, bobstate.chat_id()?, why) + could_not_establish_secure_connection(context, bobstate.chat_id(), why) .await; Ok(HandshakeMessage::Done) } Some(_stage) => { // Can only be BobHandshakeStage::Completed - secure_connection_established(context, bobstate.chat_id()?).await; + secure_connection_established(context, bobstate.chat_id()).await; Ok(retval) } None => Ok(retval), @@ -1379,7 +902,7 @@ mod tests { use crate::chat::ProtectionStatus; use crate::events::Event; use crate::peerstate::Peerstate; - use crate::test_utils::{LogSink, TestContext}; + use crate::test_utils::TestContext; #[async_std::test] async fn test_setup_contact() {