1use 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
46pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
51 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 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 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 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
140pub 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 error!(context, "QR process failed");
151 err
152 })
153}
154
155async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
156 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
169async 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
195async 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
201async 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#[derive(Debug, PartialEq, Eq)]
249pub(crate) enum HandshakeMessage {
250 Done,
254 Ignore,
261 Propagate,
266}
267
268pub(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 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 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 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 bob::handle_auth_required(context, mime_message).await
353 }
354 "vg-request-with-auth" | "vc-request-with-auth" => {
355 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 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 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 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 Ok(HandshakeMessage::Done)
456 } else {
457 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) }
472 }
473 "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 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 Ok(HandshakeMessage::Done)
522 }
523 _ => {
524 warn!(context, "invalid step: {}", step);
525 Ok(HandshakeMessage::Ignore)
526 }
527 }
528}
529
530pub(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 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
696async 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
721fn 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;