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::{AsciiSet, utf8_percent_encode};
6
7use crate::chat::{
8    self, Chat, ChatId, ChatIdBlocked, add_info_msg, get_chat_id_by_grpid, load_broadcast_secret,
9};
10use crate::config::Config;
11use crate::constants::{
12    BROADCAST_INCOMPATIBILITY_MSG, Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT,
13};
14use crate::contact::mark_contact_id_as_verified;
15use crate::contact::{Contact, ContactId, Origin};
16use crate::context::Context;
17use crate::e2ee::ensure_secret_key_exists;
18use crate::events::EventType;
19use crate::headerdef::HeaderDef;
20use crate::key::{DcKey, Fingerprint, load_self_public_key, self_fingerprint};
21use crate::log::LogExt as _;
22use crate::log::warn;
23use crate::message::{self, Message, MsgId, Viewtype};
24use crate::mimeparser::{MimeMessage, SystemMessage};
25use crate::param::Param;
26use crate::qr::check_qr;
27use crate::securejoin::bob::JoinerProgress;
28use crate::sync::Sync::*;
29use crate::tools::{create_id, create_outgoing_rfc724_mid, time};
30use crate::{SecurejoinSource, mimefactory, stats};
31use crate::{SecurejoinUiPath, token};
32
33mod bob;
34mod qrinvite;
35
36pub(crate) use qrinvite::QrInvite;
37
38use crate::token::Namespace;
39
40/// Only new QR codes cause a verification on Alice's side.
41/// When a QR code is too old, it is assumed that there was no direct QR scan,
42/// and that the QR code was potentially published on a website,
43/// so, Alice doesn't mark Bob as verified.
44// TODO For backwards compatibility reasons, this is still using a rather large value.
45// Set this to a lower value (e.g. 10 minutes)
46// when Delta Chat v2.22.0 is sufficiently rolled out
47const VERIFICATION_TIMEOUT_SECONDS: i64 = 7 * 24 * 3600;
48
49const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
50
51fn inviter_progress(
52    context: &Context,
53    contact_id: ContactId,
54    chat_id: ChatId,
55    chat_type: Chattype,
56) -> Result<()> {
57    // No other values are used.
58    let progress = 1000;
59    context.emit_event(EventType::SecurejoinInviterProgress {
60        contact_id,
61        chat_id,
62        chat_type,
63        progress,
64    });
65
66    Ok(())
67}
68
69/// Shorten name to max. `length` characters.
70/// This is to not make QR codes or invite links arbitrary long.
71fn shorten_name(name: &str, length: usize) -> String {
72    if name.chars().count() > length {
73        // We use _ rather than ... to avoid dots at the end of the URL, which would confuse linkifiers
74        format!(
75            "{}_",
76            &name
77                .chars()
78                .take(length.saturating_sub(1))
79                .collect::<String>()
80        )
81    } else {
82        name.to_string()
83    }
84}
85
86/// Generates a Secure Join QR code.
87///
88/// With `chat` set to `None` this generates a setup-contact QR code, with `chat` set to a
89/// [`ChatId`] generates a join-group/join-broadcast-channel QR code for the given chat.
90pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
91    /*=======================================================
92    ====             Alice - the inviter side            ====
93    ====   Step 1 in "Setup verified contact" protocol   ====
94    =======================================================*/
95
96    ensure_secret_key_exists(context).await.ok();
97
98    let chat = match chat {
99        Some(id) => {
100            let chat = Chat::load_from_db(context, id).await?;
101            ensure!(
102                chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
103                "Can't generate SecureJoin QR code for chat {id} of type {}",
104                chat.typ
105            );
106            if chat.grpid.is_empty() {
107                let err = format!("Can't generate QR code, chat {id} is a email thread");
108                error!(context, "get_securejoin_qr: {}.", err);
109                bail!(err);
110            }
111            if chat.typ == Chattype::OutBroadcast {
112                // If the user created the broadcast before updating Delta Chat,
113                // then the secret will be missing, and the user needs to recreate the broadcast:
114                if load_broadcast_secret(context, chat.id).await?.is_none() {
115                    error!(
116                        context,
117                        "Not creating securejoin QR for old broadcast {}, see chat for more info.",
118                        chat.id,
119                    );
120                    let text = BROADCAST_INCOMPATIBILITY_MSG;
121                    add_info_msg(context, chat.id, text).await?;
122                    bail!(text.to_string());
123                }
124            }
125            Some(chat)
126        }
127        None => None,
128    };
129    let grpid = chat.as_ref().map(|c| c.grpid.as_str());
130    // Invite number is used to request the inviter key.
131    let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
132
133    // Auth token is used to verify the key-contact
134    // if the token is not old
135    // and add the contact to the group
136    // if there is an associated group ID.
137    //
138    // We always generate a new auth token
139    // because auth tokens "expire"
140    // and can only be used to join groups
141    // without verification afterwards.
142    let auth = create_id();
143    token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
144
145    let fingerprint = get_self_fingerprint(context).await?.hex();
146
147    let self_addr = context.get_primary_self_addr().await?;
148    let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
149
150    let self_name = context
151        .get_config(Config::Displayname)
152        .await?
153        .unwrap_or_default();
154
155    let qr = if let Some(chat) = chat {
156        context
157            .sync_qr_code_tokens(Some(chat.grpid.as_str()))
158            .await?;
159        context.scheduler.interrupt_smtp().await;
160
161        let chat_name = chat.get_name();
162        let chat_name_shortened = shorten_name(chat_name, 25);
163        let chat_name_urlencoded = utf8_percent_encode(&chat_name_shortened, DISALLOWED_CHARACTERS)
164            .to_string()
165            .replace("%20", "+");
166        let grpid = &chat.grpid;
167
168        let self_name_shortened = shorten_name(&self_name, 16);
169        let self_name_urlencoded = utf8_percent_encode(&self_name_shortened, DISALLOWED_CHARACTERS)
170            .to_string()
171            .replace("%20", "+");
172
173        if chat.typ == Chattype::OutBroadcast {
174            // For historic reansons, broadcasts currently use j instead of i for the invitenumber.
175            format!(
176                "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&b={chat_name_urlencoded}",
177            )
178        } else {
179            format!(
180                "https://i.delta.chat/#{fingerprint}&v=3&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&g={chat_name_urlencoded}",
181            )
182        }
183    } else {
184        let self_name_shortened = shorten_name(&self_name, 25);
185        let self_name_urlencoded = utf8_percent_encode(&self_name_shortened, DISALLOWED_CHARACTERS)
186            .to_string()
187            .replace("%20", "+");
188
189        context.sync_qr_code_tokens(None).await?;
190        context.scheduler.interrupt_smtp().await;
191
192        format!(
193            "https://i.delta.chat/#{fingerprint}&v=3&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}",
194        )
195    };
196
197    info!(context, "Generated QR code.");
198    Ok(qr)
199}
200
201async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
202    let key = load_self_public_key(context)
203        .await
204        .context("Failed to load key")?;
205    Ok(key.dc_fingerprint())
206}
207
208/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
209///
210/// This is the start of the process for the joiner.  See the module and ffi documentation
211/// for more details.
212///
213/// The function returns immediately and the handshake will run in background.
214pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
215    join_securejoin_with_ux_info(context, qr, None, None).await
216}
217
218/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
219///
220/// This is the start of the process for the joiner.  See the module and ffi documentation
221/// for more details.
222///
223/// The function returns immediately and the handshake will run in background.
224///
225/// **source** and **uipath** are for statistics-sending,
226/// if the user enabled it in the settings;
227/// if you don't have statistics-sending implemented, just pass `None` here.
228pub async fn join_securejoin_with_ux_info(
229    context: &Context,
230    qr: &str,
231    source: Option<SecurejoinSource>,
232    uipath: Option<SecurejoinUiPath>,
233) -> Result<ChatId> {
234    let res = securejoin(context, qr).await.map_err(|err| {
235        warn!(context, "Fatal joiner error: {:#}", err);
236        // The user just scanned this QR code so has context on what failed.
237        error!(context, "QR process failed");
238        err
239    })?;
240
241    stats::count_securejoin_ux_info(context, source, uipath)
242        .await
243        .log_err(context)
244        .ok();
245
246    Ok(res)
247}
248
249async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
250    /*========================================================
251    ====             Bob - the joiner's side             =====
252    ====   Step 2 in "Setup verified contact" protocol   =====
253    ========================================================*/
254
255    info!(context, "Requesting secure-join ...",);
256    let qr_scan = check_qr(context, qr).await?;
257
258    let invite = QrInvite::try_from(qr_scan)?;
259
260    stats::count_securejoin_invite(context, &invite)
261        .await
262        .log_err(context)
263        .ok();
264
265    bob::start_protocol(context, invite).await
266}
267
268/// Send handshake message from Alice's device.
269async fn send_alice_handshake_msg(
270    context: &Context,
271    contact_id: ContactId,
272    step: &str,
273) -> Result<()> {
274    let mut msg = Message {
275        viewtype: Viewtype::Text,
276        text: format!("Secure-Join: {step}"),
277        hidden: true,
278        ..Default::default()
279    };
280    msg.param.set_cmd(SystemMessage::SecurejoinMessage);
281    msg.param.set(Param::Arg, step);
282    msg.param.set_int(Param::GuaranteeE2ee, 1);
283    chat::send_msg(
284        context,
285        ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
286            .await?
287            .id,
288        &mut msg,
289    )
290    .await?;
291    Ok(())
292}
293
294/// Get an unblocked chat that can be used for info messages.
295async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
296    let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
297    Ok(chat_id_blocked.id)
298}
299
300/// Checks fingerprint and marks the contact as verified
301/// if fingerprint matches.
302async fn verify_sender_by_fingerprint(
303    context: &Context,
304    fingerprint: &Fingerprint,
305    contact_id: ContactId,
306) -> Result<bool> {
307    let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? else {
308        return Ok(false);
309    };
310    let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
311    if is_verified {
312        mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
313    }
314    Ok(is_verified)
315}
316
317/// What to do with a Secure-Join handshake message after it was handled.
318///
319/// This status is returned to [`receive_imf_inner`] which will use it to decide what to do
320/// next with this incoming setup-contact/secure-join handshake message.
321///
322/// [`receive_imf_inner`]: crate::receive_imf::receive_imf_inner
323#[derive(Debug, PartialEq, Eq)]
324pub(crate) enum HandshakeMessage {
325    /// The message has been fully handled and should be removed/delete.
326    ///
327    /// This removes the message both locally and on the IMAP server.
328    Done,
329    /// The message should be ignored/hidden, but not removed/deleted.
330    ///
331    /// This leaves it on the IMAP server.  It means other devices on this account can
332    /// receive and potentially process this message as well.  This is useful for example
333    /// when the other device is running the protocol and has the relevant QR-code
334    /// information while this device does not have the joiner state.
335    Ignore,
336    /// The message should be further processed by incoming message handling.
337    ///
338    /// This may for example result in a group being created if it is a message which added
339    /// us to a group (a `vg-member-added` message).
340    Propagate,
341}
342
343/// Step of Secure-Join protocol.
344#[derive(Debug, Display, PartialEq, Eq)]
345pub(crate) enum SecureJoinStep {
346    /// vc-request or vg-request; only used in legacy securejoin
347    Request { invitenumber: String },
348
349    /// vc-auth-required or vg-auth-required; only used in legacy securejoin
350    AuthRequired,
351
352    /// vc-request-pubkey; only used in securejoin v3
353    RequestPubkey,
354
355    /// vc-pubkey; only used in securejoin v3
356    Pubkey,
357
358    /// vc-request-with-auth or vg-request-with-auth
359    RequestWithAuth,
360
361    /// vc-contact-confirm
362    ContactConfirm,
363
364    /// vg-member-added
365    MemberAdded,
366
367    /// Deprecated step such as `vg-member-added-received` or `vc-contact-confirm-received`.
368    Deprecated,
369
370    /// Unknown step.
371    Unknown { step: String },
372}
373
374/// Parses message headers to find out which Secure-Join step the message represents.
375///
376/// Returns `None` if the message is not a Secure-Join message.
377pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
378    if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
379        // We do not care about presence of `Secure-Join: vc-request` or `Secure-Join: vg-request` header.
380        // This allows us to always treat `Secure-Join` header as protected and ignore it
381        // in the unencrypted part even though it is sent there for backwards compatibility.
382        Some(SecureJoinStep::Request {
383            invitenumber: invitenumber.to_string(),
384        })
385    } else if let Some(step) = mime_message.get_header(HeaderDef::SecureJoin) {
386        match step {
387            "vc-request-pubkey" => Some(SecureJoinStep::RequestPubkey),
388            "vc-pubkey" => Some(SecureJoinStep::Pubkey),
389            "vg-auth-required" | "vc-auth-required" => Some(SecureJoinStep::AuthRequired),
390            "vg-request-with-auth" | "vc-request-with-auth" => {
391                Some(SecureJoinStep::RequestWithAuth)
392            }
393            "vc-contact-confirm" => Some(SecureJoinStep::ContactConfirm),
394            "vg-member-added" => Some(SecureJoinStep::MemberAdded),
395            "vg-member-added-received" | "vc-contact-confirm-received" => {
396                Some(SecureJoinStep::Deprecated)
397            }
398            step => Some(SecureJoinStep::Unknown {
399                step: step.to_string(),
400            }),
401        }
402    } else {
403        None
404    }
405}
406
407/// Handle incoming secure-join handshake.
408///
409/// This function will update the securejoin state in the database as the protocol
410/// progresses.
411///
412/// A message which results in [`Err`] will be hidden from the user but not deleted, it may
413/// be a valid message for something else we are not aware off.  E.g. it could be part of a
414/// handshake performed by another DC app on the same account.
415///
416/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
417/// database; this is done by `receive_imf()` later on as needed.
418#[expect(clippy::arithmetic_side_effects)]
419pub(crate) async fn handle_securejoin_handshake(
420    context: &Context,
421    mime_message: &mut MimeMessage,
422    contact_id: ContactId,
423) -> Result<HandshakeMessage> {
424    if contact_id.is_special() {
425        return Err(Error::msg("Can not be called with special contact ID"));
426    }
427
428    let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
429
430    info!(context, "Received secure-join message {step:?}.");
431
432    // Opportunistically protect against a theoretical 'surreptitious forwarding' attack:
433    // If Eve obtains a QR code from Alice and starts a securejoin with her,
434    // and also lets Bob scan a manipulated QR code,
435    // she could reencrypt the v*-request-with-auth message to Bob while maintaining the signature,
436    // and Bob would regard the message as valid.
437    //
438    // This attack is not actually relevant in any threat model,
439    // because if Eve can see Alice's QR code and have Bob scan a manipulated QR code,
440    // she can just do a classical MitM attack.
441    //
442    // Protecting all messages sent by Delta Chat against 'surreptitious forwarding'
443    // by checking the 'intended recipient fingerprint'
444    // will improve security (completely unrelated to the securejoin protocol)
445    // and is something we want to do in the future:
446    // https://www.rfc-editor.org/rfc/rfc9580.html#name-surreptitious-forwarding
447    if !matches!(
448        step,
449        SecureJoinStep::Request { .. } | SecureJoinStep::RequestPubkey | SecureJoinStep::Pubkey
450    ) {
451        let mut self_found = false;
452        let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
453        for (addr, key) in &mime_message.gossiped_keys {
454            if key.public_key.dc_fingerprint() == self_fingerprint
455                && context.is_self_addr(addr).await?
456            {
457                self_found = true;
458                break;
459            }
460        }
461        if !self_found {
462            // This message isn't intended for us. Possibly the peer doesn't own the key which the
463            // message is signed with but forwarded someone's message to us.
464            warn!(context, "Step {step}: No self addr+pubkey gossip found.");
465            return Ok(HandshakeMessage::Ignore);
466        }
467    }
468
469    match step {
470        SecureJoinStep::Request { ref invitenumber } => {
471            /*=======================================================
472            ====             Alice - the inviter side            ====
473            ====   Step 3 in "Setup verified contact" protocol   ====
474            =======================================================*/
475
476            // this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
477            // it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
478            // send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
479            // verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
480            if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
481                warn!(context, "Secure-join denied (bad invitenumber).");
482                return Ok(HandshakeMessage::Ignore);
483            }
484
485            let from_addr = ContactAddress::new(&mime_message.from.addr)?;
486            let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
487            let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
488                context,
489                "",
490                &from_addr,
491                autocrypt_fingerprint,
492                Origin::IncomingUnknownFrom,
493            )
494            .await?;
495
496            let prefix = mime_message
497                .get_header(HeaderDef::SecureJoin)
498                .and_then(|step| step.get(..2))
499                .unwrap_or("vc");
500
501            // Alice -> Bob
502            send_alice_handshake_msg(
503                context,
504                autocrypt_contact_id,
505                &format!("{prefix}-auth-required"),
506            )
507            .await
508            .context("failed sending auth-required handshake message")?;
509            Ok(HandshakeMessage::Done)
510        }
511        SecureJoinStep::AuthRequired => {
512            /*========================================================
513            ====             Bob - the joiner's side             =====
514            ====   Step 4 in "Setup verified contact" protocol   =====
515            ========================================================*/
516            bob::handle_auth_required_or_pubkey(context, mime_message).await
517        }
518        SecureJoinStep::RequestPubkey => {
519            /*========================================================
520            ====             Alice - the inviter's side          =====
521            ====   Bob requests our public key (Securejoin v3)   =====
522            ========================================================*/
523
524            debug_assert!(
525                mime_message.signature.is_none(),
526                "RequestPubkey is not supposed to be signed"
527            );
528            let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
529                warn!(
530                    context,
531                    "Ignoring {step} message because of missing auth code."
532                );
533                return Ok(HandshakeMessage::Ignore);
534            };
535            if !token::exists(context, token::Namespace::Auth, auth).await? {
536                warn!(context, "Secure-join denied (bad auth).");
537                return Ok(HandshakeMessage::Ignore);
538            }
539
540            let rfc724_mid = create_outgoing_rfc724_mid();
541            let addr = ContactAddress::new(&mime_message.from.addr)?;
542            let attach_self_pubkey = true;
543            let self_fp = self_fingerprint(context).await?;
544            let shared_secret = format!("securejoin/{self_fp}/{auth}");
545            let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
546                context,
547                "vc-pubkey",
548                &rfc724_mid,
549                attach_self_pubkey,
550                auth,
551                &shared_secret,
552            )
553            .await?;
554
555            let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
556            insert_into_smtp(context, &rfc724_mid, &addr, rendered_message, msg_id).await?;
557            context.scheduler.interrupt_smtp().await;
558
559            Ok(HandshakeMessage::Done)
560        }
561        SecureJoinStep::Pubkey => {
562            /*========================================================
563            ====             Bob - the joiner's side             =====
564            ====     Alice sent us her pubkey (Securejoin v3)    =====
565            ========================================================*/
566            bob::handle_auth_required_or_pubkey(context, mime_message).await
567        }
568        SecureJoinStep::RequestWithAuth => {
569            /*==========================================================
570            ====              Alice - the inviter side              ====
571            ====   Steps 5+6 in "Setup verified contact" protocol   ====
572            ====  Step 6 in "Out-of-band verified groups" protocol  ====
573            ==========================================================*/
574
575            // verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
576            let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
577                warn!(
578                    context,
579                    "Ignoring {step} message because fingerprint is not provided."
580                );
581                return Ok(HandshakeMessage::Ignore);
582            };
583            let fingerprint: Fingerprint = fp.parse()?;
584            if !encrypted_and_signed(context, mime_message, &fingerprint) {
585                warn!(
586                    context,
587                    "Ignoring {step} message because the message is not encrypted."
588                );
589                return Ok(HandshakeMessage::Ignore);
590            }
591            // verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
592            let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
593                warn!(
594                    context,
595                    "Ignoring {step} message because of missing auth code."
596                );
597                return Ok(HandshakeMessage::Ignore);
598            };
599            let Some((grpid, timestamp)) = context
600                .sql
601                .query_row_optional(
602                    "SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
603                    (Namespace::Auth, auth),
604                    |row| {
605                        let foreign_key: String = row.get(0)?;
606                        let timestamp: i64 = row.get(1)?;
607                        Ok((foreign_key, timestamp))
608                    },
609                )
610                .await?
611            else {
612                warn!(
613                    context,
614                    "Ignoring {step} message because of invalid auth code."
615                );
616                return Ok(HandshakeMessage::Ignore);
617            };
618            let joining_chat_id = match grpid.as_str() {
619                "" => None,
620                id => {
621                    let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
622                        warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
623                        return Ok(HandshakeMessage::Ignore);
624                    };
625                    Some(chat_id)
626                }
627            };
628
629            let sender_contact = Contact::get_by_id(context, contact_id).await?;
630            if sender_contact
631                .fingerprint()
632                .is_none_or(|fp| fp != fingerprint)
633            {
634                warn!(
635                    context,
636                    "Ignoring {step} message because of fingerprint mismatch."
637                );
638                return Ok(HandshakeMessage::Ignore);
639            }
640            info!(context, "Fingerprint verified via Auth code.",);
641
642            // Mark the contact as verified if auth code is less than VERIFICATION_TIMEOUT_SECONDS seconds old.
643            if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
644                mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
645            }
646            contact_id.regossip_keys(context).await?;
647            // for setup-contact, make Alice's one-to-one chat with Bob visible
648            // (secure-join-information are shown in the group chat)
649            if grpid.is_empty() {
650                ChatId::create_for_contact(context, contact_id).await?;
651            }
652            if let Some(joining_chat_id) = joining_chat_id {
653                chat::add_contact_to_chat_ex(context, Nosync, joining_chat_id, contact_id, true)
654                    .await?;
655
656                let chat = Chat::load_from_db(context, joining_chat_id).await?;
657
658                if chat.typ == Chattype::OutBroadcast {
659                    // We don't use the membership consistency algorithm for broadcast channels,
660                    // so, sync the memberlist when adding a contact
661                    chat.sync_contacts(context).await.log_err(context).ok();
662                } else {
663                    ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited)
664                        .await?;
665                    context.emit_event(EventType::ContactsChanged(Some(contact_id)));
666                }
667
668                inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
669                // IMAP-delete the message to avoid handling it by another device and adding the
670                // member twice. Another device will know the member's key from Autocrypt-Gossip.
671                Ok(HandshakeMessage::Done)
672            } else {
673                let chat_id = info_chat_id(context, contact_id).await?;
674                // Setup verified contact.
675                send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
676                    .await
677                    .context("failed sending vc-contact-confirm message")?;
678
679                inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
680                Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
681            }
682        }
683        /*=======================================================
684        ====             Bob - the joiner's side             ====
685        ====   Step 7 in "Setup verified contact" protocol   ====
686        =======================================================*/
687        SecureJoinStep::ContactConfirm => {
688            context.emit_event(EventType::SecurejoinJoinerProgress {
689                contact_id,
690                progress: JoinerProgress::Succeeded.into_u16(),
691            });
692            Ok(HandshakeMessage::Ignore)
693        }
694        SecureJoinStep::MemberAdded => {
695            let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
696            else {
697                warn!(
698                    context,
699                    "vg-member-added without Chat-Group-Member-Added header."
700                );
701                return Ok(HandshakeMessage::Propagate);
702            };
703            if !context.is_self_addr(member_added).await? {
704                info!(
705                    context,
706                    "Member {member_added} added by unrelated SecureJoin process."
707                );
708                return Ok(HandshakeMessage::Propagate);
709            }
710
711            context.emit_event(EventType::SecurejoinJoinerProgress {
712                contact_id,
713                progress: JoinerProgress::Succeeded.into_u16(),
714            });
715            Ok(HandshakeMessage::Propagate)
716        }
717        SecureJoinStep::Deprecated => {
718            // Deprecated steps, delete them immediately.
719            Ok(HandshakeMessage::Done)
720        }
721        SecureJoinStep::Unknown { ref step } => {
722            warn!(context, "Invalid SecureJoin step: {step:?}.");
723            Ok(HandshakeMessage::Ignore)
724        }
725    }
726}
727
728async fn insert_into_smtp(
729    context: &Context,
730    rfc724_mid: &str,
731    recipient: &str,
732    rendered_message: String,
733    msg_id: MsgId,
734) -> Result<(), Error> {
735    context
736        .sql
737        .execute(
738            "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
739            VALUES            (?1,         ?2,         ?3,   ?4)",
740            (&rfc724_mid, &recipient, &rendered_message, msg_id),
741        )
742        .await?;
743    Ok(())
744}
745
746/// Observe self-sent Securejoin message.
747///
748/// In a multi-device-setup, there may be other devices that "see" the handshake messages.
749/// If we see self-sent messages encrypted+signed correctly with our key,
750/// we can make some conclusions of it.
751///
752/// If we see self-sent {vc,vg}-request-with-auth,
753/// we know that we are Bob (joiner-observer)
754/// that just marked peer (Alice) as verified
755/// either after receiving {vc,vg}-auth-required
756/// or immediately after scanning the QR-code
757/// if the key was already known.
758///
759/// If we see self-sent vc-contact-confirm or vg-member-added message,
760/// we know that we are Alice (inviter-observer)
761/// that just marked peer (Bob) as verified
762/// in response to correct vc-request-with-auth message.
763pub(crate) async fn observe_securejoin_on_other_device(
764    context: &Context,
765    mime_message: &MimeMessage,
766    contact_id: ContactId,
767) -> Result<HandshakeMessage> {
768    if contact_id.is_special() {
769        return Err(Error::msg("Can not be called with special contact ID"));
770    }
771    let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
772    info!(context, "Observing secure-join message {step:?}.");
773
774    match step {
775        SecureJoinStep::Request { .. }
776        | SecureJoinStep::AuthRequired
777        | SecureJoinStep::RequestPubkey
778        | SecureJoinStep::Pubkey
779        | SecureJoinStep::Deprecated
780        | SecureJoinStep::Unknown { .. } => {
781            return Ok(HandshakeMessage::Ignore);
782        }
783        SecureJoinStep::RequestWithAuth
784        | SecureJoinStep::MemberAdded
785        | SecureJoinStep::ContactConfirm => {}
786    }
787
788    if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
789        warn!(
790            context,
791            "Observed SecureJoin message is not encrypted correctly."
792        );
793        return Ok(HandshakeMessage::Ignore);
794    }
795
796    let contact = Contact::get_by_id(context, contact_id).await?;
797    let addr = contact.get_addr().to_lowercase();
798
799    let Some(key) = mime_message.gossiped_keys.get(&addr) else {
800        warn!(context, "No gossip header for {addr} at step {step}.");
801        return Ok(HandshakeMessage::Ignore);
802    };
803
804    let Some(contact_fingerprint) = contact.fingerprint() else {
805        // Not a key-contact, should not happen.
806        warn!(context, "Contact does not have a fingerprint.");
807        return Ok(HandshakeMessage::Ignore);
808    };
809
810    if key.public_key.dc_fingerprint() != contact_fingerprint {
811        // Fingerprint does not match, ignore.
812        warn!(context, "Fingerprint does not match.");
813        return Ok(HandshakeMessage::Ignore);
814    }
815
816    mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
817
818    if matches!(
819        step,
820        SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
821    ) {
822        let chat_type = if mime_message
823            .get_header(HeaderDef::ChatGroupMemberAdded)
824            .is_none()
825        {
826            Chattype::Single
827        } else if mime_message.get_header(HeaderDef::ListId).is_some() {
828            Chattype::OutBroadcast
829        } else {
830            Chattype::Group
831        };
832
833        // We don't know the chat ID
834        // as we may not know about the group yet.
835        //
836        // Event is mostly used for bots
837        // which only have a single device
838        // and tests which don't care about the chat ID,
839        // so we pass invalid chat ID here.
840        let chat_id = ChatId::new(0);
841        inviter_progress(context, contact_id, chat_id, chat_type)?;
842    }
843
844    if matches!(step, SecureJoinStep::RequestWithAuth) {
845        // This actually reflects what happens on the first device (which does the secure
846        // join) and causes a subsequent "vg-member-added" message to create an unblocked
847        // verified group.
848        ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
849    }
850
851    if matches!(step, SecureJoinStep::MemberAdded) {
852        Ok(HandshakeMessage::Propagate)
853    } else {
854        Ok(HandshakeMessage::Ignore)
855    }
856}
857
858/* ******************************************************************************
859 * Tools: Misc.
860 ******************************************************************************/
861
862fn encrypted_and_signed(
863    context: &Context,
864    mimeparser: &MimeMessage,
865    expected_fingerprint: &Fingerprint,
866) -> bool {
867    if let Some((signature, _)) = mimeparser.signature.as_ref() {
868        if signature == expected_fingerprint {
869            true
870        } else {
871            warn!(
872                context,
873                "Message does not match expected fingerprint {expected_fingerprint}.",
874            );
875            false
876        }
877    } else {
878        warn!(context, "Message not encrypted.",);
879        false
880    }
881}
882
883#[cfg(test)]
884mod securejoin_tests;