Skip to main content

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