3.6 KBRaw
| 1 | use argon2::{ |
| 2 | Argon2, |
| 3 | password_hash::{ |
| 4 | PasswordHash as EncodedHash, PasswordHasher as _, PasswordVerifier, SaltString, |
| 5 | rand_core::OsRng, |
| 6 | }, |
| 7 | }; |
| 8 | |
| 9 | use crate::{ |
| 10 | application::port::{PasswordError, PasswordHasher}, |
| 11 | domain::PasswordHash, |
| 12 | }; |
| 13 | |
| 14 | /// Argon2id password hashing with the crate's default parameters. |
| 15 | #[derive(Debug, Default, Clone)] |
| 16 | pub struct Argon2Hasher; |
| 17 | |
| 18 | impl Argon2Hasher { |
| 19 | pub fn new() -> Self { |
| 20 | Self |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | impl PasswordHasher for Argon2Hasher { |
| 25 | fn hash(&self, plaintext: &str) -> Result<PasswordHash, PasswordError> { |
| 26 | let salt = SaltString::generate(&mut OsRng); |
| 27 | |
| 28 | Argon2::default() |
| 29 | .hash_password(plaintext.as_bytes(), &salt) |
| 30 | .map(|hash| PasswordHash::from_trusted(hash.to_string())) |
| 31 | .map_err(|error| PasswordError::new(error.to_string())) |
| 32 | } |
| 33 | |
| 34 | fn verify(&self, plaintext: &str, hash: &PasswordHash) -> Result<bool, PasswordError> { |
| 35 | let parsed = EncodedHash::new(hash.as_str()) |
| 36 | .map_err(|error| PasswordError::new(format!("stored hash is unreadable: {error}")))?; |
| 37 | |
| 38 | match Argon2::default().verify_password(plaintext.as_bytes(), &parsed) { |
| 39 | Ok(()) => Ok(true), |
| 40 | Err(argon2::password_hash::Error::Password) => Ok(false), |
| 41 | Err(error) => Err(PasswordError::new(error.to_string())), |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | /// A fast, insecure hasher for tests. |
| 47 | /// |
| 48 | /// Argon2 is deliberately slow; using it across a use case suite costs seconds per |
| 49 | /// test. This keeps those tests fast. Never construct it outside `#[cfg(test)]` paths. |
| 50 | #[derive(Debug, Default, Clone)] |
| 51 | pub struct StubHasher; |
| 52 | |
| 53 | impl StubHasher { |
| 54 | pub fn new() -> Self { |
| 55 | Self |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | impl PasswordHasher for StubHasher { |
| 60 | fn hash(&self, plaintext: &str) -> Result<PasswordHash, PasswordError> { |
| 61 | Ok(PasswordHash::from_trusted(format!("stub:{plaintext}"))) |
| 62 | } |
| 63 | |
| 64 | fn verify(&self, plaintext: &str, hash: &PasswordHash) -> Result<bool, PasswordError> { |
| 65 | let stored = hash |
| 66 | .as_str() |
| 67 | .strip_prefix("stub:") |
| 68 | .ok_or_else(|| PasswordError::new("not a stub hash"))?; |
| 69 | |
| 70 | Ok(stored == plaintext) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | #[cfg(test)] |
| 75 | mod tests { |
| 76 | use super::*; |
| 77 | |
| 78 | #[test] |
| 79 | fn argon2_accepts_the_correct_password() { |
| 80 | let hasher = Argon2Hasher::new(); |
| 81 | let hash = hasher.hash("correct horse battery staple").expect("hash"); |
| 82 | |
| 83 | assert!( |
| 84 | hasher |
| 85 | .verify("correct horse battery staple", &hash) |
| 86 | .expect("verify") |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | #[test] |
| 91 | fn argon2_rejects_a_wrong_password_without_erroring() { |
| 92 | let hasher = Argon2Hasher::new(); |
| 93 | let hash = hasher.hash("correct horse battery staple").expect("hash"); |
| 94 | |
| 95 | assert!(!hasher.verify("wrong password", &hash).expect("verify")); |
| 96 | } |
| 97 | |
| 98 | #[test] |
| 99 | fn argon2_salts_each_hash_separately() { |
| 100 | let hasher = Argon2Hasher::new(); |
| 101 | |
| 102 | let first = hasher.hash("same password").expect("hash"); |
| 103 | let second = hasher.hash("same password").expect("hash"); |
| 104 | |
| 105 | assert_ne!(first.as_str(), second.as_str()); |
| 106 | } |
| 107 | |
| 108 | #[test] |
| 109 | fn an_unreadable_stored_hash_is_an_error_not_a_failed_login() { |
| 110 | let hasher = Argon2Hasher::new(); |
| 111 | let corrupt = PasswordHash::from_trusted("not-a-real-hash"); |
| 112 | |
| 113 | assert!(hasher.verify("anything", &corrupt).is_err()); |
| 114 | } |
| 115 | |
| 116 | #[test] |
| 117 | fn the_stub_round_trips() { |
| 118 | let hasher = StubHasher::new(); |
| 119 | let hash = hasher.hash("password").expect("hash"); |
| 120 | |
| 121 | assert!(hasher.verify("password", &hash).expect("verify")); |
| 122 | assert!(!hasher.verify("other", &hash).expect("verify")); |
| 123 | } |
| 124 | } |