deltachat/
securejoin.rs

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