1use anyhow::{Context as _, Result};
4
5use super::qrinvite::QrInvite;
6use super::HandshakeMessage;
7use crate::chat::{self, is_contact_in_chat, ChatId, ProtectionStatus};
8use crate::constants::{Blocked, Chattype};
9use crate::contact::Origin;
10use crate::context::Context;
11use crate::events::EventType;
12use crate::key::self_fingerprint;
13use crate::log::info;
14use crate::message::{Message, Viewtype};
15use crate::mimeparser::{MimeMessage, SystemMessage};
16use crate::param::Param;
17use crate::securejoin::{encrypted_and_signed, verify_sender_by_fingerprint, ContactId};
18use crate::stock_str;
19use crate::sync::Sync::*;
20use crate::tools::{create_smeared_timestamp, time};
21
22pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
44 let hidden = match invite {
48 QrInvite::Contact { .. } => Blocked::Not,
49 QrInvite::Group { .. } => Blocked::Yes,
50 };
51 let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
52 .await
53 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
54
55 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
56 context.emit_event(EventType::ContactsChanged(None));
57
58 {
60 let has_key = context
61 .sql
62 .exists(
63 "SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
64 (invite.fingerprint().hex(),),
65 )
66 .await?;
67
68 if has_key
69 && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
70 .await?
71 {
72 info!(context, "Taking securejoin protocol shortcut");
74 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
75 .await?;
76
77 chat_id
79 .set_protection(
80 context,
81 ProtectionStatus::Protected,
82 time(),
83 Some(invite.contact_id()),
84 )
85 .await?;
86
87 context.emit_event(EventType::SecurejoinJoinerProgress {
88 contact_id: invite.contact_id(),
89 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
90 });
91 } else {
92 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
93
94 insert_new_db_entry(context, invite.clone(), chat_id).await?;
95 }
96 }
97
98 match invite {
99 QrInvite::Group { .. } => {
100 let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
103 if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
104 chat::add_to_chat_contacts_table(
105 context,
106 time(),
107 group_chat_id,
108 &[invite.contact_id()],
109 )
110 .await?;
111 }
112 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
113 chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
114 Ok(group_chat_id)
115 }
116 QrInvite::Contact { .. } => {
117 let sort_to_bottom = true;
122 let (received, incoming) = (false, false);
123 let ts_sort = chat_id
124 .calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
125 .await?;
126 if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
127 let ts_start = time();
128 chat::add_info_msg_with_cmd(
129 context,
130 chat_id,
131 &stock_str::securejoin_wait(context).await,
132 SystemMessage::SecurejoinWait,
133 ts_sort,
134 Some(ts_start),
135 None,
136 None,
137 None,
138 )
139 .await?;
140 }
141 Ok(chat_id)
142 }
143 }
144}
145
146async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
150 context
151 .sql
152 .insert(
153 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
154 (invite, 0, chat_id),
155 )
156 .await
157}
158
159pub(super) async fn handle_auth_required(
164 context: &Context,
165 message: &MimeMessage,
166) -> Result<HandshakeMessage> {
167 let bob_states: Vec<(i64, QrInvite, ChatId)> = context
169 .sql
170 .query_map(
171 "SELECT id, invite, chat_id FROM bobstate",
172 (),
173 |row| {
174 let row_id: i64 = row.get(0)?;
175 let invite: QrInvite = row.get(1)?;
176 let chat_id: ChatId = row.get(2)?;
177 Ok((row_id, invite, chat_id))
178 },
179 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
180 )
181 .await?;
182
183 info!(
184 context,
185 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
186 );
187
188 let mut auth_sent = false;
189 for (bobstate_row_id, invite, chat_id) in bob_states {
190 if !encrypted_and_signed(context, message, invite.fingerprint()) {
191 continue;
192 }
193
194 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
195 {
196 continue;
197 }
198
199 info!(context, "Fingerprint verified.",);
200 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
201 context
202 .sql
203 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
204 .await?;
205
206 match invite {
207 QrInvite::Contact { .. } => {}
208 QrInvite::Group { .. } => {
209 let contact_id = invite.contact_id();
212 let msg = stock_str::secure_join_replies(context, contact_id).await;
213 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
214 chat::add_info_msg(context, chat_id, &msg, time()).await?;
215 }
216 }
217
218 chat_id
219 .set_protection(
220 context,
221 ProtectionStatus::Protected,
222 message.timestamp_sent,
223 Some(invite.contact_id()),
224 )
225 .await?;
226
227 context.emit_event(EventType::SecurejoinJoinerProgress {
228 contact_id: invite.contact_id(),
229 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
230 });
231
232 auth_sent = true;
233 }
234
235 if auth_sent {
236 Ok(HandshakeMessage::Done)
238 } else {
239 Ok(HandshakeMessage::Ignore)
244 }
245}
246
247pub(crate) async fn send_handshake_message(
249 context: &Context,
250 invite: &QrInvite,
251 chat_id: ChatId,
252 step: BobHandshakeMsg,
253) -> Result<()> {
254 let mut msg = Message {
255 viewtype: Viewtype::Text,
256 text: step.body_text(invite),
257 hidden: true,
258 ..Default::default()
259 };
260 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
261
262 msg.param.set(Param::Arg, step.securejoin_header(invite));
264
265 match step {
266 BobHandshakeMsg::Request => {
267 msg.param.set(Param::Arg2, invite.invitenumber());
269 msg.force_plaintext();
270 }
271 BobHandshakeMsg::RequestWithAuth => {
272 msg.param.set(Param::Arg2, invite.authcode());
274 msg.param.set_int(Param::GuaranteeE2ee, 1);
275
276 let bob_fp = self_fingerprint(context).await?;
278 msg.param.set(Param::Arg3, bob_fp);
279
280 if let QrInvite::Group { ref grpid, .. } = invite {
289 msg.param.set(Param::Arg4, grpid);
290 }
291 }
292 };
293
294 chat::send_msg(context, chat_id, &mut msg).await?;
295 Ok(())
296}
297
298pub(crate) enum BobHandshakeMsg {
300 Request,
302 RequestWithAuth,
304}
305
306impl BobHandshakeMsg {
307 fn body_text(&self, invite: &QrInvite) -> String {
313 format!("Secure-Join: {}", self.securejoin_header(invite))
314 }
315
316 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
322 match self {
323 Self::Request => match invite {
324 QrInvite::Contact { .. } => "vc-request",
325 QrInvite::Group { .. } => "vg-request",
326 },
327 Self::RequestWithAuth => match invite {
328 QrInvite::Contact { .. } => "vc-request-with-auth",
329 QrInvite::Group { .. } => "vg-request-with-auth",
330 },
331 }
332 }
333}
334
335async fn joining_chat_id(
343 context: &Context,
344 invite: &QrInvite,
345 alice_chat_id: ChatId,
346) -> Result<ChatId> {
347 match invite {
348 QrInvite::Contact { .. } => Ok(alice_chat_id),
349 QrInvite::Group {
350 ref grpid,
351 ref name,
352 ..
353 } => {
354 let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
355 Some((chat_id, _protected, _blocked)) => {
356 chat_id.unblock_ex(context, Nosync).await?;
357 chat_id
358 }
359 None => {
360 ChatId::create_multiuser_record(
361 context,
362 Chattype::Group,
363 grpid,
364 name,
365 Blocked::Not,
366 ProtectionStatus::Unprotected, None,
368 create_smeared_timestamp(context),
369 )
370 .await?
371 }
372 };
373 Ok(group_chat_id)
374 }
375 }
376}
377
378pub(crate) enum JoinerProgress {
383 RequestWithAuthSent,
387 Succeeded,
389}
390
391impl JoinerProgress {
392 #[expect(clippy::wrong_self_convention)]
393 pub(crate) fn to_usize(self) -> usize {
394 match self {
395 JoinerProgress::RequestWithAuthSent => 400,
396 JoinerProgress::Succeeded => 1000,
397 }
398 }
399}