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    #[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 score of new passwords (between 0 and 4) as evaluated
80    /// by zxcvbn.
81    minimum_complexity: u8,
82    current_hasher: Hasher,
83    current_version: SchemeVersion,
84
85    /// A map of "old" hashers used only for verification
86    other_hashers: HashMap<SchemeVersion, Hasher>,
87}
88
89impl PasswordManager {
90    /// Creates a new [`PasswordManager`] from an iterator and a minimum allowed
91    /// complexity score between 0 and 4. The first item in
92    /// the iterator will be the default hashing scheme.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the iterator was empty
97    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        // Take the first hasher as the current hasher
104        let (current_version, current_hasher) = iter
105            .next()
106            .context("Iterator must have at least one item")?;
107
108        // Collect the other hashers in a map used only in verification
109        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    /// Creates a new disabled password manager
122    #[must_use]
123    pub const fn disabled() -> Self {
124        Self { inner: None }
125    }
126
127    /// Checks if the password manager is enabled or not
128    #[must_use]
129    pub const fn is_enabled(&self) -> bool {
130        self.inner.is_some()
131    }
132
133    /// Get the inner password manager
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if the password manager is disabled
138    fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
139        self.inner.clone().ok_or(PasswordManagerDisabledError)
140    }
141
142    /// Returns true if and only if the given password satisfies the minimum
143    /// complexity requirements.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if the password manager is disabled
148    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    /// Hash a password with the default hashing scheme.
158    /// Returns the version of the hashing scheme used and the hashed password.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the hashing failed or if the password manager is
163    /// disabled
164    #[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        // Seed a future-local RNG so the RNG passed in parameters doesn't have to be
173        // 'static
174        let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
175        let span = tracing::Span::current();
176
177        // `inner` is being moved in the blocking task, so we need to copy the version
178        // first
179        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    /// Verify a password hash for the given hashing scheme.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if the password hash verification failed or if the
194    /// password manager is disabled
195    #[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    /// Verify a password hash for the given hashing scheme, and upgrade it on
225    /// the fly, if it was not hashed with the default scheme
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if the password hash verification failed or if the
230    /// password manager is disabled
231    #[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        // If the current scheme isn't the default one, we also hash with the default
242        // one so that
243        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
258/// A hashing scheme, with an optional pepper
259pub struct Hasher {
260    algorithm: Algorithm,
261    unicode_normalization: bool,
262    pepper: Option<Vec<u8>>,
263}
264
265impl Hasher {
266    /// Creates a new hashing scheme based on the bcrypt algorithm
267    #[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    /// Creates a new hashing scheme based on the argon2id algorithm
282    #[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    /// Creates a new hashing scheme based on the pbkdf2 algorithm
293    #[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            // This is the normalization method used by Synapse
306            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        // Hash with a pepper
465        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        // Hash without pepper
492        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        // Hash with a pepper
524        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        // Hash without pepper
551        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        // Hash with a pepper
584        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        // Hash without pepper
611        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        // Tests the whole password manager, by hashing a password and upgrading it
636        // after changing the hashing schemes. The salt generation is done with a seeded
637        // RNG, so that we can do stable snapshots of hashed passwords
638        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                // Start with one hashing scheme: the one used by synapse, bcrypt + pepper
646                (
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        // Just verifying works
663        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        // And doesn't work with the wrong password
670        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        // Verifying with the wrong version doesn't work
677        manager
678            .verify(2, password.clone(), hash.clone())
679            .await
680            .expect_err("Verification should have failed");
681
682        // Upgrading does nothing
683        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        // Upgrading still verify that the password matches
691        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        // Verifying still works
710        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        // And doesn't work with the wrong password
717        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        // Upgrading does re-hash
724        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        // Upgrading works with the new hash, but does not upgrade
736        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        // Upgrading still verify that the password matches
744        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        // Upgrading still verify that the password matches
751        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        // Verifying still works
774        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        // And doesn't work with the wrong password
781        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        // Upgrading does re-hash
788        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        // Upgrading works with the new hash, but does not upgrade
801        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        // Upgrading still verify that the password matches
809        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}