| 1 | use std::fmt; |
| 2 | |
| 3 | /// An opaque password hash. |
| 4 | /// |
| 5 | /// `Debug` is implemented by hand to redact the digest: the whole point of this type |
| 6 | /// is that a hash can never reach a log line by accident. There is deliberately no |
| 7 | /// `Display`. |
| 8 | #[derive(Clone, PartialEq, Eq)] |
| 9 | pub struct PasswordHash(String); |
| 10 | |
| 11 | impl PasswordHash { |
| 12 | /// Wraps a hash produced by a [`PasswordHasher`](crate::application::port::PasswordHasher). |
| 13 | /// |
| 14 | /// Takes a hash, never a plaintext password. Hashing belongs to the adapter. |
| 15 | pub fn from_trusted(value: impl Into<String>) -> Self { |
| 16 | Self(value.into()) |
| 17 | } |
| 18 | |
| 19 | /// Exposes the encoded hash, for storage or verification only. |
| 20 | pub fn as_str(&self) -> &str { |
| 21 | &self.0 |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | impl fmt::Debug for PasswordHash { |
| 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 27 | f.write_str("PasswordHash(redacted)") |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | #[cfg(test)] |
| 32 | mod tests { |
| 33 | use super::*; |
| 34 | |
| 35 | #[test] |
| 36 | fn debug_output_redacts_the_hash() { |
| 37 | let hash = PasswordHash::from_trusted("$argon2id$v=19$m=19456,t=2,p=1$abc$def"); |
| 38 | |
| 39 | let rendered = format!("{hash:?}"); |
| 40 | |
| 41 | assert_eq!(rendered, "PasswordHash(redacted)"); |
| 42 | assert!(!rendered.contains("argon2")); |
| 43 | assert!(!rendered.contains("def")); |
| 44 | } |
| 45 | |
| 46 | #[test] |
| 47 | fn the_hash_is_still_reachable_for_storage() { |
| 48 | let hash = PasswordHash::from_trusted("$argon2id$stored"); |
| 49 | |
| 50 | assert_eq!(hash.as_str(), "$argon2id$stored"); |
| 51 | } |
| 52 | } |