1use anyhow::{Context as _, Error, Result, ensure};
4use deltachat_contact_tools::ContactAddress;
5use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
6
7use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
8use crate::chatlist_events;
9use crate::config::Config;
10use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
11use crate::contact::mark_contact_id_as_verified;
12use crate::contact::{Contact, ContactId, Origin};
13use crate::context::Context;
14use crate::e2ee::ensure_secret_key_exists;
15use crate::events::EventType;
16use crate::headerdef::HeaderDef;
17use crate::key::{DcKey, Fingerprint, load_self_public_key};
18use crate::log::{error, info, warn};
19use crate::logged_debug_assert;
20use crate::message::{Message, Viewtype};
21use crate::mimeparser::{MimeMessage, SystemMessage};
22use crate::param::Param;
23use crate::qr::check_qr;
24use crate::securejoin::bob::JoinerProgress;
25use crate::sync::Sync::*;
26use crate::token;
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 logged_debug_assert!(
37 context,
38 progress <= 1000,
39 "inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success."
40 );
41 context.emit_event(EventType::SecurejoinInviterProgress {
42 contact_id,
43 progress,
44 });
45}
46
47pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
52 ensure_secret_key_exists(context).await.ok();
58
59 let chat = match group {
60 Some(id) => {
61 let chat = Chat::load_from_db(context, id).await?;
62 ensure!(
63 chat.typ == Chattype::Group,
64 "Can't generate SecureJoin QR code for 1:1 chat {id}"
65 );
66 ensure!(
67 !chat.grpid.is_empty(),
68 "Can't generate SecureJoin QR code for ad-hoc group {id}"
69 );
70 Some(chat)
71 }
72 None => None,
73 };
74 let grpid = chat.as_ref().map(|c| c.grpid.as_str());
75 let sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
76 .await?
77 .is_none();
78 let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
81 let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?;
82 let self_addr = context.get_primary_self_addr().await?;
83 let self_name = context
84 .get_config(Config::Displayname)
85 .await?
86 .unwrap_or_default();
87
88 let fingerprint = get_self_fingerprint(context).await?;
89
90 let self_addr_urlencoded =
91 utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
92 let self_name_urlencoded =
93 utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
94
95 let qr = if let Some(chat) = chat {
96 let group_name = chat.get_name();
98 let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
99 if sync_token {
100 context
101 .sync_qr_code_tokens(Some(chat.grpid.as_str()))
102 .await?;
103 context.scheduler.interrupt_inbox().await;
104 }
105 format!(
106 "https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
107 fingerprint.hex(),
108 self_addr_urlencoded,
109 &group_name_urlencoded,
110 &chat.grpid,
111 &invitenumber,
112 &auth,
113 )
114 } else {
115 if sync_token {
117 context.sync_qr_code_tokens(None).await?;
118 context.scheduler.interrupt_inbox().await;
119 }
120 format!(
121 "https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
122 fingerprint.hex(),
123 self_addr_urlencoded,
124 self_name_urlencoded,
125 &invitenumber,
126 &auth,
127 )
128 };
129
130 info!(context, "Generated QR code.");
131 Ok(qr)
132}
133
134async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
135 let key = load_self_public_key(context)
136 .await
137 .context("Failed to load key")?;
138 Ok(key.dc_fingerprint())
139}
140
141pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
148 securejoin(context, qr).await.map_err(|err| {
149 warn!(context, "Fatal joiner error: {:#}", err);
150 error!(context, "QR process failed");
152 err
153 })
154}
155
156async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
157 info!(context, "Requesting secure-join ...",);
163 let qr_scan = check_qr(context, qr).await?;
164
165 let invite = QrInvite::try_from(qr_scan)?;
166
167 bob::start_protocol(context, invite).await
168}
169
170async fn send_alice_handshake_msg(
172 context: &Context,
173 contact_id: ContactId,
174 step: &str,
175) -> Result<()> {
176 let mut msg = Message {
177 viewtype: Viewtype::Text,
178 text: format!("Secure-Join: {step}"),
179 hidden: true,
180 ..Default::default()
181 };
182 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
183 msg.param.set(Param::Arg, step);
184 msg.param.set_int(Param::GuaranteeE2ee, 1);
185 chat::send_msg(
186 context,
187 ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
188 .await?
189 .id,
190 &mut msg,
191 )
192 .await?;
193 Ok(())
194}
195
196async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
198 let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
199 Ok(chat_id_blocked.id)
200}
201
202async fn verify_sender_by_fingerprint(
205 context: &Context,
206 fingerprint: &Fingerprint,
207 contact_id: ContactId,
208) -> Result<bool> {
209 let contact = Contact::get_by_id(context, contact_id).await?;
210 let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
211 if is_verified {
212 mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?;
213 }
214 Ok(is_verified)
215}
216
217#[derive(Debug, PartialEq, Eq)]
224pub(crate) enum HandshakeMessage {
225 Done,
229 Ignore,
236 Propagate,
241}
242
243pub(crate) async fn handle_securejoin_handshake(
255 context: &Context,
256 mime_message: &mut MimeMessage,
257 contact_id: ContactId,
258) -> Result<HandshakeMessage> {
259 if contact_id.is_special() {
260 return Err(Error::msg("Can not be called with special contact ID"));
261 }
262 let step = mime_message
263 .get_header(HeaderDef::SecureJoin)
264 .context("Not a Secure-Join message")?;
265
266 info!(context, "Received secure-join message {step:?}.");
267
268 let join_vg = step.starts_with("vg-");
269
270 if !matches!(step, "vg-request" | "vc-request") {
271 let mut self_found = false;
272 let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
273 for (addr, key) in &mime_message.gossiped_keys {
274 if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
275 self_found = true;
276 break;
277 }
278 }
279 if !self_found {
280 warn!(context, "Step {step}: No self addr+pubkey gossip found.");
283 return Ok(HandshakeMessage::Ignore);
284 }
285 }
286
287 match step {
288 "vg-request" | "vc-request" => {
289 let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
299 Some(n) => n,
300 None => {
301 warn!(context, "Secure-join denied (invitenumber missing)");
302 return Ok(HandshakeMessage::Ignore);
303 }
304 };
305 if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
306 warn!(context, "Secure-join denied (bad invitenumber).");
307 return Ok(HandshakeMessage::Ignore);
308 }
309
310 inviter_progress(context, contact_id, 300);
311
312 let from_addr = ContactAddress::new(&mime_message.from.addr)?;
313 let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
314 let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
315 context,
316 "",
317 &from_addr,
318 autocrypt_fingerprint,
319 Origin::IncomingUnknownFrom,
320 )
321 .await?;
322
323 send_alice_handshake_msg(
325 context,
326 autocrypt_contact_id,
327 &format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
328 )
329 .await
330 .context("failed sending auth-required handshake message")?;
331 Ok(HandshakeMessage::Done)
332 }
333 "vg-auth-required" | "vc-auth-required" => {
334 bob::handle_auth_required(context, mime_message).await
339 }
340 "vg-request-with-auth" | "vc-request-with-auth" => {
341 let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
349 warn!(
350 context,
351 "Ignoring {step} message because fingerprint is not provided."
352 );
353 return Ok(HandshakeMessage::Ignore);
354 };
355 let fingerprint: Fingerprint = fp.parse()?;
356 if !encrypted_and_signed(context, mime_message, &fingerprint) {
357 warn!(
358 context,
359 "Ignoring {step} message because the message is not encrypted."
360 );
361 return Ok(HandshakeMessage::Ignore);
362 }
363 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
365 warn!(
366 context,
367 "Ignoring {step} message because of missing auth code."
368 );
369 return Ok(HandshakeMessage::Ignore);
370 };
371 let Some(grpid) = token::auth_foreign_key(context, auth).await? else {
372 warn!(
373 context,
374 "Ignoring {step} message because of invalid auth code."
375 );
376 return Ok(HandshakeMessage::Ignore);
377 };
378 let group_chat_id = match grpid.as_str() {
379 "" => None,
380 id => {
381 let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
382 warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
383 return Ok(HandshakeMessage::Ignore);
384 };
385 Some(chat_id)
386 }
387 };
388
389 if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
390 warn!(
391 context,
392 "Ignoring {step} message because of fingerprint mismatch."
393 );
394 return Ok(HandshakeMessage::Ignore);
395 }
396 info!(context, "Fingerprint verified via Auth code.",);
397 contact_id.regossip_keys(context).await?;
398 ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
399 if !join_vg {
402 ChatId::create_for_contact(context, contact_id).await?;
403 }
404 context.emit_event(EventType::ContactsChanged(Some(contact_id)));
405 inviter_progress(context, contact_id, 600);
406 if let Some(group_chat_id) = group_chat_id {
407 secure_connection_established(
409 context,
410 contact_id,
411 group_chat_id,
412 mime_message.timestamp_sent,
413 )
414 .await?;
415 chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
416 .await?;
417 inviter_progress(context, contact_id, 800);
418 inviter_progress(context, contact_id, 1000);
419 Ok(HandshakeMessage::Done)
422 } else {
423 secure_connection_established(
425 context,
426 contact_id,
427 info_chat_id(context, contact_id).await?,
428 mime_message.timestamp_sent,
429 )
430 .await?;
431 send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
432 .await
433 .context("failed sending vc-contact-confirm message")?;
434
435 inviter_progress(context, contact_id, 1000);
436 Ok(HandshakeMessage::Ignore) }
438 }
439 "vc-contact-confirm" => {
444 context.emit_event(EventType::SecurejoinJoinerProgress {
445 contact_id,
446 progress: JoinerProgress::Succeeded.to_usize(),
447 });
448 Ok(HandshakeMessage::Ignore)
449 }
450 "vg-member-added" => {
451 let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
452 else {
453 warn!(
454 context,
455 "vg-member-added without Chat-Group-Member-Added header."
456 );
457 return Ok(HandshakeMessage::Propagate);
458 };
459 if !context.is_self_addr(member_added).await? {
460 info!(
461 context,
462 "Member {member_added} added by unrelated SecureJoin process."
463 );
464 return Ok(HandshakeMessage::Propagate);
465 }
466
467 context.emit_event(EventType::SecurejoinJoinerProgress {
468 contact_id,
469 progress: JoinerProgress::Succeeded.to_usize(),
470 });
471 Ok(HandshakeMessage::Propagate)
472 }
473
474 "vg-member-added-received" | "vc-contact-confirm-received" => {
475 Ok(HandshakeMessage::Done)
477 }
478 _ => {
479 warn!(context, "invalid step: {}", step);
480 Ok(HandshakeMessage::Ignore)
481 }
482 }
483}
484
485pub(crate) async fn observe_securejoin_on_other_device(
503 context: &Context,
504 mime_message: &MimeMessage,
505 contact_id: ContactId,
506) -> Result<HandshakeMessage> {
507 if contact_id.is_special() {
508 return Err(Error::msg("Can not be called with special contact ID"));
509 }
510 let step = mime_message
511 .get_header(HeaderDef::SecureJoin)
512 .context("Not a Secure-Join message")?;
513 info!(context, "Observing secure-join message {step:?}.");
514
515 if !matches!(
516 step,
517 "vg-request-with-auth" | "vc-request-with-auth" | "vg-member-added" | "vc-contact-confirm"
518 ) {
519 return Ok(HandshakeMessage::Ignore);
520 };
521
522 if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
523 warn!(
524 context,
525 "Observed SecureJoin message is not encrypted correctly."
526 );
527 return Ok(HandshakeMessage::Ignore);
528 }
529
530 let contact = Contact::get_by_id(context, contact_id).await?;
531 let addr = contact.get_addr().to_lowercase();
532
533 let Some(key) = mime_message.gossiped_keys.get(&addr) else {
534 warn!(context, "No gossip header for {addr} at step {step}.");
535 return Ok(HandshakeMessage::Ignore);
536 };
537
538 let Some(contact_fingerprint) = contact.fingerprint() else {
539 warn!(context, "Contact does not have a fingerprint.");
541 return Ok(HandshakeMessage::Ignore);
542 };
543
544 if key.dc_fingerprint() != contact_fingerprint {
545 warn!(context, "Fingerprint does not match.");
547 return Ok(HandshakeMessage::Ignore);
548 }
549
550 mark_contact_id_as_verified(context, contact_id, ContactId::SELF).await?;
551
552 ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
553
554 if step == "vg-member-added" {
555 inviter_progress(context, contact_id, 800);
556 }
557 if step == "vg-member-added" || step == "vc-contact-confirm" {
558 inviter_progress(context, contact_id, 1000);
559 }
560
561 if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
562 ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
566 }
567
568 if step == "vg-member-added" {
569 Ok(HandshakeMessage::Propagate)
570 } else {
571 Ok(HandshakeMessage::Ignore)
572 }
573}
574
575async fn secure_connection_established(
576 context: &Context,
577 contact_id: ContactId,
578 chat_id: ChatId,
579 timestamp: i64,
580) -> Result<()> {
581 let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
582 .await?
583 .id;
584 private_chat_id
585 .set_protection(
586 context,
587 ProtectionStatus::Protected,
588 timestamp,
589 Some(contact_id),
590 )
591 .await?;
592 context.emit_event(EventType::ChatModified(chat_id));
593 chatlist_events::emit_chatlist_item_changed(context, chat_id);
594 Ok(())
595}
596
597fn encrypted_and_signed(
602 context: &Context,
603 mimeparser: &MimeMessage,
604 expected_fingerprint: &Fingerprint,
605) -> bool {
606 if !mimeparser.was_encrypted() {
607 warn!(context, "Message not encrypted.",);
608 false
609 } else if !mimeparser.signatures.contains(expected_fingerprint) {
610 warn!(
611 context,
612 "Message does not match expected fingerprint {}.", expected_fingerprint,
613 );
614 false
615 } else {
616 true
617 }
618}
619
620#[cfg(test)]
621mod securejoin_tests;