1use serde::de::{self, Visitor};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use std::fmt;
10
11#[derive(Clone, PartialEq, Eq)]
18pub struct SecretString(String);
19
20impl SecretString {
21 pub fn new(s: impl Into<String>) -> Self {
22 Self(s.into())
23 }
24
25 pub fn expose(&self) -> &str {
28 &self.0
29 }
30
31 pub fn into_inner(self) -> String {
32 self.0
33 }
34
35 pub fn is_empty(&self) -> bool {
36 self.0.is_empty()
37 }
38
39 pub const MIN_REDACT_LEN: usize = 8;
41}
42
43impl fmt::Debug for SecretString {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str("SecretString(***)")
46 }
47}
48
49impl fmt::Display for SecretString {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str("***")
52 }
53}
54
55impl Serialize for SecretString {
56 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57 where
58 S: Serializer,
59 {
60 serializer.serialize_str("***")
62 }
63}
64
65impl<'de> Deserialize<'de> for SecretString {
66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67 where
68 D: Deserializer<'de>,
69 {
70 struct V;
71 impl Visitor<'_> for V {
72 type Value = SecretString;
73
74 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
75 f.write_str("a secret string")
76 }
77
78 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
79 Ok(SecretString::new(v))
80 }
81
82 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
83 Ok(SecretString::new(v))
84 }
85 }
86 deserializer.deserialize_string(V)
87 }
88}
89
90impl From<String> for SecretString {
91 fn from(s: String) -> Self {
92 Self(s)
93 }
94}
95
96impl From<&str> for SecretString {
97 fn from(s: &str) -> Self {
98 Self(s.to_string())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn secret_never_in_debug_or_display() {
108 let s = SecretString::new("sk-super-secret");
109 let d = format!("{s:?}");
110 let disp = format!("{s}");
111 assert!(!d.contains("sk-super-secret"));
112 assert!(!disp.contains("sk-super-secret"));
113 assert!(d.contains("***"));
114 assert_eq!(s.expose(), "sk-super-secret");
115 }
116
117 #[test]
118 fn serialize_never_emits_plaintext() {
119 let s = SecretString::new("sk-or-v1-canary-plaintext-value-xyz");
120 let json = serde_json::to_string(&s).expect("serialize");
121 assert!(!json.contains("canary"));
122 assert!(!json.contains("plaintext"));
123 assert_eq!(json, "\"***\"");
124 }
125
126 #[test]
127 fn deserialize_loads_value() {
128 let s: SecretString = serde_json::from_str("\"sk-loaded-from-config\"").unwrap();
129 assert_eq!(s.expose(), "sk-loaded-from-config");
130 assert!(!format!("{s:?}").contains("sk-loaded"));
131 }
132
133 #[test]
134 fn option_serialize_redacts() {
135 let v: Option<SecretString> = Some(SecretString::new("sk-nested-secret-value"));
136 let json = serde_json::to_string(&v).unwrap();
137 assert!(!json.contains("nested-secret"));
138 assert!(json.contains("***"));
139 }
140}