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).await,
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 attach_self_pubkey = false;
316 let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
317 context,
318 "vc-request-pubkey",
319 &rfc724_mid,
320 attach_self_pubkey,
321 invite.authcode(),
322 )
323 .await?;
324
325 let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
326 insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
327 context.scheduler.interrupt_smtp().await;
328 } else {
329 let mut msg = Message {
330 viewtype: Viewtype::Text,
331 text: step.body_text(invite),
332 hidden: true,
333 ..Default::default()
334 };
335
336 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
337
338 msg.param.set(Param::Arg, step.securejoin_header(invite));
340
341 match step {
342 BobHandshakeMsg::Request => {
343 msg.param.set(Param::Arg2, invite.invitenumber());
345 msg.force_plaintext();
346 }
347 BobHandshakeMsg::RequestWithAuth => {
348 msg.param.set(Param::Arg2, invite.authcode());
350 msg.param.set_int(Param::GuaranteeE2ee, 1);
351
352 let bob_fp = self_fingerprint(context).await?;
354 msg.param.set(Param::Arg3, bob_fp);
355
356 if let QrInvite::Group { grpid, .. } = invite {
365 msg.param.set(Param::Arg4, grpid);
366 }
367 }
368 };
369
370 chat::send_msg(context, chat_id, &mut msg).await?;
371 }
372 Ok(())
373}
374
375pub(crate) enum BobHandshakeMsg {
377 Request,
379 RequestWithAuth,
381}
382
383impl BobHandshakeMsg {
384 fn body_text(&self, invite: &QrInvite) -> String {
390 format!("Secure-Join: {}", self.securejoin_header(invite))
391 }
392
393 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
399 match self {
400 Self::Request => match invite {
401 QrInvite::Contact { .. } => "vc-request",
402 QrInvite::Group { .. } => "vg-request",
403 QrInvite::Broadcast { .. } => "vg-request",
404 },
405 Self::RequestWithAuth => match invite {
406 QrInvite::Contact { .. } => "vc-request-with-auth",
407 QrInvite::Group { .. } => "vg-request-with-auth",
408 QrInvite::Broadcast { .. } => "vg-request-with-auth",
409 },
410 }
411 }
412}
413
414async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
419 let hidden = match invite {
420 QrInvite::Contact { .. } => Blocked::Not,
421 QrInvite::Group { .. } => Blocked::Yes,
422 QrInvite::Broadcast { .. } => Blocked::Yes,
423 };
424
425 ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
426 .await
427 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
428}
429
430async fn joining_chat_id(
438 context: &Context,
439 invite: &QrInvite,
440 alice_chat_id: ChatId,
441) -> Result<ChatId> {
442 match invite {
443 QrInvite::Contact { .. } => Ok(alice_chat_id),
444 QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
445 let chattype = if matches!(invite, QrInvite::Group { .. }) {
446 Chattype::Group
447 } else {
448 Chattype::InBroadcast
449 };
450
451 let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
452 Some((chat_id, _blocked)) => {
453 chat_id.unblock_ex(context, Nosync).await?;
454 chat_id
455 }
456 None => {
457 ChatId::create_multiuser_record(
458 context,
459 chattype,
460 grpid,
461 name,
462 Blocked::Not,
463 None,
464 smeared_time(context),
465 )
466 .await?
467 }
468 };
469 Ok(chat_id)
470 }
471 }
472}
473
474pub(crate) enum JoinerProgress {
479 RequestWithAuthSent,
483 Succeeded,
485}
486
487impl JoinerProgress {
488 pub(crate) fn into_u16(self) -> u16 {
489 match self {
490 JoinerProgress::RequestWithAuthSent => 400,
491 JoinerProgress::Succeeded => 1000,
492 }
493 }
494}