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