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::log::info;
14use crate::message::{Message, Viewtype};
15use crate::mimeparser::{MimeMessage, SystemMessage};
16use crate::param::Param;
17use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint};
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 let group_chat_id = match invite {
71 QrInvite::Group { ref grpid, .. } => {
72 if let Some((group_chat_id, _blocked)) =
73 chat::get_chat_id_by_grpid(context, grpid).await?
74 {
75 if is_contact_in_chat(context, group_chat_id, ContactId::SELF).await? {
76 Some(group_chat_id)
77 } else {
78 None
79 }
80 } else {
81 None
82 }
83 }
84 QrInvite::Contact { .. } => None,
85 };
86
87 if let Some(group_chat_id) = group_chat_id {
88 context.emit_event(EventType::SecurejoinJoinerProgress {
93 contact_id: invite.contact_id(),
94 progress: JoinerProgress::Succeeded.to_usize(),
95 });
96 return Ok(group_chat_id);
97 } else if has_key
98 && verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
99 .await?
100 {
101 info!(context, "Taking securejoin protocol shortcut");
103 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
104 .await?;
105
106 context.emit_event(EventType::SecurejoinJoinerProgress {
107 contact_id: invite.contact_id(),
108 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
109 });
110 } else {
111 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
112
113 insert_new_db_entry(context, invite.clone(), chat_id).await?;
114 }
115 }
116
117 match invite {
118 QrInvite::Group { .. } => {
119 let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
122 if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
123 chat::add_to_chat_contacts_table(
124 context,
125 time(),
126 group_chat_id,
127 &[invite.contact_id()],
128 )
129 .await?;
130 }
131 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
132 chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
133 Ok(group_chat_id)
134 }
135 QrInvite::Contact { .. } => {
136 let sort_to_bottom = true;
141 let (received, incoming) = (false, false);
142 let ts_sort = chat_id
143 .calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
144 .await?;
145 let ts_start = time();
146 chat::add_info_msg_with_cmd(
147 context,
148 chat_id,
149 &stock_str::securejoin_wait(context).await,
150 SystemMessage::SecurejoinWait,
151 ts_sort,
152 Some(ts_start),
153 None,
154 None,
155 None,
156 )
157 .await?;
158 Ok(chat_id)
159 }
160 }
161}
162
163async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
167 context
168 .sql
169 .insert(
170 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
171 (invite, 0, chat_id),
172 )
173 .await
174}
175
176pub(super) async fn handle_auth_required(
181 context: &Context,
182 message: &MimeMessage,
183) -> Result<HandshakeMessage> {
184 let bob_states = context
186 .sql
187 .query_map_vec("SELECT id, invite, chat_id FROM bobstate", (), |row| {
188 let row_id: i64 = row.get(0)?;
189 let invite: QrInvite = row.get(1)?;
190 let chat_id: ChatId = row.get(2)?;
191 Ok((row_id, invite, chat_id))
192 })
193 .await?;
194
195 info!(
196 context,
197 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
198 );
199
200 let mut auth_sent = false;
201 for (bobstate_row_id, invite, chat_id) in bob_states {
202 if !encrypted_and_signed(context, message, invite.fingerprint()) {
203 continue;
204 }
205
206 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
207 {
208 continue;
209 }
210
211 info!(context, "Fingerprint verified.",);
212 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
213 context
214 .sql
215 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
216 .await?;
217
218 match invite {
219 QrInvite::Contact { .. } => {}
220 QrInvite::Group { .. } => {
221 let contact_id = invite.contact_id();
224 let msg = stock_str::secure_join_replies(context, contact_id).await;
225 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
226 chat::add_info_msg(context, chat_id, &msg, time()).await?;
227 }
228 }
229
230 context.emit_event(EventType::SecurejoinJoinerProgress {
231 contact_id: invite.contact_id(),
232 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
233 });
234
235 auth_sent = true;
236 }
237
238 if auth_sent {
239 Ok(HandshakeMessage::Done)
241 } else {
242 Ok(HandshakeMessage::Ignore)
247 }
248}
249
250pub(crate) async fn send_handshake_message(
252 context: &Context,
253 invite: &QrInvite,
254 chat_id: ChatId,
255 step: BobHandshakeMsg,
256) -> Result<()> {
257 let mut msg = Message {
258 viewtype: Viewtype::Text,
259 text: step.body_text(invite),
260 hidden: true,
261 ..Default::default()
262 };
263 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
264
265 msg.param.set(Param::Arg, step.securejoin_header(invite));
267
268 match step {
269 BobHandshakeMsg::Request => {
270 msg.param.set(Param::Arg2, invite.invitenumber());
272 msg.force_plaintext();
273 }
274 BobHandshakeMsg::RequestWithAuth => {
275 msg.param.set(Param::Arg2, invite.authcode());
277 msg.param.set_int(Param::GuaranteeE2ee, 1);
278
279 let bob_fp = self_fingerprint(context).await?;
281 msg.param.set(Param::Arg3, bob_fp);
282
283 if let QrInvite::Group { grpid, .. } = invite {
292 msg.param.set(Param::Arg4, grpid);
293 }
294 }
295 };
296
297 chat::send_msg(context, chat_id, &mut msg).await?;
298 Ok(())
299}
300
301pub(crate) enum BobHandshakeMsg {
303 Request,
305 RequestWithAuth,
307}
308
309impl BobHandshakeMsg {
310 fn body_text(&self, invite: &QrInvite) -> String {
316 format!("Secure-Join: {}", self.securejoin_header(invite))
317 }
318
319 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
325 match self {
326 Self::Request => match invite {
327 QrInvite::Contact { .. } => "vc-request",
328 QrInvite::Group { .. } => "vg-request",
329 },
330 Self::RequestWithAuth => match invite {
331 QrInvite::Contact { .. } => "vc-request-with-auth",
332 QrInvite::Group { .. } => "vg-request-with-auth",
333 },
334 }
335 }
336}
337
338async fn joining_chat_id(
346 context: &Context,
347 invite: &QrInvite,
348 alice_chat_id: ChatId,
349) -> Result<ChatId> {
350 match invite {
351 QrInvite::Contact { .. } => Ok(alice_chat_id),
352 QrInvite::Group { grpid, name, .. } => {
353 let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
354 Some((chat_id, _blocked)) => {
355 chat_id.unblock_ex(context, Nosync).await?;
356 chat_id
357 }
358 None => {
359 ChatId::create_multiuser_record(
360 context,
361 Chattype::Group,
362 grpid,
363 name,
364 Blocked::Not,
365 None,
366 create_smeared_timestamp(context),
367 )
368 .await?
369 }
370 };
371 Ok(group_chat_id)
372 }
373 }
374}
375
376pub(crate) enum JoinerProgress {
381 RequestWithAuthSent,
385 Succeeded,
387}
388
389impl JoinerProgress {
390 #[expect(clippy::wrong_self_convention)]
391 pub(crate) fn to_usize(self) -> usize {
392 match self {
393 JoinerProgress::RequestWithAuthSent => 400,
394 JoinerProgress::Succeeded => 1000,
395 }
396 }
397}