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