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