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::{Message, 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, time};
30use crate::{SecurejoinSource, 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 sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
131 .await?
132 .is_none();
133 let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
135
136 let auth = create_id();
146 token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
147
148 let fingerprint = get_self_fingerprint(context).await?.hex();
149
150 let self_addr = context.get_primary_self_addr().await?;
151 let self_addr_urlencoded = utf8_percent_encode(&self_addr, DISALLOWED_CHARACTERS).to_string();
152
153 let self_name = context
154 .get_config(Config::Displayname)
155 .await?
156 .unwrap_or_default();
157
158 let qr = if let Some(chat) = chat {
159 if sync_token {
160 context
161 .sync_qr_code_tokens(Some(chat.grpid.as_str()))
162 .await?;
163 context.scheduler.interrupt_smtp().await;
164 }
165
166 let chat_name = chat.get_name();
167 let chat_name_shortened = shorten_name(chat_name, 25);
168 let chat_name_urlencoded = utf8_percent_encode(&chat_name_shortened, DISALLOWED_CHARACTERS)
169 .to_string()
170 .replace("%20", "+");
171 let grpid = &chat.grpid;
172
173 let self_name_shortened = shorten_name(&self_name, 16);
174 let self_name_urlencoded = utf8_percent_encode(&self_name_shortened, DISALLOWED_CHARACTERS)
175 .to_string()
176 .replace("%20", "+");
177
178 if chat.typ == Chattype::OutBroadcast {
179 format!(
181 "https://i.delta.chat/#{fingerprint}&x={grpid}&j={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&b={chat_name_urlencoded}",
182 )
183 } else {
184 format!(
185 "https://i.delta.chat/#{fingerprint}&x={grpid}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}&g={chat_name_urlencoded}",
186 )
187 }
188 } else {
189 let self_name_shortened = shorten_name(&self_name, 25);
190 let self_name_urlencoded = utf8_percent_encode(&self_name_shortened, DISALLOWED_CHARACTERS)
191 .to_string()
192 .replace("%20", "+");
193 if sync_token {
194 context.sync_qr_code_tokens(None).await?;
195 context.scheduler.interrupt_smtp().await;
196 }
197 format!(
198 "https://i.delta.chat/#{fingerprint}&i={invitenumber}&s={auth}&a={self_addr_urlencoded}&n={self_name_urlencoded}",
199 )
200 };
201
202 info!(context, "Generated QR code.");
203 Ok(qr)
204}
205
206async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
207 let key = load_self_public_key(context)
208 .await
209 .context("Failed to load key")?;
210 Ok(key.dc_fingerprint())
211}
212
213pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
220 join_securejoin_with_ux_info(context, qr, None, None).await
221}
222
223pub async fn join_securejoin_with_ux_info(
234 context: &Context,
235 qr: &str,
236 source: Option<SecurejoinSource>,
237 uipath: Option<SecurejoinUiPath>,
238) -> Result<ChatId> {
239 let res = securejoin(context, qr).await.map_err(|err| {
240 warn!(context, "Fatal joiner error: {:#}", err);
241 error!(context, "QR process failed");
243 err
244 })?;
245
246 stats::count_securejoin_ux_info(context, source, uipath)
247 .await
248 .log_err(context)
249 .ok();
250
251 Ok(res)
252}
253
254async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
255 info!(context, "Requesting secure-join ...",);
261 let qr_scan = check_qr(context, qr).await?;
262
263 let invite = QrInvite::try_from(qr_scan)?;
264
265 stats::count_securejoin_invite(context, &invite)
266 .await
267 .log_err(context)
268 .ok();
269
270 bob::start_protocol(context, invite).await
271}
272
273async fn send_alice_handshake_msg(
275 context: &Context,
276 contact_id: ContactId,
277 step: &str,
278) -> Result<()> {
279 let mut msg = Message {
280 viewtype: Viewtype::Text,
281 text: format!("Secure-Join: {step}"),
282 hidden: true,
283 ..Default::default()
284 };
285 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
286 msg.param.set(Param::Arg, step);
287 msg.param.set_int(Param::GuaranteeE2ee, 1);
288 chat::send_msg(
289 context,
290 ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
291 .await?
292 .id,
293 &mut msg,
294 )
295 .await?;
296 Ok(())
297}
298
299async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
301 let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
302 Ok(chat_id_blocked.id)
303}
304
305async fn verify_sender_by_fingerprint(
308 context: &Context,
309 fingerprint: &Fingerprint,
310 contact_id: ContactId,
311) -> Result<bool> {
312 let contact = Contact::get_by_id(context, contact_id).await?;
313 let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
314 if is_verified {
315 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
316 }
317 Ok(is_verified)
318}
319
320#[derive(Debug, PartialEq, Eq)]
327pub(crate) enum HandshakeMessage {
328 Done,
332 Ignore,
339 Propagate,
344}
345
346#[derive(Debug, Display, PartialEq, Eq)]
348pub(crate) enum SecureJoinStep {
349 Request { invitenumber: String },
351
352 AuthRequired,
354
355 RequestWithAuth,
357
358 ContactConfirm,
360
361 MemberAdded,
363
364 Deprecated,
366
367 Unknown { step: String },
369}
370
371pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJoinStep> {
375 if let Some(invitenumber) = mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
376 Some(SecureJoinStep::Request {
380 invitenumber: invitenumber.to_string(),
381 })
382 } else if let Some(step) = mime_message.get_header(HeaderDef::SecureJoin) {
383 match step {
384 "vg-auth-required" | "vc-auth-required" => Some(SecureJoinStep::AuthRequired),
385 "vg-request-with-auth" | "vc-request-with-auth" => {
386 Some(SecureJoinStep::RequestWithAuth)
387 }
388 "vc-contact-confirm" => Some(SecureJoinStep::ContactConfirm),
389 "vg-member-added" => Some(SecureJoinStep::MemberAdded),
390 "vg-member-added-received" | "vc-contact-confirm-received" => {
391 Some(SecureJoinStep::Deprecated)
392 }
393 step => Some(SecureJoinStep::Unknown {
394 step: step.to_string(),
395 }),
396 }
397 } else {
398 None
399 }
400}
401
402pub(crate) async fn handle_securejoin_handshake(
414 context: &Context,
415 mime_message: &mut MimeMessage,
416 contact_id: ContactId,
417) -> Result<HandshakeMessage> {
418 if contact_id.is_special() {
419 return Err(Error::msg("Can not be called with special contact ID"));
420 }
421
422 let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
423
424 info!(context, "Received secure-join message {step:?}.");
425
426 if !matches!(step, SecureJoinStep::Request { .. }) {
442 let mut self_found = false;
443 let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
444 for (addr, key) in &mime_message.gossiped_keys {
445 if key.public_key.dc_fingerprint() == self_fingerprint
446 && context.is_self_addr(addr).await?
447 {
448 self_found = true;
449 break;
450 }
451 }
452 if !self_found {
453 warn!(context, "Step {step}: No self addr+pubkey gossip found.");
456 return Ok(HandshakeMessage::Ignore);
457 }
458 }
459
460 match step {
461 SecureJoinStep::Request { ref invitenumber } => {
462 if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
472 warn!(context, "Secure-join denied (bad invitenumber).");
473 return Ok(HandshakeMessage::Ignore);
474 }
475
476 let from_addr = ContactAddress::new(&mime_message.from.addr)?;
477 let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
478 let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
479 context,
480 "",
481 &from_addr,
482 autocrypt_fingerprint,
483 Origin::IncomingUnknownFrom,
484 )
485 .await?;
486
487 let prefix = mime_message
488 .get_header(HeaderDef::SecureJoin)
489 .and_then(|step| step.get(..2))
490 .unwrap_or("vc");
491
492 send_alice_handshake_msg(
494 context,
495 autocrypt_contact_id,
496 &format!("{prefix}-auth-required"),
497 )
498 .await
499 .context("failed sending auth-required handshake message")?;
500 Ok(HandshakeMessage::Done)
501 }
502 SecureJoinStep::AuthRequired => {
503 bob::handle_auth_required(context, mime_message).await
508 }
509 SecureJoinStep::RequestWithAuth => {
510 let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
518 warn!(
519 context,
520 "Ignoring {step} message because fingerprint is not provided."
521 );
522 return Ok(HandshakeMessage::Ignore);
523 };
524 let fingerprint: Fingerprint = fp.parse()?;
525 if !encrypted_and_signed(context, mime_message, &fingerprint) {
526 warn!(
527 context,
528 "Ignoring {step} message because the message is not encrypted."
529 );
530 return Ok(HandshakeMessage::Ignore);
531 }
532 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
534 warn!(
535 context,
536 "Ignoring {step} message because of missing auth code."
537 );
538 return Ok(HandshakeMessage::Ignore);
539 };
540 let Some((grpid, timestamp)) = context
541 .sql
542 .query_row_optional(
543 "SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
544 (Namespace::Auth, auth),
545 |row| {
546 let foreign_key: String = row.get(0)?;
547 let timestamp: i64 = row.get(1)?;
548 Ok((foreign_key, timestamp))
549 },
550 )
551 .await?
552 else {
553 warn!(
554 context,
555 "Ignoring {step} message because of invalid auth code."
556 );
557 return Ok(HandshakeMessage::Ignore);
558 };
559 let joining_chat_id = match grpid.as_str() {
560 "" => None,
561 id => {
562 let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
563 warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
564 return Ok(HandshakeMessage::Ignore);
565 };
566 Some(chat_id)
567 }
568 };
569
570 let sender_contact = Contact::get_by_id(context, contact_id).await?;
571 if sender_contact
572 .fingerprint()
573 .is_none_or(|fp| fp != fingerprint)
574 {
575 warn!(
576 context,
577 "Ignoring {step} message because of fingerprint mismatch."
578 );
579 return Ok(HandshakeMessage::Ignore);
580 }
581 info!(context, "Fingerprint verified via Auth code.",);
582
583 if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
585 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
586 }
587 contact_id.regossip_keys(context).await?;
588 ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
589 if grpid.is_empty() {
592 ChatId::create_for_contact(context, contact_id).await?;
593 }
594 context.emit_event(EventType::ContactsChanged(Some(contact_id)));
595 if let Some(joining_chat_id) = joining_chat_id {
596 chat::add_contact_to_chat_ex(context, Nosync, joining_chat_id, contact_id, true)
598 .await?;
599
600 let chat = Chat::load_from_db(context, joining_chat_id).await?;
601
602 if chat.typ == Chattype::OutBroadcast {
603 chat.sync_contacts(context).await.log_err(context).ok();
606 }
607
608 inviter_progress(context, contact_id, joining_chat_id, chat.typ)?;
609 Ok(HandshakeMessage::Done)
612 } else {
613 let chat_id = info_chat_id(context, contact_id).await?;
614 send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
616 .await
617 .context("failed sending vc-contact-confirm message")?;
618
619 inviter_progress(context, contact_id, chat_id, Chattype::Single)?;
620 Ok(HandshakeMessage::Ignore) }
622 }
623 SecureJoinStep::ContactConfirm => {
628 context.emit_event(EventType::SecurejoinJoinerProgress {
629 contact_id,
630 progress: JoinerProgress::Succeeded.into_u16(),
631 });
632 Ok(HandshakeMessage::Ignore)
633 }
634 SecureJoinStep::MemberAdded => {
635 let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
636 else {
637 warn!(
638 context,
639 "vg-member-added without Chat-Group-Member-Added header."
640 );
641 return Ok(HandshakeMessage::Propagate);
642 };
643 if !context.is_self_addr(member_added).await? {
644 info!(
645 context,
646 "Member {member_added} added by unrelated SecureJoin process."
647 );
648 return Ok(HandshakeMessage::Propagate);
649 }
650
651 context.emit_event(EventType::SecurejoinJoinerProgress {
652 contact_id,
653 progress: JoinerProgress::Succeeded.into_u16(),
654 });
655 Ok(HandshakeMessage::Propagate)
656 }
657 SecureJoinStep::Deprecated => {
658 Ok(HandshakeMessage::Done)
660 }
661 SecureJoinStep::Unknown { ref step } => {
662 warn!(context, "Invalid SecureJoin step: {step:?}.");
663 Ok(HandshakeMessage::Ignore)
664 }
665 }
666}
667
668pub(crate) async fn observe_securejoin_on_other_device(
686 context: &Context,
687 mime_message: &MimeMessage,
688 contact_id: ContactId,
689) -> Result<HandshakeMessage> {
690 if contact_id.is_special() {
691 return Err(Error::msg("Can not be called with special contact ID"));
692 }
693 let step = get_secure_join_step(mime_message).context("Not a Secure-Join message")?;
694 info!(context, "Observing secure-join message {step:?}.");
695
696 match step {
697 SecureJoinStep::Request { .. }
698 | SecureJoinStep::AuthRequired
699 | SecureJoinStep::Deprecated
700 | SecureJoinStep::Unknown { .. } => {
701 return Ok(HandshakeMessage::Ignore);
702 }
703 SecureJoinStep::RequestWithAuth
704 | SecureJoinStep::MemberAdded
705 | SecureJoinStep::ContactConfirm => {}
706 }
707
708 if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
709 warn!(
710 context,
711 "Observed SecureJoin message is not encrypted correctly."
712 );
713 return Ok(HandshakeMessage::Ignore);
714 }
715
716 let contact = Contact::get_by_id(context, contact_id).await?;
717 let addr = contact.get_addr().to_lowercase();
718
719 let Some(key) = mime_message.gossiped_keys.get(&addr) else {
720 warn!(context, "No gossip header for {addr} at step {step}.");
721 return Ok(HandshakeMessage::Ignore);
722 };
723
724 let Some(contact_fingerprint) = contact.fingerprint() else {
725 warn!(context, "Contact does not have a fingerprint.");
727 return Ok(HandshakeMessage::Ignore);
728 };
729
730 if key.public_key.dc_fingerprint() != contact_fingerprint {
731 warn!(context, "Fingerprint does not match.");
733 return Ok(HandshakeMessage::Ignore);
734 }
735
736 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
737
738 if matches!(
739 step,
740 SecureJoinStep::MemberAdded | SecureJoinStep::ContactConfirm
741 ) {
742 let chat_type = if mime_message
743 .get_header(HeaderDef::ChatGroupMemberAdded)
744 .is_none()
745 {
746 Chattype::Single
747 } else if mime_message.get_header(HeaderDef::ListId).is_some() {
748 Chattype::OutBroadcast
749 } else {
750 Chattype::Group
751 };
752
753 let chat_id = ChatId::new(0);
761 inviter_progress(context, contact_id, chat_id, chat_type)?;
762 }
763
764 if matches!(step, SecureJoinStep::RequestWithAuth) {
765 ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
769 }
770
771 if matches!(step, SecureJoinStep::MemberAdded) {
772 Ok(HandshakeMessage::Propagate)
773 } else {
774 Ok(HandshakeMessage::Ignore)
775 }
776}
777
778fn encrypted_and_signed(
783 context: &Context,
784 mimeparser: &MimeMessage,
785 expected_fingerprint: &Fingerprint,
786) -> bool {
787 if let Some(signature) = mimeparser.signature.as_ref() {
788 if signature == expected_fingerprint {
789 true
790 } else {
791 warn!(
792 context,
793 "Message does not match expected fingerprint {expected_fingerprint}.",
794 );
795 false
796 }
797 } else {
798 warn!(context, "Message not encrypted.",);
799 false
800 }
801}
802
803#[cfg(test)]
804mod securejoin_tests;