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),
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 alice_fp = invite.fingerprint().hex();
316        let auth = invite.authcode();
317        let shared_secret = format!("securejoin/{alice_fp}/{auth}");
318        let attach_self_pubkey = false;
319        let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
320            context,
321            "vc-request-pubkey",
322            &rfc724_mid,
323            attach_self_pubkey,
324            auth,
325            &shared_secret,
326        )
327        .await?;
328
329        let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
330        insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
331        context.scheduler.interrupt_smtp().await;
332    } else {
333        let mut msg = Message {
334            viewtype: Viewtype::Text,
335            text: step.body_text(invite),
336            hidden: true,
337            ..Default::default()
338        };
339
340        msg.param.set_cmd(SystemMessage::SecurejoinMessage);
341
342        // Sends the step in Secure-Join header.
343        msg.param.set(Param::Arg, step.securejoin_header(invite));
344
345        match step {
346            BobHandshakeMsg::Request => {
347                // Sends the Secure-Join-Invitenumber header in mimefactory.rs.
348                msg.param.set(Param::Arg2, invite.invitenumber());
349                msg.force_plaintext();
350            }
351            BobHandshakeMsg::RequestWithAuth => {
352                // Sends the Secure-Join-Auth header in mimefactory.rs.
353                msg.param.set(Param::Arg2, invite.authcode());
354                msg.param.set_int(Param::GuaranteeE2ee, 1);
355
356                // Sends our own fingerprint in the Secure-Join-Fingerprint header.
357                let bob_fp = self_fingerprint(context).await?;
358                msg.param.set(Param::Arg3, bob_fp);
359
360                // Sends the grpid in the Secure-Join-Group header.
361                //
362                // `Secure-Join-Group` header is deprecated,
363                // but old Delta Chat core requires that Alice receives it.
364                //
365                // Previous Delta Chat core also sent `Secure-Join-Group` header
366                // in `vg-request` messages,
367                // but it was not used on the receiver.
368                if let QrInvite::Group { grpid, .. } = invite {
369                    msg.param.set(Param::Arg4, grpid);
370                }
371            }
372        };
373
374        chat::send_msg(context, chat_id, &mut msg).await?;
375    }
376    Ok(())
377}
378
379/// Identifies the SecureJoin handshake messages Bob can send.
380pub(crate) enum BobHandshakeMsg {
381    /// vc-request or vg-request
382    Request,
383    /// vc-request-with-auth or vg-request-with-auth
384    RequestWithAuth,
385}
386
387impl BobHandshakeMsg {
388    /// Returns the text to send in the body of the handshake message.
389    ///
390    /// This text has no significance to the protocol, but would be visible if users see
391    /// this email message directly, e.g. when accessing their email without using
392    /// DeltaChat.
393    fn body_text(&self, invite: &QrInvite) -> String {
394        format!("Secure-Join: {}", self.securejoin_header(invite))
395    }
396
397    /// Returns the `Secure-Join` header value.
398    ///
399    /// This identifies the step this message is sending information about.  Most protocol
400    /// steps include additional information into other headers, see
401    /// [`send_handshake_message`] for these.
402    fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
403        match self {
404            Self::Request => match invite {
405                QrInvite::Contact { .. } => "vc-request",
406                QrInvite::Group { .. } => "vg-request",
407                QrInvite::Broadcast { .. } => "vg-request",
408            },
409            Self::RequestWithAuth => match invite {
410                QrInvite::Contact { .. } => "vc-request-with-auth",
411                QrInvite::Group { .. } => "vg-request-with-auth",
412                QrInvite::Broadcast { .. } => "vg-request-with-auth",
413            },
414        }
415    }
416}
417
418/// Returns the 1:1 chat with the inviter.
419///
420/// This is the chat in which securejoin messages are sent.
421/// The 1:1 chat will be created if it does not yet exist.
422async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
423    let hidden = match invite {
424        QrInvite::Contact { .. } => Blocked::Not,
425        QrInvite::Group { .. } => Blocked::Yes,
426        QrInvite::Broadcast { .. } => Blocked::Yes,
427    };
428
429    ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
430        .await
431        .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
432}
433
434/// Returns the [`ChatId`] of the chat being joined.
435///
436/// This is the chat in which you want to notify the user as well.
437///
438/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
439/// contact this is the [`ChatId`] of the 1:1 chat.
440/// The group chat will be created if it does not yet exist.
441async fn joining_chat_id(
442    context: &Context,
443    invite: &QrInvite,
444    alice_chat_id: ChatId,
445) -> Result<ChatId> {
446    match invite {
447        QrInvite::Contact { .. } => Ok(alice_chat_id),
448        QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
449            let chattype = if matches!(invite, QrInvite::Group { .. }) {
450                Chattype::Group
451            } else {
452                Chattype::InBroadcast
453            };
454
455            let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
456                Some((chat_id, _blocked)) => {
457                    chat_id.unblock_ex(context, Nosync).await?;
458                    chat_id
459                }
460                None => {
461                    ChatId::create_multiuser_record(
462                        context,
463                        chattype,
464                        grpid,
465                        name,
466                        Blocked::Not,
467                        None,
468                        smeared_time(context),
469                    )
470                    .await?
471                }
472            };
473            Ok(chat_id)
474        }
475    }
476}
477
478/// Progress updates for [`EventType::SecurejoinJoinerProgress`].
479///
480/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
481/// which can be shown as a progress bar.
482pub(crate) enum JoinerProgress {
483    /// vg-vc-request-with-auth sent.
484    ///
485    /// Typically shows as "alice@addr verified, introducing myself."
486    RequestWithAuthSent,
487    /// Completed securejoin.
488    Succeeded,
489}
490
491impl JoinerProgress {
492    pub(crate) fn into_u16(self) -> u16 {
493        match self {
494            JoinerProgress::RequestWithAuthSent => 400,
495            JoinerProgress::Succeeded => 1000,
496        }
497    }
498}