mas_handlers/
passwords.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE 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;
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#[derive(Debug, Error)]
21#[error("Password manager is disabled")]
22pub struct PasswordManagerDisabledError;
23
24#[derive(Clone)]
25pub struct PasswordManager {
26    inner: Option<Arc<InnerPasswordManager>>,
27}
28
29struct InnerPasswordManager {
30    /// Minimum complexity score of new passwords (between 0 and 4) as evaluated
31    /// by zxcvbn.
32    minimum_complexity: u8,
33    current_hasher: Hasher,
34    current_version: SchemeVersion,
35
36    /// A map of "old" hashers used only for verification
37    other_hashers: HashMap<SchemeVersion, Hasher>,
38}
39
40impl PasswordManager {
41    /// Creates a new [`PasswordManager`] from an iterator and a minimum allowed
42    /// complexity score between 0 and 4. The first item in
43    /// the iterator will be the default hashing scheme.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the iterator was empty
48    pub fn new<I: IntoIterator<Item = (SchemeVersion, Hasher)>>(
49        minimum_complexity: u8,
50        iter: I,
51    ) -> Result<Self, anyhow::Error> {
52        let mut iter = iter.into_iter();
53
54        // Take the first hasher as the current hasher
55        let (current_version, current_hasher) = iter
56            .next()
57            .context("Iterator must have at least one item")?;
58
59        // Collect the other hashers in a map used only in verification
60        let other_hashers = iter.collect();
61
62        Ok(Self {
63            inner: Some(Arc::new(InnerPasswordManager {
64                minimum_complexity,
65                current_hasher,
66                current_version,
67                other_hashers,
68            })),
69        })
70    }
71
72    /// Creates a new disabled password manager
73    #[must_use]
74    pub const fn disabled() -> Self {
75        Self { inner: None }
76    }
77
78    /// Checks if the password manager is enabled or not
79    #[must_use]
80    pub const fn is_enabled(&self) -> bool {
81        self.inner.is_some()
82    }
83
84    /// Get the inner password manager
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the password manager is disabled
89    fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
90        self.inner.clone().ok_or(PasswordManagerDisabledError)
91    }
92
93    /// Returns true if and only if the given password satisfies the minimum
94    /// complexity requirements.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if the password manager is disabled
99    pub fn is_password_complex_enough(
100        &self,
101        password: &str,
102    ) -> Result<bool, PasswordManagerDisabledError> {
103        let inner = self.get_inner()?;
104        let score = zxcvbn(password, &[]);
105        Ok(u8::from(score.score()) >= inner.minimum_complexity)
106    }
107
108    /// Hash a password with the default hashing scheme.
109    /// Returns the version of the hashing scheme used and the hashed password.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the hashing failed or if the password manager is
114    /// disabled
115    #[tracing::instrument(name = "passwords.hash", skip_all)]
116    pub async fn hash<R: CryptoRng + RngCore + Send>(
117        &self,
118        rng: R,
119        password: Zeroizing<Vec<u8>>,
120    ) -> Result<(SchemeVersion, String), anyhow::Error> {
121        let inner = self.get_inner()?;
122
123        // Seed a future-local RNG so the RNG passed in parameters doesn't have to be
124        // 'static
125        let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
126        let span = tracing::Span::current();
127
128        // `inner` is being moved in the blocking task, so we need to copy the version
129        // first
130        let version = inner.current_version;
131
132        let hashed = tokio::task::spawn_blocking(move || {
133            span.in_scope(move || inner.current_hasher.hash_blocking(rng, &password))
134        })
135        .await??;
136
137        Ok((version, hashed))
138    }
139
140    /// Verify a password hash for the given hashing scheme.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the password hash verification failed or if the
145    /// password manager is disabled
146    #[tracing::instrument(name = "passwords.verify", skip_all, fields(%scheme))]
147    pub async fn verify(
148        &self,
149        scheme: SchemeVersion,
150        password: Zeroizing<Vec<u8>>,
151        hashed_password: String,
152    ) -> Result<(), anyhow::Error> {
153        let inner = self.get_inner()?;
154        let span = tracing::Span::current();
155
156        tokio::task::spawn_blocking(move || {
157            span.in_scope(move || {
158                let hasher = if scheme == inner.current_version {
159                    &inner.current_hasher
160                } else {
161                    inner
162                        .other_hashers
163                        .get(&scheme)
164                        .context("Hashing scheme not found")?
165                };
166
167                hasher.verify_blocking(&hashed_password, &password)
168            })
169        })
170        .await??;
171
172        Ok(())
173    }
174
175    /// Verify a password hash for the given hashing scheme, and upgrade it on
176    /// the fly, if it was not hashed with the default scheme
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the password hash verification failed or if the
181    /// password manager is disabled
182    #[tracing::instrument(name = "passwords.verify_and_upgrade", skip_all, fields(%scheme))]
183    pub async fn verify_and_upgrade<R: CryptoRng + RngCore + Send>(
184        &self,
185        rng: R,
186        scheme: SchemeVersion,
187        password: Zeroizing<Vec<u8>>,
188        hashed_password: String,
189    ) -> Result<Option<(SchemeVersion, String)>, anyhow::Error> {
190        let inner = self.get_inner()?;
191
192        // If the current scheme isn't the default one, we also hash with the default
193        // one so that
194        let new_hash_fut: OptionFuture<_> = (scheme != inner.current_version)
195            .then(|| self.hash(rng, password.clone()))
196            .into();
197
198        let verify_fut = self.verify(scheme, password, hashed_password);
199
200        let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut);
201        verify_res?;
202
203        let new_hash = new_hash_res.transpose()?;
204
205        Ok(new_hash)
206    }
207}
208
209/// A hashing scheme, with an optional pepper
210pub struct Hasher {
211    algorithm: Algorithm,
212    pepper: Option<Vec<u8>>,
213}
214
215impl Hasher {
216    /// Creates a new hashing scheme based on the bcrypt algorithm
217    #[must_use]
218    pub const fn bcrypt(cost: Option<u32>, pepper: Option<Vec<u8>>) -> Self {
219        let algorithm = Algorithm::Bcrypt { cost };
220        Self { algorithm, pepper }
221    }
222
223    /// Creates a new hashing scheme based on the argon2id algorithm
224    #[must_use]
225    pub const fn argon2id(pepper: Option<Vec<u8>>) -> Self {
226        let algorithm = Algorithm::Argon2id;
227        Self { algorithm, pepper }
228    }
229
230    /// Creates a new hashing scheme based on the pbkdf2 algorithm
231    #[must_use]
232    pub const fn pbkdf2(pepper: Option<Vec<u8>>) -> Self {
233        let algorithm = Algorithm::Pbkdf2;
234        Self { algorithm, pepper }
235    }
236
237    fn hash_blocking<R: CryptoRng + RngCore>(
238        &self,
239        rng: R,
240        password: &[u8],
241    ) -> Result<String, anyhow::Error> {
242        self.algorithm
243            .hash_blocking(rng, password, self.pepper.as_deref())
244    }
245
246    fn verify_blocking(&self, hashed_password: &str, password: &[u8]) -> Result<(), anyhow::Error> {
247        self.algorithm
248            .verify_blocking(hashed_password, password, self.pepper.as_deref())
249    }
250}
251
252#[derive(Debug, Clone, Copy)]
253enum Algorithm {
254    Bcrypt { cost: Option<u32> },
255    Argon2id,
256    Pbkdf2,
257}
258
259impl Algorithm {
260    fn hash_blocking<R: CryptoRng + RngCore>(
261        self,
262        mut rng: R,
263        password: &[u8],
264        pepper: Option<&[u8]>,
265    ) -> Result<String, anyhow::Error> {
266        match self {
267            Self::Bcrypt { cost } => {
268                let mut password = Zeroizing::new(password.to_vec());
269                if let Some(pepper) = pepper {
270                    password.extend_from_slice(pepper);
271                }
272
273                let salt = Standard.sample(&mut rng);
274
275                let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
276                Ok(hashed.format_for_version(bcrypt::Version::TwoB))
277            }
278
279            Self::Argon2id => {
280                let algorithm = argon2::Algorithm::default();
281                let version = argon2::Version::default();
282                let params = argon2::Params::default();
283
284                let phf = if let Some(secret) = pepper {
285                    Argon2::new_with_secret(secret, algorithm, version, params)?
286                } else {
287                    Argon2::new(algorithm, version, params)
288                };
289
290                let salt = SaltString::generate(rng);
291                let hashed = phf.hash_password(password.as_ref(), &salt)?;
292                Ok(hashed.to_string())
293            }
294
295            Self::Pbkdf2 => {
296                let mut password = Zeroizing::new(password.to_vec());
297                if let Some(pepper) = pepper {
298                    password.extend_from_slice(pepper);
299                }
300
301                let salt = SaltString::generate(rng);
302                let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
303                Ok(hashed.to_string())
304            }
305        }
306    }
307
308    fn verify_blocking(
309        self,
310        hashed_password: &str,
311        password: &[u8],
312        pepper: Option<&[u8]>,
313    ) -> Result<(), anyhow::Error> {
314        match self {
315            Algorithm::Bcrypt { .. } => {
316                let mut password = Zeroizing::new(password.to_vec());
317                if let Some(pepper) = pepper {
318                    password.extend_from_slice(pepper);
319                }
320
321                let result = bcrypt::verify(password, hashed_password)?;
322                anyhow::ensure!(result, "wrong password");
323            }
324
325            Algorithm::Argon2id => {
326                let algorithm = argon2::Algorithm::default();
327                let version = argon2::Version::default();
328                let params = argon2::Params::default();
329
330                let phf = if let Some(secret) = pepper {
331                    Argon2::new_with_secret(secret, algorithm, version, params)?
332                } else {
333                    Argon2::new(algorithm, version, params)
334                };
335
336                let hashed_password = PasswordHash::new(hashed_password)?;
337
338                phf.verify_password(password.as_ref(), &hashed_password)?;
339            }
340
341            Algorithm::Pbkdf2 => {
342                let mut password = Zeroizing::new(password.to_vec());
343                if let Some(pepper) = pepper {
344                    password.extend_from_slice(pepper);
345                }
346
347                let hashed_password = PasswordHash::new(hashed_password)?;
348
349                Pbkdf2.verify_password(password.as_ref(), &hashed_password)?;
350            }
351        }
352
353        Ok(())
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use rand::SeedableRng;
360
361    use super::*;
362
363    #[test]
364    fn hashing_bcrypt() {
365        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
366        let password = b"hunter2";
367        let password2 = b"wrong-password";
368        let pepper = b"a-secret-pepper";
369        let pepper2 = b"the-wrong-pepper";
370
371        let alg = Algorithm::Bcrypt { cost: Some(10) };
372        // Hash with a pepper
373        let hash = alg
374            .hash_blocking(&mut rng, password, Some(pepper))
375            .expect("Couldn't hash password");
376        insta::assert_snapshot!(hash);
377
378        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
379        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
380        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
381        assert!(alg.verify_blocking(&hash, password, None).is_err());
382
383        // Hash without pepper
384        let hash = alg
385            .hash_blocking(&mut rng, password, None)
386            .expect("Couldn't hash password");
387        insta::assert_snapshot!(hash);
388
389        assert!(alg.verify_blocking(&hash, password, None).is_ok());
390        assert!(alg.verify_blocking(&hash, password2, None).is_err());
391        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
392    }
393
394    #[test]
395    fn hashing_argon2id() {
396        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
397        let password = b"hunter2";
398        let password2 = b"wrong-password";
399        let pepper = b"a-secret-pepper";
400        let pepper2 = b"the-wrong-pepper";
401
402        let alg = Algorithm::Argon2id;
403        // Hash with a pepper
404        let hash = alg
405            .hash_blocking(&mut rng, password, Some(pepper))
406            .expect("Couldn't hash password");
407        insta::assert_snapshot!(hash);
408
409        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
410        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
411        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
412        assert!(alg.verify_blocking(&hash, password, None).is_err());
413
414        // Hash without pepper
415        let hash = alg
416            .hash_blocking(&mut rng, password, None)
417            .expect("Couldn't hash password");
418        insta::assert_snapshot!(hash);
419
420        assert!(alg.verify_blocking(&hash, password, None).is_ok());
421        assert!(alg.verify_blocking(&hash, password2, None).is_err());
422        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
423    }
424
425    #[test]
426    #[ignore = "this is particularly slow (20s+ seconds)"]
427    fn hashing_pbkdf2() {
428        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
429        let password = b"hunter2";
430        let password2 = b"wrong-password";
431        let pepper = b"a-secret-pepper";
432        let pepper2 = b"the-wrong-pepper";
433
434        let alg = Algorithm::Pbkdf2;
435        // Hash with a pepper
436        let hash = alg
437            .hash_blocking(&mut rng, password, Some(pepper))
438            .expect("Couldn't hash password");
439        insta::assert_snapshot!(hash);
440
441        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
442        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
443        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
444        assert!(alg.verify_blocking(&hash, password, None).is_err());
445
446        // Hash without pepper
447        let hash = alg
448            .hash_blocking(&mut rng, password, None)
449            .expect("Couldn't hash password");
450        insta::assert_snapshot!(hash);
451
452        assert!(alg.verify_blocking(&hash, password, None).is_ok());
453        assert!(alg.verify_blocking(&hash, password2, None).is_err());
454        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
455    }
456
457    #[allow(clippy::too_many_lines)]
458    #[tokio::test]
459    async fn hash_verify_and_upgrade() {
460        // Tests the whole password manager, by hashing a password and upgrading it
461        // after changing the hashing schemes. The salt generation is done with a seeded
462        // RNG, so that we can do stable snapshots of hashed passwords
463        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
464        let password = Zeroizing::new(b"hunter2".to_vec());
465        let wrong_password = Zeroizing::new(b"wrong-password".to_vec());
466
467        let manager = PasswordManager::new(
468            0,
469            [
470                // Start with one hashing scheme: the one used by synapse, bcrypt + pepper
471                (
472                    1,
473                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
474                ),
475            ],
476        )
477        .unwrap();
478
479        let (version, hash) = manager
480            .hash(&mut rng, password.clone())
481            .await
482            .expect("Failed to hash");
483
484        assert_eq!(version, 1);
485        insta::assert_snapshot!(hash);
486
487        // Just verifying works
488        manager
489            .verify(version, password.clone(), hash.clone())
490            .await
491            .expect("Failed to verify");
492
493        // And doesn't work with the wrong password
494        manager
495            .verify(version, wrong_password.clone(), hash.clone())
496            .await
497            .expect_err("Verification should have failed");
498
499        // Verifying with the wrong version doesn't work
500        manager
501            .verify(2, password.clone(), hash.clone())
502            .await
503            .expect_err("Verification should have failed");
504
505        // Upgrading does nothing
506        let res = manager
507            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
508            .await
509            .expect("Failed to verify");
510
511        assert!(res.is_none());
512
513        // Upgrading still verify that the password matches
514        manager
515            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
516            .await
517            .expect_err("Verification should have failed");
518
519        let manager = PasswordManager::new(
520            0,
521            [
522                (2, Hasher::argon2id(None)),
523                (
524                    1,
525                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
526                ),
527            ],
528        )
529        .unwrap();
530
531        // Verifying still works
532        manager
533            .verify(version, password.clone(), hash.clone())
534            .await
535            .expect("Failed to verify");
536
537        // And doesn't work with the wrong password
538        manager
539            .verify(version, wrong_password.clone(), hash.clone())
540            .await
541            .expect_err("Verification should have failed");
542
543        // Upgrading does re-hash
544        let res = manager
545            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
546            .await
547            .expect("Failed to verify");
548
549        assert!(res.is_some());
550        let (version, hash) = res.unwrap();
551
552        assert_eq!(version, 2);
553        insta::assert_snapshot!(hash);
554
555        // Upgrading works with the new hash, but does not upgrade
556        let res = manager
557            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
558            .await
559            .expect("Failed to verify");
560
561        assert!(res.is_none());
562
563        // Upgrading still verify that the password matches
564        manager
565            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
566            .await
567            .expect_err("Verification should have failed");
568
569        // Upgrading still verify that the password matches
570        manager
571            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
572            .await
573            .expect_err("Verification should have failed");
574
575        let manager = PasswordManager::new(
576            0,
577            [
578                (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
579                (2, Hasher::argon2id(None)),
580                (
581                    1,
582                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
583                ),
584            ],
585        )
586        .unwrap();
587
588        // Verifying still works
589        manager
590            .verify(version, password.clone(), hash.clone())
591            .await
592            .expect("Failed to verify");
593
594        // And doesn't work with the wrong password
595        manager
596            .verify(version, wrong_password.clone(), hash.clone())
597            .await
598            .expect_err("Verification should have failed");
599
600        // Upgrading does re-hash
601        let res = manager
602            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
603            .await
604            .expect("Failed to verify");
605
606        assert!(res.is_some());
607        let (version, hash) = res.unwrap();
608
609        assert_eq!(version, 3);
610        insta::assert_snapshot!(hash);
611
612        // Upgrading works with the new hash, but does not upgrade
613        let res = manager
614            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
615            .await
616            .expect("Failed to verify");
617
618        assert!(res.is_none());
619
620        // Upgrading still verify that the password matches
621        manager
622            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
623            .await
624            .expect_err("Verification should have failed");
625    }
626}