1use 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
40const 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 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
69fn shorten_name(name: &str, length: usize) -> String {
72 if name.chars().count() > length {
73 format!(
75 "{}_",
76 name.chars()
77 .take(length.saturating_sub(1))
78 .collect::<String>()
79 )
80 } else {
81 name.to_string()
82 }
83}
84
85pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
90 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 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 let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
131
132 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 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
207pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
214 join_securejoin_with_ux_info(context, qr, None, None).await
215}
216
217pub 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 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 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
267async 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
293async 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
299async 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#[derive(Debug, PartialEq, Eq)]
323pub(crate) enum HandshakeMessage {
324 Done,
328 Ignore,
335 Propagate,
340}
341
342#[derive(Debug, Display, PartialEq, Eq)]
344pub(crate) enum SecureJoinStep {
345 Request { invitenumber: String },
347
348 AuthRequired,
350
351 RequestPubkey,
353
354 Pubkey,
356
357 RequestWithAuth,
359
360 ContactConfirm,
362
363 MemberAdded,
365
366 Deprecated,
368
369 Unknown { step: String },
371}
372
373pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
377 if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
378 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#[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 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 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 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 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 bob::handle_auth_required_or_pubkey(context, mime_message).await
514 }
515 SecureJoinStep::RequestPubkey => {
516 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 bob::handle_auth_required_or_pubkey(context, mime_message).await
576 }
577 SecureJoinStep::RequestWithAuth => {
578 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 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 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 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 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 Ok(HandshakeMessage::Done)
685 } else {
686 let chat_id = info_chat_id(context, contact_id).await?;
687 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) }
695 }
696 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 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
759pub(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 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 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 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 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
870fn 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;