1use serde::{Deserialize, Serialize};
10
11#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(transparent)]
14pub struct SecretString(String);
15
16impl SecretString {
17 pub fn new(value: impl Into<String>) -> Self {
19 Self(value.into())
20 }
21
22 #[must_use]
24 pub fn expose_secret(&self) -> &str {
25 &self.0
26 }
27
28 pub fn into_inner(self) -> String {
30 self.0
31 }
32
33 pub fn len(&self) -> usize {
35 self.0.len()
36 }
37
38 pub fn is_empty(&self) -> bool {
40 self.0.is_empty()
41 }
42}
43
44impl From<String> for SecretString {
45 fn from(value: String) -> Self {
46 Self(value)
47 }
48}
49
50impl From<&str> for SecretString {
51 fn from(value: &str) -> Self {
52 Self(value.to_string())
53 }
54}
55
56impl std::fmt::Debug for SecretString {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "[REDACTED, {} chars]", self.0.len())
59 }
60}
61
62impl std::fmt::Display for SecretString {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "[REDACTED]")
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::SecretString;
71
72 #[test]
73 fn debug_and_display_do_not_leak_value() {
74 let secret = SecretString::new("sk_test_123456789");
75 let debug = format!("{secret:?}");
76 let display = format!("{secret}");
77 assert!(debug.contains("[REDACTED"));
78 assert_eq!(display, "[REDACTED]");
79 assert!(!debug.contains("sk_test_123456789"));
80 assert!(!display.contains("sk_test_123456789"));
81 }
82
83 #[test]
84 fn serde_round_trip_preserves_value() {
85 let secret = SecretString::new("super-secret-value");
86 let json = serde_json::to_string(&secret).expect("serialize secret");
87 let parsed: SecretString = serde_json::from_str(&json).expect("deserialize secret");
88 assert_eq!(parsed.expose_secret(), "super-secret-value");
89 }
90}