1use anyhow::{Context as _, Result};
4use pgp::composed::SignedPublicKey;
5
6use super::HandshakeMessage;
7use super::qrinvite::QrInvite;
8use crate::chat::{self, ChatId, is_contact_in_chat};
9use crate::constants::{Blocked, Chattype};
10use crate::contact::Origin;
11use crate::context::Context;
12use crate::events::EventType;
13use crate::key::{DcKey as _, self_fingerprint};
14use crate::log::LogExt;
15use crate::message::{self, Message, MsgId, Viewtype};
16use crate::mimeparser::{MimeMessage, SystemMessage};
17use crate::param::{Param, Params};
18use crate::pgp::addresses_from_public_key;
19use crate::securejoin::{
20 ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
21};
22use crate::stock_str;
23use crate::sync::Sync::*;
24use crate::tools::{create_outgoing_rfc724_mid, time};
25use crate::{chatlist_events, mimefactory};
26
27pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
49 let private_chat_id = private_chat_id(context, &invite).await?;
53
54 match invite {
55 QrInvite::Group { .. } | QrInvite::Contact { .. } => {
56 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined)
57 .await?;
58 context.emit_event(EventType::ContactsChanged(None));
59 }
60 QrInvite::Broadcast { .. } => {}
61 }
62
63 let public_key_bytes: Option<Vec<u8>> = context
64 .sql
65 .query_get_value(
66 "SELECT public_key FROM public_keys WHERE fingerprint=?",
67 (invite.fingerprint().hex(),),
68 )
69 .await?;
70
71 let key_contains_all_invite_addrs = if let Some(public_key_bytes) = public_key_bytes {
72 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
73 if let Some(addrs_in_key) = addresses_from_public_key(&public_key) {
74 invite.addrs().iter().all(|a| addrs_in_key.contains(a))
75 } else {
76 false
80 }
81 } else {
82 false
83 };
84
85 {
87 let joining_chat_id = match invite {
90 QrInvite::Group { ref grpid, .. } | QrInvite::Broadcast { ref grpid, .. } => {
91 if let Some((joining_chat_id, _blocked)) =
92 chat::get_chat_id_by_grpid(context, grpid).await?
93 {
94 if is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
95 Some(joining_chat_id)
96 } else {
97 None
98 }
99 } else {
100 None
101 }
102 }
103 QrInvite::Contact { .. } => None,
104 };
105
106 if let Some(joining_chat_id) = joining_chat_id {
107 context.emit_event(EventType::SecurejoinJoinerProgress {
112 contact_id: invite.contact_id(),
113 progress: JoinerProgress::Succeeded.into_u16(),
114 });
115 return Ok(joining_chat_id);
116 } else if key_contains_all_invite_addrs
117 && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
118 .await?
119 {
120 info!(context, "Taking securejoin protocol shortcut");
122 send_handshake_message(
123 context,
124 &invite,
125 private_chat_id,
126 BobHandshakeMsg::RequestWithAuth,
127 )
128 .await?;
129
130 context.emit_event(EventType::SecurejoinJoinerProgress {
131 contact_id: invite.contact_id(),
132 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
133 });
134 } else {
135 send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
136 .await?;
137
138 insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
139 }
140 }
141
142 match invite {
143 QrInvite::Group { .. } => {
144 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
145 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
146 chat::add_info_msg(context, joining_chat_id, &msg).await?;
147 Ok(joining_chat_id)
148 }
149 QrInvite::Broadcast { .. } => {
150 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
151 if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
153 chat::add_to_chat_contacts_table(
154 context,
155 time(),
156 joining_chat_id,
157 &[invite.contact_id()],
158 )
159 .await?;
160 }
161
162 if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
164 let msg =
165 stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
166 chat::add_info_msg(context, joining_chat_id, &msg).await?;
167 }
168 Ok(joining_chat_id)
169 }
170 QrInvite::Contact { .. } => {
171 if !key_contains_all_invite_addrs {
174 chat::add_info_msg_with_cmd(
175 context,
176 private_chat_id,
177 &stock_str::securejoin_wait(context),
178 SystemMessage::SecurejoinWait,
179 None,
180 time(),
181 None,
182 None,
183 None,
184 )
185 .await?;
186 }
187 Ok(private_chat_id)
188 }
189 }
190}
191
192async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
196 context
200 .sql
201 .insert(
202 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
203 (invite, 0, chat_id),
204 )
205 .await
206}
207
208async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
209 if let Some((msg_id, param)) = context
210 .sql
211 .query_row_optional(
212 "
213SELECT id, param FROM msgs
214WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
215 AND chat_id=? AND hidden=0
216LIMIT 1
217 ",
218 (chat_id, chat_id),
219 |row| {
220 let id: MsgId = row.get(0)?;
221 let param: String = row.get(1)?;
222 let param: Params = param.parse().unwrap_or_default();
223 Ok((id, param))
224 },
225 )
226 .await?
227 && param.get_cmd() == SystemMessage::SecurejoinWait
228 {
229 let on_server = false;
230 msg_id.trash(context, on_server).await?;
231 context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
232 context.emit_msgs_changed_without_msg_id(chat_id);
233 chatlist_events::emit_chatlist_item_changed(context, chat_id);
234 context.emit_msgs_changed_without_ids();
235 chatlist_events::emit_chatlist_changed(context);
236 }
237 Ok(())
238}
239
240pub(super) async fn handle_auth_required_or_pubkey(
245 context: &Context,
246 message: &MimeMessage,
247) -> Result<HandshakeMessage> {
248 let bob_states = context
250 .sql
251 .query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
252 let row_id: i64 = row.get(0)?;
253 let invite: QrInvite = row.get(1)?;
254 Ok((row_id, invite))
255 })
256 .await?;
257
258 info!(
259 context,
260 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
261 );
262
263 let mut auth_sent = false;
264 for (bobstate_row_id, invite) in bob_states {
265 if !encrypted_and_signed(context, message, invite.fingerprint()) {
266 continue;
267 }
268
269 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
270 {
271 continue;
272 }
273
274 info!(context, "Fingerprint verified.",);
275 let chat_id = private_chat_id(context, &invite).await?;
276 delete_securejoin_wait_msg(context, chat_id)
277 .await
278 .context("delete_securejoin_wait_msg")
279 .log_err(context)
280 .ok();
281 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
282 context
283 .sql
284 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
285 .await?;
286
287 match invite {
288 QrInvite::Contact { .. } | QrInvite::Broadcast { .. } => {}
289 QrInvite::Group { .. } => {
290 let contact_id = invite.contact_id();
293 let msg = stock_str::secure_join_replies(context, contact_id).await;
294 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
295 chat::add_info_msg(context, chat_id, &msg).await?;
296 }
297 }
298
299 context.emit_event(EventType::SecurejoinJoinerProgress {
300 contact_id: invite.contact_id(),
301 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
302 });
303
304 auth_sent = true;
305 }
306
307 if auth_sent {
308 Ok(HandshakeMessage::Done)
310 } else {
311 Ok(HandshakeMessage::Ignore)
316 }
317}
318
319pub(crate) async fn send_handshake_message(
321 context: &Context,
322 invite: &QrInvite,
323 chat_id: ChatId,
324 step: BobHandshakeMsg,
325) -> Result<()> {
326 if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
327 let rfc724_mid = create_outgoing_rfc724_mid();
329 let recipients = invite.addrs().join(" ");
330 let alice_fp = invite.fingerprint().hex();
331 let auth = invite.authcode();
332 let shared_secret = format!("securejoin/{alice_fp}/{auth}");
333 let attach_self_pubkey = false;
334 let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
335 context,
336 "vc-request-pubkey",
337 &rfc724_mid,
338 attach_self_pubkey,
339 auth,
340 &shared_secret,
341 )
342 .await?;
343
344 let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
345 insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message, msg_id).await?;
346 context.scheduler.interrupt_smtp().await;
347 } else {
348 let mut msg = Message {
349 viewtype: Viewtype::Text,
350 text: step.body_text(invite),
351 hidden: true,
352 ..Default::default()
353 };
354
355 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
356
357 msg.param.set(Param::Arg, step.securejoin_header(invite));
359
360 match step {
361 BobHandshakeMsg::Request => {
362 msg.param.set(Param::Arg2, invite.invitenumber());
364 msg.force_plaintext();
365 }
366 BobHandshakeMsg::RequestWithAuth => {
367 msg.param.set(Param::Arg2, invite.authcode());
369 msg.param.set_int(Param::GuaranteeE2ee, 1);
370
371 let bob_fp = self_fingerprint(context).await?;
373 msg.param.set(Param::Arg3, bob_fp);
374
375 if let QrInvite::Group { grpid, .. } = invite {
384 msg.param.set(Param::Arg4, grpid);
385 }
386 }
387 };
388
389 chat::send_msg(context, chat_id, &mut msg).await?;
390 }
391 Ok(())
392}
393
394pub(crate) enum BobHandshakeMsg {
396 Request,
398 RequestWithAuth,
400}
401
402impl BobHandshakeMsg {
403 fn body_text(&self, invite: &QrInvite) -> String {
409 format!("Secure-Join: {}", self.securejoin_header(invite))
410 }
411
412 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
418 match self {
419 Self::Request => match invite {
420 QrInvite::Contact { .. } => "vc-request",
421 QrInvite::Group { .. } => "vg-request",
422 QrInvite::Broadcast { .. } => "vg-request",
423 },
424 Self::RequestWithAuth => match invite {
425 QrInvite::Contact { .. } => "vc-request-with-auth",
426 QrInvite::Group { .. } => "vg-request-with-auth",
427 QrInvite::Broadcast { .. } => "vg-request-with-auth",
428 },
429 }
430 }
431}
432
433async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
438 let hidden = match invite {
439 QrInvite::Contact { .. } => Blocked::Not,
440 QrInvite::Group { .. } => Blocked::Yes,
441 QrInvite::Broadcast { .. } => Blocked::Yes,
442 };
443
444 ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
445 .await
446 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
447}
448
449async fn joining_chat_id(
457 context: &Context,
458 invite: &QrInvite,
459 alice_chat_id: ChatId,
460) -> Result<ChatId> {
461 match invite {
462 QrInvite::Contact { .. } => Ok(alice_chat_id),
463 QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
464 let chattype = if matches!(invite, QrInvite::Group { .. }) {
465 Chattype::Group
466 } else {
467 Chattype::InBroadcast
468 };
469
470 let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
471 Some((chat_id, _blocked)) => {
472 chat_id.unblock_ex(context, Nosync).await?;
473 chat_id
474 }
475 None => {
476 ChatId::create_multiuser_record(
477 context,
478 chattype,
479 grpid,
480 name,
481 Blocked::Not,
482 None,
483 time(),
484 )
485 .await?
486 }
487 };
488 Ok(chat_id)
489 }
490 }
491}
492
493pub(crate) enum JoinerProgress {
498 RequestWithAuthSent,
502 Succeeded,
504}
505
506impl JoinerProgress {
507 pub(crate) fn into_u16(self) -> u16 {
508 match self {
509 JoinerProgress::RequestWithAuthSent => 400,
510 JoinerProgress::Succeeded => 1000,
511 }
512 }
513}