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