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(
36 context: &Context,
37 contact_id: ContactId,
38 is_group: bool,
39 progress: usize,
40) -> Result<()> {
41 logged_debug_assert!(
42 context,
43 progress == 0 || progress == 1000,
44 "inviter_progress: contact {contact_id}, progress={progress}, but value is not 0 (error) or 1000 (success)."
45 );
46 let chat_type = if is_group {
47 Chattype::Group
48 } else {
49 Chattype::Single
50 };
51
52 context.emit_event(EventType::SecurejoinInviterProgress {
53 contact_id,
54 chat_type,
55 progress,
56 });
57
58 Ok(())
59}
60
61pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
66 ensure_secret_key_exists(context).await.ok();
72
73 let chat = match group {
74 Some(id) => {
75 let chat = Chat::load_from_db(context, id).await?;
76 ensure!(
77 chat.typ == Chattype::Group,
78 "Can't generate SecureJoin QR code for 1:1 chat {id}"
79 );
80 if chat.grpid.is_empty() {
81 let err = format!("Can't generate QR code, chat {id} is a email thread");
82 error!(context, "get_securejoin_qr: {}.", err);
83 bail!(err);
84 }
85 Some(chat)
86 }
87 None => None,
88 };
89 let grpid = chat.as_ref().map(|c| c.grpid.as_str());
90 let sync_token = token::lookup(context, Namespace::InviteNumber, grpid)
91 .await?
92 .is_none();
93 let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
96 let auth = token::lookup_or_new(context, Namespace::Auth, grpid).await?;
97 let self_addr = context.get_primary_self_addr().await?;
98 let self_name = context
99 .get_config(Config::Displayname)
100 .await?
101 .unwrap_or_default();
102
103 let fingerprint = get_self_fingerprint(context).await?;
104
105 let self_addr_urlencoded =
106 utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
107 let self_name_urlencoded =
108 utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
109
110 let qr = if let Some(chat) = chat {
111 let group_name = chat.get_name();
113 let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
114 if sync_token {
115 context
116 .sync_qr_code_tokens(Some(chat.grpid.as_str()))
117 .await?;
118 context.scheduler.interrupt_inbox().await;
119 }
120 format!(
121 "https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
122 fingerprint.hex(),
123 self_addr_urlencoded,
124 &group_name_urlencoded,
125 &chat.grpid,
126 &invitenumber,
127 &auth,
128 )
129 } else {
130 if sync_token {
132 context.sync_qr_code_tokens(None).await?;
133 context.scheduler.interrupt_inbox().await;
134 }
135 format!(
136 "https://i.delta.chat/#{}&a={}&n={}&i={}&s={}",
137 fingerprint.hex(),
138 self_addr_urlencoded,
139 self_name_urlencoded,
140 &invitenumber,
141 &auth,
142 )
143 };
144
145 info!(context, "Generated QR code.");
146 Ok(qr)
147}
148
149async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
150 let key = load_self_public_key(context)
151 .await
152 .context("Failed to load key")?;
153 Ok(key.dc_fingerprint())
154}
155
156pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
163 securejoin(context, qr).await.map_err(|err| {
164 warn!(context, "Fatal joiner error: {:#}", err);
165 error!(context, "QR process failed");
167 err
168 })
169}
170
171async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
172 info!(context, "Requesting secure-join ...",);
178 let qr_scan = check_qr(context, qr).await?;
179
180 let invite = QrInvite::try_from(qr_scan)?;
181
182 bob::start_protocol(context, invite).await
183}
184
185async fn send_alice_handshake_msg(
187 context: &Context,
188 contact_id: ContactId,
189 step: &str,
190) -> Result<()> {
191 let mut msg = Message {
192 viewtype: Viewtype::Text,
193 text: format!("Secure-Join: {step}"),
194 hidden: true,
195 ..Default::default()
196 };
197 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
198 msg.param.set(Param::Arg, step);
199 msg.param.set_int(Param::GuaranteeE2ee, 1);
200 chat::send_msg(
201 context,
202 ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
203 .await?
204 .id,
205 &mut msg,
206 )
207 .await?;
208 Ok(())
209}
210
211async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
213 let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
214 Ok(chat_id_blocked.id)
215}
216
217async fn verify_sender_by_fingerprint(
220 context: &Context,
221 fingerprint: &Fingerprint,
222 contact_id: ContactId,
223) -> Result<bool> {
224 let contact = Contact::get_by_id(context, contact_id).await?;
225 let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
226 if is_verified {
227 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
228 }
229 Ok(is_verified)
230}
231
232#[derive(Debug, PartialEq, Eq)]
239pub(crate) enum HandshakeMessage {
240 Done,
244 Ignore,
251 Propagate,
256}
257
258pub(crate) async fn handle_securejoin_handshake(
270 context: &Context,
271 mime_message: &mut MimeMessage,
272 contact_id: ContactId,
273) -> Result<HandshakeMessage> {
274 if contact_id.is_special() {
275 return Err(Error::msg("Can not be called with special contact ID"));
276 }
277 let step = mime_message
278 .get_header(HeaderDef::SecureJoin)
279 .context("Not a Secure-Join message")?;
280
281 info!(context, "Received secure-join message {step:?}.");
282
283 if !matches!(step, "vg-request" | "vc-request") {
284 let mut self_found = false;
285 let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
286 for (addr, key) in &mime_message.gossiped_keys {
287 if key.public_key.dc_fingerprint() == self_fingerprint
288 && context.is_self_addr(addr).await?
289 {
290 self_found = true;
291 break;
292 }
293 }
294 if !self_found {
295 warn!(context, "Step {step}: No self addr+pubkey gossip found.");
298 return Ok(HandshakeMessage::Ignore);
299 }
300 }
301
302 match step {
303 "vg-request" | "vc-request" => {
304 let invitenumber = match mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
314 Some(n) => n,
315 None => {
316 warn!(context, "Secure-join denied (invitenumber missing)");
317 return Ok(HandshakeMessage::Ignore);
318 }
319 };
320 if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await? {
321 warn!(context, "Secure-join denied (bad invitenumber).");
322 return Ok(HandshakeMessage::Ignore);
323 }
324
325 let from_addr = ContactAddress::new(&mime_message.from.addr)?;
326 let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
327 let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
328 context,
329 "",
330 &from_addr,
331 autocrypt_fingerprint,
332 Origin::IncomingUnknownFrom,
333 )
334 .await?;
335
336 send_alice_handshake_msg(
338 context,
339 autocrypt_contact_id,
340 &format!("{}-auth-required", &step.get(..2).unwrap_or_default()),
341 )
342 .await
343 .context("failed sending auth-required handshake message")?;
344 Ok(HandshakeMessage::Done)
345 }
346 "vg-auth-required" | "vc-auth-required" => {
347 bob::handle_auth_required(context, mime_message).await
352 }
353 "vg-request-with-auth" | "vc-request-with-auth" => {
354 let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
362 warn!(
363 context,
364 "Ignoring {step} message because fingerprint is not provided."
365 );
366 return Ok(HandshakeMessage::Ignore);
367 };
368 let fingerprint: Fingerprint = fp.parse()?;
369 if !encrypted_and_signed(context, mime_message, &fingerprint) {
370 warn!(
371 context,
372 "Ignoring {step} message because the message is not encrypted."
373 );
374 return Ok(HandshakeMessage::Ignore);
375 }
376 let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
378 warn!(
379 context,
380 "Ignoring {step} message because of missing auth code."
381 );
382 return Ok(HandshakeMessage::Ignore);
383 };
384 let Some(grpid) = token::auth_foreign_key(context, auth).await? else {
385 warn!(
386 context,
387 "Ignoring {step} message because of invalid auth code."
388 );
389 return Ok(HandshakeMessage::Ignore);
390 };
391 let group_chat_id = match grpid.as_str() {
392 "" => None,
393 id => {
394 let Some((chat_id, ..)) = get_chat_id_by_grpid(context, id).await? else {
395 warn!(context, "Ignoring {step} message: unknown grpid {id}.",);
396 return Ok(HandshakeMessage::Ignore);
397 };
398 Some(chat_id)
399 }
400 };
401
402 if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
403 warn!(
404 context,
405 "Ignoring {step} message because of fingerprint mismatch."
406 );
407 return Ok(HandshakeMessage::Ignore);
408 }
409 info!(context, "Fingerprint verified via Auth code.",);
410 contact_id.regossip_keys(context).await?;
411 ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
412 if grpid.is_empty() {
415 ChatId::create_for_contact(context, contact_id).await?;
416 }
417 context.emit_event(EventType::ContactsChanged(Some(contact_id)));
418 if let Some(group_chat_id) = group_chat_id {
419 secure_connection_established(
421 context,
422 contact_id,
423 group_chat_id,
424 mime_message.timestamp_sent,
425 )
426 .await?;
427 chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
428 .await?;
429 let is_group = true;
430 inviter_progress(context, contact_id, is_group, 1000)?;
431 Ok(HandshakeMessage::Done)
434 } else {
435 secure_connection_established(
437 context,
438 contact_id,
439 info_chat_id(context, contact_id).await?,
440 mime_message.timestamp_sent,
441 )
442 .await?;
443 send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
444 .await
445 .context("failed sending vc-contact-confirm message")?;
446
447 let is_group = false;
448 inviter_progress(context, contact_id, is_group, 1000)?;
449 Ok(HandshakeMessage::Ignore) }
451 }
452 "vc-contact-confirm" => {
457 context.emit_event(EventType::SecurejoinJoinerProgress {
458 contact_id,
459 progress: JoinerProgress::Succeeded.to_usize(),
460 });
461 Ok(HandshakeMessage::Ignore)
462 }
463 "vg-member-added" => {
464 let Some(member_added) = mime_message.get_header(HeaderDef::ChatGroupMemberAdded)
465 else {
466 warn!(
467 context,
468 "vg-member-added without Chat-Group-Member-Added header."
469 );
470 return Ok(HandshakeMessage::Propagate);
471 };
472 if !context.is_self_addr(member_added).await? {
473 info!(
474 context,
475 "Member {member_added} added by unrelated SecureJoin process."
476 );
477 return Ok(HandshakeMessage::Propagate);
478 }
479
480 context.emit_event(EventType::SecurejoinJoinerProgress {
481 contact_id,
482 progress: JoinerProgress::Succeeded.to_usize(),
483 });
484 Ok(HandshakeMessage::Propagate)
485 }
486
487 "vg-member-added-received" | "vc-contact-confirm-received" => {
488 Ok(HandshakeMessage::Done)
490 }
491 _ => {
492 warn!(context, "invalid step: {}", step);
493 Ok(HandshakeMessage::Ignore)
494 }
495 }
496}
497
498pub(crate) async fn observe_securejoin_on_other_device(
516 context: &Context,
517 mime_message: &MimeMessage,
518 contact_id: ContactId,
519) -> Result<HandshakeMessage> {
520 if contact_id.is_special() {
521 return Err(Error::msg("Can not be called with special contact ID"));
522 }
523 let step = mime_message
524 .get_header(HeaderDef::SecureJoin)
525 .context("Not a Secure-Join message")?;
526 info!(context, "Observing secure-join message {step:?}.");
527
528 if !matches!(
529 step,
530 "vg-request-with-auth" | "vc-request-with-auth" | "vg-member-added" | "vc-contact-confirm"
531 ) {
532 return Ok(HandshakeMessage::Ignore);
533 };
534
535 if !encrypted_and_signed(context, mime_message, &get_self_fingerprint(context).await?) {
536 warn!(
537 context,
538 "Observed SecureJoin message is not encrypted correctly."
539 );
540 return Ok(HandshakeMessage::Ignore);
541 }
542
543 let contact = Contact::get_by_id(context, contact_id).await?;
544 let addr = contact.get_addr().to_lowercase();
545
546 let Some(key) = mime_message.gossiped_keys.get(&addr) else {
547 warn!(context, "No gossip header for {addr} at step {step}.");
548 return Ok(HandshakeMessage::Ignore);
549 };
550
551 let Some(contact_fingerprint) = contact.fingerprint() else {
552 warn!(context, "Contact does not have a fingerprint.");
554 return Ok(HandshakeMessage::Ignore);
555 };
556
557 if key.public_key.dc_fingerprint() != contact_fingerprint {
558 warn!(context, "Fingerprint does not match.");
560 return Ok(HandshakeMessage::Ignore);
561 }
562
563 mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
564
565 ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
566
567 if step == "vg-member-added" || step == "vc-contact-confirm" {
568 let is_group = mime_message
569 .get_header(HeaderDef::ChatGroupMemberAdded)
570 .is_some();
571 inviter_progress(context, contact_id, is_group, 1000)?;
572 }
573
574 if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
575 ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await?;
579 }
580
581 if step == "vg-member-added" {
582 Ok(HandshakeMessage::Propagate)
583 } else {
584 Ok(HandshakeMessage::Ignore)
585 }
586}
587
588async fn secure_connection_established(
589 context: &Context,
590 contact_id: ContactId,
591 chat_id: ChatId,
592 timestamp: i64,
593) -> Result<()> {
594 let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
595 .await?
596 .id;
597 private_chat_id
598 .set_protection(
599 context,
600 ProtectionStatus::Protected,
601 timestamp,
602 Some(contact_id),
603 )
604 .await?;
605 context.emit_event(EventType::ChatModified(chat_id));
606 chatlist_events::emit_chatlist_item_changed(context, chat_id);
607 Ok(())
608}
609
610fn encrypted_and_signed(
615 context: &Context,
616 mimeparser: &MimeMessage,
617 expected_fingerprint: &Fingerprint,
618) -> bool {
619 if !mimeparser.was_encrypted() {
620 warn!(context, "Message not encrypted.",);
621 false
622 } else if !mimeparser.signatures.contains(expected_fingerprint) {
623 warn!(
624 context,
625 "Message does not match expected fingerprint {}.", expected_fingerprint,
626 );
627 false
628 } else {
629 true
630 }
631}
632
633#[cfg(test)]
634mod securejoin_tests;