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