1use 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 env: Arc<Environment>,
59
60 account_db: Arc<Database<'static>>,
63
64 state_db: Arc<Database<'static>>,
66
67 invite_db: Arc<Database<'static>>,
69
70 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 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 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 const MIN_REGISTRATION_START_LEN: usize = 1 + 32;
251
252 #[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 const MIN_REGISTRATION_FINISH_LEN: usize = 1 + 32 + 192;
364
365 #[allow(clippy::type_complexity, clippy::too_many_lines)]
366 #[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 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 const MIN_LOGIN_START_LEN: usize = 1 + 96;
577
578 #[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 const MIN_LOGIN_FINISH_LEN: usize = 1 + 32 + 16 + 24 + 64;
648
649 #[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 #[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 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 }
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 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 }
1436 InviteMode::Viral => (),
1437 }
1438
1439 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 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 #[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 #[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}