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