deltachat/
securejoin.rs

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