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
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 key in mime_message.gossiped_keys.values() {
454 if key.public_key.dc_fingerprint() == self_fingerprint {
455 self_found = true;
456 break;
457 }
458 }
459 if !self_found {
460 warn!(context, "Step {step}: No self addr+pubkey gossip found.");
463 return Ok(HandshakeMessage::Ignore);
464 }
465 }
466
467 match step {
468 SecureJoinStep::Request { ref invitenumber } => {
469 if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
479 warn!(context, "Secure-join denied (bad invitenumber).");
480 return Ok(HandshakeMessage::Ignore);
481 }
482
483 let from_addr = ContactAddress::new(&mime_message.from.addr)?;
484 let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
485 let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
486 context,
487 "",
488 &from_addr,
489 autocrypt_fingerprint,
490 Origin::IncomingUnknownFrom,
491 )
492 .await?;
493
494 let prefix = mime_message
495 .get_header(HeaderDef::SecureJoin)
496 .and_then(|step| step.get(..2))
497 .unwrap_or("vc");
498
499 send_alice_handshake_msg(
501 context,
502 autocrypt_contact_id,
503 &format!("{prefix}-auth-required"),
504 )
505 .await
506 .context("failed sending auth-required handshake message")?;
507 Ok(HandshakeMessage::Done)
508 }
509 SecureJoinStep::AuthRequired => {
510 bob::handle_auth_required_or_pubkey(context, mime_message).await
515 }
516 SecureJoinStep::RequestPubkey => {
517 debug_assert!(
523 mime_message.signature.is_none(),
524 "RequestPubkey is not supposed to be signed"
525 );
526 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
527 warn!(
528 context,
529 "Ignoring {step} message because of missing auth code."
530 );
531 return Ok(HandshakeMessage::Ignore);
532 };
533 if !token::exists(context, token::Namespace::Auth, auth).await? {
534 warn!(context, "Secure-join denied (bad auth).");
535 return Ok(HandshakeMessage::Ignore);
536 }
537
538 let rfc724_mid = create_outgoing_rfc724_mid();
539 let addr = ContactAddress::new(&mime_message.from.addr)?;
540 let attach_self_pubkey = true;
541 let self_fp = self_fingerprint(context).await?;
542 let shared_secret = format!("securejoin/{self_fp}/{auth}");
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 &shared_secret,
550 )
551 .await?;
552
553 let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
554 insert_into_smtp(context, &rfc724_mid, &addr, rendered_message, msg_id).await?;
555 context.scheduler.interrupt_smtp().await;
556
557 Ok(HandshakeMessage::Done)
558 }
559 SecureJoinStep::Pubkey => {
560 bob::handle_auth_required_or_pubkey(context, mime_message).await
565 }
566 SecureJoinStep::RequestWithAuth => {
567 let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
575 warn!(
576 context,
577 "Ignoring {step} message because fingerprint is not provided."
578 );
579 return Ok(HandshakeMessage::Ignore);
580 };
581 let fingerprint: Fingerprint = fp.parse()?;
582 if !encrypted_and_signed(context, mime_message, &fingerprint) {
583 warn!(
584 context,
585 "Ignoring {step} message because the message is not encrypted."
586 );
587 return Ok(HandshakeMessage::Ignore);
588 }
589 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
591 warn!(
592 context,
593 "Ignoring {step} message because of missing auth code."
594 );
595 return Ok(HandshakeMessage::Ignore);
596 };
597 let Some((grpid, timestamp)) = context
598 .sql
599 .query_row_optional(
600 "SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
601 (Namespace::Auth, auth),
602 |row| {
603 let foreign_key: String = row.get(0)?;
604 let timestamp: i64 = row.get(1)?;
605 Ok((foreign_key, timestamp))
606 },
607 )
608 .await?
609 else {
610 warn!(
611 context,
612 "Ignoring {step} message because of invalid auth code."
613 );
614 return Ok(HandshakeMessage::Ignore);
615 };
616 let joining_chat_id = match grpid.as_str() {
617 "" => None,
618 id => {
619 let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
620 warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
621 return Ok(HandshakeMessage::Ignore);
622 };
623 Some(chat_id)
624 }
625 };
626
627 let sender_contact = Contact::get_by_id(context, contact_id).await?;
628 if sender_contact
629 .fingerprint()
630 .is_none_or(|fp| fp != fingerprint)
631 {
632 warn!(
633 context,
634 "Ignoring {step} message because of fingerprint mismatch."
635 );
636 return Ok(HandshakeMessage::Ignore);
637 }
638 info!(context, "Fingerprint verified via Auth code.",);
639
640 if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
642 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
643 }
644 contact_id.regossip_keys(context).await?;
645 if grpid.is_empty() {
648 ChatId::create_for_contact(context, contact_id).await?;
649 }
650 if let Some(joining_chat_id) = joining_chat_id {
651 chat::add_contact_to_chat_ex(context, Nosync, joining_chat_id, contact_id, true)
652 .await?;
653
654 let chat = Chat::load_from_db(context, joining_chat_id).await?;
655
656 if chat.typ == Chattype::OutBroadcast {
657 chat.sync_contacts(context).await.log_err(context).ok();
660 } else {
661 ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited)
662 .await?;
663 context.emit_event(EventType::ContactsChanged(Some(contact_id)));
664 }
665
666 inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
667 Ok(HandshakeMessage::Done)
670 } else {
671 let chat_id = info_chat_id(context, contact_id).await?;
672 send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
674 .await
675 .context("failed sending vc-contact-confirm message")?;
676
677 inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
678 Ok(HandshakeMessage::Ignore) }
680 }
681 SecureJoinStep::ContactConfirm => {
686 context.emit_event(EventType::SecurejoinJoinerProgress {
687 contact_id,
688 progress: JoinerProgress::Succeeded.into_u16(),
689 });
690 Ok(HandshakeMessage::Ignore)
691 }
692 SecureJoinStep::MemberAdded => {
693 let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
694 else {
695 warn!(
696 context,
697 "vg-member-added without Chat-Group-Member-Added header."
698 );
699 return Ok(HandshakeMessage::Propagate);
700 };
701 if !context.is_self_addr(member_added).await? {
702 info!(
703 context,
704 "Member {member_added} added by unrelated SecureJoin process."
705 );
706 return Ok(HandshakeMessage::Propagate);
707 }
708
709 context.emit_event(EventType::SecurejoinJoinerProgress {
710 contact_id,
711 progress: JoinerProgress::Succeeded.into_u16(),
712 });
713 Ok(HandshakeMessage::Propagate)
714 }
715 SecureJoinStep::Deprecated => {
716 Ok(HandshakeMessage::Done)
718 }
719 SecureJoinStep::Unknown { ref step } => {
720 warn!(context, "Invalid SecureJoin step: {step:?}.");
721 Ok(HandshakeMessage::Ignore)
722 }
723 }
724}
725
726async fn insert_into_smtp(
727 context: &Context,
728 rfc724_mid: &str,
729 recipient: &str,
730 rendered_message: String,
731 msg_id: MsgId,
732) -> Result<(), Error> {
733 context
734 .sql
735 .execute(
736 "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
737 VALUES (?1, ?2, ?3, ?4)",
738 (&rfc724_mid, &recipient, &rendered_message, msg_id),
739 )
740 .await?;
741 Ok(())
742}
743
744pub(crate) async fn observe_securejoin_on_other_device(
762 context: &Context,
763 mime_message: &MimeMessage,
764 contact_id: ContactId,
765) -> Result<HandshakeMessage> {
766 if contact_id.is_special() {
767 return Err(Error::msg("Can not be called with special contact ID"));
768 }
769 let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
770 info!(context, "Observing secure-join message {step:?}.");
771
772 match step {
773 SecureJoinStep::Request { .. }
774 | SecureJoinStep::AuthRequired
775 | SecureJoinStep::RequestPubkey
776 | SecureJoinStep::Pubkey
777 | SecureJoinStep::Deprecated
778 | SecureJoinStep::Unknown { .. } => {
779 return Ok(HandshakeMessage::Ignore);
780 }
781 SecureJoinStep::RequestWithAuth
782 | SecureJoinStep::MemberAdded
783 | SecureJoinStep::ContactConfirm => {}
784 }
785
786 if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
787 warn!(
788 context,
789 "Observed SecureJoin message is not encrypted correctly."
790 );
791 return Ok(HandshakeMessage::Ignore);
792 }
793
794 let contact = Contact::get_by_id(context, contact_id).await?;
795 let addr = contact.get_addr().to_lowercase();
796
797 let Some(key) = mime_message.gossiped_keys.get(&addr) else {
798 warn!(context, "No gossip header for {addr} at step {step}.");
799 return Ok(HandshakeMessage::Ignore);
800 };
801
802 let Some(contact_fingerprint) = contact.fingerprint() else {
803 warn!(context, "Contact does not have a fingerprint.");
805 return Ok(HandshakeMessage::Ignore);
806 };
807
808 if key.public_key.dc_fingerprint() != contact_fingerprint {
809 warn!(context, "Fingerprint does not match.");
811 return Ok(HandshakeMessage::Ignore);
812 }
813
814 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
815
816 if matches!(
817 step,
818 SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
819 ) {
820 let chat_type = if mime_message
821 .get_header(HeaderDef::ChatGroupMemberAdded)
822 .is_none()
823 {
824 Chattype::Single
825 } else if mime_message.get_header(HeaderDef::ListId).is_some() {
826 Chattype::OutBroadcast
827 } else {
828 Chattype::Group
829 };
830
831 let chat_id = ChatId::new(0);
839 inviter_progress(context, contact_id, chat_id, chat_type)?;
840 }
841
842 if matches!(step, SecureJoinStep::RequestWithAuth) {
843 ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
847 }
848
849 if matches!(step, SecureJoinStep::MemberAdded) {
850 Ok(HandshakeMessage::Propagate)
851 } else {
852 Ok(HandshakeMessage::Ignore)
853 }
854}
855
856fn encrypted_and_signed(
861 context: &Context,
862 mimeparser: &MimeMessage,
863 expected_fingerprint: &Fingerprint,
864) -> bool {
865 if let Some((signature, _)) = mimeparser.signature.as_ref() {
866 if signature == expected_fingerprint {
867 true
868 } else {
869 warn!(
870 context,
871 "Message does not match expected fingerprint {expected_fingerprint}.",
872 );
873 false
874 }
875 } else {
876 warn!(context, "Message not encrypted.",);
877 false
878 }
879}
880
881#[cfg(test)]
882mod securejoin_tests;