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