1use anyhow::{Context as _, Result};
4
5use super::HandshakeMessage;
6use super::qrinvite::QrInvite;
7use crate::chat::{self, ChatId, is_contact_in_chat};
8use crate::constants::{Blocked, Chattype};
9use crate::contact::{Contact, Origin};
10use crate::context::Context;
11use crate::events::EventType;
12use crate::key::self_fingerprint;
13use crate::log::LogExt;
14use crate::message::{self, Message, MsgId, Viewtype};
15use crate::mimeparser::{MimeMessage, SystemMessage};
16use crate::param::{Param, Params};
17use crate::securejoin::{
18 ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
19};
20use crate::stock_str;
21use crate::sync::Sync::*;
22use crate::tools::{create_outgoing_rfc724_mid, smeared_time, time};
23use crate::{chatlist_events, mimefactory};
24
25pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
47 let private_chat_id = private_chat_id(context, &invite).await?;
51
52 match invite {
53 QrInvite::Group { .. } | QrInvite::Contact { .. } => {
54 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined)
55 .await?;
56 context.emit_event(EventType::ContactsChanged(None));
57 }
58 QrInvite::Broadcast { .. } => {}
59 }
60
61 let has_key = context
62 .sql
63 .exists(
64 "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
65 (invite.fingerprint().hex(),),
66 )
67 .await?;
68
69 {
71 let joining_chat_id = match invite {
74 QrInvite::Group { ref grpid, .. } | QrInvite::Broadcast { ref grpid, .. } => {
75 if let Some((joining_chat_id, _blocked)) =
76 chat::get_chat_id_by_grpid(context, grpid).await?
77 {
78 if is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
79 Some(joining_chat_id)
80 } else {
81 None
82 }
83 } else {
84 None
85 }
86 }
87 QrInvite::Contact { .. } => None,
88 };
89
90 if let Some(joining_chat_id) = joining_chat_id {
91 context.emit_event(EventType::SecurejoinJoinerProgress {
96 contact_id: invite.contact_id(),
97 progress: JoinerProgress::Succeeded.into_u16(),
98 });
99 return Ok(joining_chat_id);
100 } else if has_key
101 && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
102 .await?
103 {
104 info!(context, "Taking securejoin protocol shortcut");
106 send_handshake_message(
107 context,
108 &invite,
109 private_chat_id,
110 BobHandshakeMsg::RequestWithAuth,
111 )
112 .await?;
113
114 context.emit_event(EventType::SecurejoinJoinerProgress {
115 contact_id: invite.contact_id(),
116 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
117 });
118 } else {
119 send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
120 .await?;
121
122 insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
123 }
124 }
125
126 match invite {
127 QrInvite::Group { .. } => {
128 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
129 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
130 chat::add_info_msg(context, joining_chat_id, &msg).await?;
131 Ok(joining_chat_id)
132 }
133 QrInvite::Broadcast { .. } => {
134 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
135 if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
137 chat::add_to_chat_contacts_table(
138 context,
139 time(),
140 joining_chat_id,
141 &[invite.contact_id()],
142 )
143 .await?;
144 }
145
146 if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
148 let msg =
149 stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
150 chat::add_info_msg(context, joining_chat_id, &msg).await?;
151 }
152 Ok(joining_chat_id)
153 }
154 QrInvite::Contact { .. } => {
155 if !has_key {
158 chat::add_info_msg_with_cmd(
159 context,
160 private_chat_id,
161 &stock_str::securejoin_wait(context),
162 SystemMessage::SecurejoinWait,
163 None,
164 time(),
165 None,
166 None,
167 None,
168 )
169 .await?;
170 }
171 Ok(private_chat_id)
172 }
173 }
174}
175
176async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
180 context
184 .sql
185 .insert(
186 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
187 (invite, 0, chat_id),
188 )
189 .await
190}
191
192async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
193 if let Some((msg_id, param)) = context
194 .sql
195 .query_row_optional(
196 "
197SELECT id, param FROM msgs
198WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
199 AND chat_id=? AND hidden=0
200LIMIT 1
201 ",
202 (chat_id, chat_id),
203 |row| {
204 let id: MsgId = row.get(0)?;
205 let param: String = row.get(1)?;
206 let param: Params = param.parse().unwrap_or_default();
207 Ok((id, param))
208 },
209 )
210 .await?
211 && param.get_cmd() == SystemMessage::SecurejoinWait
212 {
213 let on_server = false;
214 msg_id.trash(context, on_server).await?;
215 context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
216 context.emit_msgs_changed_without_msg_id(chat_id);
217 chatlist_events::emit_chatlist_item_changed(context, chat_id);
218 context.emit_msgs_changed_without_ids();
219 chatlist_events::emit_chatlist_changed(context);
220 }
221 Ok(())
222}
223
224pub(super) async fn handle_auth_required_or_pubkey(
229 context: &Context,
230 message: &MimeMessage,
231) -> Result<HandshakeMessage> {
232 let bob_states = context
234 .sql
235 .query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
236 let row_id: i64 = row.get(0)?;
237 let invite: QrInvite = row.get(1)?;
238 Ok((row_id, invite))
239 })
240 .await?;
241
242 info!(
243 context,
244 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
245 );
246
247 let mut auth_sent = false;
248 for (bobstate_row_id, invite) in bob_states {
249 if !encrypted_and_signed(context, message, invite.fingerprint()) {
250 continue;
251 }
252
253 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
254 {
255 continue;
256 }
257
258 info!(context, "Fingerprint verified.",);
259 let chat_id = private_chat_id(context, &invite).await?;
260 delete_securejoin_wait_msg(context, chat_id)
261 .await
262 .context("delete_securejoin_wait_msg")
263 .log_err(context)
264 .ok();
265 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
266 context
267 .sql
268 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
269 .await?;
270
271 match invite {
272 QrInvite::Contact { .. } | QrInvite::Broadcast { .. } => {}
273 QrInvite::Group { .. } => {
274 let contact_id = invite.contact_id();
277 let msg = stock_str::secure_join_replies(context, contact_id).await;
278 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
279 chat::add_info_msg(context, chat_id, &msg).await?;
280 }
281 }
282
283 context.emit_event(EventType::SecurejoinJoinerProgress {
284 contact_id: invite.contact_id(),
285 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
286 });
287
288 auth_sent = true;
289 }
290
291 if auth_sent {
292 Ok(HandshakeMessage::Done)
294 } else {
295 Ok(HandshakeMessage::Ignore)
300 }
301}
302
303pub(crate) async fn send_handshake_message(
305 context: &Context,
306 invite: &QrInvite,
307 chat_id: ChatId,
308 step: BobHandshakeMsg,
309) -> Result<()> {
310 if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
311 let rfc724_mid = create_outgoing_rfc724_mid();
313 let contact = Contact::get_by_id(context, invite.contact_id()).await?;
314 let recipient = contact.get_addr();
315 let alice_fp = invite.fingerprint().hex();
316 let auth = invite.authcode();
317 let shared_secret = format!("securejoin/{alice_fp}/{auth}");
318 let attach_self_pubkey = false;
319 let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
320 context,
321 "vc-request-pubkey",
322 &rfc724_mid,
323 attach_self_pubkey,
324 auth,
325 &shared_secret,
326 )
327 .await?;
328
329 let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
330 insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
331 context.scheduler.interrupt_smtp().await;
332 } else {
333 let mut msg = Message {
334 viewtype: Viewtype::Text,
335 text: step.body_text(invite),
336 hidden: true,
337 ..Default::default()
338 };
339
340 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
341
342 msg.param.set(Param::Arg, step.securejoin_header(invite));
344
345 match step {
346 BobHandshakeMsg::Request => {
347 msg.param.set(Param::Arg2, invite.invitenumber());
349 msg.force_plaintext();
350 }
351 BobHandshakeMsg::RequestWithAuth => {
352 msg.param.set(Param::Arg2, invite.authcode());
354 msg.param.set_int(Param::GuaranteeE2ee, 1);
355
356 let bob_fp = self_fingerprint(context).await?;
358 msg.param.set(Param::Arg3, bob_fp);
359
360 if let QrInvite::Group { grpid, .. } = invite {
369 msg.param.set(Param::Arg4, grpid);
370 }
371 }
372 };
373
374 chat::send_msg(context, chat_id, &mut msg).await?;
375 }
376 Ok(())
377}
378
379pub(crate) enum BobHandshakeMsg {
381 Request,
383 RequestWithAuth,
385}
386
387impl BobHandshakeMsg {
388 fn body_text(&self, invite: &QrInvite) -> String {
394 format!("Secure-Join: {}", self.securejoin_header(invite))
395 }
396
397 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
403 match self {
404 Self::Request => match invite {
405 QrInvite::Contact { .. } => "vc-request",
406 QrInvite::Group { .. } => "vg-request",
407 QrInvite::Broadcast { .. } => "vg-request",
408 },
409 Self::RequestWithAuth => match invite {
410 QrInvite::Contact { .. } => "vc-request-with-auth",
411 QrInvite::Group { .. } => "vg-request-with-auth",
412 QrInvite::Broadcast { .. } => "vg-request-with-auth",
413 },
414 }
415 }
416}
417
418async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
423 let hidden = match invite {
424 QrInvite::Contact { .. } => Blocked::Not,
425 QrInvite::Group { .. } => Blocked::Yes,
426 QrInvite::Broadcast { .. } => Blocked::Yes,
427 };
428
429 ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
430 .await
431 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
432}
433
434async fn joining_chat_id(
442 context: &Context,
443 invite: &QrInvite,
444 alice_chat_id: ChatId,
445) -> Result<ChatId> {
446 match invite {
447 QrInvite::Contact { .. } => Ok(alice_chat_id),
448 QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
449 let chattype = if matches!(invite, QrInvite::Group { .. }) {
450 Chattype::Group
451 } else {
452 Chattype::InBroadcast
453 };
454
455 let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
456 Some((chat_id, _blocked)) => {
457 chat_id.unblock_ex(context, Nosync).await?;
458 chat_id
459 }
460 None => {
461 ChatId::create_multiuser_record(
462 context,
463 chattype,
464 grpid,
465 name,
466 Blocked::Not,
467 None,
468 smeared_time(context),
469 )
470 .await?
471 }
472 };
473 Ok(chat_id)
474 }
475 }
476}
477
478pub(crate) enum JoinerProgress {
483 RequestWithAuthSent,
487 Succeeded,
489}
490
491impl JoinerProgress {
492 pub(crate) fn into_u16(self) -> u16 {
493 match self {
494 JoinerProgress::RequestWithAuthSent => 400,
495 JoinerProgress::Succeeded => 1000,
496 }
497 }
498}