deltachat/securejoin/
bob.rs

1//! Bob's side of SecureJoin handling, the joiner-side.
2
3use anyhow::{Context as _, Result};
4
5use super::HandshakeMessage;
6use super::qrinvite::QrInvite;
7use crate::chat::{self, ChatId, is_contact_in_chat};
8use crate::constants::{Blocked, Chattype};
9use crate::contact::{Contact, Origin};
10use crate::context::Context;
11use crate::events::EventType;
12use crate::key::self_fingerprint;
13use crate::log::LogExt;
14use crate::message::{self, Message, MsgId, Viewtype};
15use crate::mimeparser::{MimeMessage, SystemMessage};
16use crate::param::{Param, Params};
17use crate::securejoin::{
18    ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
19};
20use crate::stock_str;
21use crate::sync::Sync::*;
22use crate::tools::{create_outgoing_rfc724_mid, smeared_time, time};
23use crate::{chatlist_events, mimefactory};
24
25/// Starts the securejoin protocol with the QR `invite`.
26///
27/// This will try to start the securejoin protocol for the given QR `invite`.
28///
29/// If Bob already has Alice's key, he sends `AUTH` token
30/// and forgets about the invite.
31/// If Bob does not yet have Alice's key, he sends `vc-request`
32/// or `vg-request` message and stores a row in the `bobstate` table
33/// so he can check Alice's key against the fingerprint
34/// and send `AUTH` token later.
35///
36/// This function takes care of handling multiple concurrent joins and handling errors while
37/// starting the protocol.
38///
39/// # Bob - the joiner's side
40/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0
41///
42/// # Returns
43///
44/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
45/// chat with Alice, for a SecureJoin QR this is the group chat.
46pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
47    // A 1:1 chat is needed to send messages to Alice.  When joining a group this chat is
48    // hidden, if a user starts sending messages in it it will be unhidden in
49    // receive_imf.
50    let private_chat_id = private_chat_id(context, &invite).await?;
51
52    match invite {
53        QrInvite::Group { .. } | QrInvite::Contact { .. } => {
54            ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined)
55                .await?;
56            context.emit_event(EventType::ContactsChanged(None));
57        }
58        QrInvite::Broadcast { .. } => {}
59    }
60
61    let has_key = context
62        .sql
63        .exists(
64            "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
65            (invite.fingerprint().hex(),),
66        )
67        .await?;
68
69    // Now start the protocol and initialise the state.
70    {
71        // `joining_chat_id` is `Some` if group chat
72        // already exists and we are in the chat.
73        let joining_chat_id = match invite {
74            QrInvite::Group { ref grpid, .. } | QrInvite::Broadcast { ref grpid, .. } => {
75                if let Some((joining_chat_id, _blocked)) =
76                    chat::get_chat_id_by_grpid(context, grpid).await?
77                {
78                    if is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
79                        Some(joining_chat_id)
80                    } else {
81                        None
82                    }
83                } else {
84                    None
85                }
86            }
87            QrInvite::Contact { .. } => None,
88        };
89
90        if let Some(joining_chat_id) = joining_chat_id {
91            // If QR code is a group invite
92            // and we are already in the chat,
93            // nothing needs to be done.
94            // Even if Alice is not verified, we don't send anything.
95            context.emit_event(EventType::SecurejoinJoinerProgress {
96                contact_id: invite.contact_id(),
97                progress: JoinerProgress::Succeeded.into_u16(),
98            });
99            return Ok(joining_chat_id);
100        } else if has_key
101            && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
102                .await?
103        {
104            // The scanned fingerprint matches Alice's key, we can proceed to step 4b.
105            info!(context, "Taking securejoin protocol shortcut");
106            send_handshake_message(
107                context,
108                &invite,
109                private_chat_id,
110                BobHandshakeMsg::RequestWithAuth,
111            )
112            .await?;
113
114            context.emit_event(EventType::SecurejoinJoinerProgress {
115                contact_id: invite.contact_id(),
116                progress: JoinerProgress::RequestWithAuthSent.into_u16(),
117            });
118        } else {
119            send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
120                .await?;
121
122            insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
123        }
124    }
125
126    match invite {
127        QrInvite::Group { .. } => {
128            let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
129            let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
130            chat::add_info_msg(context, joining_chat_id, &msg).await?;
131            Ok(joining_chat_id)
132        }
133        QrInvite::Broadcast { .. } => {
134            let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
135            // We created the broadcast channel already, now we need to add Alice to it.
136            if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
137                chat::add_to_chat_contacts_table(
138                    context,
139                    time(),
140                    joining_chat_id,
141                    &[invite.contact_id()],
142                )
143                .await?;
144            }
145
146            // If we were not in the broadcast channel before, show a 'please wait' info message.
147            if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
148                let msg =
149                    stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
150                chat::add_info_msg(context, joining_chat_id, &msg).await?;
151            }
152            Ok(joining_chat_id)
153        }
154        QrInvite::Contact { .. } => {
155            // For setup-contact the BobState already ensured the 1:1 chat exists because it is
156            // used to send the handshake messages.
157            if !has_key {
158                chat::add_info_msg_with_cmd(
159                    context,
160                    private_chat_id,
161                    &stock_str::securejoin_wait(context).await,
162                    SystemMessage::SecurejoinWait,
163                    None,
164                    time(),
165                    None,
166                    None,
167                    None,
168                )
169                .await?;
170            }
171            Ok(private_chat_id)
172        }
173    }
174}
175
176/// Inserts a new entry in the bobstate table.
177///
178/// Returns the ID of the newly inserted entry.
179async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
180    // The `chat_id` isn't actually needed anymore,
181    // but we still save it;
182    // can be removed as a future improvement.
183    context
184        .sql
185        .insert(
186            "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
187            (invite, 0, chat_id),
188        )
189        .await
190}
191
192async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
193    if let Some((msg_id, param)) = context
194        .sql
195        .query_row_optional(
196            "
197SELECT id, param FROM msgs
198WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
199    AND chat_id=? AND hidden=0
200LIMIT 1
201            ",
202            (chat_id, chat_id),
203            |row| {
204                let id: MsgId = row.get(0)?;
205                let param: String = row.get(1)?;
206                let param: Params = param.parse().unwrap_or_default();
207                Ok((id, param))
208            },
209        )
210        .await?
211        && param.get_cmd() == SystemMessage::SecurejoinWait
212    {
213        let on_server = false;
214        msg_id.trash(context, on_server).await?;
215        context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
216        context.emit_msgs_changed_without_msg_id(chat_id);
217        chatlist_events::emit_chatlist_item_changed(context, chat_id);
218        context.emit_msgs_changed_without_ids();
219        chatlist_events::emit_chatlist_changed(context);
220    }
221    Ok(())
222}
223
224/// Handles `vc-auth-required`, `vg-auth-required`, and `vc-pubkey` handshake messages.
225///
226/// # Bob - the joiner's side
227/// ## Step 4 in the "Setup Contact protocol"
228pub(super) async fn handle_auth_required_or_pubkey(
229    context: &Context,
230    message: &MimeMessage,
231) -> Result<HandshakeMessage> {
232    // Load all Bob states that expect `vc-auth-required` or `vg-auth-required`.
233    let bob_states = context
234        .sql
235        .query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
236            let row_id: i64 = row.get(0)?;
237            let invite: QrInvite = row.get(1)?;
238            Ok((row_id, invite))
239        })
240        .await?;
241
242    info!(
243        context,
244        "Bob Step 4 - handling {{vc,vg}}-auth-required message."
245    );
246
247    let mut auth_sent = false;
248    for (bobstate_row_id, invite) in bob_states {
249        if !encrypted_and_signed(context, message, invite.fingerprint()) {
250            continue;
251        }
252
253        if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
254        {
255            continue;
256        }
257
258        info!(context, "Fingerprint verified.",);
259        let chat_id = private_chat_id(context, &invite).await?;
260        delete_securejoin_wait_msg(context, chat_id)
261            .await
262            .context("delete_securejoin_wait_msg")
263            .log_err(context)
264            .ok();
265        send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
266        context
267            .sql
268            .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
269            .await?;
270
271        match invite {
272            QrInvite::Contact { .. } | QrInvite::Broadcast { .. } => {}
273            QrInvite::Group { .. } => {
274                // The message reads "Alice replied, waiting to be added to the group…",
275                // so only show it when joining a group and not for a 1:1 chat or broadcast channel.
276                let contact_id = invite.contact_id();
277                let msg = stock_str::secure_join_replies(context, contact_id).await;
278                let chat_id = joining_chat_id(context, &invite, chat_id).await?;
279                chat::add_info_msg(context, chat_id, &msg).await?;
280            }
281        }
282
283        context.emit_event(EventType::SecurejoinJoinerProgress {
284            contact_id: invite.contact_id(),
285            progress: JoinerProgress::RequestWithAuthSent.into_u16(),
286        });
287
288        auth_sent = true;
289    }
290
291    if auth_sent {
292        // Delete the message from IMAP server.
293        Ok(HandshakeMessage::Done)
294    } else {
295        // We have not found any corresponding AUTH codes,
296        // maybe another Bob device has scanned the QR code.
297        // Leave the message on IMAP server and let the other device
298        // process it.
299        Ok(HandshakeMessage::Ignore)
300    }
301}
302
303/// Sends the requested handshake message to Alice.
304pub(crate) async fn send_handshake_message(
305    context: &Context,
306    invite: &QrInvite,
307    chat_id: ChatId,
308    step: BobHandshakeMsg,
309) -> Result<()> {
310    if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
311        // Send a minimal symmetrically-encrypted vc-request-pubkey message
312        let rfc724_mid = create_outgoing_rfc724_mid();
313        let contact = Contact::get_by_id(context, invite.contact_id()).await?;
314        let recipient = contact.get_addr();
315        let attach_self_pubkey = false;
316        let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
317            context,
318            "vc-request-pubkey",
319            &rfc724_mid,
320            attach_self_pubkey,
321            invite.authcode(),
322        )
323        .await?;
324
325        let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
326        insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
327        context.scheduler.interrupt_smtp().await;
328    } else {
329        let mut msg = Message {
330            viewtype: Viewtype::Text,
331            text: step.body_text(invite),
332            hidden: true,
333            ..Default::default()
334        };
335
336        msg.param.set_cmd(SystemMessage::SecurejoinMessage);
337
338        // Sends the step in Secure-Join header.
339        msg.param.set(Param::Arg, step.securejoin_header(invite));
340
341        match step {
342            BobHandshakeMsg::Request => {
343                // Sends the Secure-Join-Invitenumber header in mimefactory.rs.
344                msg.param.set(Param::Arg2, invite.invitenumber());
345                msg.force_plaintext();
346            }
347            BobHandshakeMsg::RequestWithAuth => {
348                // Sends the Secure-Join-Auth header in mimefactory.rs.
349                msg.param.set(Param::Arg2, invite.authcode());
350                msg.param.set_int(Param::GuaranteeE2ee, 1);
351
352                // Sends our own fingerprint in the Secure-Join-Fingerprint header.
353                let bob_fp = self_fingerprint(context).await?;
354                msg.param.set(Param::Arg3, bob_fp);
355
356                // Sends the grpid in the Secure-Join-Group header.
357                //
358                // `Secure-Join-Group` header is deprecated,
359                // but old Delta Chat core requires that Alice receives it.
360                //
361                // Previous Delta Chat core also sent `Secure-Join-Group` header
362                // in `vg-request` messages,
363                // but it was not used on the receiver.
364                if let QrInvite::Group { grpid, .. } = invite {
365                    msg.param.set(Param::Arg4, grpid);
366                }
367            }
368        };
369
370        chat::send_msg(context, chat_id, &mut msg).await?;
371    }
372    Ok(())
373}
374
375/// Identifies the SecureJoin handshake messages Bob can send.
376pub(crate) enum BobHandshakeMsg {
377    /// vc-request or vg-request
378    Request,
379    /// vc-request-with-auth or vg-request-with-auth
380    RequestWithAuth,
381}
382
383impl BobHandshakeMsg {
384    /// Returns the text to send in the body of the handshake message.
385    ///
386    /// This text has no significance to the protocol, but would be visible if users see
387    /// this email message directly, e.g. when accessing their email without using
388    /// DeltaChat.
389    fn body_text(&self, invite: &QrInvite) -> String {
390        format!("Secure-Join: {}", self.securejoin_header(invite))
391    }
392
393    /// Returns the `Secure-Join` header value.
394    ///
395    /// This identifies the step this message is sending information about.  Most protocol
396    /// steps include additional information into other headers, see
397    /// [`send_handshake_message`] for these.
398    fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
399        match self {
400            Self::Request => match invite {
401                QrInvite::Contact { .. } => "vc-request",
402                QrInvite::Group { .. } => "vg-request",
403                QrInvite::Broadcast { .. } => "vg-request",
404            },
405            Self::RequestWithAuth => match invite {
406                QrInvite::Contact { .. } => "vc-request-with-auth",
407                QrInvite::Group { .. } => "vg-request-with-auth",
408                QrInvite::Broadcast { .. } => "vg-request-with-auth",
409            },
410        }
411    }
412}
413
414/// Returns the 1:1 chat with the inviter.
415///
416/// This is the chat in which securejoin messages are sent.
417/// The 1:1 chat will be created if it does not yet exist.
418async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
419    let hidden = match invite {
420        QrInvite::Contact { .. } => Blocked::Not,
421        QrInvite::Group { .. } => Blocked::Yes,
422        QrInvite::Broadcast { .. } => Blocked::Yes,
423    };
424
425    ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
426        .await
427        .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
428}
429
430/// Returns the [`ChatId`] of the chat being joined.
431///
432/// This is the chat in which you want to notify the user as well.
433///
434/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
435/// contact this is the [`ChatId`] of the 1:1 chat.
436/// The group chat will be created if it does not yet exist.
437async fn joining_chat_id(
438    context: &Context,
439    invite: &QrInvite,
440    alice_chat_id: ChatId,
441) -> Result<ChatId> {
442    match invite {
443        QrInvite::Contact { .. } => Ok(alice_chat_id),
444        QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
445            let chattype = if matches!(invite, QrInvite::Group { .. }) {
446                Chattype::Group
447            } else {
448                Chattype::InBroadcast
449            };
450
451            let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
452                Some((chat_id, _blocked)) => {
453                    chat_id.unblock_ex(context, Nosync).await?;
454                    chat_id
455                }
456                None => {
457                    ChatId::create_multiuser_record(
458                        context,
459                        chattype,
460                        grpid,
461                        name,
462                        Blocked::Not,
463                        None,
464                        smeared_time(context),
465                    )
466                    .await?
467                }
468            };
469            Ok(chat_id)
470        }
471    }
472}
473
474/// Progress updates for [`EventType::SecurejoinJoinerProgress`].
475///
476/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
477/// which can be shown as a progress bar.
478pub(crate) enum JoinerProgress {
479    /// vg-vc-request-with-auth sent.
480    ///
481    /// Typically shows as "alice@addr verified, introducing myself."
482    RequestWithAuthSent,
483    /// Completed securejoin.
484    Succeeded,
485}
486
487impl JoinerProgress {
488    pub(crate) fn into_u16(self) -> u16 {
489        match self {
490            JoinerProgress::RequestWithAuthSent => 400,
491            JoinerProgress::Succeeded => 1000,
492        }
493    }
494}