mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 05:22:14 +03:00
Move BobState to a private submodule
This does not only organise things better, but most importantly the BobStateHandle is now not in the same module as its users. This means it can guarantee safety about how it is initialised and use unreachable!() to simplify it's API.
This commit is contained in:
532
src/securejoin/bobstate.rs
Normal file
532
src/securejoin/bobstate.rs
Normal file
@@ -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<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(guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
|
||||
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<BobHandshakeStage> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Option<BobState>>,
|
||||
@@ -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<BobState>>,
|
||||
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<BobState>>) -> Option<Self> {
|
||||
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<ChatId> {
|
||||
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<BobHandshakeStage> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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<Option<BobHandshakeStage>> {
|
||||
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<String> {
|
||||
/*=======================================================
|
||||
==== 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() {
|
||||
|
||||
Reference in New Issue
Block a user