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