1use blake2::{
6 Blake2bVar,
7 digest::{Update, VariableOutput},
8};
9use bytes::{BufMut, Bytes, BytesMut};
10use chacha20poly1305::{
11 XChaCha20Poly1305, XNonce,
12 aead::{Aead, AeadCore, KeyInit, OsRng},
13};
14use qrcodegen::QrCode;
15use x25519_dalek::{PublicKey, StaticSecret};
16
17use totp_rs::{Algorithm, Secret, TOTP};
18
19use crate::{ZEROED_KEY, validate_account};
20use anyhow::bail;
21use std::fmt::Write;
22
23pub struct AuthClient {}
24
25impl AuthClient {
26 pub fn registration_start_req(
29 account: &[u8],
30 password: &[u8],
31 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
32 let (client_state, client_start) = crate::registration::client_start(password)?;
33
34 let raw_account_str = std::str::from_utf8(account)?;
35
36 let account_str = validate_account(raw_account_str)?;
37 let account = account_str.as_bytes();
38 let account_len = account.len();
39
40 let mut buf = BytesMut::with_capacity(1 + account_len + 32);
41
42 buf.put_u8(u8::try_from(account_len)?);
43 buf.put(account);
44 buf.put(&client_start[..]);
45
46 Ok((client_state, buf.into()))
47 }
48
49 pub fn registration_finish_req(
52 account: &[u8],
53 password: &[u8],
54 client_state: &[u8],
55 server_message: &[u8],
56 ) -> anyhow::Result<([u8; 32], Bytes)> {
57 let client_finish =
58 crate::registration::client_finish(password, client_state, server_message)?;
59
60 let raw_account_str = std::str::from_utf8(account)?;
61
62 let account_str = validate_account(raw_account_str)?;
63 let account = account_str.as_bytes();
64 let account_len = account.len();
65
66 let static_secret = StaticSecret::random_from_rng(OsRng);
67 let public_key = PublicKey::from(&static_secret);
68
69 let private_key = *static_secret.as_bytes();
70
71 let mut buf = BytesMut::with_capacity(1 + account_len + client_finish.len());
72
73 buf.put_u8(u8::try_from(account_len)?);
74 buf.put(account);
75 buf.put(&public_key.as_bytes()[..]);
76 buf.put(&client_finish[..]);
77
78 Ok((private_key, buf.into()))
79 }
80
81 pub fn decrypt_totp_mfa(
82 response: &Bytes,
83 private_key: [u8; 32],
84 app_name: String,
85 account: String,
86 ) -> anyhow::Result<(TOTP, String)> {
87 let static_secret = StaticSecret::from(private_key);
88 let public_key_bytes: [u8; 32] = response[115..147].try_into()?;
89
90 if public_key_bytes == ZEROED_KEY {
91 bail!("public key cannot be all 0s");
92 }
93
94 let public_key = PublicKey::from(public_key_bytes);
95 let shared_secret = static_secret.diffie_hellman(&public_key);
96
97 if !shared_secret.was_contributory() {
98 bail!("non-contributory shared secret");
99 }
100
101 let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
102
103 let nonce = XNonce::from_slice(&response[91..115]);
104 let (secret, codes) = match cipher.decrypt(nonce, &response[0..91]) {
105 Ok(secret_and_codes) => (
106 secret_and_codes[0..20].to_vec(),
107 std::str::from_utf8(&secret_and_codes[20..])?.to_string(),
108 ),
109 Err(err) => bail!("{err}"),
110 };
111
112 let totp = TOTP::new(
113 Algorithm::SHA1,
114 6,
115 1,
116 30,
117 Secret::Raw(secret).to_bytes()?,
118 Some(app_name),
119 account,
120 )?;
121
122 Ok((totp, codes))
123 }
124
125 pub fn decrypt_totp_mfa_code(
126 response: &Bytes,
127 private_key: [u8; 32],
128 app_name: String,
129 account: String,
130 ) -> anyhow::Result<(String, String)> {
131 let (totp, recovery_codes) =
132 Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
133 let mfa_code = totp.generate_current()?;
134 Ok((mfa_code, recovery_codes))
135 }
136
137 pub fn decrypt_totp_mfa_to_qr_svg(
138 response: &Bytes,
139 private_key: [u8; 32],
140 app_name: String,
141 account: String,
142 ) -> anyhow::Result<(String, String)> {
143 let (totp, recovery_codes) =
144 Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
145 let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
146
147 let svg = to_svg_string(&qr)?;
148 Ok((svg, recovery_codes))
149 }
150
151 pub fn login_start_req(account: &[u8], password: &[u8]) -> anyhow::Result<(Vec<u8>, Bytes)> {
154 let (client_state, client_start) = crate::login::client_start(password)?;
155
156 let raw_account_str = std::str::from_utf8(account)?;
157
158 let account_str = validate_account(raw_account_str)?;
159 let account = account_str.as_bytes();
160 let account_len = account.len();
161
162 let mut buf = BytesMut::with_capacity(1 + account_len + client_start.len());
163
164 buf.put_u8(u8::try_from(account_len)?);
165 buf.put(account);
166 buf.put(&client_start[..]);
167
168 Ok((client_state, buf.into()))
169 }
170
171 pub fn login_finish_req(
174 account: &[u8],
175 password: &[u8],
176 mfa_hash: &[u8],
177 client_state: &[u8],
178 server_message: &[u8],
179 client_verifier: Option<&[u8]>,
180 ) -> anyhow::Result<(Bytes, Vec<u8>)> {
181 let (client_finish, session_key) =
182 crate::login::client_finish(password, client_state, server_message)?;
183
184 let raw_account_str = std::str::from_utf8(account)?;
185
186 let account_str = validate_account(raw_account_str)?;
187 let account = account_str.as_bytes();
188 let account_len = account.len();
189
190 let mut buf = BytesMut::with_capacity(1 + account_len + 32 + 32 + 16 + 24 + 64);
191
192 buf.put_u8(u8::try_from(account_len)?);
193 buf.put(account);
194
195 let mut key = [0u8; 32];
196
197 let mut hasher = match Blake2bVar::new(32) {
198 Ok(h) => h,
199 Err(err) => bail!("{err}"),
200 };
201 hasher.update(&session_key[..]);
202 if let Err(err) = hasher.finalize_variable(&mut key) {
203 bail!("{err}");
204 }
205
206 let cipher = XChaCha20Poly1305::new(&key.into());
207
208 let mut mfa_hash_verifier_and_verifier_pub_key = BytesMut::with_capacity(64);
209 mfa_hash_verifier_and_verifier_pub_key.put(mfa_hash);
210
211 if let Some(client_verifier) = client_verifier {
212 mfa_hash_verifier_and_verifier_pub_key.put(client_verifier);
213 }
214
215 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
216 let mut encrypted_mfa_hash =
217 match cipher.encrypt(&nonce, &mfa_hash_verifier_and_verifier_pub_key[..]) {
218 Ok(e) => e,
219 Err(err) => bail!("{err}"),
220 };
221 encrypted_mfa_hash.extend_from_slice(&nonce);
222
223 buf.put(&encrypted_mfa_hash[..]);
224 buf.put(&client_finish[..]);
225
226 Ok((buf.into(), session_key))
227 }
228
229 pub fn decrypt_token(response: &Bytes, session_key: &[u8]) -> anyhow::Result<Bytes> {
230 crate::login::decrypt_token(response, session_key)
231 }
232
233 pub fn reset_password_login_start_req(
234 account: &[u8],
235 password: &[u8],
236 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
237 Self::login_start_req(account, password)
238 }
239
240 pub fn reset_password_login_finish_req(
241 account: &[u8],
242 password: &[u8],
243 mfa_hash: &[u8],
244 client_state: &[u8],
245 server_message: &[u8],
246 client_verifier: Option<&[u8]>,
247 ) -> anyhow::Result<(Bytes, Vec<u8>)> {
248 Self::login_finish_req(
249 account,
250 password,
251 mfa_hash,
252 client_state,
253 server_message,
254 client_verifier,
255 )
256 }
257
258 pub fn password_reset_registration_start_req(
259 account: &[u8],
260 password: &[u8],
261 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
262 Self::registration_start_req(account, password)
263 }
264
265 pub fn password_reset_registration_finish_req(
266 account: &[u8],
267 password: &[u8],
268 client_state: &[u8],
269 server_message: &[u8],
270 ) -> anyhow::Result<Bytes> {
271 let (_, req) =
272 Self::registration_finish_req(account, password, client_state, server_message)?;
273
274 Ok(req)
275 }
276
277 pub fn forgot_password_start_req(
278 account: &[u8],
279 new_password: &[u8],
280 recovery_code: &[u8],
281 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
282 let (state, payload) = Self::registration_start_req(account, new_password)?;
283
284 let mut payload: BytesMut = payload.into();
285 payload.extend_from_slice(recovery_code);
286
287 Ok((state, payload.into()))
288 }
289
290 pub fn forgot_password_finish_req(
291 account: &[u8],
292 new_password: &[u8],
293 client_state: &[u8],
294 server_message: &[u8],
295 recovery_code: &[u8],
296 ) -> anyhow::Result<Bytes> {
297 let (_, payload) =
298 Self::registration_finish_req(account, new_password, client_state, server_message)?;
299
300 let mut payload: BytesMut = payload.into();
301 payload.extend_from_slice(recovery_code);
302
303 Ok(payload.into())
304 }
305
306 pub fn reset_totp_mfa_start_req(
307 account: &[u8],
308 password: &[u8],
309 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
310 Self::login_start_req(account, password)
311 }
312
313 pub fn reset_totp_mfa_finish_req(
314 account: &[u8],
315 password: &[u8],
316 mfa_hash: &[u8],
317 client_state: &[u8],
318 server_message: &[u8],
319 ) -> anyhow::Result<(Bytes, Vec<u8>)> {
320 Self::login_finish_req(
321 account,
322 password,
323 mfa_hash,
324 client_state,
325 server_message,
326 None,
327 )
328 }
329
330 pub fn decrypt_reset_totp_mfa(
331 response: &Bytes,
332 session_key: &[u8],
333 app_name: String,
334 account: String,
335 ) -> anyhow::Result<TOTP> {
336 let secret = crate::login::decrypt_token(response, session_key)?;
337
338 let totp = TOTP::new(
339 Algorithm::SHA1,
340 6,
341 1,
342 30,
343 Secret::Raw(secret.to_vec()).to_bytes()?,
344 Some(app_name),
345 account,
346 )?;
347
348 Ok(totp)
349 }
350
351 pub fn decrypt_reset_totp_mfa_code(
352 response: &Bytes,
353 session_key: &[u8],
354 app_name: String,
355 account: String,
356 ) -> anyhow::Result<String> {
357 let totp = Self::decrypt_reset_totp_mfa(response, session_key, app_name, account)?;
358
359 let mfa_code = totp.generate_current()?;
360
361 Ok(mfa_code)
362 }
363
364 pub fn decrypt_reset_totp_mfa_to_qr_svg(
365 response: &Bytes,
366 session_key: &[u8],
367 app_name: String,
368 account: String,
369 ) -> anyhow::Result<String> {
370 let totp = Self::decrypt_reset_totp_mfa(response, session_key, app_name, account)?;
371
372 let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
373 let svg = to_svg_string(&qr)?;
374
375 Ok(svg)
376 }
377
378 pub fn lost_totp_mfa_start_req(
379 account: &[u8],
380 password: &[u8],
381 recovery_code: &[u8],
382 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
383 let (state, payload) = Self::login_start_req(account, password)?;
384
385 let mut payload: BytesMut = payload.into();
386 payload.extend_from_slice(recovery_code);
387
388 Ok((state, payload.into()))
389 }
390
391 pub fn lost_totp_mfa_finish_req(
392 account: &[u8],
393 password: &[u8],
394 client_state: &[u8],
395 server_message: &[u8],
396 recovery_code: &[u8],
397 ) -> anyhow::Result<(Bytes, Vec<u8>)> {
398 let (payload, session_key) = Self::login_finish_req(
399 account,
400 password,
401 &[0u8; 32][..],
402 client_state,
403 server_message,
404 None,
405 )?;
406
407 let mut payload: BytesMut = payload.into();
408 payload.extend_from_slice(recovery_code);
409
410 Ok((payload.into(), session_key))
411 }
412
413 pub fn decrypt_lost_totp_mfa(
414 response: &Bytes,
415 session_key: &[u8],
416 app_name: String,
417 account: String,
418 ) -> anyhow::Result<TOTP> {
419 Self::decrypt_reset_totp_mfa(response, session_key, app_name, account)
420 }
421
422 pub fn decrypt_lost_totp_mfa_code(
423 response: &Bytes,
424 session_key: &[u8],
425 app_name: String,
426 account: String,
427 ) -> anyhow::Result<String> {
428 Self::decrypt_reset_totp_mfa_code(response, session_key, app_name, account)
429 }
430
431 pub fn decrypt_lost_totp_mfa_to_qr_svg(
432 response: &Bytes,
433 session_key: &[u8],
434 app_name: String,
435 account: String,
436 ) -> anyhow::Result<String> {
437 Self::decrypt_reset_totp_mfa_to_qr_svg(response, session_key, app_name, account)
438 }
439
440 pub fn reset_recovery_codes_start_req(
441 account: &[u8],
442 password: &[u8],
443 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
444 Self::login_start_req(account, password)
445 }
446
447 pub fn reset_recovery_codes_finish_req(
448 account: &[u8],
449 password: &[u8],
450 mfa_hash: &[u8],
451 client_state: &[u8],
452 server_message: &[u8],
453 ) -> anyhow::Result<(Bytes, Vec<u8>)> {
454 Self::login_finish_req(
455 account,
456 password,
457 mfa_hash,
458 client_state,
459 server_message,
460 None,
461 )
462 }
463
464 pub fn decrypt_reset_recovery_codes(
465 response: &Bytes,
466 session_key: &[u8],
467 ) -> anyhow::Result<String> {
468 let codes = crate::login::decrypt_token(response, session_key)?;
469 let codes_str = std::str::from_utf8(&codes)?.to_string();
470
471 Ok(codes_str)
472 }
473
474 pub fn delete_account_start_req(
475 account: &[u8],
476 password: &[u8],
477 ) -> anyhow::Result<(Vec<u8>, Bytes)> {
478 Self::login_start_req(account, password)
479 }
480
481 pub fn delete_account_finish_req(
482 account: &[u8],
483 password: &[u8],
484 mfa_hash: &[u8],
485 client_state: &[u8],
486 server_message: &[u8],
487 ) -> anyhow::Result<Bytes> {
488 let (req, _) = Self::login_finish_req(
489 account,
490 password,
491 mfa_hash,
492 client_state,
493 server_message,
494 None,
495 )?;
496
497 Ok(req)
498 }
499}
500
501fn to_svg_string(qr: &QrCode) -> anyhow::Result<String> {
502 let border: i32 = 4;
503 let mut result = String::new();
504
505 if let Some(border) = border.checked_mul(2)
506 && let Some(dimension) = qr.size().checked_add(border)
507 {
508 writeln!(
509 result,
510 "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">"
511 )?;
512 result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
513 result += "\t<path d=\"";
514 for y in 0..qr.size() {
515 for x in 0..qr.size() {
516 if qr.get_module(x, y) {
517 if x != 0 || y != 0 {
518 result += " ";
519 }
520 write!(result, "M{},{}h1v1h-1z", x + border, y + border)?;
521 }
522 }
523 }
524 result += "\" fill=\"#000000\"/>\n";
525 result += "</svg>\n";
526 }
527
528 Ok(result)
529}