hyperi_rustlib/
sensitive.rs1use std::fmt;
42
43use serde::de::Deserializer;
44use serde::ser::Serializer;
45
46const REDACTED: &str = "***REDACTED***";
47
48#[derive(Clone, Default, PartialEq, Eq)]
59pub struct SensitiveString(String);
60
61impl SensitiveString {
62 #[must_use]
64 pub fn new(value: impl Into<String>) -> Self {
65 Self(value.into())
66 }
67
68 #[must_use]
73 pub fn expose(&self) -> &str {
74 &self.0
75 }
76
77 #[must_use]
79 pub fn is_empty(&self) -> bool {
80 self.0.is_empty()
81 }
82}
83
84impl serde::Serialize for SensitiveString {
85 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
86 serializer.serialize_str(REDACTED)
87 }
88}
89
90impl<'de> serde::Deserialize<'de> for SensitiveString {
91 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92 String::deserialize(deserializer).map(SensitiveString)
93 }
94}
95
96impl fmt::Display for SensitiveString {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "{REDACTED}")
99 }
100}
101
102impl fmt::Debug for SensitiveString {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(f, "SensitiveString({REDACTED})")
105 }
106}
107
108impl From<String> for SensitiveString {
109 fn from(s: String) -> Self {
110 Self(s)
111 }
112}
113
114impl From<&str> for SensitiveString {
115 fn from(s: &str) -> Self {
116 Self(s.to_string())
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn serialize_always_redacted() {
126 let s = SensitiveString::new("my_actual_secret");
127 let json = serde_json::to_string(&s).unwrap();
128 assert_eq!(json, format!("\"{REDACTED}\""));
129 assert!(!json.contains("my_actual_secret"));
130 }
131
132 #[test]
133 fn deserialize_reads_actual_value() {
134 let json = "\"my_actual_secret\"";
135 let s: SensitiveString = serde_json::from_str(json).unwrap();
136 assert_eq!(s.expose(), "my_actual_secret");
137 }
138
139 #[test]
140 fn display_is_redacted() {
141 let s = SensitiveString::new("secret123");
142 assert_eq!(format!("{s}"), REDACTED);
143 assert!(!format!("{s}").contains("secret123"));
144 }
145
146 #[test]
147 fn debug_is_redacted() {
148 let s = SensitiveString::new("secret123");
149 let debug = format!("{s:?}");
150 assert!(debug.contains(REDACTED));
151 assert!(!debug.contains("secret123"));
152 }
153
154 #[test]
155 fn expose_returns_actual_value() {
156 let s = SensitiveString::new("the_real_value");
157 assert_eq!(s.expose(), "the_real_value");
158 }
159
160 #[test]
161 fn default_is_empty() {
162 let s = SensitiveString::default();
163 assert!(s.is_empty());
164 assert_eq!(s.expose(), "");
165 }
166
167 #[test]
168 fn from_string() {
169 let s: SensitiveString = "hello".into();
170 assert_eq!(s.expose(), "hello");
171
172 let s: SensitiveString = String::from("world").into();
173 assert_eq!(s.expose(), "world");
174 }
175
176 #[test]
177 fn struct_with_sensitive_field_serialises_safely() {
178 #[derive(serde::Serialize, serde::Deserialize)]
179 struct Config {
180 host: String,
181 connection_string: SensitiveString,
182 }
183
184 let config = Config {
185 host: "db.example.com".into(),
186 connection_string: SensitiveString::new("postgres://user:pass@host/db"),
187 };
188
189 let json = serde_json::to_string(&config).unwrap();
190 assert!(json.contains("db.example.com"));
191 assert!(json.contains(REDACTED));
192 assert!(!json.contains("postgres://"));
193 assert!(!json.contains("user:pass"));
194 }
195
196 #[test]
197 fn struct_with_sensitive_field_deserialises_correctly() {
198 #[derive(serde::Serialize, serde::Deserialize)]
199 struct Config {
200 host: String,
201 connection_string: SensitiveString,
202 }
203
204 let json =
205 r#"{"host":"db.example.com","connection_string":"postgres://user:pass@host/db"}"#;
206 let config: Config = serde_json::from_str(json).unwrap();
207 assert_eq!(config.host, "db.example.com");
208 assert_eq!(
209 config.connection_string.expose(),
210 "postgres://user:pass@host/db"
211 );
212 }
213
214 #[test]
215 fn no_leak_through_any_serialisation_path() {
216 let secret = "super_secret_value_12345";
217 let s = SensitiveString::new(secret);
218
219 assert!(!serde_json::to_string(&s).unwrap().contains(secret));
221 assert!(!format!("{s}").contains(secret));
223 assert!(!format!("{s:?}").contains(secret));
225 assert_eq!(s.expose(), secret);
227 }
228}