modkit_utils/
secret_string.rs1use std::fmt;
2
3use zeroize::{Zeroize, ZeroizeOnDrop};
4
5#[derive(Zeroize, ZeroizeOnDrop)]
13pub struct SecretString(String);
14
15impl SecretString {
16 pub fn new(value: impl Into<String>) -> Self {
18 Self(value.into())
19 }
20
21 #[must_use]
25 pub fn expose(&self) -> &str {
26 &self.0
27 }
28}
29
30impl Clone for SecretString {
31 fn clone(&self) -> Self {
32 Self(self.0.clone())
33 }
34}
35
36impl fmt::Debug for SecretString {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str("[REDACTED]")
39 }
40}
41
42impl fmt::Display for SecretString {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str("[REDACTED]")
45 }
46}
47
48#[cfg(test)]
49#[cfg_attr(coverage_nightly, coverage(off))]
50mod tests {
51 use super::*;
52 use zeroize::Zeroize;
53
54 #[test]
55 fn debug_is_redacted() {
56 let s = SecretString::new("hunter2");
57 assert_eq!(format!("{s:?}"), "[REDACTED]");
58 }
59
60 #[test]
61 fn display_is_redacted() {
62 let s = SecretString::new("hunter2");
63 assert_eq!(format!("{s}"), "[REDACTED]");
64 }
65
66 #[test]
67 fn debug_does_not_contain_secret() {
68 let secret = "super-secret-value-12345";
69 let s = SecretString::new(secret);
70 let dbg = format!("{s:?}");
71 assert!(!dbg.contains(secret), "Debug must not contain the secret");
72 }
73
74 #[test]
75 fn expose_returns_original_value() {
76 let s = SecretString::new("hunter2");
77 assert_eq!(s.expose(), "hunter2");
78 }
79
80 #[test]
81 fn clone_preserves_value() {
82 let s = SecretString::new("value");
83 #[allow(clippy::redundant_clone)]
84 let c = s.clone();
85 assert_eq!(c.expose(), "value");
86 }
87
88 #[test]
89 fn zeroize_clears_buffer() {
90 let mut s = SecretString::new("sensitive");
91 assert_eq!(s.expose(), "sensitive");
92
93 s.zeroize();
94 assert!(s.0.is_empty(), "buffer should be empty after zeroize");
95 }
96}