Skip to main content

mas_handlers/
passwords.rs

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