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};
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
77 .chars()
78 .take(length.saturating_sub(1))
79 .collect::<String>()
80 )
81 } else {
82 name.to_string()
83 }
84}
85
86pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
91 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 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 let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
132
133 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 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
208pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
215 join_securejoin_with_ux_info(context, qr, None, None).await
216}
217
218pub 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 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 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
268async 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
294async 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
300async 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#[derive(Debug, PartialEq, Eq)]
324pub(crate) enum HandshakeMessage {
325 Done,
329 Ignore,
336 Propagate,
341}
342
343#[derive(Debug, Display, PartialEq, Eq)]
345pub(crate) enum SecureJoinStep {
346 Request { invitenumber: String },
348
349 AuthRequired,
351
352 RequestPubkey,
354
355 Pubkey,
357
358 RequestWithAuth,
360
361 ContactConfirm,
363
364 MemberAdded,
366
367 Deprecated,
369
370 Unknown { step: String },
372}
373
374pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
378 if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
379 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#[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 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 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 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 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 bob::handle_auth_required_or_pubkey(context, mime_message).await
517 }
518 SecureJoinStep::RequestPubkey => {
519 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 rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
544 context,
545 "vc-pubkey",
546 &rfc724_mid,
547 attach_self_pubkey,
548 auth,
549 )
550 .await?;
551
552 let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
553 insert_into_smtp(context, &rfc724_mid, &addr, rendered_message, msg_id).await?;
554 context.scheduler.interrupt_smtp().await;
555
556 Ok(HandshakeMessage::Done)
557 }
558 SecureJoinStep::Pubkey => {
559 bob::handle_auth_required_or_pubkey(context, mime_message).await
564 }
565 SecureJoinStep::RequestWithAuth => {
566 let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
574 warn!(
575 context,
576 "Ignoring {step} message because fingerprint is not provided."
577 );
578 return Ok(HandshakeMessage::Ignore);
579 };
580 let fingerprint: Fingerprint = fp.parse()?;
581 if !encrypted_and_signed(context, mime_message, &fingerprint) {
582 warn!(
583 context,
584 "Ignoring {step} message because the message is not encrypted."
585 );
586 return Ok(HandshakeMessage::Ignore);
587 }
588 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
590 warn!(
591 context,
592 "Ignoring {step} message because of missing auth code."
593 );
594 return Ok(HandshakeMessage::Ignore);
595 };
596 let Some((grpid, timestamp)) = context
597 .sql
598 .query_row_optional(
599 "SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
600 (Namespace::Auth, auth),
601 |row| {
602 let foreign_key: String = row.get(0)?;
603 let timestamp: i64 = row.get(1)?;
604 Ok((foreign_key, timestamp))
605 },
606 )
607 .await?
608 else {
609 warn!(
610 context,
611 "Ignoring {step} message because of invalid auth code."
612 );
613 return Ok(HandshakeMessage::Ignore);
614 };
615 let joining_chat_id = match grpid.as_str() {
616 "" => None,
617 id => {
618 let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
619 warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
620 return Ok(HandshakeMessage::Ignore);
621 };
622 Some(chat_id)
623 }
624 };
625
626 let sender_contact = Contact::get_by_id(context, contact_id).await?;
627 if sender_contact
628 .fingerprint()
629 .is_none_or(|fp| fp != fingerprint)
630 {
631 warn!(
632 context,
633 "Ignoring {step} message because of fingerprint mismatch."
634 );
635 return Ok(HandshakeMessage::Ignore);
636 }
637 info!(context, "Fingerprint verified via Auth code.",);
638
639 if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
641 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
642 }
643 contact_id.regossip_keys(context).await?;
644 ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
645 if grpid.is_empty() {
648 ChatId::create_for_contact(context, contact_id).await?;
649 }
650 context.emit_event(EventType::ContactsChanged(Some(contact_id)));
651 if let Some(joining_chat_id) = joining_chat_id {
652 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 chat.sync_contacts(context).await.log_err(context).ok();
662 }
663
664 inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
665 Ok(HandshakeMessage::Done)
668 } else {
669 let chat_id = info_chat_id(context, contact_id).await?;
670 send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
672 .await
673 .context("failed sending vc-contact-confirm message")?;
674
675 inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
676 Ok(HandshakeMessage::Ignore) }
678 }
679 SecureJoinStep::ContactConfirm => {
684 context.emit_event(EventType::SecurejoinJoinerProgress {
685 contact_id,
686 progress: JoinerProgress::Succeeded.into_u16(),
687 });
688 Ok(HandshakeMessage::Ignore)
689 }
690 SecureJoinStep::MemberAdded => {
691 let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
692 else {
693 warn!(
694 context,
695 "vg-member-added without Chat-Group-Member-Added header."
696 );
697 return Ok(HandshakeMessage::Propagate);
698 };
699 if !context.is_self_addr(member_added).await? {
700 info!(
701 context,
702 "Member {member_added} added by unrelated SecureJoin process."
703 );
704 return Ok(HandshakeMessage::Propagate);
705 }
706
707 context.emit_event(EventType::SecurejoinJoinerProgress {
708 contact_id,
709 progress: JoinerProgress::Succeeded.into_u16(),
710 });
711 Ok(HandshakeMessage::Propagate)
712 }
713 SecureJoinStep::Deprecated => {
714 Ok(HandshakeMessage::Done)
716 }
717 SecureJoinStep::Unknown { ref step } => {
718 warn!(context, "Invalid SecureJoin step: {step:?}.");
719 Ok(HandshakeMessage::Ignore)
720 }
721 }
722}
723
724async fn insert_into_smtp(
725 context: &Context,
726 rfc724_mid: &str,
727 recipient: &str,
728 rendered_message: String,
729 msg_id: MsgId,
730) -> Result<(), Error> {
731 context
732 .sql
733 .execute(
734 "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
735 VALUES (?1, ?2, ?3, ?4)",
736 (&rfc724_mid, &recipient, &rendered_message, msg_id),
737 )
738 .await?;
739 Ok(())
740}
741
742pub(crate) async fn observe_securejoin_on_other_device(
760 context: &Context,
761 mime_message: &MimeMessage,
762 contact_id: ContactId,
763) -> Result<HandshakeMessage> {
764 if contact_id.is_special() {
765 return Err(Error::msg("Can not be called with special contact ID"));
766 }
767 let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
768 info!(context, "Observing secure-join message {step:?}.");
769
770 match step {
771 SecureJoinStep::Request { .. }
772 | SecureJoinStep::AuthRequired
773 | SecureJoinStep::RequestPubkey
774 | SecureJoinStep::Pubkey
775 | SecureJoinStep::Deprecated
776 | SecureJoinStep::Unknown { .. } => {
777 return Ok(HandshakeMessage::Ignore);
778 }
779 SecureJoinStep::RequestWithAuth
780 | SecureJoinStep::MemberAdded
781 | SecureJoinStep::ContactConfirm => {}
782 }
783
784 if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
785 warn!(
786 context,
787 "Observed SecureJoin message is not encrypted correctly."
788 );
789 return Ok(HandshakeMessage::Ignore);
790 }
791
792 let contact = Contact::get_by_id(context, contact_id).await?;
793 let addr = contact.get_addr().to_lowercase();
794
795 let Some(key) = mime_message.gossiped_keys.get(&addr) else {
796 warn!(context, "No gossip header for {addr} at step {step}.");
797 return Ok(HandshakeMessage::Ignore);
798 };
799
800 let Some(contact_fingerprint) = contact.fingerprint() else {
801 warn!(context, "Contact does not have a fingerprint.");
803 return Ok(HandshakeMessage::Ignore);
804 };
805
806 if key.public_key.dc_fingerprint() != contact_fingerprint {
807 warn!(context, "Fingerprint does not match.");
809 return Ok(HandshakeMessage::Ignore);
810 }
811
812 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
813
814 if matches!(
815 step,
816 SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
817 ) {
818 let chat_type = if mime_message
819 .get_header(HeaderDef::ChatGroupMemberAdded)
820 .is_none()
821 {
822 Chattype::Single
823 } else if mime_message.get_header(HeaderDef::ListId).is_some() {
824 Chattype::OutBroadcast
825 } else {
826 Chattype::Group
827 };
828
829 let chat_id = ChatId::new(0);
837 inviter_progress(context, contact_id, chat_id, chat_type)?;
838 }
839
840 if matches!(step, SecureJoinStep::RequestWithAuth) {
841 ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
845 }
846
847 if matches!(step, SecureJoinStep::MemberAdded) {
848 Ok(HandshakeMessage::Propagate)
849 } else {
850 Ok(HandshakeMessage::Ignore)
851 }
852}
853
854fn encrypted_and_signed(
859 context: &Context,
860 mimeparser: &MimeMessage,
861 expected_fingerprint: &Fingerprint,
862) -> bool {
863 if let Some((signature, _)) = mimeparser.signature.as_ref() {
864 if signature == expected_fingerprint {
865 true
866 } else {
867 warn!(
868 context,
869 "Message does not match expected fingerprint {expected_fingerprint}.",
870 );
871 false
872 }
873 } else {
874 warn!(context, "Message not encrypted.",);
875 false
876 }
877}
878
879#[cfg(test)]
880mod securejoin_tests;