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::Origin;
10use crate::context::Context;
11use crate::events::EventType;
12use crate::key::self_fingerprint;
13use crate::message::{Message, Viewtype};
14use crate::mimeparser::{MimeMessage, SystemMessage};
15use crate::param::Param;
16use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
17use crate::stock_str;
18use crate::sync::Sync::*;
19use crate::tools::{smeared_time, time};
20
21pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
43 let hidden = match invite {
47 QrInvite::Contact { .. } => Blocked::Not,
48 QrInvite::Group { .. } => Blocked::Yes,
49 QrInvite::Broadcast { .. } => Blocked::Yes,
50 };
51
52 let private_chat_id =
54 ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
55 .await
56 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
57
58 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
59 context.emit_event(EventType::ContactsChanged(None));
60
61 {
63 let has_key = context
64 .sql
65 .exists(
66 "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
67 (invite.fingerprint().hex(),),
68 )
69 .await?;
70
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.to_usize(),
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.to_usize(),
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 chat::add_info_msg_with_cmd(
158 context,
159 private_chat_id,
160 &stock_str::securejoin_wait(context).await,
161 SystemMessage::SecurejoinWait,
162 None,
163 time(),
164 None,
165 None,
166 None,
167 )
168 .await?;
169 Ok(private_chat_id)
170 }
171 }
172}
173
174async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
178 context
179 .sql
180 .insert(
181 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
182 (invite, 0, chat_id),
183 )
184 .await
185}
186
187pub(super) async fn handle_auth_required(
192 context: &Context,
193 message: &MimeMessage,
194) -> Result<HandshakeMessage> {
195 let bob_states = context
197 .sql
198 .query_map_vec("SELECT id, invite, chat_id FROM bobstate", (), |row| {
199 let row_id: i64 = row.get(0)?;
200 let invite: QrInvite = row.get(1)?;
201 let chat_id: ChatId = row.get(2)?;
202 Ok((row_id, invite, chat_id))
203 })
204 .await?;
205
206 info!(
207 context,
208 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
209 );
210
211 let mut auth_sent = false;
212 for (bobstate_row_id, invite, chat_id) in bob_states {
213 if !encrypted_and_signed(context, message, invite.fingerprint()) {
214 continue;
215 }
216
217 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
218 {
219 continue;
220 }
221
222 info!(context, "Fingerprint verified.",);
223 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
224 context
225 .sql
226 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
227 .await?;
228
229 match invite {
230 QrInvite::Contact { .. } | QrInvite::Broadcast { .. } => {}
231 QrInvite::Group { .. } => {
232 let contact_id = invite.contact_id();
235 let msg = stock_str::secure_join_replies(context, contact_id).await;
236 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
237 chat::add_info_msg(context, chat_id, &msg).await?;
238 }
239 }
240
241 context.emit_event(EventType::SecurejoinJoinerProgress {
242 contact_id: invite.contact_id(),
243 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
244 });
245
246 auth_sent = true;
247 }
248
249 if auth_sent {
250 Ok(HandshakeMessage::Done)
252 } else {
253 Ok(HandshakeMessage::Ignore)
258 }
259}
260
261pub(crate) async fn send_handshake_message(
263 context: &Context,
264 invite: &QrInvite,
265 chat_id: ChatId,
266 step: BobHandshakeMsg,
267) -> Result<()> {
268 let mut msg = Message {
269 viewtype: Viewtype::Text,
270 text: step.body_text(invite),
271 hidden: true,
272 ..Default::default()
273 };
274 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
275
276 msg.param.set(Param::Arg, step.securejoin_header(invite));
278
279 match step {
280 BobHandshakeMsg::Request => {
281 msg.param.set(Param::Arg2, invite.invitenumber());
283 msg.force_plaintext();
284 }
285 BobHandshakeMsg::RequestWithAuth => {
286 msg.param.set(Param::Arg2, invite.authcode());
288 msg.param.set_int(Param::GuaranteeE2ee, 1);
289
290 let bob_fp = self_fingerprint(context).await?;
292 msg.param.set(Param::Arg3, bob_fp);
293
294 if let QrInvite::Group { grpid, .. } = invite {
303 msg.param.set(Param::Arg4, grpid);
304 }
305 }
306 };
307
308 chat::send_msg(context, chat_id, &mut msg).await?;
309 Ok(())
310}
311
312pub(crate) enum BobHandshakeMsg {
314 Request,
316 RequestWithAuth,
318}
319
320impl BobHandshakeMsg {
321 fn body_text(&self, invite: &QrInvite) -> String {
327 format!("Secure-Join: {}", self.securejoin_header(invite))
328 }
329
330 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
336 match self {
337 Self::Request => match invite {
338 QrInvite::Contact { .. } => "vc-request",
339 QrInvite::Group { .. } => "vg-request",
340 QrInvite::Broadcast { .. } => "vg-request",
341 },
342 Self::RequestWithAuth => match invite {
343 QrInvite::Contact { .. } => "vc-request-with-auth",
344 QrInvite::Group { .. } => "vg-request-with-auth",
345 QrInvite::Broadcast { .. } => "vg-request-with-auth",
346 },
347 }
348 }
349}
350
351async fn joining_chat_id(
359 context: &Context,
360 invite: &QrInvite,
361 alice_chat_id: ChatId,
362) -> Result<ChatId> {
363 match invite {
364 QrInvite::Contact { .. } => Ok(alice_chat_id),
365 QrInvite::Group { grpid, name, .. } | QrInvite::Broadcast { name, grpid, .. } => {
366 let chattype = if matches!(invite, QrInvite::Group { .. }) {
367 Chattype::Group
368 } else {
369 Chattype::InBroadcast
370 };
371
372 let chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
373 Some((chat_id, _blocked)) => {
374 chat_id.unblock_ex(context, Nosync).await?;
375 chat_id
376 }
377 None => {
378 ChatId::create_multiuser_record(
379 context,
380 chattype,
381 grpid,
382 name,
383 Blocked::Not,
384 None,
385 smeared_time(context),
386 )
387 .await?
388 }
389 };
390 Ok(chat_id)
391 }
392 }
393}
394
395pub(crate) enum JoinerProgress {
400 RequestWithAuthSent,
404 Succeeded,
406}
407
408impl JoinerProgress {
409 #[expect(clippy::wrong_self_convention)]
410 pub(crate) fn to_usize(self) -> usize {
411 match self {
412 JoinerProgress::RequestWithAuthSent => 400,
413 JoinerProgress::Succeeded => 1000,
414 }
415 }
416}