1use std::{collections::HashMap, sync::Arc};
9
10use anyhow::Context;
11use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
12use futures_util::future::OptionFuture;
13use pbkdf2::{Pbkdf2, password_hash};
14use rand::{CryptoRng, RngCore, SeedableRng, distributions::Standard, prelude::Distribution};
15use thiserror::Error;
16use zeroize::Zeroizing;
17use zxcvbn::zxcvbn;
18
19pub type SchemeVersion = u16;
20
21#[must_use]
27#[derive(Debug, PartialEq, Eq, Clone)]
28pub enum PasswordVerificationResult<T = ()> {
29 Success(T),
31 Failure,
33}
34
35impl PasswordVerificationResult<()> {
36 fn success() -> Self {
37 Self::Success(())
38 }
39
40 fn failure() -> Self {
41 Self::Failure
42 }
43}
44
45impl<T> PasswordVerificationResult<T> {
46 fn with_data<N>(self, data: N) -> PasswordVerificationResult<N> {
48 match self {
49 Self::Success(_) => PasswordVerificationResult::Success(data),
50 Self::Failure => PasswordVerificationResult::Failure,
51 }
52 }
53
54 #[must_use]
55 pub fn is_success(&self) -> bool {
56 matches!(self, Self::Success(_))
57 }
58}
59
60impl From<bool> for PasswordVerificationResult<()> {
61 fn from(value: bool) -> Self {
62 if value {
63 Self::success()
64 } else {
65 Self::failure()
66 }
67 }
68}
69
70#[derive(Debug, Error)]
71#[error("Password manager is disabled")]
72pub struct PasswordManagerDisabledError;
73
74#[derive(Clone)]
75pub struct PasswordManager {
76 inner: Option<Arc<InnerPasswordManager>>,
77}
78
79struct InnerPasswordManager {
80 minimum_complexity: u8,
83 current_hasher: Hasher,
84 current_version: SchemeVersion,
85
86 other_hashers: HashMap<SchemeVersion, Hasher>,
88}
89
90impl PasswordManager {
91 pub fn new<I: IntoIterator<Item = (SchemeVersion, Hasher)>>(
99 minimum_complexity: u8,
100 iter: I,
101 ) -> Result<Self, anyhow::Error> {
102 let mut iter = iter.into_iter();
103
104 let (current_version, current_hasher) = iter
106 .next()
107 .context("Iterator must have at least one item")?;
108
109 let other_hashers = iter.collect();
111
112 Ok(Self {
113 inner: Some(Arc::new(InnerPasswordManager {
114 minimum_complexity,
115 current_hasher,
116 current_version,
117 other_hashers,
118 })),
119 })
120 }
121
122 #[must_use]
124 pub const fn disabled() -> Self {
125 Self { inner: None }
126 }
127
128 #[must_use]
130 pub const fn is_enabled(&self) -> bool {
131 self.inner.is_some()
132 }
133
134 fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
140 self.inner.clone().ok_or(PasswordManagerDisabledError)
141 }
142
143 pub fn is_password_complex_enough(
150 &self,
151 password: &str,
152 ) -> Result<bool, PasswordManagerDisabledError> {
153 let inner = self.get_inner()?;
154 let score = zxcvbn(password, &[]);
155 Ok(u8::from(score.score()) >= inner.minimum_complexity)
156 }
157
158 #[tracing::instrument(name = "passwords.hash", skip_all)]
166 pub async fn hash<R: CryptoRng + RngCore + Send>(
167 &self,
168 rng: R,
169 password: Zeroizing<String>,
170 ) -> Result<(SchemeVersion, String), anyhow::Error> {
171 let inner = self.get_inner()?;
172
173 let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
176 let span = tracing::Span::current();
177
178 let version = inner.current_version;
181
182 let hashed = tokio::task::spawn_blocking(move || {
183 span.in_scope(move || inner.current_hasher.hash_blocking(rng, password))
184 })
185 .await??;
186
187 Ok((version, hashed))
188 }
189
190 #[tracing::instrument(name = "passwords.verify", skip_all, fields(%scheme))]
197 pub async fn verify(
198 &self,
199 scheme: SchemeVersion,
200 password: Zeroizing<String>,
201 hashed_password: String,
202 ) -> Result<PasswordVerificationResult, anyhow::Error> {
203 let inner = self.get_inner()?;
204 let span = tracing::Span::current();
205
206 let result = tokio::task::spawn_blocking(move || {
207 span.in_scope(move || {
208 let hasher = if scheme == inner.current_version {
209 &inner.current_hasher
210 } else {
211 inner
212 .other_hashers
213 .get(&scheme)
214 .context("Hashing scheme not found")?
215 };
216
217 hasher.verify_blocking(&hashed_password, password)
218 })
219 })
220 .await??;
221
222 Ok(result)
223 }
224
225 #[tracing::instrument(name = "passwords.verify_and_upgrade", skip_all, fields(%scheme))]
233 pub async fn verify_and_upgrade<R: CryptoRng + RngCore + Send>(
234 &self,
235 rng: R,
236 scheme: SchemeVersion,
237 password: Zeroizing<String>,
238 hashed_password: String,
239 ) -> Result<PasswordVerificationResult<Option<(SchemeVersion, String)>>, anyhow::Error> {
240 let inner = self.get_inner()?;
241
242 let new_hash_fut: OptionFuture<_> = (scheme != inner.current_version)
245 .then(|| self.hash(rng, password.clone()))
246 .into();
247
248 let verify_fut = self.verify(scheme, password, hashed_password);
249
250 let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut);
251 let password_result = verify_res?;
252
253 let new_hash = new_hash_res.transpose()?;
254
255 Ok(password_result.with_data(new_hash))
256 }
257}
258
259pub struct Hasher {
261 algorithm: Algorithm,
262 unicode_normalization: bool,
263 pepper: Option<Vec<u8>>,
264}
265
266impl Hasher {
267 #[must_use]
269 pub const fn bcrypt(
270 cost: Option<u32>,
271 pepper: Option<Vec<u8>>,
272 unicode_normalization: bool,
273 ) -> Self {
274 let algorithm = Algorithm::Bcrypt { cost };
275 Self {
276 algorithm,
277 unicode_normalization,
278 pepper,
279 }
280 }
281
282 #[must_use]
284 pub const fn argon2id(pepper: Option<Vec<u8>>, unicode_normalization: bool) -> Self {
285 let algorithm = Algorithm::Argon2id;
286 Self {
287 algorithm,
288 unicode_normalization,
289 pepper,
290 }
291 }
292
293 #[must_use]
295 pub const fn pbkdf2(pepper: Option<Vec<u8>>, unicode_normalization: bool) -> Self {
296 let algorithm = Algorithm::Pbkdf2;
297 Self {
298 algorithm,
299 unicode_normalization,
300 pepper,
301 }
302 }
303
304 fn normalize_password(&self, password: Zeroizing<String>) -> Zeroizing<String> {
305 if self.unicode_normalization {
306 let normalizer = icu_normalizer::ComposingNormalizerBorrowed::new_nfkc();
308 Zeroizing::new(normalizer.normalize(&password).into_owned())
309 } else {
310 password
311 }
312 }
313
314 fn hash_blocking<R: CryptoRng + RngCore>(
315 &self,
316 rng: R,
317 password: Zeroizing<String>,
318 ) -> Result<String, anyhow::Error> {
319 let password = self.normalize_password(password);
320
321 self.algorithm
322 .hash_blocking(rng, password.as_bytes(), self.pepper.as_deref())
323 }
324
325 fn verify_blocking(
326 &self,
327 hashed_password: &str,
328 password: Zeroizing<String>,
329 ) -> Result<PasswordVerificationResult, anyhow::Error> {
330 let password = self.normalize_password(password);
331
332 self.algorithm
333 .verify_blocking(hashed_password, password.as_bytes(), self.pepper.as_deref())
334 }
335}
336
337#[derive(Debug, Clone, Copy)]
338enum Algorithm {
339 Bcrypt { cost: Option<u32> },
340 Argon2id,
341 Pbkdf2,
342}
343
344impl Algorithm {
345 fn hash_blocking<R: CryptoRng + RngCore>(
346 self,
347 mut rng: R,
348 password: &[u8],
349 pepper: Option<&[u8]>,
350 ) -> Result<String, anyhow::Error> {
351 match self {
352 Self::Bcrypt { cost } => {
353 let mut password = Zeroizing::new(password.to_vec());
354 if let Some(pepper) = pepper {
355 password.extend_from_slice(pepper);
356 }
357
358 let salt = Standard.sample(&mut rng);
359
360 let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
361 Ok(hashed.format_for_version(bcrypt::Version::TwoB))
362 }
363
364 Self::Argon2id => {
365 let algorithm = argon2::Algorithm::default();
366 let version = argon2::Version::default();
367 let params = argon2::Params::default();
368
369 let phf = if let Some(secret) = pepper {
370 Argon2::new_with_secret(secret, algorithm, version, params)?
371 } else {
372 Argon2::new(algorithm, version, params)
373 };
374
375 let salt = SaltString::generate(rng);
376 let hashed = phf.hash_password(password.as_ref(), &salt)?;
377 Ok(hashed.to_string())
378 }
379
380 Self::Pbkdf2 => {
381 let mut password = Zeroizing::new(password.to_vec());
382 if let Some(pepper) = pepper {
383 password.extend_from_slice(pepper);
384 }
385
386 let salt = SaltString::generate(rng);
387 let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
388 Ok(hashed.to_string())
389 }
390 }
391 }
392
393 fn verify_blocking(
394 self,
395 hashed_password: &str,
396 password: &[u8],
397 pepper: Option<&[u8]>,
398 ) -> Result<PasswordVerificationResult, anyhow::Error> {
399 let result = match self {
400 Algorithm::Bcrypt { .. } => {
401 let mut password = Zeroizing::new(password.to_vec());
402 if let Some(pepper) = pepper {
403 password.extend_from_slice(pepper);
404 }
405
406 let result = bcrypt::verify(password, hashed_password)?;
407 PasswordVerificationResult::from(result)
408 }
409
410 Algorithm::Argon2id => {
411 let algorithm = argon2::Algorithm::default();
412 let version = argon2::Version::default();
413 let params = argon2::Params::default();
414
415 let phf = if let Some(secret) = pepper {
416 Argon2::new_with_secret(secret, algorithm, version, params)?
417 } else {
418 Argon2::new(algorithm, version, params)
419 };
420
421 let hashed_password = PasswordHash::new(hashed_password)?;
422
423 match phf.verify_password(password.as_ref(), &hashed_password) {
424 Ok(()) => PasswordVerificationResult::success(),
425 Err(password_hash::Error::Password) => PasswordVerificationResult::failure(),
426 Err(e) => Err(e)?,
427 }
428 }
429
430 Algorithm::Pbkdf2 => {
431 let mut password = Zeroizing::new(password.to_vec());
432 if let Some(pepper) = pepper {
433 password.extend_from_slice(pepper);
434 }
435
436 let hashed_password = PasswordHash::new(hashed_password)?;
437
438 match Pbkdf2.verify_password(password.as_ref(), &hashed_password) {
439 Ok(()) => PasswordVerificationResult::success(),
440 Err(password_hash::Error::Password) => PasswordVerificationResult::failure(),
441 Err(e) => Err(e)?,
442 }
443 }
444 };
445
446 Ok(result)
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use rand::SeedableRng;
453
454 use super::*;
455
456 #[test]
457 fn hashing_bcrypt() {
458 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
459 let password = b"hunter2";
460 let password2 = b"wrong-password";
461 let pepper = b"a-secret-pepper";
462 let pepper2 = b"the-wrong-pepper";
463
464 let alg = Algorithm::Bcrypt { cost: Some(10) };
465 let hash = alg
467 .hash_blocking(&mut rng, password, Some(pepper))
468 .expect("Couldn't hash password");
469 insta::assert_snapshot!(hash);
470
471 assert_eq!(
472 alg.verify_blocking(&hash, password, Some(pepper))
473 .expect("Verification failed"),
474 PasswordVerificationResult::Success(())
475 );
476 assert_eq!(
477 alg.verify_blocking(&hash, password2, Some(pepper))
478 .expect("Verification failed"),
479 PasswordVerificationResult::Failure
480 );
481 assert_eq!(
482 alg.verify_blocking(&hash, password, Some(pepper2))
483 .expect("Verification failed"),
484 PasswordVerificationResult::Failure
485 );
486 assert_eq!(
487 alg.verify_blocking(&hash, password, None)
488 .expect("Verification failed"),
489 PasswordVerificationResult::Failure
490 );
491
492 let hash = alg
494 .hash_blocking(&mut rng, password, None)
495 .expect("Couldn't hash password");
496 insta::assert_snapshot!(hash);
497
498 assert_eq!(
499 alg.verify_blocking(&hash, password, None)
500 .expect("Verification failed"),
501 PasswordVerificationResult::Success(())
502 );
503 assert_eq!(
504 alg.verify_blocking(&hash, password2, None)
505 .expect("Verification failed"),
506 PasswordVerificationResult::Failure
507 );
508 assert_eq!(
509 alg.verify_blocking(&hash, password, Some(pepper))
510 .expect("Verification failed"),
511 PasswordVerificationResult::Failure
512 );
513 }
514
515 #[test]
516 fn hashing_argon2id() {
517 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
518 let password = b"hunter2";
519 let password2 = b"wrong-password";
520 let pepper = b"a-secret-pepper";
521 let pepper2 = b"the-wrong-pepper";
522
523 let alg = Algorithm::Argon2id;
524 let hash = alg
526 .hash_blocking(&mut rng, password, Some(pepper))
527 .expect("Couldn't hash password");
528 insta::assert_snapshot!(hash);
529
530 assert_eq!(
531 alg.verify_blocking(&hash, password, Some(pepper))
532 .expect("Verification failed"),
533 PasswordVerificationResult::Success(())
534 );
535 assert_eq!(
536 alg.verify_blocking(&hash, password2, Some(pepper))
537 .expect("Verification failed"),
538 PasswordVerificationResult::Failure
539 );
540 assert_eq!(
541 alg.verify_blocking(&hash, password, Some(pepper2))
542 .expect("Verification failed"),
543 PasswordVerificationResult::Failure
544 );
545 assert_eq!(
546 alg.verify_blocking(&hash, password, None)
547 .expect("Verification failed"),
548 PasswordVerificationResult::Failure
549 );
550
551 let hash = alg
553 .hash_blocking(&mut rng, password, None)
554 .expect("Couldn't hash password");
555 insta::assert_snapshot!(hash);
556
557 assert_eq!(
558 alg.verify_blocking(&hash, password, None)
559 .expect("Verification failed"),
560 PasswordVerificationResult::Success(())
561 );
562 assert_eq!(
563 alg.verify_blocking(&hash, password2, None)
564 .expect("Verification failed"),
565 PasswordVerificationResult::Failure
566 );
567 assert_eq!(
568 alg.verify_blocking(&hash, password, Some(pepper))
569 .expect("Verification failed"),
570 PasswordVerificationResult::Failure
571 );
572 }
573
574 #[test]
575 #[ignore = "this is particularly slow (20s+ seconds)"]
576 fn hashing_pbkdf2() {
577 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
578 let password = b"hunter2";
579 let password2 = b"wrong-password";
580 let pepper = b"a-secret-pepper";
581 let pepper2 = b"the-wrong-pepper";
582
583 let alg = Algorithm::Pbkdf2;
584 let hash = alg
586 .hash_blocking(&mut rng, password, Some(pepper))
587 .expect("Couldn't hash password");
588 insta::assert_snapshot!(hash);
589
590 assert_eq!(
591 alg.verify_blocking(&hash, password, Some(pepper))
592 .expect("Verification failed"),
593 PasswordVerificationResult::Success(())
594 );
595 assert_eq!(
596 alg.verify_blocking(&hash, password2, Some(pepper))
597 .expect("Verification failed"),
598 PasswordVerificationResult::Failure
599 );
600 assert_eq!(
601 alg.verify_blocking(&hash, password, Some(pepper2))
602 .expect("Verification failed"),
603 PasswordVerificationResult::Failure
604 );
605 assert_eq!(
606 alg.verify_blocking(&hash, password, None)
607 .expect("Verification failed"),
608 PasswordVerificationResult::Failure
609 );
610
611 let hash = alg
613 .hash_blocking(&mut rng, password, None)
614 .expect("Couldn't hash password");
615 insta::assert_snapshot!(hash);
616
617 assert_eq!(
618 alg.verify_blocking(&hash, password, None)
619 .expect("Verification failed"),
620 PasswordVerificationResult::Success(())
621 );
622 assert_eq!(
623 alg.verify_blocking(&hash, password2, None)
624 .expect("Verification failed"),
625 PasswordVerificationResult::Failure
626 );
627 assert_eq!(
628 alg.verify_blocking(&hash, password, Some(pepper))
629 .expect("Verification failed"),
630 PasswordVerificationResult::Failure
631 );
632 }
633
634 #[tokio::test]
635 async fn hash_verify_and_upgrade() {
636 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
640 let password = Zeroizing::new("hunter2".to_owned());
641 let wrong_password = Zeroizing::new("wrong-password".to_owned());
642
643 let manager = PasswordManager::new(
644 0,
645 [
646 (
648 1,
649 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
650 ),
651 ],
652 )
653 .unwrap();
654
655 let (version, hash) = manager
656 .hash(&mut rng, password.clone())
657 .await
658 .expect("Failed to hash");
659
660 assert_eq!(version, 1);
661 insta::assert_snapshot!(hash);
662
663 let res = manager
665 .verify(version, password.clone(), hash.clone())
666 .await
667 .expect("Failed to verify");
668 assert_eq!(res, PasswordVerificationResult::Success(()));
669
670 let res = manager
672 .verify(version, wrong_password.clone(), hash.clone())
673 .await
674 .expect("Failed to verify");
675 assert_eq!(res, PasswordVerificationResult::Failure);
676
677 manager
679 .verify(2, password.clone(), hash.clone())
680 .await
681 .expect_err("Verification should have failed");
682
683 let res = manager
685 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
686 .await
687 .expect("Failed to verify");
688
689 assert_eq!(res, PasswordVerificationResult::Success(None));
690
691 let res = manager
693 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
694 .await
695 .expect("Failed to verify");
696 assert_eq!(res, PasswordVerificationResult::Failure);
697
698 let manager = PasswordManager::new(
699 0,
700 [
701 (2, Hasher::argon2id(None, false)),
702 (
703 1,
704 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
705 ),
706 ],
707 )
708 .unwrap();
709
710 let res = manager
712 .verify(version, password.clone(), hash.clone())
713 .await
714 .expect("Failed to verify");
715 assert_eq!(res, PasswordVerificationResult::Success(()));
716
717 let res = manager
719 .verify(version, wrong_password.clone(), hash.clone())
720 .await
721 .expect("Failed to verify");
722 assert_eq!(res, PasswordVerificationResult::Failure);
723
724 let res = manager
726 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
727 .await
728 .expect("Failed to verify");
729
730 let PasswordVerificationResult::Success(Some((version, hash))) = res else {
731 panic!("Expected a successful upgrade");
732 };
733 assert_eq!(version, 2);
734 insta::assert_snapshot!(hash);
735
736 let res = manager
738 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
739 .await
740 .expect("Failed to verify");
741
742 assert_eq!(res, PasswordVerificationResult::Success(None));
743
744 let res = manager
746 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
747 .await
748 .expect("Failed to verify");
749 assert_eq!(res, PasswordVerificationResult::Failure);
750
751 let res = manager
753 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
754 .await
755 .expect("Failed to verify");
756 assert_eq!(res, PasswordVerificationResult::Failure);
757
758 let manager = PasswordManager::new(
759 0,
760 [
761 (
762 3,
763 Hasher::argon2id(Some(b"a-secret-pepper".to_vec()), false),
764 ),
765 (2, Hasher::argon2id(None, false)),
766 (
767 1,
768 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
769 ),
770 ],
771 )
772 .unwrap();
773
774 let res = manager
776 .verify(version, password.clone(), hash.clone())
777 .await
778 .expect("Failed to verify");
779 assert_eq!(res, PasswordVerificationResult::Success(()));
780
781 let res = manager
783 .verify(version, wrong_password.clone(), hash.clone())
784 .await
785 .expect("Failed to verify");
786 assert_eq!(res, PasswordVerificationResult::Failure);
787
788 let res = manager
790 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
791 .await
792 .expect("Failed to verify");
793
794 let PasswordVerificationResult::Success(Some((version, hash))) = res else {
795 panic!("Expected a successful upgrade");
796 };
797
798 assert_eq!(version, 3);
799 insta::assert_snapshot!(hash);
800
801 let res = manager
803 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
804 .await
805 .expect("Failed to verify");
806
807 assert_eq!(res, PasswordVerificationResult::Success(None));
808
809 let res = manager
811 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
812 .await
813 .expect("Failed to verify");
814 assert_eq!(res, PasswordVerificationResult::Failure);
815 }
816}