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    #[tokio::test]
629    async fn hash_verify_and_upgrade() {
630        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
634        let password = Zeroizing::new("hunter2".to_owned());
635        let wrong_password = Zeroizing::new("wrong-password".to_owned());
636
637        let manager = PasswordManager::new(
638            0,
639            [
640                (
642                    1,
643                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
644                ),
645            ],
646        )
647        .unwrap();
648
649        let (version, hash) = manager
650            .hash(&mut rng, password.clone())
651            .await
652            .expect("Failed to hash");
653
654        assert_eq!(version, 1);
655        insta::assert_snapshot!(hash);
656
657        let res = manager
659            .verify(version, password.clone(), hash.clone())
660            .await
661            .expect("Failed to verify");
662        assert_eq!(res, PasswordVerificationResult::Success(()));
663
664        let res = manager
666            .verify(version, wrong_password.clone(), hash.clone())
667            .await
668            .expect("Failed to verify");
669        assert_eq!(res, PasswordVerificationResult::Failure);
670
671        manager
673            .verify(2, password.clone(), hash.clone())
674            .await
675            .expect_err("Verification should have failed");
676
677        let res = manager
679            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
680            .await
681            .expect("Failed to verify");
682
683        assert_eq!(res, PasswordVerificationResult::Success(None));
684
685        let res = manager
687            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
688            .await
689            .expect("Failed to verify");
690        assert_eq!(res, PasswordVerificationResult::Failure);
691
692        let manager = PasswordManager::new(
693            0,
694            [
695                (2, Hasher::argon2id(None, false)),
696                (
697                    1,
698                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
699                ),
700            ],
701        )
702        .unwrap();
703
704        let res = manager
706            .verify(version, password.clone(), hash.clone())
707            .await
708            .expect("Failed to verify");
709        assert_eq!(res, PasswordVerificationResult::Success(()));
710
711        let res = manager
713            .verify(version, wrong_password.clone(), hash.clone())
714            .await
715            .expect("Failed to verify");
716        assert_eq!(res, PasswordVerificationResult::Failure);
717
718        let res = manager
720            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
721            .await
722            .expect("Failed to verify");
723
724        let PasswordVerificationResult::Success(Some((version, hash))) = res else {
725            panic!("Expected a successful upgrade");
726        };
727        assert_eq!(version, 2);
728        insta::assert_snapshot!(hash);
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        assert_eq!(res, PasswordVerificationResult::Success(None));
737
738        let res = manager
740            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
741            .await
742            .expect("Failed to verify");
743        assert_eq!(res, PasswordVerificationResult::Failure);
744
745        let res = manager
747            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
748            .await
749            .expect("Failed to verify");
750        assert_eq!(res, PasswordVerificationResult::Failure);
751
752        let manager = PasswordManager::new(
753            0,
754            [
755                (
756                    3,
757                    Hasher::argon2id(Some(b"a-secret-pepper".to_vec()), false),
758                ),
759                (2, Hasher::argon2id(None, false)),
760                (
761                    1,
762                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
763                ),
764            ],
765        )
766        .unwrap();
767
768        let res = manager
770            .verify(version, password.clone(), hash.clone())
771            .await
772            .expect("Failed to verify");
773        assert_eq!(res, PasswordVerificationResult::Success(()));
774
775        let res = manager
777            .verify(version, wrong_password.clone(), hash.clone())
778            .await
779            .expect("Failed to verify");
780        assert_eq!(res, PasswordVerificationResult::Failure);
781
782        let res = manager
784            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
785            .await
786            .expect("Failed to verify");
787
788        let PasswordVerificationResult::Success(Some((version, hash))) = res else {
789            panic!("Expected a successful upgrade");
790        };
791
792        assert_eq!(version, 3);
793        insta::assert_snapshot!(hash);
794
795        let res = manager
797            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
798            .await
799            .expect("Failed to verify");
800
801        assert_eq!(res, PasswordVerificationResult::Success(None));
802
803        let res = manager
805            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
806            .await
807            .expect("Failed to verify");
808        assert_eq!(res, PasswordVerificationResult::Failure);
809    }
810}