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