Skip to main content

deltachat/securejoin/
bob.rs

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