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