@jpgilldev / steid

steid/src/domain/password.rs
1.4 KBRaw
1use 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)]
9pub struct PasswordHash(String);
10
11impl 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
25impl 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)]
32mod 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}