mas_handlers/
passwords.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use 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/// The result of a password verification, which is `true` if the password
21/// matches the hashed password, and `false` otherwise.
22///
23/// In the success case it can also contain additional data, such as the new
24/// hashing scheme and the new hashed password.
25#[must_use]
26#[derive(Debug, PartialEq, Eq, Clone)]
27pub enum PasswordVerificationResult<T = ()> {
28    /// The password matches the stored password hash
29    Success(T),
30    /// The password does not match the stored password hash
31    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    /// Converts the result into a new result with the given data.
46    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 score of new passwords (between 0 and 4) as evaluated
75    /// by zxcvbn.
76    minimum_complexity: u8,
77    current_hasher: Hasher,
78    current_version: SchemeVersion,
79
80    /// A map of "old" hashers used only for verification
81    other_hashers: HashMap<SchemeVersion, Hasher>,
82}
83
84impl PasswordManager {
85    /// Creates a new [`PasswordManager`] from an iterator and a minimum allowed
86    /// complexity score between 0 and 4. The first item in
87    /// the iterator will be the default hashing scheme.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the iterator was empty
92    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        // Take the first hasher as the current hasher
99        let (current_version, current_hasher) = iter
100            .next()
101            .context("Iterator must have at least one item")?;
102
103        // Collect the other hashers in a map used only in verification
104        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    /// Creates a new disabled password manager
117    #[must_use]
118    pub const fn disabled() -> Self {
119        Self { inner: None }
120    }
121
122    /// Checks if the password manager is enabled or not
123    #[must_use]
124    pub const fn is_enabled(&self) -> bool {
125        self.inner.is_some()
126    }
127
128    /// Get the inner password manager
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the password manager is disabled
133    fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
134        self.inner.clone().ok_or(PasswordManagerDisabledError)
135    }
136
137    /// Returns true if and only if the given password satisfies the minimum
138    /// complexity requirements.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if the password manager is disabled
143    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    /// Hash a password with the default hashing scheme.
153    /// Returns the version of the hashing scheme used and the hashed password.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the hashing failed or if the password manager is
158    /// disabled
159    #[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        // Seed a future-local RNG so the RNG passed in parameters doesn't have to be
168        // 'static
169        let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
170        let span = tracing::Span::current();
171
172        // `inner` is being moved in the blocking task, so we need to copy the version
173        // first
174        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    /// Verify a password hash for the given hashing scheme.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if the password hash verification failed or if the
189    /// password manager is disabled
190    #[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    /// Verify a password hash for the given hashing scheme, and upgrade it on
220    /// the fly, if it was not hashed with the default scheme
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the password hash verification failed or if the
225    /// password manager is disabled
226    #[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        // If the current scheme isn't the default one, we also hash with the default
237        // one so that
238        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
253/// A hashing scheme, with an optional pepper
254pub struct Hasher {
255    algorithm: Algorithm,
256    unicode_normalization: bool,
257    pepper: Option<Vec<u8>>,
258}
259
260impl Hasher {
261    /// Creates a new hashing scheme based on the bcrypt algorithm
262    #[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    /// Creates a new hashing scheme based on the argon2id algorithm
277    #[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    /// Creates a new hashing scheme based on the pbkdf2 algorithm
288    #[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            // This is the normalization method used by Synapse
301            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        // Hash with a pepper
460        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        // Hash without pepper
487        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        // Hash with a pepper
519        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        // Hash without pepper
546        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        // Hash with a pepper
579        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        // Hash without pepper
606        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        // Tests the whole password manager, by hashing a password and upgrading it
632        // after changing the hashing schemes. The salt generation is done with a seeded
633        // RNG, so that we can do stable snapshots of hashed passwords
634        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                // Start with one hashing scheme: the one used by synapse, bcrypt + pepper
642                (
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        // Just verifying works
659        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        // And doesn't work with the wrong password
666        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        // Verifying with the wrong version doesn't work
673        manager
674            .verify(2, password.clone(), hash.clone())
675            .await
676            .expect_err("Verification should have failed");
677
678        // Upgrading does nothing
679        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        // Upgrading still verify that the password matches
687        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        // Verifying still works
706        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        // And doesn't work with the wrong password
713        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        // Upgrading does re-hash
720        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        // Upgrading works with the new hash, but does not upgrade
732        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        // Upgrading still verify that the password matches
740        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        // Upgrading still verify that the password matches
747        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        // Verifying still works
770        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        // And doesn't work with the wrong password
777        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        // Upgrading does re-hash
784        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        // Upgrading works with the new hash, but does not upgrade
797        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        // Upgrading still verify that the password matches
805        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}