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