Skip to main content

ordinary_auth/
client.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use 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    /// `account_len.account.client_start`
27    /// (`client_state`, payload)
28    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    /// `account_len.account.client_finish`
50    /// payload
51    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    /// `account_len.account.client_start`
152    /// (`client_state`, payload)
153    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    /// `account_len.account.client_finish`
172    /// payload
173    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}