deltachat/
securejoin.rs

1//! Implementation of [SecureJoin protocols](https://securejoin.delta.chat/).
2
3use anyhow::{Context as _, Error, Result, bail, ensure};
4use deltachat_contact_tools::ContactAddress;
5use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
6
7use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
8use crate::chatlist_events;
9use crate::config::Config;
10use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
11use crate::contact::mark_contact_id_as_verified;
12use crate::contact::{Contact, ContactId, Origin};
13use crate::context::Context;
14use crate::e2ee::ensure_secret_key_exists;
15use crate::events::EventType;
16use crate::headerdef::HeaderDef;
17use crate::key::{DcKey, Fingerprint, load_self_public_key};
18use crate::log::{error, info, warn};
19use crate::logged_debug_assert;
20use crate::message::{Message, Viewtype};
21use crate::mimeparser::{MimeMessage, SystemMessage};
22use crate::param::Param;
23use crate::qr::check_qr;
24use crate::securejoin::bob::JoinerProgress;
25use crate::sync::Sync::*;
26use crate::token;
27
28mod bob;
29mod qrinvite;
30
31use qrinvite::QrInvite;
32
33use crate::token::Namespace;
34
35fn inviter_progress(
36    context: &Context,
37    contact_id: ContactId,
38    is_group: bool,
39    progress: usize,
40) -> Result<()> {
41    logged_debug_assert!(
42        context,
43        progress == 0 || progress == 1000,
44        "inviter_progress: contact {contact_id}, progress={progress}, but value is not 0 (error) or 1000 (success)."
45    );
46    let chat_type = if is_group {
47        Chattype::Group
48    } else {
49        Chattype::Single
50    };
51
52    context.emit_event(EventType::SecurejoinInviterProgress {
53        contact_id,
54        chat_type,
55        progress,
56    });
57
58    Ok(())
59}
60
61/// Generates a Secure Join QR code.
62///
63/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
64/// [`ChatId`] generates a join-group QR code for the given chat.
65pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
66    /*=======================================================
67    ====             Alice - the inviter side            ====
68    ====   Step 1 in "Setup verified contact" protocol   ====
69    =======================================================*/
70
71    ensure_secret_key_exists(context).await.ok();
72
73    let chat = match group {
74        Some(id) => {
75            let chat = Chat::load_from_db(context, id).await?;
76            ensure!(
77                chat.typ == Chattype::Group,
78                "Can't generate SecureJoin QR code for 1:1 chat {id}"
79            );
80            if chat.grpid.is_empty() {
81                let err = format!("Can't generate QR code, chat {id} is a email thread");
82                error!(context, "get_securejoin_qr: {}.", err);
83                bail!(err);
84            }
85            Some(chat)
86        }
87        None => None,
88    };
89    let grpid = chat.as_ref().map(|c| c.grpid.as_str());
90    let sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
91        .await?
92        .is_none();
93    // invitenumber will be used to allow starting the handshake,
94    // auth will be used to verify the fingerprint
95    let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
96    let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?;
97    let self_addr = context.get_primary_self_addr().await?;
98    let self_name = context
99        .get_config(Config::Displayname)
100        .await?
101        .unwrap_or_default();
102
103    let fingerprint = get_self_fingerprint(context).await?;
104
105    let self_addr_urlencoded =
106        utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
107    let self_name_urlencoded =
108        utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
109
110    let qr = if let Some(chat) = chat {
111        // parameters used: a=g=x=i=s=
112        let group_name = chat.get_name();
113        let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
114        if sync_token {
115            context
116                .sync_qr_code_tokens(Some(chat.grpid.as_str()))
117                .await?;
118            context.scheduler.interrupt_inbox().await;
119        }
120        format!(
121            "https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
122            fingerprint.hex(),
123            self_addr_urlencoded,
124            &group_name_urlencoded,
125            &chat.grpid,
126            &invitenumber,
127            &auth,
128        )
129    } else {
130        // parameters used: a=n=i=s=
131        if sync_token {
132            context.sync_qr_code_tokens(None).await?;
133            context.scheduler.interrupt_inbox().await;
134        }
135        format!(
136            "https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
137            fingerprint.hex(),
138            self_addr_urlencoded,
139            self_name_urlencoded,
140            &invitenumber,
141            &auth,
142        )
143    };
144
145    info!(context, "Generated QR code.");
146    Ok(qr)
147}
148
149async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
150    let key = load_self_public_key(context)
151        .await
152        .context("Failed to load key")?;
153    Ok(key.dc_fingerprint())
154}
155
156/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
157///
158/// This is the start of the process for the joiner.  See the module and ffi documentation
159/// for more details.
160///
161/// The function returns immediately and the handshake will run in background.
162pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
163    securejoin(context, qr).await.map_err(|err| {
164        warn!(context, "Fatal joiner error: {:#}", err);
165        // The user just scanned this QR code so has context on what failed.
166        error!(context, "QR process failed");
167        err
168    })
169}
170
171async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
172    /*========================================================
173    ====             Bob - the joiner's side             =====
174    ====   Step 2 in "Setup verified contact" protocol   =====
175    ========================================================*/
176
177    info!(context, "Requesting secure-join ...",);
178    let qr_scan = check_qr(context, qr).await?;
179
180    let invite = QrInvite::try_from(qr_scan)?;
181
182    bob::start_protocol(context, invite).await
183}
184
185/// Send handshake message from Alice's device.
186async fn send_alice_handshake_msg(
187    context: &Context,
188    contact_id: ContactId,
189    step: &str,
190) -> Result<()> {
191    let mut msg = Message {
192        viewtype: Viewtype::Text,
193        text: format!("Secure-Join: {step}"),
194        hidden: true,
195        ..Default::default()
196    };
197    msg.param.set_cmd(SystemMessage::SecurejoinMessage);
198    msg.param.set(Param::Arg, step);
199    msg.param.set_int(Param::GuaranteeE2ee, 1);
200    chat::send_msg(
201        context,
202        ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
203            .await?
204            .id,
205        &mut msg,
206    )
207    .await?;
208    Ok(())
209}
210
211/// Get an unblocked chat that can be used for info messages.
212async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
213    let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
214    Ok(chat_id_blocked.id)
215}
216
217/// Checks fingerprint and marks the contact as verified
218/// if fingerprint matches.
219async fn verify_sender_by_fingerprint(
220    context: &Context,
221    fingerprint: &Fingerprint,
222    contact_id: ContactId,
223) -> Result<bool> {
224    let contact = Contact::get_by_id(context, contact_id).await?;
225    let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
226    if is_verified {
227        mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
228    }
229    Ok(is_verified)
230}
231
232/// What to do with a Secure-Join handshake message after it was handled.
233///
234/// This status is returned to [`receive_imf_inner`] which will use it to decide what to do
235/// next with this incoming setup-contact/secure-join handshake message.
236///
237/// [`receive_imf_inner`]: crate::receive_imf::receive_imf_inner
238#[derive(Debug, PartialEq, Eq)]
239pub(crate) enum HandshakeMessage {
240    /// The message has been fully handled and should be removed/delete.
241    ///
242    /// This removes the message both locally and on the IMAP server.
243    Done,
244    /// The message should be ignored/hidden, but not removed/deleted.
245    ///
246    /// This leaves it on the IMAP server.  It means other devices on this account can
247    /// receive and potentially process this message as well.  This is useful for example
248    /// when the other device is running the protocol and has the relevant QR-code
249    /// information while this device does not have the joiner state.
250    Ignore,
251    /// The message should be further processed by incoming message handling.
252    ///
253    /// This may for example result in a group being created if it is a message which added
254    /// us to a group (a `vg-member-added` message).
255    Propagate,
256}
257
258/// Handle incoming secure-join handshake.
259///
260/// This function will update the securejoin state in the database as the protocol
261/// progresses.
262///
263/// A message which results in [`Err`] will be hidden from the user but not deleted, it may
264/// be a valid message for something else we are not aware off.  E.g. it could be part of a
265/// handshake performed by another DC app on the same account.
266///
267/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
268/// database; this is done by `receive_imf()` later on as needed.
269pub(crate) async fn handle_securejoin_handshake(
270    context: &Context,
271    mime_message: &mut MimeMessage,
272    contact_id: ContactId,
273) -> Result<HandshakeMessage> {
274    if contact_id.is_special() {
275        return Err(Error::msg("Can not be called with special contact ID"));
276    }
277    let step = mime_message
278        .get_header(HeaderDef::SecureJoin)
279        .context("Not a Secure-Join message")?;
280
281    info!(context, "Received secure-join message {step:?}.");
282
283    if !matches!(step, "vg-request" | "vc-request") {
284        let mut self_found = false;
285        let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
286        for (addr, key) in &mime_message.gossiped_keys {
287            if key.public_key.dc_fingerprint() == self_fingerprint
288                && context.is_self_addr(addr).await?
289            {
290                self_found = true;
291                break;
292            }
293        }
294        if !self_found {
295            // This message isn't intended for us. Possibly the peer doesn't own the key which the
296            // message is signed with but forwarded someone's message to us.
297            warn!(context, "Step {step}: No self addr+pubkey gossip found.");
298            return Ok(HandshakeMessage::Ignore);
299        }
300    }
301
302    match step {
303        "vg-request" | "vc-request" => {
304            /*=======================================================
305            ====             Alice - the inviter side            ====
306            ====   Step 3 in "Setup verified contact" protocol   ====
307            =======================================================*/
308
309            // this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
310            // it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
311            // send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
312            // verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
313            let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
314                Some(n) => n,
315                None => {
316                    warn!(context, "Secure-join denied (invitenumber missing)");
317                    return Ok(HandshakeMessage::Ignore);
318                }
319            };
320            if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
321                warn!(context, "Secure-join denied (bad invitenumber).");
322                return Ok(HandshakeMessage::Ignore);
323            }
324
325            let from_addr = ContactAddress::new(&mime_message.from.addr)?;
326            let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
327            let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
328                context,
329                "",
330                &from_addr,
331                autocrypt_fingerprint,
332                Origin::IncomingUnknownFrom,
333            )
334            .await?;
335
336            // Alice -> Bob
337            send_alice_handshake_msg(
338                context,
339                autocrypt_contact_id,
340                &format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
341            )
342            .await
343            .context("failed sending auth-required handshake message")?;
344            Ok(HandshakeMessage::Done)
345        }
346        "vg-auth-required" | "vc-auth-required" => {
347            /*========================================================
348            ====             Bob - the joiner's side             =====
349            ====   Step 4 in "Setup verified contact" protocol   =====
350            ========================================================*/
351            bob::handle_auth_required(context, mime_message).await
352        }
353        "vg-request-with-auth" | "vc-request-with-auth" => {
354            /*==========================================================
355            ====              Alice - the inviter side              ====
356            ====   Steps 5+6 in "Setup verified contact" protocol   ====
357            ====  Step 6 in "Out-of-band verified groups" protocol  ====
358            ==========================================================*/
359
360            // verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
361            let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
362                warn!(
363                    context,
364                    "Ignoring {step} message because fingerprint is not provided."
365                );
366                return Ok(HandshakeMessage::Ignore);
367            };
368            let fingerprint: Fingerprint = fp.parse()?;
369            if !encrypted_and_signed(context, mime_message, &fingerprint) {
370                warn!(
371                    context,
372                    "Ignoring {step} message because the message is not encrypted."
373                );
374                return Ok(HandshakeMessage::Ignore);
375            }
376            // verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
377            let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
378                warn!(
379                    context,
380                    "Ignoring {step} message because of missing auth code."
381                );
382                return Ok(HandshakeMessage::Ignore);
383            };
384            let Some(grpid) = token::auth_foreign_key(context, auth).await? else {
385                warn!(
386                    context,
387                    "Ignoring {step} message because of invalid auth code."
388                );
389                return Ok(HandshakeMessage::Ignore);
390            };
391            let group_chat_id = match grpid.as_str() {
392                "" => None,
393                id => {
394                    let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
395                        warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
396                        return Ok(HandshakeMessage::Ignore);
397                    };
398                    Some(chat_id)
399                }
400            };
401
402            if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
403                warn!(
404                    context,
405                    "Ignoring {step} message because of fingerprint mismatch."
406                );
407                return Ok(HandshakeMessage::Ignore);
408            }
409            info!(context, "Fingerprint verified via Auth code.",);
410            contact_id.regossip_keys(context).await?;
411            ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
412            // for setup-contact, make Alice's one-to-one chat with Bob visible
413            // (secure-join-information are shown in the group chat)
414            if grpid.is_empty() {
415                ChatId::create_for_contact(context, contact_id).await?;
416            }
417            context.emit_event(EventType::ContactsChanged(Some(contact_id)));
418            if let Some(group_chat_id) = group_chat_id {
419                // Join group.
420                secure_connection_established(
421                    context,
422                    contact_id,
423                    group_chat_id,
424                    mime_message.timestamp_sent,
425                )
426                .await?;
427                chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
428                    .await?;
429                let is_group = true;
430                inviter_progress(context, contact_id, is_group, 1000)?;
431                // IMAP-delete the message to avoid handling it by another device and adding the
432                // member twice. Another device will know the member's key from Autocrypt-Gossip.
433                Ok(HandshakeMessage::Done)
434            } else {
435                // Setup verified contact.
436                secure_connection_established(
437                    context,
438                    contact_id,
439                    info_chat_id(context, contact_id).await?,
440                    mime_message.timestamp_sent,
441                )
442                .await?;
443                send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
444                    .await
445                    .context("failed sending vc-contact-confirm message")?;
446
447                let is_group = false;
448                inviter_progress(context, contact_id, is_group, 1000)?;
449                Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
450            }
451        }
452        /*=======================================================
453        ====             Bob - the joiner's side             ====
454        ====   Step 7 in "Setup verified contact" protocol   ====
455        =======================================================*/
456        "vc-contact-confirm" => {
457            context.emit_event(EventType::SecurejoinJoinerProgress {
458                contact_id,
459                progress: JoinerProgress::Succeeded.to_usize(),
460            });
461            Ok(HandshakeMessage::Ignore)
462        }
463        "vg-member-added" => {
464            let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
465            else {
466                warn!(
467                    context,
468                    "vg-member-added without Chat-Group-Member-Added header."
469                );
470                return Ok(HandshakeMessage::Propagate);
471            };
472            if !context.is_self_addr(member_added).await? {
473                info!(
474                    context,
475                    "Member {member_added} added by unrelated SecureJoin process."
476                );
477                return Ok(HandshakeMessage::Propagate);
478            }
479
480            context.emit_event(EventType::SecurejoinJoinerProgress {
481                contact_id,
482                progress: JoinerProgress::Succeeded.to_usize(),
483            });
484            Ok(HandshakeMessage::Propagate)
485        }
486
487        "vg-member-added-received" | "vc-contact-confirm-received" => {
488            // Deprecated steps, delete them immediately.
489            Ok(HandshakeMessage::Done)
490        }
491        _ => {
492            warn!(context, "invalid step: {}", step);
493            Ok(HandshakeMessage::Ignore)
494        }
495    }
496}
497
498/// Observe self-sent Securejoin message.
499///
500/// In a multi-device-setup, there may be other devices that "see" the handshake messages.
501/// If we see self-sent messages encrypted+signed correctly with our key,
502/// we can make some conclusions of it.
503///
504/// If we see self-sent {vc,vg}-request-with-auth,
505/// we know that we are Bob (joiner-observer)
506/// that just marked peer (Alice) as verified
507/// either after receiving {vc,vg}-auth-required
508/// or immediately after scanning the QR-code
509/// if the key was already known.
510///
511/// If we see self-sent vc-contact-confirm or vg-member-added message,
512/// we know that we are Alice (inviter-observer)
513/// that just marked peer (Bob) as verified
514/// in response to correct vc-request-with-auth message.
515pub(crate) async fn observe_securejoin_on_other_device(
516    context: &Context,
517    mime_message: &MimeMessage,
518    contact_id: ContactId,
519) -> Result<HandshakeMessage> {
520    if contact_id.is_special() {
521        return Err(Error::msg("Can not be called with special contact ID"));
522    }
523    let step = mime_message
524        .get_header(HeaderDef::SecureJoin)
525        .context("Not a Secure-Join message")?;
526    info!(context, "Observing secure-join message {step:?}.");
527
528    if !matches!(
529        step,
530        "vg-request-with-auth" | "vc-request-with-auth" | "vg-member-added" | "vc-contact-confirm"
531    ) {
532        return Ok(HandshakeMessage::Ignore);
533    };
534
535    if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
536        warn!(
537            context,
538            "Observed SecureJoin message is not encrypted correctly."
539        );
540        return Ok(HandshakeMessage::Ignore);
541    }
542
543    let contact = Contact::get_by_id(context, contact_id).await?;
544    let addr = contact.get_addr().to_lowercase();
545
546    let Some(key) = mime_message.gossiped_keys.get(&addr) else {
547        warn!(context, "No gossip header for {addr} at step {step}.");
548        return Ok(HandshakeMessage::Ignore);
549    };
550
551    let Some(contact_fingerprint) = contact.fingerprint() else {
552        // Not a key-contact, should not happen.
553        warn!(context, "Contact does not have a fingerprint.");
554        return Ok(HandshakeMessage::Ignore);
555    };
556
557    if key.public_key.dc_fingerprint() != contact_fingerprint {
558        // Fingerprint does not match, ignore.
559        warn!(context, "Fingerprint does not match.");
560        return Ok(HandshakeMessage::Ignore);
561    }
562
563    mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
564
565    ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
566
567    if step == "vg-member-added" || step == "vc-contact-confirm" {
568        let is_group = mime_message
569            .get_header(HeaderDef::ChatGroupMemberAdded)
570            .is_some();
571        inviter_progress(context, contact_id, is_group, 1000)?;
572    }
573
574    if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
575        // This actually reflects what happens on the first device (which does the secure
576        // join) and causes a subsequent "vg-member-added" message to create an unblocked
577        // verified group.
578        ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
579    }
580
581    if step == "vg-member-added" {
582        Ok(HandshakeMessage::Propagate)
583    } else {
584        Ok(HandshakeMessage::Ignore)
585    }
586}
587
588async fn secure_connection_established(
589    context: &Context,
590    contact_id: ContactId,
591    chat_id: ChatId,
592    timestamp: i64,
593) -> Result<()> {
594    let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
595        .await?
596        .id;
597    private_chat_id
598        .set_protection(
599            context,
600            ProtectionStatus::Protected,
601            timestamp,
602            Some(contact_id),
603        )
604        .await?;
605    context.emit_event(EventType::ChatModified(chat_id));
606    chatlist_events::emit_chatlist_item_changed(context, chat_id);
607    Ok(())
608}
609
610/* ******************************************************************************
611 * Tools: Misc.
612 ******************************************************************************/
613
614fn encrypted_and_signed(
615    context: &Context,
616    mimeparser: &MimeMessage,
617    expected_fingerprint: &Fingerprint,
618) -> bool {
619    if !mimeparser.was_encrypted() {
620        warn!(context, "Message not encrypted.",);
621        false
622    } else if !mimeparser.signatures.contains(expected_fingerprint) {
623        warn!(
624            context,
625            "Message does not match expected fingerprint {}.", expected_fingerprint,
626        );
627        false
628    } else {
629        true
630    }
631}
632
633#[cfg(test)]
634mod securejoin_tests;