1use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::fmt;
8
9#[derive(Clone, PartialEq, Eq)]
29pub struct SecretString(String);
30
31impl SecretString {
32 pub fn new(value: String) -> Self {
34 Self(value)
35 }
36
37 pub fn expose_secret(&self) -> &str {
43 &self.0
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.0.is_empty()
49 }
50
51 pub fn len(&self) -> usize {
53 self.0.len()
54 }
55
56 pub fn mask(&self) -> String {
61 if self.0.is_empty() {
62 return "[empty]".to_string();
63 }
64
65 let chars_to_show = std::cmp::min(4, self.0.len().saturating_sub(4));
66 if chars_to_show == 0 {
67 return "***".to_string();
68 }
69
70 let prefix: String = self.0.chars().take(chars_to_show).collect();
71 format!("{}...", prefix)
72 }
73
74 pub fn from_string(s: &str) -> Self {
76 Self(s.to_string())
77 }
78}
79
80impl fmt::Debug for SecretString {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "SecretString(\"{}\")", self.mask())
83 }
84}
85
86impl fmt::Display for SecretString {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(f, "{}", self.mask())
89 }
90}
91
92impl Serialize for SecretString {
93 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94 where
95 S: Serializer,
96 {
97 serializer.serialize_str(&self.mask())
99 }
100}
101
102impl<'de> Deserialize<'de> for SecretString {
103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104 where
105 D: Deserializer<'de>,
106 {
107 let s = String::deserialize(deserializer)?;
108 Ok(SecretString::new(s))
109 }
110}
111
112impl From<String> for SecretString {
113 fn from(s: String) -> Self {
114 SecretString::new(s)
115 }
116}
117
118impl From<&str> for SecretString {
119 fn from(s: &str) -> Self {
120 SecretString::new(s.to_string())
121 }
122}
123
124pub trait MaskApiKey {
126 fn mask_api_key(&self) -> String;
128}
129
130impl MaskApiKey for String {
131 fn mask_api_key(&self) -> String {
132 mask_sensitive_patterns(self)
133 }
134}
135
136impl MaskApiKey for &str {
137 fn mask_api_key(&self) -> String {
138 mask_sensitive_patterns(self)
139 }
140}
141
142fn mask_sensitive_patterns(input: &str) -> String {
150 let mut result = input.to_string();
151
152 let ant_pattern = regex::Regex::new(r"sk-ant-[a-zA-Z0-9_-]{20,}").unwrap();
154 result = ant_pattern
155 .replace_all(&result, |caps: ®ex::Captures| {
156 let matched = caps.get(0).unwrap().as_str();
157 format!("{}...", &matched[..7])
158 })
159 .to_string();
160
161 let sk_pattern = regex::Regex::new(r"sk-[a-zA-Z0-9_-]{20,}").unwrap();
163 result = sk_pattern
164 .replace_all(&result, |caps: ®ex::Captures| {
165 let matched = caps.get(0).unwrap().as_str();
166 format!("{}...", &matched[..4])
167 })
168 .to_string();
169
170 let bearer_pattern = regex::Regex::new(r"Bearer\s+([a-zA-Z0-9_\-\.]+)").unwrap();
172 result = bearer_pattern
173 .replace_all(&result, "Bearer [masked]")
174 .to_string();
175
176 let api_key_pattern =
178 regex::Regex::new(r"(?i)(api[_-]?key\s*[=:]\s*)([a-zA-Z0-9_\-\.]+)").unwrap();
179 result = api_key_pattern
180 .replace_all(&result, "$1[masked]")
181 .to_string();
182
183 result
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_secret_string_masking() {
192 let secret = SecretString::new("sk-1234567890abcdef".to_string());
193 assert_eq!(secret.mask(), "sk-1...");
194 assert_eq!(format!("{}", secret), "sk-1...");
195 assert_eq!(format!("{:?}", secret), "SecretString(\"sk-1...\")");
196 }
197
198 #[test]
199 fn test_secret_string_short() {
200 let secret = SecretString::new("abc".to_string());
201 assert_eq!(secret.mask(), "***");
202 }
203
204 #[test]
205 fn test_secret_string_empty() {
206 let secret = SecretString::new("".to_string());
207 assert_eq!(secret.mask(), "[empty]");
208 assert!(secret.is_empty());
209 }
210
211 #[test]
212 fn test_secret_string_expose() {
213 let secret = SecretString::new("sk-1234567890abcdef".to_string());
214 assert_eq!(secret.expose_secret(), "sk-1234567890abcdef");
215 }
216
217 #[test]
218 fn test_secret_string_clone() {
219 let secret1 = SecretString::new("sk-1234567890abcdef".to_string());
220 let secret2 = secret1.clone();
221 assert_eq!(secret1, secret2);
222 }
223
224 #[test]
225 fn test_mask_openai_key() {
226 let input = "Error with API key sk-1234567890abcdefghijklmnop";
227 let masked = mask_sensitive_patterns(input);
228 assert!(masked.contains("sk-1..."));
229 assert!(!masked.contains("sk-1234567890"));
230 }
231
232 #[test]
233 fn test_mask_anthropic_key() {
234 let input = "Using key sk-ant-1234567890abcdefghijklmnop";
235 let masked = mask_sensitive_patterns(input);
236 assert!(masked.contains("sk-ant-..."));
237 assert!(!masked.contains("1234567890"));
238 }
239
240 #[test]
241 fn test_mask_bearer_token() {
242 let input = "Authorization: Bearer abc123.def456.ghi789";
243 let masked = mask_sensitive_patterns(input);
244 assert!(masked.contains("Bearer [masked]"));
245 assert!(!masked.contains("abc123"));
246 }
247
248 #[test]
249 fn test_mask_api_key_assignment() {
250 let input = "api_key=secret123456";
251 let masked = mask_sensitive_patterns(input);
252 assert!(masked.contains("api_key=[masked]"));
253 assert!(!masked.contains("secret123"));
254 }
255
256 #[test]
257 fn test_mask_api_key_trait() {
258 let text = "My key is sk-1234567890abcdefghijklmnop".to_string();
259 let masked = text.mask_api_key();
260 assert!(masked.contains("sk-1..."));
261 assert!(!masked.contains("sk-1234567890"));
262 }
263
264 #[test]
265 fn test_secret_string_serialization() {
266 let secret = SecretString::new("sk-1234567890abcdef".to_string());
267 let json = serde_json::to_string(&secret).unwrap();
268 assert!(json.contains("sk-1..."));
269 assert!(!json.contains("sk-1234567890"));
270 }
271
272 #[test]
273 fn test_secret_string_from_conversions() {
274 let from_string = SecretString::from("test".to_string());
275 let from_str = SecretString::from("test");
276 assert_eq!(from_string, from_str);
277 }
278}