//! Bob's side of SecureJoin handling, the joiner-side. use anyhow::{Context as _, Result}; use super::HandshakeMessage; use super::qrinvite::QrInvite; use crate::chat::{self, ChatId, is_contact_in_chat}; use crate::chatlist_events; use crate::constants::{Blocked, Chattype}; use crate::contact::Origin; use crate::context::Context; use crate::events::EventType; use crate::key::self_fingerprint; use crate::log::LogExt; use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::{Param, Params}; use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint}; use crate::stock_str; use crate::sync::Sync::*; use crate::tools::{smeared_time, time}; /// Starts the securejoin protocol with the QR `invite`. /// /// 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 /// chat with Alice, for a SecureJoin QR this is the group chat. pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result { // A 1:1 chat is needed to send messages to Alice. When joining a group this chat is // hidden, if a user starts sending messages in it it will be unhidden in // receive_imf. let private_chat_id = private_chat_id(context, &invite).await?; ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?; context.emit_event(EventType::ContactsChanged(None)); let has_key = context .sql .exists( "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?", (invite.fingerprint().hex(),), ) .await?; // Now start the protocol and initialise the state. { // `joining_chat_id` is `Some` if group chat // already exists and we are in the chat. let joining_chat_id = match invite { QrInvite::Group { ref grpid, .. } | QrInvite::Broadcast { ref grpid, .. } => { if let Some((joining_chat_id, _blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? { if is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? { Some(joining_chat_id) } else { None } } else { None } } QrInvite::Contact { .. } => None, }; if let Some(joining_chat_id) = joining_chat_id { // If QR code is a group invite // and we are already in the chat, // nothing needs to be done. // Even if Alice is not verified, we don't send anything. context.emit_event(EventType::SecurejoinJoinerProgress { contact_id: invite.contact_id(), progress: JoinerProgress::Succeeded.into_u16(), }); return Ok(joining_chat_id); } else if has_key && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()) .await? { // The scanned fingerprint matches Alice's key, we can proceed to step 4b. info!(context, "Taking securejoin protocol shortcut"); send_handshake_message( context, &invite, private_chat_id, BobHandshakeMsg::RequestWithAuth, ) .await?; context.emit_event(EventType::SecurejoinJoinerProgress { contact_id: invite.contact_id(), progress: JoinerProgress::RequestWithAuthSent.into_u16(), }); } else { send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request) .await?; insert_new_db_entry(context, invite.clone(), private_chat_id).await?; } } match invite { QrInvite::Group { .. } => { let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?; let msg = stock_str::secure_join_started(context, invite.contact_id()).await; chat::add_info_msg(context, joining_chat_id, &msg).await?; Ok(joining_chat_id) } QrInvite::Broadcast { .. } => { let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?; // We created the broadcast channel already, now we need to add Alice to it. if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? { chat::add_to_chat_contacts_table( context, time(), joining_chat_id, &[invite.contact_id()], ) .await?; } // If we were not in the broadcast channel before, show a 'please wait' info message. if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? { let msg = stock_str::secure_join_broadcast_started(context, invite.contact_id()).await; chat::add_info_msg(context, joining_chat_id, &msg).await?; } Ok(joining_chat_id) } QrInvite::Contact { .. } => { // For setup-contact the BobState already ensured the 1:1 chat exists because it is // used to send the handshake messages. if !has_key { chat::add_info_msg_with_cmd( context, private_chat_id, &stock_str::securejoin_wait(context).await, SystemMessage::SecurejoinWait, None, time(), None, None, None, ) .await?; } Ok(private_chat_id) } } } /// 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 { // The `chat_id` isn't actually needed anymore, // but we still save it; // can be removed as a future improvement. context .sql .insert( "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);", (invite, 0, chat_id), ) .await } async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> { if let Some((msg_id, param)) = context .sql .query_row_optional( " SELECT id, param FROM msgs WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0) AND chat_id=? AND hidden=0 LIMIT 1 ", (chat_id, chat_id), |row| { let id: MsgId = row.get(0)?; let param: String = row.get(1)?; let param: Params = param.parse().unwrap_or_default(); Ok((id, param)) }, ) .await? && param.get_cmd() == SystemMessage::SecurejoinWait { let on_server = false; msg_id.trash(context, on_server).await?; context.emit_event(EventType::MsgDeleted { chat_id, msg_id }); context.emit_msgs_changed_without_msg_id(chat_id); chatlist_events::emit_chatlist_item_changed(context, chat_id); context.emit_msgs_changed_without_ids(); chatlist_events::emit_chatlist_changed(context); } Ok(()) } /// Handles `vc-auth-required` and `vg-auth-required` handshake messages. /// /// # Bob - the joiner's side /// ## Step 4 in the "Setup Contact protocol" pub(super) async fn handle_auth_required( context: &Context, message: &MimeMessage, ) -> Result { // Load all Bob states that expect `vc-auth-required` or `vg-auth-required`. let bob_states = context .sql .query_map_vec("SELECT id, invite FROM bobstate", (), |row| { let row_id: i64 = row.get(0)?; let invite: QrInvite = row.get(1)?; Ok((row_id, invite)) }) .await?; info!( context, "Bob Step 4 - handling {{vc,vg}}-auth-required message." ); let mut auth_sent = false; for (bobstate_row_id, invite) in bob_states { if !encrypted_and_signed(context, message, invite.fingerprint()) { continue; } if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await? { continue; } info!(context, "Fingerprint verified.",); let chat_id = private_chat_id(context, &invite).await?; delete_securejoin_wait_msg(context, chat_id) .await .context("delete_securejoin_wait_msg") .log_err(context) .ok(); 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::Broadcast { .. } => {} QrInvite::Group { .. } => { // The message reads "Alice replied, waiting to be added to the group…", // so only show it when joining a group and not for a 1:1 chat or broadcast channel. let contact_id = invite.contact_id(); let msg = stock_str::secure_join_replies(context, contact_id).await; let chat_id = joining_chat_id(context, &invite, chat_id).await?; chat::add_info_msg(context, chat_id, &msg).await?; } } context.emit_event(EventType::SecurejoinJoinerProgress { contact_id: invite.contact_id(), progress: JoinerProgress::RequestWithAuthSent.into_u16(), }); 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 = self_fingerprint(context).await?; msg.param.set(Param::Arg3, bob_fp); // 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 { 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", QrInvite::Broadcast { .. } => "vg-request", }, Self::RequestWithAuth => match invite { QrInvite::Contact { .. } => "vc-request-with-auth", QrInvite::Group { .. } => "vg-request-with-auth", QrInvite::Broadcast { .. } => "vg-request-with-auth", }, } } } /// Returns the 1:1 chat with the inviter. /// /// This is the chat in which securejoin messages are sent. /// The 1:1 chat will be created if it does not yet exist. async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result { let hidden = match invite { QrInvite::Contact { .. } => Blocked::Not, QrInvite::Group { .. } => Blocked::Yes, QrInvite::Broadcast { .. } => Blocked::Yes, }; ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden) .await .with_context(|| format!("can't create chat for contact {}", invite.contact_id())) } /// 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 { match invite { QrInvite::Contact { .. } => Ok(alice_chat_id), QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => { let chattype = if matches!(invite, QrInvite::Group { .. }) { Chattype::Group } else { Chattype::InBroadcast }; let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? { Some((chat_id, _blocked)) => { chat_id.unblock_ex(context, Nosync).await?; chat_id } None => { ChatId::create_multiuser_record( context, chattype, grpid, name, Blocked::Not, None, smeared_time(context), ) .await? } }; Ok(chat_id) } } } /// Progress updates for [`EventType::SecurejoinJoinerProgress`]. /// /// This has an `From for usize` impl yielding numbers between 0 and a 1000 /// which can be shown as a progress bar. pub(crate) enum JoinerProgress { /// vg-vc-request-with-auth sent. /// /// Typically shows as "alice@addr verified, introducing myself." RequestWithAuthSent, /// Completed securejoin. Succeeded, } impl JoinerProgress { pub(crate) fn into_u16(self) -> u16 { match self { JoinerProgress::RequestWithAuthSent => 400, JoinerProgress::Succeeded => 1000, } } }