ggen_ai/
security.rs

1//! Security utilities for protecting sensitive data
2//!
3//! Provides secure handling of API keys and other sensitive strings,
4//! ensuring they are never logged or displayed in full.
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::fmt;
8
9/// A secure wrapper for sensitive strings like API keys
10///
11/// This type ensures that sensitive data is never accidentally logged or displayed.
12/// When debugging or displaying, it shows only a masked version of the string.
13///
14/// # Security Features
15/// - Debug trait shows masked value (first 4 chars + "...")
16/// - Display trait shows masked value
17/// - Serialize shows masked value
18/// - Clone and comparison work on full value
19///
20/// # Example
21/// ```
22/// use ggen_ai::security::SecretString;
23///
24/// let api_key = SecretString::new("sk-1234567890abcdef".to_string());
25/// println!("{}", api_key); // Prints: "sk-1..."
26/// println!("{:?}", api_key); // Prints: SecretString("sk-1...")
27/// ```
28#[derive(Clone, PartialEq, Eq)]
29pub struct SecretString(String);
30
31impl SecretString {
32    /// Create a new SecretString
33    pub fn new(value: String) -> Self {
34        Self(value)
35    }
36
37    /// Get the underlying value (use with caution!)
38    ///
39    /// # Security Warning
40    /// This exposes the raw secret value. Only use this when you need
41    /// to pass the secret to an API client or similar trusted code.
42    pub fn expose_secret(&self) -> &str {
43        &self.0
44    }
45
46    /// Check if the secret is empty
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    /// Get the length of the secret
52    pub fn len(&self) -> usize {
53        self.0.len()
54    }
55
56    /// Create a masked version of the secret for display
57    ///
58    /// Shows first 4 characters followed by "..." for security.
59    /// If the secret is shorter than 8 characters, shows fewer chars.
60    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    /// Convert from a plain string
75    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        // Serialize as masked value to prevent leakage in logs/JSON
98        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
124/// Extension trait for masking API keys in error messages and logs
125pub trait MaskApiKey {
126    /// Mask API key in string, replacing it with masked version
127    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
142/// Mask sensitive patterns in a string
143///
144/// Detects common API key patterns and masks them:
145/// - OpenAI keys (sk-...)
146/// - Anthropic keys (sk-ant-...)
147/// - Bearer tokens
148/// - Generic API keys
149fn mask_sensitive_patterns(input: &str) -> String {
150    let mut result = input.to_string();
151
152    // Pattern: sk-ant-... (Anthropic) - MUST come before general sk- pattern!
153    let ant_pattern = regex::Regex::new(r"sk-ant-[a-zA-Z0-9_-]{20,}").unwrap();
154    result = ant_pattern
155        .replace_all(&result, |caps: &regex::Captures| {
156            let matched = caps.get(0).unwrap().as_str();
157            format!("{}...", &matched[..7])
158        })
159        .to_string();
160
161    // Pattern: sk-... (OpenAI and similar)
162    let sk_pattern = regex::Regex::new(r"sk-[a-zA-Z0-9_-]{20,}").unwrap();
163    result = sk_pattern
164        .replace_all(&result, |caps: &regex::Captures| {
165            let matched = caps.get(0).unwrap().as_str();
166            format!("{}...", &matched[..4])
167        })
168        .to_string();
169
170    // Pattern: Bearer tokens
171    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    // Pattern: api_key=... or api-key=... or apikey=...
177    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}