deltachat/
securejoin.rs

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