diff --git a/Cargo.lock b/Cargo.lock index 384ab7ba..a19871e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,6 +704,18 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +[[package]] +name = "bcrypt" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.2.8", + "zeroize", +] + [[package]] name = "bincode" version = "1.3.3" @@ -746,6 +758,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "brotli" version = "3.3.4" @@ -2805,12 +2827,14 @@ dependencies = [ "axum", "axum-extra", "axum-macros", + "bcrypt", "camino", "chrono", "futures-util", "headers", "hyper", "indoc", + "insta", "lettre", "mas-axum-utils", "mas-data-model", @@ -2827,6 +2851,7 @@ dependencies = [ "mas-templates", "mime", "oauth2-types", + "pbkdf2", "rand 0.8.5", "rand_chacha 0.3.1", "serde", @@ -2842,6 +2867,7 @@ dependencies = [ "tracing", "ulid", "url", + "zeroize", ] [[package]] @@ -3728,6 +3754,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", + "hmac", + "password-hash", + "sha2", ] [[package]] diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index fe644285..6c053ae4 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -40,7 +40,10 @@ serde_json = "1.0.89" serde_urlencoded = "0.7.1" # Password hashing -argon2 = { version = "0.4.1", features = ["password-hash"] } +argon2 = { version = "0.4.1", features = ["password-hash", "std"] } +bcrypt = "0.13.0" +pbkdf2 = { version = "0.11.0", features = ["password-hash", "std"] } +zeroize = "1.5.7" # Various data types and utilities camino = "1.1.1" @@ -70,6 +73,7 @@ oauth2-types = { path = "../oauth2-types" } [dev-dependencies] indoc = "1.0.7" +insta = "1.22.0" [features] # Use the native root certificates diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index b63e68ff..5b5da712 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -51,6 +51,7 @@ mod compat; mod graphql; mod health; mod oauth2; +pub mod passwords; mod upstream_oauth2; mod views; @@ -350,6 +351,8 @@ where async fn test_state(pool: PgPool) -> Result { use mas_email::MailTransport; + use crate::passwords::{Hasher, PasswordManager}; + let workspace_root = camino::Utf8Path::new(env!("CARGO_MANIFEST_DIR")) .join("..") .join(".."); @@ -363,6 +366,8 @@ async fn test_state(pool: PgPool) -> Result { let encrypter = Encrypter::new(&[0x42; 32]); + let password_manager = PasswordManager::new([(1, Hasher::argon2id(None))])?; + let transport = MailTransport::blackhole(); let mailbox = "server@example.com".parse()?; let mailer = Mailer::new(&templates, &transport, &mailbox, &mailbox); @@ -397,6 +402,7 @@ async fn test_state(pool: PgPool) -> Result { policy_factory, graphql_schema, http_client_factory, + password_manager, }) } diff --git a/crates/handlers/src/passwords.rs b/crates/handlers/src/passwords.rs new file mode 100644 index 00000000..6bfc80cf --- /dev/null +++ b/crates/handlers/src/passwords.rs @@ -0,0 +1,542 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::HashMap, sync::Arc}; + +use anyhow::Context; +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use futures_util::future::OptionFuture; +use pbkdf2::Pbkdf2; +use rand::{CryptoRng, Rng, RngCore, SeedableRng}; +use zeroize::Zeroizing; + +pub type SchemeVersion = u32; + +#[derive(Clone)] +pub struct PasswordManager { + hashers: Arc>, + default_hasher: SchemeVersion, +} + +impl PasswordManager { + /// Creates a new [`PasswordManager`] from an iterator. The first item in + /// the iterator will be the default hashing scheme. + /// + /// # Example + /// + /// ```rust + /// pub use mas_handlers::passwords::{PasswordManager, Hasher}; + /// + /// PasswordManager::new([ + /// (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))), + /// (2, Hasher::argon2id(None)), + /// (1, Hasher::bcrypt(10, None)), + /// ]).unwrap(); + /// ``` + /// + /// # Errors + /// + /// Returns an error if the iterator was empty + pub fn new>( + iter: I, + ) -> Result { + let mut iter = iter.into_iter().peekable(); + let (default_hasher, _) = iter + .peek() + .context("Iterator must have at least one item")?; + let default_hasher = *default_hasher; + + let hashers = iter.collect(); + + Ok(Self { + hashers: Arc::new(hashers), + default_hasher, + }) + } + + /// Hash a password with the default hashing scheme. + /// Returns the version of the hashing scheme used and the hashed password. + /// + /// # Errors + /// + /// Returns an error if the hashing failed + pub async fn hash( + &self, + rng: R, + password: Zeroizing>, + ) -> Result<(SchemeVersion, String), anyhow::Error> { + // Seed a future-local RNG so the RNG passed in parameters doesn't have to be + // 'static + let mut rng = rand_chacha::ChaChaRng::from_rng(rng)?; + let hashers = self.hashers.clone(); + let default_hasher_version = self.default_hasher; + + let hashed = tokio::task::spawn_blocking(move || { + let default_hasher = hashers + .get(&default_hasher_version) + .context("Default hasher not found")?; + + default_hasher.hash_blocking(&mut rng, &password) + }) + .await??; + + Ok((default_hasher_version, hashed)) + } + + /// Verify a password hash for the given hashing scheme. + async fn verify( + &self, + scheme: SchemeVersion, + password: Zeroizing>, + hashed_password: String, + ) -> Result<(), anyhow::Error> { + let hashers = self.hashers.clone(); + + tokio::task::spawn_blocking(move || { + let hasher = hashers.get(&scheme).context("Hashing scheme not found")?; + hasher.verify_blocking(&hashed_password, &password) + }) + .await??; + + Ok(()) + } + + /// Verify a password hash for the given hashing scheme, and upgrade it on + /// the fly, if it was not hashed with the default scheme + /// + /// # Errors + /// + /// Returns an error if the password hash verification failed + pub async fn verify_and_upgrade( + &self, + rng: R, + scheme: SchemeVersion, + password: Zeroizing>, + hashed_password: String, + ) -> Result, anyhow::Error> { + // If the current scheme isn't the default one, we also hash with the default + // one so that + let new_hash_fut: OptionFuture<_> = (scheme != self.default_hasher) + .then(|| self.hash(rng, password.clone())) + .into(); + + let verify_fut = self.verify(scheme, password, hashed_password); + + let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut); + verify_res?; + + let new_hash = new_hash_res.transpose()?; + + Ok(new_hash) + } +} + +/// A hashing scheme, with an optional pepper +pub struct Hasher { + algorithm: Algorithm, + pepper: Option>, +} + +impl Hasher { + /// Creates a new hashing scheme based on the bcrypt algorithm + #[must_use] + pub const fn bcrypt(cost: u32, pepper: Option>) -> Self { + let algorithm = Algorithm::Bcrypt { cost }; + Self { algorithm, pepper } + } + + /// Creates a new hashing scheme based on the argon2id algorithm + #[must_use] + pub const fn argon2id(pepper: Option>) -> Self { + let algorithm = Algorithm::Argon2id; + Self { algorithm, pepper } + } + + /// Creates a new hashing scheme based on the pbkdf2 algorithm + #[must_use] + pub const fn pbkdf2(pepper: Option>) -> Self { + let algorithm = Algorithm::Pbkdf2; + Self { algorithm, pepper } + } + + fn hash_blocking( + &self, + rng: &mut R, + password: &[u8], + ) -> Result { + self.algorithm + .hash_blocking(rng, password, self.pepper.as_deref()) + } + + fn verify_blocking(&self, hashed_password: &str, password: &[u8]) -> Result<(), anyhow::Error> { + self.algorithm + .verify_blocking(hashed_password, password, self.pepper.as_deref()) + } +} + +#[derive(Debug, Clone, Copy)] +enum Algorithm { + Bcrypt { cost: u32 }, + Argon2id, + Pbkdf2, +} + +impl Algorithm { + fn hash_blocking( + self, + rng: &mut R, + password: &[u8], + pepper: Option<&[u8]>, + ) -> Result { + match self { + Self::Bcrypt { cost } => { + let mut password = Zeroizing::new(password.to_vec()); + if let Some(pepper) = pepper { + password.extend_from_slice(pepper); + } + + let salt = rng.gen(); + + let hashed = bcrypt::hash_with_salt(password, cost, salt)?; + Ok(hashed.format_for_version(bcrypt::Version::TwoB)) + } + + Self::Argon2id => { + let algorithm = argon2::Algorithm::default(); + let version = argon2::Version::default(); + let params = argon2::Params::default(); + + let phf = if let Some(secret) = pepper { + Argon2::new_with_secret(secret, algorithm, version, params)? + } else { + Argon2::new(algorithm, version, params) + }; + + let salt = SaltString::generate(rng); + let hashed = phf.hash_password(password.as_ref(), &salt)?; + Ok(hashed.to_string()) + } + + Self::Pbkdf2 => { + let mut password = Zeroizing::new(password.to_vec()); + if let Some(pepper) = pepper { + password.extend_from_slice(pepper); + } + + let salt = SaltString::generate(rng); + let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?; + Ok(hashed.to_string()) + } + } + } + + fn verify_blocking( + self, + hashed_password: &str, + password: &[u8], + pepper: Option<&[u8]>, + ) -> Result<(), anyhow::Error> { + match self { + Algorithm::Bcrypt { .. } => { + let mut password = Zeroizing::new(password.to_vec()); + if let Some(pepper) = pepper { + password.extend_from_slice(pepper); + } + + let result = bcrypt::verify(password, hashed_password)?; + anyhow::ensure!(result, "wrong password"); + } + + Algorithm::Argon2id => { + let algorithm = argon2::Algorithm::default(); + let version = argon2::Version::default(); + let params = argon2::Params::default(); + + let phf = if let Some(secret) = pepper { + Argon2::new_with_secret(secret, algorithm, version, params)? + } else { + Argon2::new(algorithm, version, params) + }; + + let hashed_password = PasswordHash::new(hashed_password)?; + + phf.verify_password(password.as_ref(), &hashed_password)?; + } + + Algorithm::Pbkdf2 => { + let mut password = Zeroizing::new(password.to_vec()); + if let Some(pepper) = pepper { + password.extend_from_slice(pepper); + } + + let hashed_password = PasswordHash::new(hashed_password)?; + + Pbkdf2.verify_password(password.as_ref(), &hashed_password)?; + } + }; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rand::SeedableRng; + + use super::*; + + #[test] + fn hashing_bcrypt() { + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42); + let password = b"hunter2"; + let password2 = b"wrong-password"; + let pepper = b"a-secret-pepper"; + let pepper2 = b"the-wrong-pepper"; + + let alg = Algorithm::Bcrypt { cost: 10 }; + // Hash with a pepper + let hash = alg + .hash_blocking(&mut rng, password, Some(pepper)) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); + assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); + assert!(alg.verify_blocking(&hash, password, None).is_err()); + + // Hash without pepper + let hash = alg + .hash_blocking(&mut rng, password, None) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, None).is_ok()); + assert!(alg.verify_blocking(&hash, password2, None).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + } + + #[test] + fn hashing_argon2id() { + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42); + let password = b"hunter2"; + let password2 = b"wrong-password"; + let pepper = b"a-secret-pepper"; + let pepper2 = b"the-wrong-pepper"; + + let alg = Algorithm::Argon2id; + // Hash with a pepper + let hash = alg + .hash_blocking(&mut rng, password, Some(pepper)) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); + assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); + assert!(alg.verify_blocking(&hash, password, None).is_err()); + + // Hash without pepper + let hash = alg + .hash_blocking(&mut rng, password, None) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, None).is_ok()); + assert!(alg.verify_blocking(&hash, password2, None).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + } + + #[test] + fn hashing_pbkdf2() { + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42); + let password = b"hunter2"; + let password2 = b"wrong-password"; + let pepper = b"a-secret-pepper"; + let pepper2 = b"the-wrong-pepper"; + + let alg = Algorithm::Pbkdf2; + // Hash with a pepper + let hash = alg + .hash_blocking(&mut rng, password, Some(pepper)) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); + assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); + assert!(alg.verify_blocking(&hash, password, None).is_err()); + + // Hash without pepper + let hash = alg + .hash_blocking(&mut rng, password, None) + .expect("Couldn't hash password"); + insta::assert_snapshot!(hash); + + assert!(alg.verify_blocking(&hash, password, None).is_ok()); + assert!(alg.verify_blocking(&hash, password2, None).is_err()); + assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + } + + #[tokio::test] + async fn hash_verify_and_upgrade() { + // Tests the whole password manager, by hashing a password and upgrading it + // after changing the hashing schemes. The salt generation is done with a seeded + // RNG, so that we can do stable snapshots of hashed passwords + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42); + let password = Zeroizing::new(b"hunter2".to_vec()); + let wrong_password = Zeroizing::new(b"wrong-password".to_vec()); + + let manager = PasswordManager::new([ + // Start with one hashing scheme: the one used by synapse, bcrypt + pepper + (1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))), + ]) + .unwrap(); + + let (version, hash) = manager + .hash(&mut rng, password.clone()) + .await + .expect("Failed to hash"); + + assert_eq!(version, 1); + insta::assert_snapshot!(hash); + + // Just verifying works + manager + .verify(version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + // And doesn't work with the wrong password + manager + .verify(version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + // Verifying with the wrong version doesn't work + manager + .verify(2, password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + // Upgrading does nothing + let res = manager + .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + assert!(res.is_none()); + + // Upgrading still verify that the password matches + manager + .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + let manager = PasswordManager::new([ + (2, Hasher::argon2id(None)), + (1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))), + ]) + .unwrap(); + + // Verifying still works + manager + .verify(version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + // And doesn't work with the wrong password + manager + .verify(version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + // Upgrading does re-hash + let res = manager + .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + assert!(res.is_some()); + let (version, hash) = res.unwrap(); + + assert_eq!(version, 2); + insta::assert_snapshot!(hash); + + // Upgrading works with the new hash, but does not upgrade + let res = manager + .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + assert!(res.is_none()); + + // Upgrading still verify that the password matches + manager + .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + // Upgrading still verify that the password matches + manager + .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + let manager = PasswordManager::new([ + (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))), + (2, Hasher::argon2id(None)), + (1, Hasher::bcrypt(10, Some(b"a-secret-pepper".to_vec()))), + ]) + .unwrap(); + + // Verifying still works + manager + .verify(version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + // And doesn't work with the wrong password + manager + .verify(version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + + // Upgrading does re-hash + let res = manager + .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + assert!(res.is_some()); + let (version, hash) = res.unwrap(); + + assert_eq!(version, 3); + insta::assert_snapshot!(hash); + + // Upgrading works with the new hash, but does not upgrade + let res = manager + .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone()) + .await + .expect("Failed to verify"); + + assert!(res.is_none()); + + // Upgrading still verify that the password matches + manager + .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) + .await + .expect_err("Verification should have failed"); + } +} diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-2.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-2.snap new file mode 100644 index 00000000..667a6db8 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$argon2id$v=19$m=4096,t=3,p=1$4aRFZH7bgRs24delZVap/Q$x9rbM2Yx2N/aWfSuyVJGZGaQ+zyoE4Vz1FO2+q9fu2Q diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-3.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-3.snap new file mode 100644 index 00000000..6aeca73d --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$argon2id$v=19$m=4096,t=3,p=1$1Ke64U6Mrdl5imSjjFRU+g$nL9kuMffxzJtFwANOEudh7FCpNJFPcYOA7xTbBLTCKI diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade.snap new file mode 100644 index 00000000..cf56edf4 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hash_verify_and_upgrade.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$2b$10$1Mgv9BLlKUPw2H3LIWlseeWUiTWF2yZC/.TyzuC3bGuB9XacoEUu6 diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id-2.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id-2.snap new file mode 100644 index 00000000..38302828 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$argon2id$v=19$m=4096,t=3,p=1$1WdxAF1UChkYSTnJ6NDbKg$5Gxr/7C+gWUwqDLQmLJ2JiAzg/VxVb5Z+A65bqVoFkU diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id.snap new file mode 100644 index 00000000..b7514c33 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_argon2id.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$argon2id$v=19$m=4096,t=3,p=1$eEi11xG8mIOZYxej+ckCaQ$pHZ/JwntSCS5qx6+MPK8XJUQSmSZ5rdXtxUew+rnXQI diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt-2.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt-2.snap new file mode 100644 index 00000000..935f4b08 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$2b$10$mqjtwG6w3GawhuQQdwBCqOt0TQ0V4vGhB.tMuCZO8WL.ycBHkOLca diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt.snap new file mode 100644 index 00000000..381361e2 --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_bcrypt.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$2b$10$c/EX8bTbEMfTn4oCvcQyBOR1zPyLmGzZ2pMXoElLASqv2qpq5X15i diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2-2.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2-2.snap new file mode 100644 index 00000000..3377788d --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$pbkdf2-sha256$i=10000,l=32$1WdxAF1UChkYSTnJ6NDbKg$zxGEM53FY8GA0fV4augAeFIaoPbdia6Tni7yr77kdhg diff --git a/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2.snap b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2.snap new file mode 100644 index 00000000..16bbca5a --- /dev/null +++ b/crates/handlers/src/snapshots/mas_handlers__passwords__tests__hashing_pbkdf2.snap @@ -0,0 +1,5 @@ +--- +source: crates/handlers/src/passwords.rs +expression: hash +--- +$pbkdf2-sha256$i=10000,l=32$eEi11xG8mIOZYxej+ckCaQ$aDuBA/KhwMgMTrjrBqVHaZ8Zsyzs9IN8aq0iPgWoebc