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