1use anyhow::{Context as _, Result};
4
5use super::HandshakeMessage;
6use super::qrinvite::QrInvite;
7use crate::chat::{self, ChatId, is_contact_in_chat};
8use crate::chatlist_events;
9use crate::constants::{Blocked, Chattype};
10use crate::contact::Origin;
11use crate::context::Context;
12use crate::events::EventType;
13use crate::key::self_fingerprint;
14use crate::log::LogExt;
15use crate::message::{Message, MsgId, Viewtype};
16use crate::mimeparser::{MimeMessage, SystemMessage};
17use crate::param::{Param, Params};
18use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
19use crate::stock_str;
20use crate::sync::Sync::*;
21use crate::tools::{smeared_time, time};
22
23pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
45 let private_chat_id = private_chat_id(context, &invite).await?;
49
50 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
51 context.emit_event(EventType::ContactsChanged(None));
52
53 let has_key = context
54 .sql
55 .exists(
56 "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
57 (invite.fingerprint().hex(),),
58 )
59 .await?;
60
61 {
63 let joining_chat_id = match invite {
66 QrInvite::Group { ref grpid, .. } | QrInvite::Broadcast { ref grpid, .. } => {
67 if let Some((joining_chat_id, _blocked)) =
68 chat::get_chat_id_by_grpid(context, grpid).await?
69 {
70 if is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
71 Some(joining_chat_id)
72 } else {
73 None
74 }
75 } else {
76 None
77 }
78 }
79 QrInvite::Contact { .. } => None,
80 };
81
82 if let Some(joining_chat_id) = joining_chat_id {
83 context.emit_event(EventType::SecurejoinJoinerProgress {
88 contact_id: invite.contact_id(),
89 progress: JoinerProgress::Succeeded.into_u16(),
90 });
91 return Ok(joining_chat_id);
92 } else if has_key
93 && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
94 .await?
95 {
96 info!(context, "Taking securejoin protocol shortcut");
98 send_handshake_message(
99 context,
100 &invite,
101 private_chat_id,
102 BobHandshakeMsg::RequestWithAuth,
103 )
104 .await?;
105
106 context.emit_event(EventType::SecurejoinJoinerProgress {
107 contact_id: invite.contact_id(),
108 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
109 });
110 } else {
111 send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
112 .await?;
113
114 insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
115 }
116 }
117
118 match invite {
119 QrInvite::Group { .. } => {
120 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
121 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
122 chat::add_info_msg(context, joining_chat_id, &msg).await?;
123 Ok(joining_chat_id)
124 }
125 QrInvite::Broadcast { .. } => {
126 let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
127 if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
129 chat::add_to_chat_contacts_table(
130 context,
131 time(),
132 joining_chat_id,
133 &[invite.contact_id()],
134 )
135 .await?;
136 }
137
138 if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
140 let msg =
141 stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
142 chat::add_info_msg(context, joining_chat_id, &msg).await?;
143 }
144 Ok(joining_chat_id)
145 }
146 QrInvite::Contact { .. } => {
147 if !has_key {
150 chat::add_info_msg_with_cmd(
151 context,
152 private_chat_id,
153 &stock_str::securejoin_wait(context).await,
154 SystemMessage::SecurejoinWait,
155 None,
156 time(),
157 None,
158 None,
159 None,
160 )
161 .await?;
162 }
163 Ok(private_chat_id)
164 }
165 }
166}
167
168async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
172 context
176 .sql
177 .insert(
178 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
179 (invite, 0, chat_id),
180 )
181 .await
182}
183
184async fn delete_securejoin_wait_msg(context: &Context, chat_id: ChatId) -> Result<()> {
185 if let Some((msg_id, param)) = context
186 .sql
187 .query_row_optional(
188 "
189SELECT id, param FROM msgs
190WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0)
191 AND chat_id=? AND hidden=0
192LIMIT 1
193 ",
194 (chat_id, chat_id),
195 |row| {
196 let id: MsgId = row.get(0)?;
197 let param: String = row.get(1)?;
198 let param: Params = param.parse().unwrap_or_default();
199 Ok((id, param))
200 },
201 )
202 .await?
203 && param.get_cmd() == SystemMessage::SecurejoinWait
204 {
205 let on_server = false;
206 msg_id.trash(context, on_server).await?;
207 context.emit_event(EventType::MsgDeleted { chat_id, msg_id });
208 context.emit_msgs_changed_without_msg_id(chat_id);
209 chatlist_events::emit_chatlist_item_changed(context, chat_id);
210 context.emit_msgs_changed_without_ids();
211 chatlist_events::emit_chatlist_changed(context);
212 }
213 Ok(())
214}
215
216pub(super) async fn handle_auth_required(
221 context: &Context,
222 message: &MimeMessage,
223) -> Result<HandshakeMessage> {
224 let bob_states = context
226 .sql
227 .query_map_vec("SELECT id, invite FROM bobstate", (), |row| {
228 let row_id: i64 = row.get(0)?;
229 let invite: QrInvite = row.get(1)?;
230 Ok((row_id, invite))
231 })
232 .await?;
233
234 info!(
235 context,
236 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
237 );
238
239 let mut auth_sent = false;
240 for (bobstate_row_id, invite) in bob_states {
241 if !encrypted_and_signed(context, message, invite.fingerprint()) {
242 continue;
243 }
244
245 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
246 {
247 continue;
248 }
249
250 info!(context, "Fingerprint verified.",);
251 let chat_id = private_chat_id(context, &invite).await?;
252 delete_securejoin_wait_msg(context, chat_id)
253 .await
254 .context("delete_securejoin_wait_msg")
255 .log_err(context)
256 .ok();
257 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
258 context
259 .sql
260 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
261 .await?;
262
263 match invite {
264 QrInvite::Contact { .. } | QrInvite::Broadcast { .. } => {}
265 QrInvite::Group { .. } => {
266 let contact_id = invite.contact_id();
269 let msg = stock_str::secure_join_replies(context, contact_id).await;
270 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
271 chat::add_info_msg(context, chat_id, &msg).await?;
272 }
273 }
274
275 context.emit_event(EventType::SecurejoinJoinerProgress {
276 contact_id: invite.contact_id(),
277 progress: JoinerProgress::RequestWithAuthSent.into_u16(),
278 });
279
280 auth_sent = true;
281 }
282
283 if auth_sent {
284 Ok(HandshakeMessage::Done)
286 } else {
287 Ok(HandshakeMessage::Ignore)
292 }
293}
294
295pub(crate) async fn send_handshake_message(
297 context: &Context,
298 invite: &QrInvite,
299 chat_id: ChatId,
300 step: BobHandshakeMsg,
301) -> Result<()> {
302 let mut msg = Message {
303 viewtype: Viewtype::Text,
304 text: step.body_text(invite),
305 hidden: true,
306 ..Default::default()
307 };
308 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
309
310 msg.param.set(Param::Arg, step.securejoin_header(invite));
312
313 match step {
314 BobHandshakeMsg::Request => {
315 msg.param.set(Param::Arg2, invite.invitenumber());
317 msg.force_plaintext();
318 }
319 BobHandshakeMsg::RequestWithAuth => {
320 msg.param.set(Param::Arg2, invite.authcode());
322 msg.param.set_int(Param::GuaranteeE2ee, 1);
323
324 let bob_fp = self_fingerprint(context).await?;
326 msg.param.set(Param::Arg3, bob_fp);
327
328 if let QrInvite::Group { grpid, .. } = invite {
337 msg.param.set(Param::Arg4, grpid);
338 }
339 }
340 };
341
342 chat::send_msg(context, chat_id, &mut msg).await?;
343 Ok(())
344}
345
346pub(crate) enum BobHandshakeMsg {
348 Request,
350 RequestWithAuth,
352}
353
354impl BobHandshakeMsg {
355 fn body_text(&self, invite: &QrInvite) -> String {
361 format!("Secure-Join: {}", self.securejoin_header(invite))
362 }
363
364 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
370 match self {
371 Self::Request => match invite {
372 QrInvite::Contact { .. } => "vc-request",
373 QrInvite::Group { .. } => "vg-request",
374 QrInvite::Broadcast { .. } => "vg-request",
375 },
376 Self::RequestWithAuth => match invite {
377 QrInvite::Contact { .. } => "vc-request-with-auth",
378 QrInvite::Group { .. } => "vg-request-with-auth",
379 QrInvite::Broadcast { .. } => "vg-request-with-auth",
380 },
381 }
382 }
383}
384
385async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId> {
390 let hidden = match invite {
391 QrInvite::Contact { .. } => Blocked::Not,
392 QrInvite::Group { .. } => Blocked::Yes,
393 QrInvite::Broadcast { .. } => Blocked::Yes,
394 };
395
396 ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
397 .await
398 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))
399}
400
401async fn joining_chat_id(
409 context: &Context,
410 invite: &QrInvite,
411 alice_chat_id: ChatId,
412) -> Result<ChatId> {
413 match invite {
414 QrInvite::Contact { .. } => Ok(alice_chat_id),
415 QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
416 let chattype = if matches!(invite, QrInvite::Group { .. }) {
417 Chattype::Group
418 } else {
419 Chattype::InBroadcast
420 };
421
422 let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
423 Some((chat_id, _blocked)) => {
424 chat_id.unblock_ex(context, Nosync).await?;
425 chat_id
426 }
427 None => {
428 ChatId::create_multiuser_record(
429 context,
430 chattype,
431 grpid,
432 name,
433 Blocked::Not,
434 None,
435 smeared_time(context),
436 )
437 .await?
438 }
439 };
440 Ok(chat_id)
441 }
442 }
443}
444
445pub(crate) enum JoinerProgress {
450 RequestWithAuthSent,
454 Succeeded,
456}
457
458impl JoinerProgress {
459 pub(crate) fn into_u16(self) -> u16 {
460 match self {
461 JoinerProgress::RequestWithAuthSent => 400,
462 JoinerProgress::Succeeded => 1000,
463 }
464 }
465}