Skip to main content

ordinary_auth/
core.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use std::{sync::Arc, time::SystemTime};
6
7use anyhow::bail;
8use blake2::digest::{FixedOutput, Mac};
9use bytes::{BufMut, Bytes, BytesMut};
10use chacha20poly1305::{
11    XChaCha20Poly1305, XNonce,
12    aead::{Aead, AeadCore, OsRng},
13};
14use flexbuffers::VectorReader;
15use ordinary_config::{
16    AccessTokenConfig, AuthConfig, ClientPasswordHash, InviteConfig, InviteMode, MfaConfig,
17    PasswordConfig, PasswordProtocol, RefreshTokenConfig, TotpAlgorithm, TotpConfig,
18};
19use tracing::instrument;
20use uuid::Uuid;
21use x25519_dalek::{EphemeralSecret, PublicKey};
22
23use saferlmdb::{
24    self as lmdb, Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put,
25};
26
27pub use opaque_ke::ServerSetup;
28
29use totp_rs::{Algorithm, Secret, TOTP};
30
31use crate::keys::{KeyAlg, generate_256_bit_key};
32use crate::{
33    DefaultCipherSuite, EXP_LEN, SIG_LEN, ZEROED_KEY,
34    keys::decrypt_256_bit_key,
35    recovery::{check_code, consume_code},
36    token::generate_hmac,
37    validate_account,
38};
39
40use crate::token::{extract_hmac_no_check, verify_client_signature, verify_hmac};
41use sha2::{Digest, Sha256};
42
43pub struct Auth {
44    pub domain: String,
45
46    pub config: AuthConfig,
47
48    totp_alg: Algorithm,
49
50    access_token_key: [u8; 32],
51    refresh_token_key: [u8; 32],
52    reset_password_token_key: [u8; 32],
53    invite_token_key: [u8; 32],
54
55    encryption_key: [u8; 32],
56
57    /// DB env
58    env: Arc<Environment>,
59
60    /// account -> `encrypted_opaque.encrypted_mfa_secret.password_file.token_claims`
61    /// [u8..255] -> ((([u8; 128][u8; 16][u8; 24])(168),([u8; 20][u8; 16][u8; 24])(60))(228),([u8; 192][u8; 16][u8; 24])(232))(),[u8; ..]
62    account_db: Arc<Database<'static>>,
63
64    /// account -> state | opaque
65    state_db: Arc<Database<'static>>,
66
67    /// `token_id` -> []
68    invite_db: Arc<Database<'static>>,
69
70    /// account -> `recovery_codes`
71    recovery_db: Arc<Database<'static>>,
72}
73
74type InviteClaimsValidator = fn(&str, &VectorReader<&[u8]>) -> anyhow::Result<bool>;
75
76impl Auth {
77    #[allow(clippy::too_many_lines)]
78    pub fn new(
79        domain: String,
80        config: Option<AuthConfig>,
81
82        encryption_key: [u8; 32],
83
84        env: Arc<Environment>,
85    ) -> anyhow::Result<Self> {
86        let account_db = Arc::new(Database::open(
87            env.clone(),
88            Some("user"),
89            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
90        )?);
91
92        let state_db = Arc::new(Database::open(
93            env.clone(),
94            Some("auth"),
95            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
96        )?);
97
98        let invite_db = Arc::new(Database::open(
99            env.clone(),
100            Some("invite"),
101            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
102        )?);
103
104        let recovery_db = Arc::new(Database::open(
105            env.clone(),
106            Some("recovery"),
107            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
108        )?);
109
110        // alg_byte -> uuid7.nonce.encrypted_key(s)
111        let key_db = Arc::new(Database::open(
112            env.clone(),
113            Some("key"),
114            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
115        )?);
116
117        let mut config = config.unwrap_or_else(|| AuthConfig {
118            password: PasswordConfig {
119                protocol: PasswordProtocol::Opaque,
120            },
121            mfa: MfaConfig {
122                totp: TotpConfig {
123                    template: None,
124                    algorithm: TotpAlgorithm::Sha1,
125                },
126            },
127            refresh_token: RefreshTokenConfig::default(),
128            access_token: AccessTokenConfig::default(),
129            client_hash: ClientPasswordHash::Sha256,
130            cookies_enabled: true,
131            invite: None,
132        });
133
134        config.access_token.claims.sort_by_key(|a| a.idx);
135
136        let txn = WriteTransaction::new(env.clone())?;
137
138        // todo: set up an interval for automatic token HMAC key refresh.
139
140        let access_token_key = Self::get_token_hmac_key(
141            &encryption_key,
142            &key_db,
143            config.access_token.rotation,
144            &txn,
145            "access",
146        )?;
147        let refresh_token_key = Self::get_token_hmac_key(
148            &encryption_key,
149            &key_db,
150            config.refresh_token.rotation,
151            &txn,
152            "refresh",
153        )?;
154        let reset_password_token_key = Self::get_token_hmac_key(
155            &encryption_key,
156            &key_db,
157            config.access_token.rotation,
158            &txn,
159            "reset_password",
160        )?;
161        let invite_token_key = Self::get_token_hmac_key(
162            &encryption_key,
163            &key_db,
164            config.access_token.rotation,
165            &txn,
166            "invite",
167        )?;
168
169        txn.commit()?;
170
171        Ok(Self {
172            domain,
173
174            totp_alg: match &config.mfa.totp.algorithm {
175                TotpAlgorithm::Sha1 => Algorithm::SHA1,
176            },
177            config,
178
179            access_token_key,
180            refresh_token_key,
181            reset_password_token_key,
182            invite_token_key,
183
184            encryption_key,
185
186            env,
187            state_db,
188            account_db,
189            invite_db,
190            recovery_db,
191        })
192    }
193
194    fn get_token_hmac_key(
195        encryption_key: &[u8; 32],
196        key_db: &Arc<Database>,
197        rotation: u32,
198        txn: &WriteTransaction,
199        key_name: &str,
200    ) -> anyhow::Result<[u8; 32]> {
201        let mut access = txn.access();
202        let mut rng = OsRng;
203
204        let cipher = {
205            use chacha20poly1305::aead::KeyInit;
206
207            XChaCha20Poly1305::new(encryption_key.as_ref().into())
208        };
209
210        let mut lookup: BytesMut = key_name.as_bytes().into();
211        lookup.put_u8(KeyAlg::Blake2SMac256.as_byte());
212
213        let hmac_key = if let Ok(kid_key) = access.get::<[u8], [u8]>(key_db, lookup.as_ref()) {
214            let (kid, curr_key) = decrypt_256_bit_key(&cipher, kid_key)?;
215
216            if let Some(timestamp) = kid.get_timestamp() {
217                if (timestamp.to_unix().0 + u64::from(rotation))
218                    < SystemTime::now()
219                        .duration_since(SystemTime::UNIX_EPOCH)?
220                        .as_secs()
221                {
222                    let (kid_key, new_key, _kid) = generate_256_bit_key(&cipher, &mut rng)?;
223
224                    access.put(key_db, lookup.as_ref(), &kid_key, &put::Flags::empty())?;
225
226                    new_key
227                } else {
228                    curr_key
229                }
230            } else {
231                curr_key
232            }
233        } else {
234            let (kid_key, hmac_key, _kid) = generate_256_bit_key(&cipher, &mut rng)?;
235
236            access.put(key_db, lookup.as_ref(), &kid_key, &put::Flags::empty())?;
237
238            hmac_key
239        };
240
241        let hashed_key: [u8; 32] = blake2::Blake2sMac256::new_from_slice(&hmac_key[..])?
242            .chain_update(lookup.as_ref())
243            .finalize_fixed()
244            .into();
245
246        Ok(hashed_key)
247    }
248
249    /// account length, client start
250    const MIN_REGISTRATION_START_LEN: usize = 1 + 32;
251
252    /// (account, `client_start`)
253    #[allow(clippy::type_complexity)]
254    #[instrument(skip_all, err)]
255    pub fn registration_start(
256        &self,
257        payload: Bytes,
258        existing_account: Option<&str>,
259        invite_claims_validator: Option<InviteClaimsValidator>,
260        checked_claims: Option<(VectorReader<&[u8]>, &[u8])>,
261    ) -> anyhow::Result<Bytes> {
262        use chacha20poly1305::aead::KeyInit;
263
264        if payload.len() < Self::MIN_REGISTRATION_START_LEN {
265            bail!("payload too small");
266        }
267
268        let account_len = payload[0] as usize;
269
270        if account_len != payload.len() - Self::MIN_REGISTRATION_START_LEN {
271            bail!("invalid format");
272        }
273
274        let raw_account = &payload[1..=account_len];
275        let raw_account_str = std::str::from_utf8(raw_account)?;
276
277        let account_str = validate_account(raw_account_str)?;
278        let account = account_str.as_bytes();
279
280        if let Some(existing_account) = existing_account
281            && existing_account != account_str
282        {
283            bail!("account mismatch");
284        }
285
286        tracing::info!(account = %account_str);
287
288        let invite_claims = if self.config.invite.is_some() && existing_account.is_none() {
289            let Some((claims_vec, claims)) = checked_claims else {
290                bail!("no checked claims")
291            };
292
293            if let Some(validator) = invite_claims_validator {
294                let res = validator(&account_str, &claims_vec)?;
295
296                if !res {
297                    bail!("failed to validate invite token claims");
298                }
299            }
300
301            Some(claims)
302        } else {
303            None
304        };
305
306        let txn = WriteTransaction::new(self.env.clone())?;
307
308        let out = {
309            let mut access = txn.access();
310
311            if existing_account.is_none() {
312                if access.get::<[u8], [u8]>(&self.account_db, account).is_ok() {
313                    bail!(
314                        "account {} has already been registered.",
315                        std::str::from_utf8(account)?
316                    );
317                }
318
319                if access.get::<[u8], [u8]>(&self.state_db, account).is_ok() {
320                    bail!(
321                        "account {} has already been registered.",
322                        std::str::from_utf8(account)?
323                    );
324                }
325            }
326
327            let client_start = &payload[account_len + 1..account_len + 1 + 32];
328
329            let mut rng = OsRng;
330            let opaque = ServerSetup::<DefaultCipherSuite>::new(&mut rng);
331
332            let out = crate::registration::server_start(&opaque, account, client_start)?;
333
334            let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
335            let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
336
337            let mut encrypted_stored = match cipher.encrypt(&nonce, opaque.serialize().as_ref()) {
338                Ok(v) => v,
339                Err(err) => bail!("{err}"),
340            };
341            encrypted_stored.extend_from_slice(&nonce);
342
343            if let Some(invite_claims) = invite_claims {
344                encrypted_stored.extend_from_slice(invite_claims);
345            }
346
347            access.put(
348                &self.state_db,
349                account,
350                &encrypted_stored,
351                &put::Flags::empty(),
352            )?;
353
354            out
355        };
356
357        txn.commit()?;
358
359        Ok(out)
360    }
361
362    /// account length, ephemeral public key, client finish
363    const MIN_REGISTRATION_FINISH_LEN: usize = 1 + 32 + 192;
364
365    #[allow(clippy::type_complexity, clippy::too_many_lines)]
366    /// (account, `public_key`, `client_finish`)
367    #[instrument(skip_all, err)]
368    pub fn registration_finish(
369        &self,
370        payload: Bytes,
371        existing_account: Option<&str>,
372    ) -> anyhow::Result<(Bytes, Vec<u8>, Option<Bytes>)> {
373        use chacha20poly1305::aead::KeyInit;
374
375        if payload.len() < Self::MIN_REGISTRATION_FINISH_LEN {
376            bail!("payload too small");
377        }
378
379        let account_len = payload[0] as usize;
380
381        if account_len != payload.len() - Self::MIN_REGISTRATION_FINISH_LEN {
382            bail!("invalid format");
383        }
384
385        let raw_account = &payload[1..=account_len];
386        let raw_account_str = std::str::from_utf8(raw_account)?;
387
388        let account_str = validate_account(raw_account_str)?;
389        let account = account_str.as_bytes();
390
391        if let Some(existing_account) = existing_account
392            && existing_account != account_str
393        {
394            bail!("account mismatch");
395        }
396
397        tracing::info!(account = %account_str);
398
399        let public_key: [u8; 32] = payload[account_len + 1..account_len + 33].try_into()?;
400
401        if public_key == ZEROED_KEY {
402            bail!("public key cannot be all 0s");
403        }
404
405        let client_finish = &payload[account_len + 33..account_len + 33 + 192];
406
407        let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
408
409        let totp_nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
410        let password_file_nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
411
412        let txn = WriteTransaction::new(self.env.clone())?;
413
414        let (mut totp_mfa_secret, recovery_codes, invite_claims) = {
415            let mut invite_claims = None;
416
417            let mut access = txn.access();
418
419            let (totp_mfa_secret, recovery_codes) = if existing_account.is_none() {
420                if access.get::<[u8], [u8]>(&self.account_db, account).is_ok() {
421                    bail!("account has already been registered");
422                }
423
424                let password_file = crate::registration::server_finish(client_finish)?;
425
426                let mut password_file =
427                    match cipher.encrypt(&password_file_nonce, password_file.as_ref()) {
428                        Ok(v) => v,
429                        Err(err) => bail!("{err}"),
430                    };
431                password_file.extend_from_slice(&password_file_nonce);
432
433                // len == 20
434                let totp_mfa_secret = Secret::generate_secret().to_bytes()?;
435
436                let mut encrypted_totp_mfa_secret =
437                    match cipher.encrypt(&totp_nonce, totp_mfa_secret.as_ref()) {
438                        Ok(v) => v,
439                        Err(err) => bail!("{err}"),
440                    };
441                encrypted_totp_mfa_secret.extend_from_slice(&totp_nonce);
442
443                password_file.splice(0..0, encrypted_totp_mfa_secret.iter().copied());
444
445                let serialized_opaque_and_invite_claims =
446                    access.get::<[u8], [u8]>(&self.state_db, account)?;
447
448                if serialized_opaque_and_invite_claims == b"deleted" {
449                    bail!("{} has been deleted", std::str::from_utf8(account)?);
450                }
451
452                if serialized_opaque_and_invite_claims.len() > 168 {
453                    invite_claims = Some(Bytes::copy_from_slice(
454                        &serialized_opaque_and_invite_claims[168..],
455                    ));
456                }
457
458                password_file.splice(
459                    0..0,
460                    serialized_opaque_and_invite_claims[0..168].iter().copied(),
461                );
462
463                let mut empty_claims_builder =
464                    flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
465                let empty_claims_vec = empty_claims_builder.start_vector();
466                empty_claims_vec.end_vector();
467
468                password_file.extend_from_slice(empty_claims_builder.view());
469
470                let (stored_recovery_codes, recovery_codes) = crate::recovery::generate_codes()?;
471
472                access.put(
473                    &self.recovery_db,
474                    account,
475                    &stored_recovery_codes[..],
476                    &put::Flags::empty(),
477                )?;
478
479                access.put(
480                    &self.account_db,
481                    account,
482                    &password_file,
483                    &put::Flags::empty(),
484                )?;
485
486                (totp_mfa_secret, recovery_codes)
487            } else {
488                let account_record: &[u8] = access.get(&self.account_db, account)?;
489
490                if account_record == b"deleted" {
491                    bail!("{} has been deleted", std::str::from_utf8(account)?);
492                }
493
494                let password_file = crate::registration::server_finish(client_finish)?;
495
496                let mut password_file =
497                    match cipher.encrypt(&password_file_nonce, password_file.as_ref()) {
498                        Ok(v) => v,
499                        Err(err) => bail!("{err}"),
500                    };
501                password_file.extend_from_slice(&password_file_nonce);
502
503                password_file.splice(0..0, account_record[168..228].iter().copied());
504
505                let serialized_opaque_and_invite_claims =
506                    access.get::<[u8], [u8]>(&self.state_db, account)?;
507
508                if serialized_opaque_and_invite_claims == b"deleted" {
509                    bail!("{} has been deleted", std::str::from_utf8(account)?);
510                }
511
512                if serialized_opaque_and_invite_claims.len() > 168 {
513                    invite_claims = Some(Bytes::copy_from_slice(
514                        &serialized_opaque_and_invite_claims[168..],
515                    ));
516                }
517
518                password_file.splice(
519                    0..0,
520                    serialized_opaque_and_invite_claims[0..168].iter().copied(),
521                );
522
523                password_file.extend_from_slice(&account_record[460..]);
524
525                access.put(
526                    &self.account_db,
527                    account,
528                    &password_file,
529                    &put::Flags::empty(),
530                )?;
531
532                (vec![], String::new())
533            };
534
535            access.del_key(&self.state_db, account)?;
536
537            (totp_mfa_secret, recovery_codes, invite_claims)
538        };
539
540        txn.commit()?;
541
542        if existing_account.is_none() {
543            let public_key = PublicKey::from(public_key);
544            let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
545            let ephemeral_public_key = PublicKey::from(&ephemeral_secret);
546
547            let shared_secret = ephemeral_secret.diffie_hellman(&public_key);
548
549            if !shared_secret.was_contributory() {
550                bail!("non-contributory shared secret");
551            }
552
553            let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
554            let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
555
556            totp_mfa_secret.extend_from_slice(recovery_codes.as_bytes());
557
558            let mut encrypted_secret = match cipher.encrypt(&nonce, totp_mfa_secret.as_ref()) {
559                Ok(es) => es,
560                Err(err) => bail!("{err}"),
561            };
562            encrypted_secret.extend_from_slice(&nonce);
563            encrypted_secret.extend_from_slice(ephemeral_public_key.as_bytes());
564
565            Ok((
566                Bytes::from(encrypted_secret),
567                account.to_vec(),
568                invite_claims,
569            ))
570        } else {
571            Ok((Bytes::new(), account.to_vec(), invite_claims))
572        }
573    }
574
575    /// account length, client start
576    const MIN_LOGIN_START_LEN: usize = 1 + 96;
577
578    /// (account, `client_start`)
579    #[instrument(skip_all, err)]
580    pub fn login_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
581        if payload.len() < Self::MIN_LOGIN_START_LEN {
582            bail!("payload too small");
583        }
584
585        let account_len = payload[0] as usize;
586
587        if account_len != payload.len() - Self::MIN_LOGIN_START_LEN {
588            bail!("invalid format");
589        }
590
591        let raw_account = &payload[1..=account_len];
592        let raw_account_str = std::str::from_utf8(raw_account)?;
593
594        let account_str = validate_account(raw_account_str)?;
595        let account = account_str.as_bytes();
596
597        tracing::info!(account = %account_str);
598
599        let client_start = &payload[account_len + 1..account_len + 1 + 96];
600
601        let txn = WriteTransaction::new(self.env.clone())?;
602
603        let message = {
604            use chacha20poly1305::aead::KeyInit;
605
606            let mut access = txn.access();
607
608            let account_record: &[u8] = access.get(&self.account_db, account)?;
609
610            if account_record == b"deleted" {
611                bail!("{} has been deleted", std::str::from_utf8(account)?);
612            }
613
614            let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
615
616            let nonce = XNonce::from_slice(&account_record[144..168]);
617            let serialized_opaque = match cipher.decrypt(nonce, &account_record[0..144]) {
618                Ok(v) => v,
619                Err(err) => bail!("{err}"),
620            };
621
622            let opaque = match ServerSetup::<DefaultCipherSuite>::deserialize(&serialized_opaque) {
623                Ok(o) => o,
624                Err(e) => bail!("{e}"),
625            };
626
627            let nonce = XNonce::from_slice(&account_record[436..460]);
628            let password_file = match cipher.decrypt(nonce, &account_record[228..436]) {
629                Ok(v) => v,
630                Err(err) => bail!("{err}"),
631            };
632
633            let (state, message) =
634                crate::login::server_start(&opaque, account, &password_file, client_start)?;
635
636            access.put(&self.state_db, account, &state[..], &put::Flags::empty())?;
637
638            message
639        };
640
641        txn.commit()?;
642
643        Ok(message)
644    }
645
646    /// account length, MFA code SHA-256 hash, poly1305 MAC, nonce, client finish
647    const MIN_LOGIN_FINISH_LEN: usize = 1 + 32 + 16 + 24 + 64;
648
649    /// (account, encrypted(`mfa_hash`, Option<`verifying_key`>), `client_finish`)
650    #[instrument(skip_all, err)]
651    #[allow(clippy::type_complexity)]
652    pub fn login_finish(
653        &self,
654        payload: Bytes,
655        check_mfa: bool,
656    ) -> anyhow::Result<(Bytes, [u8; 32], Option<Bytes>)> {
657        let payload_len = payload.len();
658
659        if payload_len < Self::MIN_LOGIN_FINISH_LEN {
660            bail!("payload is too small");
661        }
662
663        let account_len = payload[0] as usize;
664
665        if payload_len != account_len + Self::MIN_LOGIN_FINISH_LEN
666            && payload_len != account_len + Self::MIN_LOGIN_FINISH_LEN + 32
667        {
668            bail!("invalid format");
669        }
670
671        let raw_account = &payload[1..=account_len];
672        let raw_account_str = std::str::from_utf8(raw_account)?;
673
674        let account_str = validate_account(raw_account_str)?;
675        let account = account_str.as_bytes();
676
677        tracing::info!(account = %account_str);
678
679        let encrypted_mfa_code = &payload[account_len + 1..payload_len - 24 - 64];
680        let mfa_code_nonce = &payload[payload_len - 24 - 64..payload_len - 64];
681        let client_finish = &payload[payload_len - 64..];
682
683        let txn = WriteTransaction::new(self.env.clone())?;
684
685        let (encrypted_refresh_token, key, verifier) = {
686            let mut access = txn.access();
687
688            let account_record: &[u8] = access.get(&self.account_db, account)?;
689
690            if account_record == b"deleted" {
691                bail!("{} has been deleted", std::str::from_utf8(account)?);
692            }
693
694            let cipher = {
695                use chacha20poly1305::aead::KeyInit;
696                XChaCha20Poly1305::new(&self.encryption_key.into())
697            };
698
699            let nonce = XNonce::from_slice(&account_record[204..228]);
700            let mfa_secret = match cipher.decrypt(nonce, &account_record[168..204]) {
701                Ok(v) => v,
702                Err(err) => bail!("{err}"),
703            };
704
705            let mfa_hash = if check_mfa {
706                let mfa_code = TOTP::new(
707                    self.totp_alg,
708                    6,
709                    1,
710                    30,
711                    Secret::Raw(mfa_secret).to_bytes()?,
712                    Some(self.domain.clone()),
713                    std::str::from_utf8(account)?.to_string(),
714                )?
715                .generate_current()?;
716
717                let mut mfa_input = self.domain.as_bytes().to_vec();
718                mfa_input.extend_from_slice(account);
719                mfa_input.extend_from_slice(mfa_code.as_bytes());
720
721                match &self.config.client_hash {
722                    ClientPasswordHash::Sha256 => {
723                        let mut hasher = Sha256::new();
724                        hasher.update(&mfa_input);
725                        hasher.finalize().to_vec()
726                    }
727                }
728            } else {
729                vec![0u8; 32]
730            };
731
732            let server_start: &[u8] = access.get(&self.state_db, account)?;
733
734            if server_start == b"deleted" {
735                bail!("{} has been deleted", std::str::from_utf8(account)?);
736            }
737
738            let (encrypted_refresh_token, key, verifier) = crate::login::server_finish(
739                self,
740                account,
741                &mfa_hash[..],
742                encrypted_mfa_code,
743                mfa_code_nonce,
744                client_finish,
745                server_start,
746            )?;
747
748            access.del_key(&self.state_db, account)?;
749
750            (encrypted_refresh_token, key, verifier)
751        };
752
753        txn.commit()?;
754        Ok((encrypted_refresh_token, key, verifier))
755    }
756
757    #[instrument(skip_all, err)]
758    pub fn reset_password_login_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
759        self.login_start(payload)
760    }
761
762    #[instrument(skip_all, err)]
763    pub fn reset_password_login_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
764        use chacha20poly1305::aead::KeyInit;
765
766        let (_, key, verifier) = self.login_finish(payload.clone(), true)?;
767
768        let account_len = (*payload.first().expect("payload already checked")) as usize;
769        let account_str = std::str::from_utf8(&payload[1..=account_len])?;
770
771        let mut reset_password_claims =
772            flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
773        let mut reset_password_claims_vec = reset_password_claims.start_vector();
774
775        let token_uuid = Uuid::now_v7();
776        let token_uuid_bytes = token_uuid.as_bytes();
777
778        reset_password_claims_vec.push(flexbuffers::Blob(&token_uuid_bytes[..]));
779        reset_password_claims_vec.push(self.domain.as_str());
780        reset_password_claims_vec.push(account_str);
781
782        if let Some(verifier) = verifier {
783            reset_password_claims_vec.push(flexbuffers::Blob(&verifier[..]));
784        }
785
786        reset_password_claims_vec.end_vector();
787
788        let password_reset_token = generate_hmac(
789            reset_password_claims.view(),
790            60 * 15,
791            &self.reset_password_token_key,
792        )?;
793
794        let cipher = XChaCha20Poly1305::new(&key.into());
795        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
796        let mut encrypted_password_reset_token =
797            match cipher.encrypt(&nonce, password_reset_token.as_ref()) {
798                Ok(et) => et,
799                Err(err) => bail!("{err}"),
800            };
801
802        encrypted_password_reset_token.extend_from_slice(&nonce);
803
804        Ok(Bytes::copy_from_slice(&encrypted_password_reset_token))
805    }
806
807    #[instrument(skip_all, err)]
808    pub fn reset_password_registration_start(
809        &self,
810        payload: Bytes,
811        token: &[u8],
812    ) -> anyhow::Result<Bytes> {
813        let account_str = self.verify_password_reset_token(token)?;
814        self.registration_start(payload, Some(account_str), None, None)
815    }
816
817    #[instrument(skip_all, err)]
818    pub fn reset_password_registration_finish(
819        &self,
820        payload: Bytes,
821        token: &[u8],
822    ) -> anyhow::Result<()> {
823        let account_str = self.verify_password_reset_token(token)?;
824        self.registration_finish(payload, Some(account_str))?;
825
826        Ok(())
827    }
828
829    #[instrument(skip_all, err)]
830    pub fn forgot_password_start(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
831        let payload_len = payload.len();
832        if payload_len < 33 {
833            bail!("invalid payload");
834        }
835
836        let recovery_code = payload.split_off(payload_len - 32);
837
838        let account_len = (*payload.first().expect("payload already checked")) as usize;
839        if account_len > payload.len() - 1 {
840            bail!("invalid format");
841        }
842
843        let account = Bytes::copy_from_slice(&payload[1..=account_len]);
844        let account_str = std::str::from_utf8(&account[..])?;
845
846        let txn = ReadTransaction::new(self.env.clone())?;
847
848        let access = txn.access();
849
850        let read_recovery_codes = access.get(&self.recovery_db, account.as_ref())?;
851
852        if read_recovery_codes == b"deleted" {
853            bail!(
854                "{} has been deleted",
855                std::str::from_utf8(account.as_ref())?
856            );
857        }
858
859        let is_valid_recovery_code = check_code(&recovery_code[..], read_recovery_codes)?;
860
861        if !is_valid_recovery_code {
862            bail!("invalid recovery code");
863        }
864
865        self.registration_start(payload, Some(account_str), None, None)
866    }
867
868    #[instrument(skip_all, err)]
869    pub fn forgot_password_finish(&self, mut payload: Bytes) -> anyhow::Result<()> {
870        let payload_len = payload.len();
871        if payload_len < 12 {
872            bail!("invalid payload");
873        }
874
875        let recovery_code = payload.split_off(payload_len - 11);
876
877        let account_len = (*payload.first().expect("payload already checked")) as usize;
878        if account_len > payload.len() - 1 {
879            bail!("invalid format");
880        }
881
882        let account = Bytes::copy_from_slice(&payload[1..=account_len]);
883        let account_str = std::str::from_utf8(&account[..])?;
884
885        let txn = WriteTransaction::new(self.env.clone())?;
886
887        {
888            let mut access = txn.access();
889
890            let read_recovery_codes = access.get(&self.recovery_db, account.as_ref())?;
891
892            if read_recovery_codes == b"deleted" {
893                bail!(
894                    "{} has been deleted",
895                    std::str::from_utf8(account.as_ref())?
896                );
897            }
898
899            let (is_valid_recovery_code, stored_codes) =
900                consume_code(&recovery_code[..], read_recovery_codes)?;
901
902            if is_valid_recovery_code {
903                access.put(
904                    &self.recovery_db,
905                    &account[..],
906                    &stored_codes[..],
907                    &put::Flags::empty(),
908                )?;
909            } else {
910                bail!("invalid recovery code");
911            }
912        }
913
914        txn.commit()?;
915
916        self.registration_finish(payload, Some(account_str))?;
917
918        Ok(())
919    }
920
921    #[instrument(skip_all, err)]
922    pub fn reset_totp_mfa_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
923        self.login_start(payload)
924    }
925
926    #[instrument(skip_all, err)]
927    pub fn reset_totp_mfa_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
928        use chacha20poly1305::aead::KeyInit;
929
930        let (_, key, _) = self.login_finish(payload.clone(), true)?;
931
932        let account_len = (*payload.first().expect("payload already checked")) as usize;
933        let account = &payload[1..=account_len];
934
935        let txn = WriteTransaction::new(self.env.clone())?;
936
937        let mfa_secret = {
938            let mut access = txn.access();
939
940            let mfa_secret = Secret::generate_secret().to_bytes()?;
941
942            let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
943            let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
944
945            let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
946                Ok(v) => v,
947                Err(err) => bail!("{err}"),
948            };
949            encrypted_mfa_secret.extend_from_slice(&nonce);
950
951            let account_record: &[u8] = access.get(&self.account_db, account)?;
952
953            if account_record == b"deleted" {
954                bail!("{} has been deleted", std::str::from_utf8(account)?);
955            }
956
957            let mut account_record = account_record.to_vec();
958
959            account_record.splice(168..228, encrypted_mfa_secret);
960
961            access.put(
962                &self.account_db,
963                account,
964                &account_record,
965                &put::Flags::empty(),
966            )?;
967
968            mfa_secret
969        };
970
971        txn.commit()?;
972
973        let cipher = XChaCha20Poly1305::new(&key.into());
974        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
975        let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
976            Ok(es) => es,
977            Err(err) => bail!("{err}"),
978        };
979
980        encrypted_mfa_secret.extend_from_slice(&nonce);
981
982        Ok(Bytes::copy_from_slice(&encrypted_mfa_secret))
983    }
984
985    #[instrument(skip_all, err)]
986    pub fn lost_totp_mfa_start(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
987        let payload_len = payload.len();
988        if payload_len < 33 {
989            bail!("invalid payload");
990        }
991
992        let recovery_code = payload.split_off(payload_len - 32);
993
994        let server_message = self.login_start(payload.clone())?;
995
996        let account_len = (*payload.first().expect("payload already checked")) as usize;
997        let account = &payload[1..=account_len];
998
999        let txn = ReadTransaction::new(self.env.clone())?;
1000
1001        let access = txn.access();
1002
1003        let read_recovery_codes = access.get(&self.recovery_db, account)?;
1004
1005        if read_recovery_codes == b"deleted" {
1006            bail!("{} has been deleted", std::str::from_utf8(account)?);
1007        }
1008
1009        let is_valid_recovery_code = check_code(&recovery_code[..], read_recovery_codes)?;
1010
1011        if !is_valid_recovery_code {
1012            bail!("invalid recovery code");
1013        }
1014
1015        Ok(server_message)
1016    }
1017
1018    #[instrument(skip_all, err)]
1019    pub fn lost_totp_mfa_finish(&self, mut payload: Bytes) -> anyhow::Result<Bytes> {
1020        use chacha20poly1305::aead::KeyInit;
1021
1022        let payload_len = payload.len();
1023        if payload_len < 12 {
1024            bail!("invalid payload");
1025        }
1026
1027        let recovery_code = payload.split_off(payload_len - 11);
1028
1029        let (_, key, _) = self.login_finish(payload.clone(), false)?;
1030
1031        let account_len = (*payload.first().expect("payload already checked")) as usize;
1032        let account = &payload[1..=account_len];
1033
1034        let txn = WriteTransaction::new(self.env.clone())?;
1035
1036        let mfa_secret = {
1037            let mut access = txn.access();
1038
1039            let read_recovery_codes = access.get(&self.recovery_db, account)?;
1040
1041            if read_recovery_codes == b"deleted" {
1042                bail!("{} has been deleted", std::str::from_utf8(account)?);
1043            }
1044
1045            let (is_valid_recovery_code, stored_codes) =
1046                consume_code(&recovery_code[..], read_recovery_codes)?;
1047
1048            if is_valid_recovery_code {
1049                access.put(
1050                    &self.recovery_db,
1051                    account,
1052                    &stored_codes[..],
1053                    &put::Flags::empty(),
1054                )?;
1055
1056                let mfa_secret = Secret::generate_secret().to_bytes()?;
1057
1058                let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1059                let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
1060
1061                let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
1062                    Ok(v) => v,
1063                    Err(err) => bail!("{err}"),
1064                };
1065                encrypted_mfa_secret.extend_from_slice(&nonce);
1066
1067                let account_record: &[u8] = access.get(&self.account_db, account)?;
1068
1069                if account_record == b"deleted" {
1070                    bail!("{} has been deleted", std::str::from_utf8(account)?);
1071                }
1072
1073                let mut account_record = account_record.to_vec();
1074
1075                account_record.splice(168..228, encrypted_mfa_secret);
1076
1077                access.put(
1078                    &self.account_db,
1079                    account,
1080                    &account_record,
1081                    &put::Flags::empty(),
1082                )?;
1083
1084                mfa_secret
1085            } else {
1086                bail!("invalid recovery code");
1087            }
1088        };
1089
1090        txn.commit()?;
1091
1092        let cipher = XChaCha20Poly1305::new(&key.into());
1093        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1094        let mut encrypted_mfa_secret = match cipher.encrypt(&nonce, mfa_secret.as_ref()) {
1095            Ok(es) => es,
1096            Err(err) => bail!("{err}"),
1097        };
1098
1099        encrypted_mfa_secret.extend_from_slice(&nonce);
1100
1101        Ok(Bytes::copy_from_slice(&encrypted_mfa_secret))
1102    }
1103
1104    #[instrument(skip_all, err)]
1105    pub fn reset_recovery_codes_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1106        self.login_start(payload)
1107    }
1108
1109    #[instrument(skip_all, err)]
1110    pub fn reset_recovery_codes_finish(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1111        use chacha20poly1305::aead::KeyInit;
1112
1113        let (_, key, _) = self.login_finish(payload.clone(), true)?;
1114
1115        let account_len = (*payload.first().expect("payload already checked")) as usize;
1116        let account = &payload[1..=account_len];
1117
1118        let (stored_recovery_codes, recovery_codes) = crate::recovery::generate_codes()?;
1119
1120        let txn = WriteTransaction::new(self.env.clone())?;
1121
1122        {
1123            let mut access = txn.access();
1124
1125            access.put(
1126                &self.recovery_db,
1127                account,
1128                &stored_recovery_codes[..],
1129                &put::Flags::empty(),
1130            )?;
1131        }
1132
1133        txn.commit()?;
1134
1135        let cipher = XChaCha20Poly1305::new(&key.into());
1136        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1137        let mut encrypted_recovery_codes = match cipher.encrypt(&nonce, recovery_codes.as_bytes()) {
1138            Ok(rc) => rc,
1139            Err(err) => bail!("{err}"),
1140        };
1141
1142        encrypted_recovery_codes.extend_from_slice(&nonce);
1143
1144        Ok(Bytes::copy_from_slice(&encrypted_recovery_codes))
1145    }
1146
1147    #[instrument(skip_all, err)]
1148    pub fn delete_account_start(&self, payload: Bytes) -> anyhow::Result<Bytes> {
1149        self.login_start(payload)
1150    }
1151
1152    #[instrument(skip_all, err)]
1153    pub fn delete_account_finish(&self, payload: Bytes) -> anyhow::Result<()> {
1154        self.login_finish(payload.clone(), true)?;
1155
1156        let account_len = (*payload.first().expect("payload already checked")) as usize;
1157        let account = &payload[1..=account_len];
1158
1159        let txn = WriteTransaction::new(self.env.clone())?;
1160
1161        {
1162            let mut access = txn.access();
1163            access.put(&self.account_db, account, b"deleted", &put::Flags::empty())?;
1164            access.put(&self.state_db, account, b"deleted", &put::Flags::empty())?;
1165            access.put(&self.recovery_db, account, b"deleted", &put::Flags::empty())?;
1166        }
1167
1168        txn.commit()?;
1169
1170        Ok(())
1171    }
1172
1173    /// returns flexbuffer that is a vector of `account.claim_field.claim_field`...
1174    #[instrument(skip_all, err)]
1175    pub fn list_accounts(&self) -> anyhow::Result<Bytes> {
1176        let txn = ReadTransaction::new(self.env.clone())?;
1177
1178        let access = txn.access();
1179        let mut cursor = txn.cursor(self.account_db.clone())?;
1180
1181        let (first_account, first_user) = cursor.first::<[u8], [u8]>(&access)?;
1182
1183        let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1184        let mut builder_vec = builder.start_vector();
1185
1186        let mut first_user_vec = builder_vec.start_vector();
1187
1188        first_user_vec.push(first_account);
1189
1190        if first_user.len() > 460 {
1191            let claims_reader = flexbuffers::Reader::get_root(&first_user[460..])?;
1192
1193            for claims_field in &self.config.access_token.claims {
1194                claims_field.kind.copy_to(
1195                    &claims_reader.as_vector().idx(claims_field.idx as usize),
1196                    &mut first_user_vec,
1197                    None,
1198                )?;
1199            }
1200        }
1201
1202        first_user_vec.end_vector();
1203
1204        while let Ok((account, user)) = cursor.next::<[u8], [u8]>(&access) {
1205            let mut user_vec = builder_vec.start_vector();
1206
1207            user_vec.push(account);
1208
1209            if user.len() > 460 {
1210                let claims_reader = flexbuffers::Reader::get_root(&user[460..])?;
1211
1212                for claims_field in &self.config.access_token.claims {
1213                    claims_field.kind.copy_to(
1214                        &claims_reader.as_vector().idx(claims_field.idx as usize),
1215                        &mut user_vec,
1216                        None,
1217                    )?;
1218                }
1219            }
1220
1221            user_vec.end_vector();
1222        }
1223
1224        builder_vec.end_vector();
1225
1226        Ok(Bytes::copy_from_slice(builder.view()))
1227    }
1228
1229    #[instrument(skip_all, err)]
1230    pub fn set_claims(&self, account: &[u8], claims: &[u8]) -> anyhow::Result<()> {
1231        let str_account = std::str::from_utf8(account)?;
1232
1233        let claims_root = flexbuffers::Reader::get_root(claims)?;
1234
1235        let mut claims_builder =
1236            flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1237        let mut dest = claims_builder.start_vector();
1238
1239        let system_claims_gap = dest.start_vector();
1240        system_claims_gap.end_vector();
1241
1242        let claims_vec = claims_root.as_vector();
1243
1244        for claim in &self.config.access_token.claims {
1245            let src = claims_vec.idx(claim.idx as usize);
1246
1247            claim.kind.copy_to(&src, &mut dest, None)?;
1248        }
1249
1250        dest.end_vector();
1251
1252        // use the account to write this to the claims
1253        let txn = WriteTransaction::new(self.env.clone())?;
1254        {
1255            let mut access = txn.access();
1256            let account_record: &[u8] = access.get(&self.account_db, account)?;
1257
1258            if account_record == b"deleted" {
1259                bail!("{} has been deleted", std::str::from_utf8(account)?);
1260            }
1261
1262            let mut account_record = account_record.to_vec();
1263
1264            if account_record.len() > 460 {
1265                account_record.truncate(460);
1266            }
1267
1268            account_record.extend_from_slice(claims_builder.view());
1269
1270            access.put(
1271                &self.account_db,
1272                account,
1273                &account_record[..],
1274                &put::Flags::empty(),
1275            )?;
1276        }
1277
1278        txn.commit()?;
1279
1280        tracing::info!(account = %str_account);
1281
1282        Ok(())
1283    }
1284
1285    fn create_invite(
1286        &self,
1287        invite_config: &InviteConfig,
1288        inviter_account: &str,
1289        custom_claims: Option<Bytes>,
1290    ) -> anyhow::Result<Bytes> {
1291        let mut invite_claims = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1292        let mut invite_claims_vec = invite_claims.start_vector();
1293
1294        let mut system_claims_vec = invite_claims_vec.start_vector();
1295
1296        let token_uuid = Uuid::now_v7();
1297        let token_uuid_bytes = token_uuid.as_bytes();
1298
1299        tracing::info!(account = %inviter_account, token = %token_uuid);
1300
1301        system_claims_vec.push(flexbuffers::Blob(&token_uuid_bytes[..]));
1302        system_claims_vec.push(self.domain.as_str());
1303        system_claims_vec.push(inviter_account);
1304
1305        system_claims_vec.end_vector();
1306
1307        if let Some(config_claims) = &invite_config.claims
1308            && let Some(custom_claims) = custom_claims
1309        {
1310            let custom_claims_root = flexbuffers::Reader::get_root(custom_claims.as_ref())?;
1311            let custom_claims = custom_claims_root.as_vector();
1312
1313            for claim in config_claims {
1314                claim.kind.copy_to(
1315                    &custom_claims.idx(claim.idx as usize),
1316                    &mut invite_claims_vec,
1317                    None,
1318                )?;
1319            }
1320        }
1321
1322        invite_claims_vec.end_vector();
1323
1324        match &invite_config.mode {
1325            InviteMode::Root => {
1326                if inviter_account != format!("root@{}", self.domain) {
1327                    bail!("inviter must be root");
1328                }
1329            }
1330            InviteMode::Admin => {
1331                // todo: check against admin-only constraints
1332            }
1333            InviteMode::Viral => (),
1334        }
1335
1336        let invite_token = generate_hmac(
1337            invite_claims.view(),
1338            invite_config.lifetime,
1339            &self.invite_token_key,
1340        )?;
1341
1342        let txn = WriteTransaction::new(self.env.clone())?;
1343        {
1344            let mut access = txn.access();
1345            access.put(
1346                &self.invite_db,
1347                &token_uuid_bytes[..],
1348                &[] as &[u8],
1349                &put::Flags::empty(),
1350            )?;
1351        }
1352
1353        txn.commit()?;
1354
1355        Ok(invite_token)
1356    }
1357
1358    pub fn account_exists(&self, account: &str) -> anyhow::Result<bool> {
1359        let txn = ReadTransaction::new(self.env.clone())?;
1360        let access = txn.access();
1361
1362        if access
1363            .get::<[u8], [u8]>(&self.account_db, account.as_bytes())
1364            .is_ok()
1365        {
1366            return Ok(true);
1367        }
1368
1369        if access
1370            .get::<[u8], [u8]>(&self.state_db, account.as_bytes())
1371            .is_ok()
1372        {
1373            return Ok(true);
1374        }
1375
1376        Ok(false)
1377    }
1378
1379    #[instrument(skip_all, err)]
1380    pub fn api_invite_get(
1381        &self,
1382        inviter_domain: &str,
1383        inviter_account: &str,
1384        custom_claims: Option<Bytes>,
1385    ) -> anyhow::Result<Bytes> {
1386        if let Some(invite_config) = &self.config.invite {
1387            let account = format!("{inviter_account}@{inviter_domain}");
1388
1389            self.create_invite(invite_config, account.as_str(), custom_claims)
1390        } else {
1391            bail!("no invite config")
1392        }
1393    }
1394
1395    #[instrument(skip_all, err)]
1396    pub fn invite_get(&self, access_token: Bytes) -> anyhow::Result<Bytes> {
1397        if let Some(invite_config) = &self.config.invite {
1398            let (account, _) = self.verify_access_token(&access_token)?;
1399
1400            // todo: handle custom claims
1401            self.create_invite(invite_config, account, None)
1402        } else {
1403            bail!("no invite config")
1404        }
1405    }
1406
1407    #[allow(clippy::type_complexity)]
1408    #[instrument(skip_all, err)]
1409    pub fn invite_check<'a>(
1410        &self,
1411        invite_token: &'a [u8],
1412    ) -> anyhow::Result<(VectorReader<&'a [u8]>, &'a [u8])> {
1413        if let Some(invite_config) = &self.config.invite {
1414            let claims = verify_hmac(invite_token, &self.invite_token_key)?;
1415
1416            let claims_root = flexbuffers::Reader::get_root(claims)?;
1417            let claims_vec = claims_root.as_vector();
1418            let system_claims = claims_vec.idx(0).as_vector();
1419
1420            let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
1421
1422            let token_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1423            let api_or_site_domain = system_claims.idx(1).as_str();
1424            let inviter_account = system_claims.idx(2).as_str();
1425
1426            if api_or_site_domain == self.domain {
1427                match invite_config.mode {
1428                    InviteMode::Root => {
1429                        if inviter_account != format!("root@{}", self.domain) {
1430                            bail!("inviter must be root");
1431                        }
1432                    }
1433                    InviteMode::Admin => {
1434                        // todo: check against admin-only criteria
1435                    }
1436                    InviteMode::Viral => (),
1437                }
1438
1439                // todo: make this transaction the same as the registration_start calls
1440                let txn = WriteTransaction::new(self.env.clone())?;
1441                {
1442                    let mut access = txn.access();
1443
1444                    if access
1445                        .del_key(&self.invite_db, &token_uuid_bytes[..])
1446                        .is_err()
1447                    {
1448                        bail!("token id does not exist");
1449                    }
1450                }
1451
1452                txn.commit()?;
1453
1454                tracing::info!(account = %inviter_account, token = %token_uuid_str);
1455
1456                Ok((claims_vec, claims))
1457            } else {
1458                bail!(
1459                    "domain {api_or_site_domain} does not match for invite {token_uuid_str} from user {inviter_account}"
1460                )
1461            }
1462        } else {
1463            bail!("no invite config")
1464        }
1465    }
1466
1467    pub fn access_get(&self, refresh_token: &Bytes) -> anyhow::Result<Bytes> {
1468        let span = tracing::info_span!("auth");
1469        let span = span.in_scope(|| tracing::info_span!("token"));
1470        let span = span.in_scope(|| tracing::info_span!("access"));
1471        let span = span.in_scope(|| tracing::info_span!("get"));
1472
1473        span.in_scope(|| {
1474            // todo: bubble this error up in a way that the caller can send a 401
1475            let (account, client_verifying_key) = self.verify_refresh_token(&refresh_token[..])?;
1476
1477            let mut claims_builder =
1478                flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1479            let mut dest = claims_builder.start_vector();
1480
1481            let txn = ReadTransaction::new(self.env.clone())?;
1482            let access = txn.access();
1483
1484            let account_record: &[u8] = access.get(&self.account_db, account.as_bytes())?;
1485
1486            if account_record == b"deleted" {
1487                bail!("{account} has been deleted");
1488            }
1489
1490            let user_claims = flexbuffers::Reader::get_root(&account_record[460..])?.as_vector();
1491
1492            let mut system_claims = dest.start_vector();
1493
1494            let token_id = Uuid::new_v4();
1495
1496            system_claims.push(flexbuffers::Blob(&token_id.as_bytes()[..]));
1497            system_claims.push(self.domain.as_str());
1498            system_claims.push(account);
1499
1500            if let Some(client_verifying_key) = client_verifying_key {
1501                system_claims.push(flexbuffers::Blob(&client_verifying_key[..]));
1502            }
1503
1504            system_claims.end_vector();
1505
1506            for claim in &self.config.access_token.claims {
1507                let src = user_claims.idx(claim.idx as usize);
1508                claim.kind.copy_to(&src, &mut dest, None)?;
1509            }
1510
1511            dest.end_vector();
1512
1513            let access_token = generate_hmac(
1514                claims_builder.view(),
1515                self.config.access_token.lifetime,
1516                &self.access_token_key,
1517            )?;
1518
1519            tracing::info!(token = %token_id, "generated");
1520            Ok(access_token)
1521        })
1522    }
1523
1524    /// returns payload as bytes
1525    #[allow(clippy::type_complexity)]
1526    pub fn verify_access_token<'a>(
1527        &self,
1528        token: &'a [u8],
1529    ) -> anyhow::Result<(&'a str, VectorReader<&'a [u8]>)> {
1530        let span = tracing::info_span!("auth");
1531        let span = span.in_scope(|| tracing::info_span!("token"));
1532        let span = span.in_scope(|| tracing::info_span!("access"));
1533        let span = span.in_scope(|| tracing::info_span!("verify"));
1534
1535        span.in_scope(|| {
1536            let claims: &'a [u8] = extract_hmac_no_check(token)?;
1537
1538            let claims_vec = match flexbuffers::Reader::get_root(
1539                &claims[..claims.len().checked_sub(EXP_LEN + SIG_LEN).unwrap_or(claims.len())],
1540            ) {
1541                Ok(v) => v.as_vector(),
1542                Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1543            };
1544
1545            let system_claims = claims_vec.idx(0).as_vector();
1546
1547            let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
1548
1549            let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1550            let claims_domain = system_claims.idx(1).as_str();
1551            let claims_account = system_claims.idx(2).as_str();
1552
1553            if claims_domain == self.domain {
1554                let claims_verifier = system_claims.idx(3).as_blob().0;
1555
1556                if claims_verifier.is_empty() {
1557                    verify_hmac(token, &self.access_token_key)?;
1558                } else {
1559                    verify_client_signature(token, claims_verifier.try_into()?)?;
1560                    verify_hmac(&token[..token.len() - (EXP_LEN + SIG_LEN)], &self.access_token_key)?;
1561                }
1562
1563                tracing::info!(
1564                    account = %claims_account,
1565                    token = %claims_uuid_str,
1566                    "verified"
1567                );
1568
1569                Ok((claims_account, claims_vec))
1570            } else {
1571                bail!(
1572                    "domain {claims_domain} does not match for user {claims_account} with token {claims_uuid_str}"
1573                )
1574            }
1575        })
1576    }
1577
1578    pub(crate) fn generate_refresh_token(
1579        &self,
1580        account: &[u8],
1581        verifier_claim: Option<&[u8]>,
1582    ) -> anyhow::Result<Bytes> {
1583        let span = tracing::info_span!("auth");
1584        let span = span.in_scope(|| tracing::info_span!("token"));
1585        let span = span.in_scope(|| tracing::info_span!("refresh"));
1586        let span = span.in_scope(|| tracing::info_span!("generate"));
1587
1588        span.in_scope(|| {
1589            let str_account = std::str::from_utf8(account)?;
1590
1591            let mut claims_builder =
1592                flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
1593            let mut system_claims = claims_builder.start_vector();
1594
1595            let token_id = Uuid::new_v4();
1596
1597            system_claims.push(flexbuffers::Blob(&token_id.as_bytes()[..]));
1598            system_claims.push(self.domain.as_str());
1599            system_claims.push(str_account);
1600
1601            if let Some(verifier_claim) = verifier_claim {
1602                system_claims.push(flexbuffers::Blob(verifier_claim));
1603            }
1604
1605            system_claims.end_vector();
1606
1607            let refresh_token = generate_hmac(
1608                claims_builder.view(),
1609                self.config.refresh_token.lifetime,
1610                &self.refresh_token_key,
1611            )?;
1612
1613            tracing::info!(token = token_id.to_string(), "generated");
1614
1615            Ok(refresh_token)
1616        })
1617    }
1618
1619    /// returns account as bytes
1620    #[allow(clippy::type_complexity)]
1621    fn verify_refresh_token<'a>(
1622        &self,
1623        token: &'a [u8],
1624    ) -> anyhow::Result<(&'a str, Option<&'a [u8; 32]>)> {
1625        let span = tracing::info_span!("auth");
1626        let span = span.in_scope(|| tracing::info_span!("token"));
1627        let span = span.in_scope(|| tracing::info_span!("refresh"));
1628        let span = span.in_scope(|| tracing::info_span!("verify"));
1629
1630        span.in_scope(|| {
1631            let claims = extract_hmac_no_check(token)?;
1632            let claims_vec = match flexbuffers::Reader::get_root(
1633                &claims[..claims
1634                    .len()
1635                    .checked_sub(EXP_LEN + SIG_LEN)
1636                    .unwrap_or(claims.len())],
1637            ) {
1638                Ok(v) => v.as_vector(),
1639                Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1640            };
1641
1642            let token_uuid_bytes: [u8; 16] = claims_vec.idx(0).as_blob().0.try_into()?;
1643
1644            let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1645            let claims_domain = claims_vec.idx(1).as_str();
1646            let claims_account = claims_vec.idx(2).as_str();
1647
1648            let claims_verifier = claims_vec.idx(3).as_blob().0;
1649
1650            let mut client_verifying_key = None;
1651
1652            if claims_verifier.is_empty() {
1653                verify_hmac(token, &self.refresh_token_key)?;
1654            } else {
1655                let verifying_key_bytes = claims_verifier.try_into()?;
1656
1657                client_verifying_key = Some(verifying_key_bytes);
1658
1659                verify_client_signature(token, verifying_key_bytes)?;
1660
1661                verify_hmac(
1662                    &token[..token.len() - (EXP_LEN + SIG_LEN)],
1663                    &self.refresh_token_key,
1664                )?;
1665            }
1666
1667            tracing::info!(
1668                domain = %claims_domain,
1669                account = %claims_account,
1670                token = %claims_uuid_str,
1671                "verified"
1672            );
1673
1674            Ok((claims_account, client_verifying_key))
1675        })
1676    }
1677
1678    #[instrument(skip_all, err)]
1679    pub fn verify_password_reset_token<'a>(&self, token: &'a [u8]) -> anyhow::Result<&'a str> {
1680        let claims: &'a [u8] = extract_hmac_no_check(token)?;
1681
1682        let claims_vec = match flexbuffers::Reader::get_root(
1683            &claims[..claims
1684                .len()
1685                .checked_sub(EXP_LEN + SIG_LEN)
1686                .unwrap_or(claims.len())],
1687        ) {
1688            Ok(v) => v.as_vector(),
1689            Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
1690        };
1691
1692        let token_uuid_bytes: [u8; 16] = claims_vec.idx(0).as_blob().0.try_into()?;
1693
1694        let claims_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
1695        let claims_domain = claims_vec.idx(1).as_str();
1696        let claims_account = claims_vec.idx(2).as_str();
1697
1698        if claims_domain == self.domain {
1699            let claims_verifier = claims_vec.idx(3).as_blob().0;
1700
1701            if claims_verifier.is_empty() {
1702                verify_hmac(token, &self.reset_password_token_key)?;
1703            } else {
1704                verify_client_signature(token, claims_verifier.try_into()?)?;
1705                verify_hmac(
1706                    &token[..token.len() - (EXP_LEN + SIG_LEN)],
1707                    &self.reset_password_token_key,
1708                )?;
1709            }
1710
1711            tracing::info!(
1712                account = %claims_account,
1713                token = %claims_uuid_str,
1714                "verified"
1715            );
1716
1717            Ok(claims_account)
1718        } else {
1719            bail!(
1720                "domain {claims_domain} does not match for user {claims_account} with token {claims_uuid_str}"
1721            )
1722        }
1723    }
1724}