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::{self, Blocked, Chattype};
9use crate::contact::Origin;
10use crate::context::Context;
11use crate::events::EventType;
12use crate::key::{load_self_public_key, DcKey};
13use crate::message::{Message, Viewtype};
14use crate::mimeparser::{MimeMessage, SystemMessage};
15use crate::param::Param;
16use crate::securejoin::{encrypted_and_signed, verify_sender_by_fingerprint, ContactId};
17use crate::stock_str;
18use crate::sync::Sync::*;
19use crate::tools::{create_smeared_timestamp, 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 };
50 let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
51 .await
52 .with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
53
54 ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
55 context.emit_event(EventType::ContactsChanged(None));
56
57 {
59 let peer_verified =
60 verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
61 .await?;
62
63 if peer_verified {
64 info!(context, "Taking securejoin protocol shortcut");
66 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
67 .await?;
68
69 chat_id
71 .set_protection(
72 context,
73 ProtectionStatus::Protected,
74 time(),
75 Some(invite.contact_id()),
76 )
77 .await?;
78
79 context.emit_event(EventType::SecurejoinJoinerProgress {
80 contact_id: invite.contact_id(),
81 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
82 });
83 } else {
84 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
85
86 insert_new_db_entry(context, invite.clone(), chat_id).await?;
87 }
88 }
89
90 match invite {
91 QrInvite::Group { .. } => {
92 let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
95 if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
96 chat::add_to_chat_contacts_table(
97 context,
98 time(),
99 group_chat_id,
100 &[invite.contact_id()],
101 )
102 .await?;
103 }
104 let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
105 chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
106 Ok(group_chat_id)
107 }
108 QrInvite::Contact { .. } => {
109 let sort_to_bottom = true;
114 let (received, incoming) = (false, false);
115 let ts_sort = chat_id
116 .calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
117 .await?;
118 if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
119 let ts_start = time();
120 chat::add_info_msg_with_cmd(
121 context,
122 chat_id,
123 &stock_str::securejoin_wait(context).await,
124 SystemMessage::SecurejoinWait,
125 ts_sort,
126 Some(ts_start),
127 None,
128 None,
129 None,
130 )
131 .await?;
132 chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT);
133 }
134 Ok(chat_id)
135 }
136 }
137}
138
139async fn insert_new_db_entry(context: &Context, invite: QrInvite, chat_id: ChatId) -> Result<i64> {
143 context
144 .sql
145 .insert(
146 "INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
147 (invite, 0, chat_id),
148 )
149 .await
150}
151
152pub(super) async fn handle_auth_required(
157 context: &Context,
158 message: &MimeMessage,
159) -> Result<HandshakeMessage> {
160 let bob_states: Vec<(i64, QrInvite, ChatId)> = context
162 .sql
163 .query_map(
164 "SELECT id, invite, chat_id FROM bobstate",
165 (),
166 |row| {
167 let row_id: i64 = row.get(0)?;
168 let invite: QrInvite = row.get(1)?;
169 let chat_id: ChatId = row.get(2)?;
170 Ok((row_id, invite, chat_id))
171 },
172 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
173 )
174 .await?;
175
176 info!(
177 context,
178 "Bob Step 4 - handling {{vc,vg}}-auth-required message."
179 );
180
181 let mut auth_sent = false;
182 for (bobstate_row_id, invite, chat_id) in bob_states {
183 if !encrypted_and_signed(context, message, invite.fingerprint()) {
184 continue;
185 }
186
187 if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
188 {
189 continue;
190 }
191
192 info!(context, "Fingerprint verified.",);
193 send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth).await?;
194 context
195 .sql
196 .execute("DELETE FROM bobstate WHERE id=?", (bobstate_row_id,))
197 .await?;
198
199 match invite {
200 QrInvite::Contact { .. } => {}
201 QrInvite::Group { .. } => {
202 let contact_id = invite.contact_id();
205 let msg = stock_str::secure_join_replies(context, contact_id).await;
206 let chat_id = joining_chat_id(context, &invite, chat_id).await?;
207 chat::add_info_msg(context, chat_id, &msg, time()).await?;
208 }
209 }
210
211 chat_id
212 .set_protection(
213 context,
214 ProtectionStatus::Protected,
215 message.timestamp_sent,
216 Some(invite.contact_id()),
217 )
218 .await?;
219
220 context.emit_event(EventType::SecurejoinJoinerProgress {
221 contact_id: invite.contact_id(),
222 progress: JoinerProgress::RequestWithAuthSent.to_usize(),
223 });
224
225 auth_sent = true;
226 }
227
228 if auth_sent {
229 Ok(HandshakeMessage::Done)
231 } else {
232 Ok(HandshakeMessage::Ignore)
237 }
238}
239
240pub(crate) async fn send_handshake_message(
242 context: &Context,
243 invite: &QrInvite,
244 chat_id: ChatId,
245 step: BobHandshakeMsg,
246) -> Result<()> {
247 let mut msg = Message {
248 viewtype: Viewtype::Text,
249 text: step.body_text(invite),
250 hidden: true,
251 ..Default::default()
252 };
253 msg.param.set_cmd(SystemMessage::SecurejoinMessage);
254
255 msg.param.set(Param::Arg, step.securejoin_header(invite));
257
258 match step {
259 BobHandshakeMsg::Request => {
260 msg.param.set(Param::Arg2, invite.invitenumber());
262 msg.force_plaintext();
263 }
264 BobHandshakeMsg::RequestWithAuth => {
265 msg.param.set(Param::Arg2, invite.authcode());
267 msg.param.set_int(Param::GuaranteeE2ee, 1);
268
269 let bob_fp = load_self_public_key(context).await?.dc_fingerprint();
271 msg.param.set(Param::Arg3, bob_fp.hex());
272
273 if let QrInvite::Group { ref grpid, .. } = invite {
282 msg.param.set(Param::Arg4, grpid);
283 }
284 }
285 };
286
287 chat::send_msg(context, chat_id, &mut msg).await?;
288 Ok(())
289}
290
291pub(crate) enum BobHandshakeMsg {
293 Request,
295 RequestWithAuth,
297}
298
299impl BobHandshakeMsg {
300 fn body_text(&self, invite: &QrInvite) -> String {
306 format!("Secure-Join: {}", self.securejoin_header(invite))
307 }
308
309 fn securejoin_header(&self, invite: &QrInvite) -> &'static str {
315 match self {
316 Self::Request => match invite {
317 QrInvite::Contact { .. } => "vc-request",
318 QrInvite::Group { .. } => "vg-request",
319 },
320 Self::RequestWithAuth => match invite {
321 QrInvite::Contact { .. } => "vc-request-with-auth",
322 QrInvite::Group { .. } => "vg-request-with-auth",
323 },
324 }
325 }
326}
327
328async fn joining_chat_id(
336 context: &Context,
337 invite: &QrInvite,
338 alice_chat_id: ChatId,
339) -> Result<ChatId> {
340 match invite {
341 QrInvite::Contact { .. } => Ok(alice_chat_id),
342 QrInvite::Group {
343 ref grpid,
344 ref name,
345 ..
346 } => {
347 let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
348 Some((chat_id, _protected, _blocked)) => {
349 chat_id.unblock_ex(context, Nosync).await?;
350 chat_id
351 }
352 None => {
353 ChatId::create_multiuser_record(
354 context,
355 Chattype::Group,
356 grpid,
357 name,
358 Blocked::Not,
359 ProtectionStatus::Unprotected, None,
361 create_smeared_timestamp(context),
362 )
363 .await?
364 }
365 };
366 Ok(group_chat_id)
367 }
368 }
369}
370
371pub(crate) enum JoinerProgress {
376 RequestWithAuthSent,
380 Succeeded,
382}
383
384impl JoinerProgress {
385 #[expect(clippy::wrong_self_convention)]
386 pub(crate) fn to_usize(self) -> usize {
387 match self {
388 JoinerProgress::RequestWithAuthSent => 400,
389 JoinerProgress::Succeeded => 1000,
390 }
391 }
392}