1use secrecy::{ExposeSecret, SecretString};
8use std::collections::HashMap;
9
10#[derive(Clone)]
26pub struct SecureSecret {
27 inner: SecretString,
28}
29
30impl SecureSecret {
31 #[must_use]
36 pub fn new(value: String) -> Self {
37 Self {
38 inner: SecretString::from(value),
39 }
40 }
41
42 #[must_use]
51 pub fn expose(&self) -> &str {
52 self.inner.expose_secret()
53 }
54
55 #[must_use]
57 pub fn len(&self) -> usize {
58 self.inner.expose_secret().len()
59 }
60
61 #[must_use]
63 pub fn is_empty(&self) -> bool {
64 self.inner.expose_secret().is_empty()
65 }
66}
67
68impl std::fmt::Debug for SecureSecret {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.write_str("[REDACTED]")
71 }
72}
73
74impl std::fmt::Display for SecureSecret {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str("[REDACTED]")
77 }
78}
79
80#[derive(Default)]
107pub struct BatchSecrets {
108 secrets: HashMap<String, SecureSecret>,
110 fingerprints: HashMap<String, String>,
112}
113
114impl BatchSecrets {
115 #[must_use]
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 #[must_use]
123 pub fn with_capacity(capacity: usize) -> Self {
124 Self {
125 secrets: HashMap::with_capacity(capacity),
126 fingerprints: HashMap::with_capacity(capacity),
127 }
128 }
129
130 pub fn insert(&mut self, name: String, value: SecureSecret, fingerprint: Option<String>) {
138 if let Some(fp) = fingerprint {
139 self.fingerprints.insert(name.clone(), fp);
140 }
141 self.secrets.insert(name, value);
142 }
143
144 #[must_use]
146 pub fn get(&self, name: &str) -> Option<&SecureSecret> {
147 self.secrets.get(name)
148 }
149
150 #[must_use]
152 pub fn contains(&self, name: &str) -> bool {
153 self.secrets.contains_key(name)
154 }
155
156 #[must_use]
158 pub fn is_empty(&self) -> bool {
159 self.secrets.is_empty()
160 }
161
162 #[must_use]
164 pub fn len(&self) -> usize {
165 self.secrets.len()
166 }
167
168 #[must_use]
170 pub const fn fingerprints(&self) -> &HashMap<String, String> {
171 &self.fingerprints
172 }
173
174 #[must_use]
176 pub fn fingerprint(&self, name: &str) -> Option<&str> {
177 self.fingerprints.get(name).map(String::as_str)
178 }
179
180 pub fn names(&self) -> impl Iterator<Item = &String> {
182 self.secrets.keys()
183 }
184
185 #[must_use]
192 pub fn into_env_map(self) -> HashMap<String, String> {
193 self.secrets
194 .into_iter()
195 .map(|(k, v)| (k, v.expose().to_string()))
196 .collect()
197 }
198
199 #[must_use]
203 pub fn into_resolved_secrets(self) -> crate::ResolvedSecrets {
204 let fingerprints = self.fingerprints;
205 let values = self
206 .secrets
207 .into_iter()
208 .map(|(k, v)| (k, v.expose().to_string()))
209 .collect();
210 crate::ResolvedSecrets {
211 values,
212 fingerprints,
213 }
214 }
215
216 pub fn merge(&mut self, other: Self) {
220 for (name, value) in other.secrets {
221 let fingerprint = other.fingerprints.get(&name).cloned();
222 self.insert(name, value, fingerprint);
223 }
224 }
225}
226
227impl std::fmt::Debug for BatchSecrets {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("BatchSecrets")
230 .field("count", &self.secrets.len())
231 .field("names", &self.secrets.keys().collect::<Vec<_>>())
232 .field("fingerprints", &self.fingerprints.len())
233 .finish()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn secure_secret_debug_is_redacted() {
243 let secret = SecureSecret::new("my-super-secret-password".to_string());
244 let debug_output = format!("{secret:?}");
245 assert_eq!(debug_output, "[REDACTED]");
246 assert!(!debug_output.contains("password"));
247 }
248
249 #[test]
250 fn secure_secret_display_is_redacted() {
251 let secret = SecureSecret::new("my-super-secret-password".to_string());
252 let display_output = format!("{secret}");
253 assert_eq!(display_output, "[REDACTED]");
254 }
255
256 #[test]
257 fn secure_secret_expose_returns_value() {
258 let secret = SecureSecret::new("test-value".to_string());
259 assert_eq!(secret.expose(), "test-value");
260 }
261
262 #[test]
263 fn secure_secret_len_works() {
264 let secret = SecureSecret::new("12345".to_string());
265 assert_eq!(secret.len(), 5);
266 assert!(!secret.is_empty());
267 }
268
269 #[test]
270 fn batch_secrets_insert_and_get() {
271 let mut batch = BatchSecrets::new();
272 batch.insert(
273 "API_KEY".to_string(),
274 SecureSecret::new("secret123".to_string()),
275 Some("fingerprint123".to_string()),
276 );
277
278 assert!(batch.contains("API_KEY"));
279 assert!(!batch.contains("OTHER"));
280 assert_eq!(batch.len(), 1);
281
282 let secret = batch.get("API_KEY").unwrap();
283 assert_eq!(secret.expose(), "secret123");
284 assert_eq!(batch.fingerprint("API_KEY"), Some("fingerprint123"));
285 }
286
287 #[test]
288 fn batch_secrets_debug_hides_values() {
289 let mut batch = BatchSecrets::new();
290 batch.insert(
291 "SECRET".to_string(),
292 SecureSecret::new("password".to_string()),
293 None,
294 );
295
296 let debug_output = format!("{batch:?}");
297 assert!(!debug_output.contains("password"));
298 assert!(debug_output.contains("SECRET"));
299 assert!(debug_output.contains("count"));
300 }
301
302 #[test]
303 fn batch_secrets_into_env_map() {
304 let mut batch = BatchSecrets::new();
305 batch.insert(
306 "KEY1".to_string(),
307 SecureSecret::new("value1".to_string()),
308 None,
309 );
310 batch.insert(
311 "KEY2".to_string(),
312 SecureSecret::new("value2".to_string()),
313 None,
314 );
315
316 let env_map = batch.into_env_map();
317 assert_eq!(env_map.get("KEY1"), Some(&"value1".to_string()));
318 assert_eq!(env_map.get("KEY2"), Some(&"value2".to_string()));
319 }
320
321 #[test]
322 fn batch_secrets_merge() {
323 let mut batch1 = BatchSecrets::new();
324 batch1.insert(
325 "KEY1".to_string(),
326 SecureSecret::new("value1".to_string()),
327 None,
328 );
329
330 let mut batch2 = BatchSecrets::new();
331 batch2.insert(
332 "KEY2".to_string(),
333 SecureSecret::new("value2".to_string()),
334 Some("fp2".to_string()),
335 );
336
337 batch1.merge(batch2);
338
339 assert_eq!(batch1.len(), 2);
340 assert!(batch1.contains("KEY1"));
341 assert!(batch1.contains("KEY2"));
342 assert_eq!(batch1.fingerprint("KEY2"), Some("fp2"));
343 }
344}