mirror of
https://github.com/chatmail/core.git
synced 2026-04-25 09:26:30 +03:00
feat: allow scanning multiple securejoin QR codes in parallel
This commit is contained in:
@@ -1,31 +1,40 @@
|
||||
//! Bob's side of SecureJoin handling.
|
||||
//!
|
||||
//! This are some helper functions around [`BobState`] which augment the state changes with
|
||||
//! the required user interactions.
|
||||
//! Bob's side of SecureJoin handling, the joiner-side.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use super::bobstate::{BobHandshakeStage, BobState};
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::HandshakeMessage;
|
||||
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::chat::{self, is_contact_in_chat, ChatId, ProtectionStatus};
|
||||
use crate::constants::{self, Blocked, Chattype};
|
||||
use crate::contact::Contact;
|
||||
use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::securejoin::{encrypted_and_signed, verify_sender_by_fingerprint, ContactId};
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{create_smeared_timestamp, time};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// 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 will try to start the securejoin protocol for the given QR `invite`.
|
||||
///
|
||||
/// If Bob already has Alice's key, he sends `AUTH` token
|
||||
/// and forgets about the invite.
|
||||
/// If Bob does not yet have Alice's key, he sends `vc-request`
|
||||
/// or `vg-request` message and stores a row in the `bobstate` table
|
||||
/// so he can check Alice's key against the fingerprint
|
||||
/// and send `AUTH` token later.
|
||||
///
|
||||
/// This function takes care of handling multiple concurrent joins and handling errors while
|
||||
/// starting the protocol.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
|
||||
@@ -42,22 +51,47 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
|
||||
// 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 code scanned").await?;
|
||||
state.emit_progress(context, JoinerProgress::Error);
|
||||
}
|
||||
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
|
||||
state.emit_progress(context, JoinerProgress::RequestWithAuthSent);
|
||||
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
// Now start the protocol and initialise the state.
|
||||
{
|
||||
let peer_verified =
|
||||
verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
|
||||
.await?;
|
||||
|
||||
if peer_verified {
|
||||
// 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?;
|
||||
|
||||
// Mark 1:1 chat as verified already.
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(invite.contact_id()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
});
|
||||
} else {
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
|
||||
|
||||
insert_new_db_entry(context, invite.clone(), chat_id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
let group_chat_id = state.joining_chat_id(context).await?;
|
||||
let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
@@ -74,7 +108,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
// uses it to send the handshake messages.
|
||||
let chat_id = state.alice_chat();
|
||||
// Calculate the sort timestamp before checking the chat protection status so that if we
|
||||
// race with its change, we don't add our message below the protection message.
|
||||
let sort_to_bottom = true;
|
||||
@@ -102,6 +135,19 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new entry in the bobstate table.
|
||||
///
|
||||
/// Returns the ID of the newly inserted entry.
|
||||
async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
|
||||
context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
|
||||
(invite, 0, chat_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
@@ -110,121 +156,214 @@ pub(super) async fn handle_auth_required(
|
||||
context: &Context,
|
||||
message: &MimeMessage,
|
||||
) -> Result<HandshakeMessage> {
|
||||
let Some(mut bobstate) = BobState::from_db(&context.sql).await? else {
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
// Load all Bob states that expect `vc-auth-required` or `vg-auth-required`.
|
||||
let bob_states: Vec<(i64, QrInvite, ChatId)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, invite, chat_id FROM bobstate",
|
||||
(),
|
||||
|row| {
|
||||
let row_id: i64 = row.get(0)?;
|
||||
let invite: QrInvite = row.get(1)?;
|
||||
let chat_id: ChatId = row.get(2)?;
|
||||
Ok((row_id, invite, chat_id))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
match bobstate.handle_auth_required(context, message).await? {
|
||||
Some(BobHandshakeStage::Terminated(why)) => {
|
||||
bobstate.notify_aborted(context, why).await?;
|
||||
Ok(HandshakeMessage::Done)
|
||||
info!(
|
||||
context,
|
||||
"Bob Step 4 - handling {{vc,vg}}-auth-required message."
|
||||
);
|
||||
|
||||
let mut auth_sent = false;
|
||||
for (bobstate_row_id, invite, chat_id) in bob_states {
|
||||
if !encrypted_and_signed(context, message, invite.fingerprint()) {
|
||||
continue;
|
||||
}
|
||||
Some(_stage) => {
|
||||
if bobstate.is_join_group() {
|
||||
|
||||
if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(context, "Fingerprint verified.",);
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
|
||||
.await?;
|
||||
|
||||
match invite {
|
||||
QrInvite::Contact { .. } => {}
|
||||
QrInvite::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 contact_id = invite.contact_id();
|
||||
let msg = stock_str::secure_join_replies(context, contact_id).await;
|
||||
let chat_id = bobstate.joining_chat_id(context).await?;
|
||||
let chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
}
|
||||
bobstate
|
||||
.set_peer_verified(context, message.timestamp_sent)
|
||||
.await?;
|
||||
bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent);
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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<ChatId> {
|
||||
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_ex(context, Nosync).await?;
|
||||
chat_id
|
||||
}
|
||||
None => {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
grpid,
|
||||
name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
create_smeared_timestamp(context),
|
||||
)
|
||||
.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 mut msg = stock_str::contact_not_verified(context, &contact).await;
|
||||
msg += " (";
|
||||
msg += why;
|
||||
msg += ")";
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Turns 1:1 chat with SecureJoin peer into protected chat.
|
||||
pub(crate) async fn set_peer_verified(&self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
self.alice_chat()
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
timestamp,
|
||||
Some(contact.id),
|
||||
message.timestamp_sent,
|
||||
Some(invite.contact_id()),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
});
|
||||
|
||||
auth_sent = true;
|
||||
}
|
||||
|
||||
if auth_sent {
|
||||
// Delete the message from IMAP server.
|
||||
Ok(HandshakeMessage::Done)
|
||||
} else {
|
||||
// We have not found any corresponding AUTH codes,
|
||||
// maybe another Bob device has scanned the QR code.
|
||||
// Leave the message on IMAP server and let the other device
|
||||
// process it.
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the requested handshake message to Alice.
|
||||
pub(crate) async fn send_handshake_message(
|
||||
context: &Context,
|
||||
invite: &QrInvite,
|
||||
chat_id: ChatId,
|
||||
step: BobHandshakeMsg,
|
||||
) -> Result<()> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: 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);
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
//
|
||||
// `Secure-Join-Group` header is deprecated,
|
||||
// but old Delta Chat core requires that Alice receives it.
|
||||
//
|
||||
// Previous Delta Chat core also sent `Secure-Join-Group` header
|
||||
// in `vg-request` messages,
|
||||
// but it was not used on the receiver.
|
||||
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.
|
||||
pub(crate) enum BobHandshakeMsg {
|
||||
/// vc-request or vg-request
|
||||
Request,
|
||||
/// vc-request-with-auth or vg-request-with-auth
|
||||
RequestWithAuth,
|
||||
}
|
||||
|
||||
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
|
||||
/// [`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",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 group chat will be created if it does not yet exist.
|
||||
async fn joining_chat_id(
|
||||
context: &Context,
|
||||
invite: &QrInvite,
|
||||
alice_chat_id: ChatId,
|
||||
) -> Result<ChatId> {
|
||||
match invite {
|
||||
QrInvite::Contact { .. } => Ok(alice_chat_id),
|
||||
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_ex(context, Nosync).await?;
|
||||
chat_id
|
||||
}
|
||||
None => {
|
||||
ChatId::create_multiuser_record(
|
||||
context,
|
||||
Chattype::Group,
|
||||
grpid,
|
||||
name,
|
||||
Blocked::Not,
|
||||
ProtectionStatus::Unprotected, // protection is added later as needed
|
||||
None,
|
||||
create_smeared_timestamp(context),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(group_chat_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +372,6 @@ impl BobState {
|
||||
/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
|
||||
/// which can be shown as a progress bar.
|
||||
pub(crate) enum JoinerProgress {
|
||||
/// An error occurred.
|
||||
Error,
|
||||
/// vg-vc-request-with-auth sent.
|
||||
///
|
||||
/// Typically shows as "alice@addr verified, introducing myself."
|
||||
@@ -243,10 +380,10 @@ pub(crate) enum JoinerProgress {
|
||||
Succeeded,
|
||||
}
|
||||
|
||||
impl From<JoinerProgress> for usize {
|
||||
fn from(progress: JoinerProgress) -> Self {
|
||||
match progress {
|
||||
JoinerProgress::Error => 0,
|
||||
impl JoinerProgress {
|
||||
#[expect(clippy::wrong_self_convention)]
|
||||
pub(crate) fn to_usize(self) -> usize {
|
||||
match self {
|
||||
JoinerProgress::RequestWithAuthSent => 400,
|
||||
JoinerProgress::Succeeded => 1000,
|
||||
}
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
//! 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 its driver so it can perform the correct interactions.
|
||||
//!
|
||||
//! The [`BobState`] is only directly used to initially create it when starting the
|
||||
//! protocol.
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
use super::{encrypted_and_signed, verify_sender_by_fingerprint};
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::contact::{ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::securejoin::Peerstate;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::time;
|
||||
|
||||
/// 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.
|
||||
RequestSent,
|
||||
/// Step 4 completed: (vc|vg)-request-with-auth message sent.
|
||||
RequestWithAuthSent,
|
||||
/// The protocol prematurely terminated with given reason.
|
||||
Terminated(&'static str),
|
||||
}
|
||||
|
||||
/// The securejoin state kept 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 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
|
||||
///
|
||||
/// 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 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, Clone)]
|
||||
pub struct BobState {
|
||||
/// Database primary key.
|
||||
id: i64,
|
||||
/// 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`].
|
||||
///
|
||||
/// 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,
|
||||
chat_id: ChatId,
|
||||
) -> Result<(Self, BobHandshakeStage, Vec<Self>)> {
|
||||
let peer_verified =
|
||||
verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
|
||||
.await?;
|
||||
|
||||
let (stage, next);
|
||||
if peer_verified {
|
||||
// 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?;
|
||||
|
||||
stage = BobHandshakeStage::RequestWithAuthSent;
|
||||
next = SecureJoinStep::ContactConfirm;
|
||||
} else {
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
|
||||
|
||||
stage = BobHandshakeStage::RequestSent;
|
||||
next = SecureJoinStep::AuthRequired;
|
||||
};
|
||||
|
||||
let (id, aborted_states) =
|
||||
Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?;
|
||||
let state = Self {
|
||||
id,
|
||||
invite,
|
||||
next,
|
||||
chat_id,
|
||||
};
|
||||
|
||||
if peer_verified {
|
||||
// Mark 1:1 chat as verified already.
|
||||
state.set_peer_verified(context, time()).await?;
|
||||
}
|
||||
|
||||
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<Self>)> {
|
||||
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=?;"#,
|
||||
(SecureJoinStep::Terminated,),
|
||||
)?;
|
||||
let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?;
|
||||
let mut aborted = Vec::new();
|
||||
for id in stmt.query_map((), |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;", ())?;
|
||||
transaction.execute(
|
||||
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
|
||||
(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<Option<Self>> {
|
||||
// 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;",
|
||||
(),
|
||||
|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<Self> {
|
||||
connection.query_row(
|
||||
"SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;",
|
||||
(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`].
|
||||
pub fn invite(&self) -> &QrInvite {
|
||||
&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=?;",
|
||||
(next, self.id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
SecureJoinStep::Terminated | SecureJoinStep::Completed => {
|
||||
sql.execute("DELETE FROM bobstate WHERE id=?;", (self.id,))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
self.next = next;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles {vc,vg}-auth-required message of 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::Terminated`] is reached this
|
||||
/// [`BobState`] should be destroyed,
|
||||
/// further calling it will just result in the messages being unused by this handshake.
|
||||
pub(crate) async fn handle_auth_required(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
) -> Result<Option<BobHandshakeStage>> {
|
||||
let step = match mime_message.get_header(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) {
|
||||
info!(context, "{} message out of sync for BobState", step);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Bob Step 4 - handling {{vc,vg}}-auth-required message."
|
||||
);
|
||||
if !encrypted_and_signed(context, mime_message, self.invite.fingerprint()) {
|
||||
let reason = if mime_message.was_encrypted() {
|
||||
"Valid signature missing"
|
||||
} else {
|
||||
"Required encryption missing"
|
||||
};
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(reason)));
|
||||
}
|
||||
if !verify_sender_by_fingerprint(
|
||||
context,
|
||||
self.invite.fingerprint(),
|
||||
self.invite.contact_id(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
|
||||
self.update_next(&context.sql, SecureJoinStep::ContactConfirm)
|
||||
.await?;
|
||||
self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
|
||||
.await?;
|
||||
Ok(Some(BobHandshakeStage::RequestWithAuthSent))
|
||||
}
|
||||
|
||||
/// Returns `true` if the message is expected according to the protocol.
|
||||
pub(crate) 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-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
|
||||
pub(crate) async fn step_contact_confirm(&mut self, context: &Context) -> Result<()> {
|
||||
let fingerprint = self.invite.fingerprint();
|
||||
let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Mark peer as backward verified.
|
||||
peerstate.backward_verified_key_id =
|
||||
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
ContactId::scaleup_origin(
|
||||
context,
|
||||
&[self.invite.contact_id()],
|
||||
Origin::SecurejoinJoined,
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
self.update_next(&context.sql, SecureJoinStep::Completed)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
send_handshake_message(context, &self.invite, self.chat_id, step).await
|
||||
}
|
||||
|
||||
/// Returns whether we are waiting for a SecureJoin message from Alice, i.e. the protocol hasn't
|
||||
/// yet completed.
|
||||
pub(crate) fn in_progress(&self) -> bool {
|
||||
!matches!(
|
||||
self.next,
|
||||
SecureJoinStep::Terminated | SecureJoinStep::Completed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: 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);
|
||||
|
||||
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
|
||||
let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
//
|
||||
// `Secure-Join-Group` header is deprecated,
|
||||
// but old Delta Chat core requires that Alice receives it.
|
||||
//
|
||||
// Previous Delta Chat core also sent `Secure-Join-Group` header
|
||||
// in `vg-request` messages,
|
||||
// but it was not used on the receiver.
|
||||
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
|
||||
Request,
|
||||
/// vc-request-with-auth or vg-request-with-auth
|
||||
RequestWithAuth,
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub 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, "Completed state for next securejoin step");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for SecureJoinStep {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
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<Self> {
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
.unwrap();
|
||||
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
@@ -218,7 +218,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.gossiped_keys
|
||||
.insert(alice_addr.to_string(), alice_pubkey)
|
||||
.unwrap();
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &msg, contact_bob.id)
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
@@ -839,3 +839,110 @@ async fn test_shared_bobs_key() -> Result<()> {
|
||||
assert_eq!(bob_ids.len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests Bob joining two groups by scanning two QR codes
|
||||
/// from the same Alice at the same time.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parallel_securejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat1_id =
|
||||
chat::create_group_chat(alice, ProtectionStatus::Protected, "First chat").await?;
|
||||
let alice_chat2_id =
|
||||
chat::create_group_chat(alice, ProtectionStatus::Protected, "Second chat").await?;
|
||||
|
||||
let qr1 = get_securejoin_qr(alice, Some(alice_chat1_id)).await?;
|
||||
let qr2 = get_securejoin_qr(alice, Some(alice_chat2_id)).await?;
|
||||
|
||||
// Bob scans both QR codes.
|
||||
let bob_chat1_id = join_securejoin(bob, &qr1).await?;
|
||||
let sent_vg_request1 = bob.pop_sent_msg().await;
|
||||
|
||||
let bob_chat2_id = join_securejoin(bob, &qr2).await?;
|
||||
let sent_vg_request2 = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives two `vg-request` messages
|
||||
// and sends back two `vg-auth-required` messages.
|
||||
alice.recv_msg_trash(&sent_vg_request1).await;
|
||||
let sent_vg_auth_required1 = alice.pop_sent_msg().await;
|
||||
|
||||
alice.recv_msg_trash(&sent_vg_request2).await;
|
||||
let _sent_vg_auth_required2 = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob receives first `vg-auth-required` message.
|
||||
// Bob has two securejoin processes started,
|
||||
// so he should send two `vg-request-with-auth` messages.
|
||||
bob.recv_msg_trash(&sent_vg_auth_required1).await;
|
||||
|
||||
// Bob sends `vg-request-with-auth` messages.
|
||||
let sent_vg_request_with_auth2 = bob.pop_sent_msg().await;
|
||||
let sent_vg_request_with_auth1 = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice receives both `vg-request-with-auth` messages.
|
||||
alice.recv_msg_trash(&sent_vg_request_with_auth1).await;
|
||||
let sent_vg_member_added1 = alice.pop_sent_msg().await;
|
||||
let joined_chat_id1 = bob.recv_msg(&sent_vg_member_added1).await.chat_id;
|
||||
assert_eq!(joined_chat_id1, bob_chat1_id);
|
||||
|
||||
alice.recv_msg_trash(&sent_vg_request_with_auth2).await;
|
||||
let sent_vg_member_added2 = alice.pop_sent_msg().await;
|
||||
let joined_chat_id2 = bob.recv_msg(&sent_vg_member_added2).await.chat_id;
|
||||
assert_eq!(joined_chat_id2, bob_chat2_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests Bob scanning setup contact QR codes of Alice and Fiona
|
||||
/// concurrently.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parallel_setup_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
// Bob scans Alice's QR code,
|
||||
// but Alice is offline and takes a while to respond.
|
||||
let alice_qr = get_securejoin_qr(alice, None).await?;
|
||||
join_securejoin(bob, &alice_qr).await?;
|
||||
let sent_alice_vc_request = bob.pop_sent_msg().await;
|
||||
|
||||
// Bob scans Fiona's QR code while SecureJoin
|
||||
// process with Alice is not finished.
|
||||
let fiona_qr = get_securejoin_qr(fiona, None).await?;
|
||||
join_securejoin(bob, &fiona_qr).await?;
|
||||
let sent_fiona_vc_request = bob.pop_sent_msg().await;
|
||||
|
||||
fiona.recv_msg_trash(&sent_fiona_vc_request).await;
|
||||
let sent_fiona_vc_auth_required = fiona.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_trash(&sent_fiona_vc_auth_required).await;
|
||||
let sent_fiona_vc_request_with_auth = bob.pop_sent_msg().await;
|
||||
|
||||
fiona.recv_msg_trash(&sent_fiona_vc_request_with_auth).await;
|
||||
let sent_fiona_vc_contact_confirm = fiona.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap();
|
||||
assert_eq!(bob_fiona_contact.is_verified(bob).await.unwrap(), true);
|
||||
|
||||
// Alice gets online and previously started SecureJoin process finishes.
|
||||
alice.recv_msg_trash(&sent_alice_vc_request).await;
|
||||
let sent_alice_vc_auth_required = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_trash(&sent_alice_vc_auth_required).await;
|
||||
let sent_alice_vc_request_with_auth = bob.pop_sent_msg().await;
|
||||
|
||||
alice.recv_msg_trash(&sent_alice_vc_request_with_auth).await;
|
||||
let sent_alice_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await;
|
||||
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap();
|
||||
assert_eq!(bob_alice_contact.is_verified(bob).await.unwrap(), true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user