allframe_core/security/
obfuscation.rs

1//! Obfuscation utilities for safe logging of sensitive data.
2//!
3//! These functions help prevent accidental exposure of sensitive information
4//! in logs, error messages, and debug output.
5
6use url::Url;
7
8/// Obfuscate a URL by removing credentials, path, and query parameters.
9///
10/// # Example
11///
12/// ```
13/// use allframe_core::security::obfuscate_url;
14///
15/// let url = "https://user:pass@api.example.com/v1/data?key=secret";
16/// assert_eq!(obfuscate_url(url), "https://api.example.com/***");
17///
18/// let simple = "https://example.com";
19/// assert_eq!(obfuscate_url(simple), "https://example.com/***");
20/// ```
21pub fn obfuscate_url(url: &str) -> String {
22    match Url::parse(url) {
23        Ok(parsed) => {
24            let scheme = parsed.scheme();
25            let host = parsed.host_str().unwrap_or("unknown");
26            let port = parsed.port().map(|p| format!(":{}", p)).unwrap_or_default();
27
28            format!("{}://{}{}/***", scheme, host, port)
29        }
30        Err(_) => {
31            // If parsing fails, just show a generic placeholder
32            "***invalid-url***".to_string()
33        }
34    }
35}
36
37/// Obfuscate a Redis URL, preserving host and port but hiding authentication.
38///
39/// # Example
40///
41/// ```
42/// use allframe_core::security::obfuscate_redis_url;
43///
44/// let url = "redis://:password@localhost:6379/0";
45/// assert_eq!(obfuscate_redis_url(url), "redis://***@localhost:6379/***");
46///
47/// let simple = "redis://localhost:6379";
48/// assert_eq!(obfuscate_redis_url(simple), "redis://localhost:6379/***");
49/// ```
50pub fn obfuscate_redis_url(url: &str) -> String {
51    match Url::parse(url) {
52        Ok(parsed) => {
53            let scheme = parsed.scheme();
54            let host = parsed.host_str().unwrap_or("unknown");
55            let port = parsed.port().map(|p| format!(":{}", p)).unwrap_or_default();
56
57            // Check if there's authentication
58            let has_auth = !parsed.username().is_empty() || parsed.password().is_some();
59            let auth_part = if has_auth { "***@" } else { "" };
60
61            format!("{}://{}{}{}/***", scheme, auth_part, host, port)
62        }
63        Err(_) => "***invalid-redis-url***".to_string(),
64    }
65}
66
67/// Obfuscate an API key, showing only prefix and suffix.
68///
69/// # Example
70///
71/// ```
72/// use allframe_core::security::obfuscate_api_key;
73///
74/// let key = "sk_live_abcdefghijklmnop";
75/// assert_eq!(obfuscate_api_key(key), "sk_l***mnop");
76///
77/// let short = "abc";
78/// assert_eq!(obfuscate_api_key(short), "***");
79/// ```
80pub fn obfuscate_api_key(key: &str) -> String {
81    let len = key.len();
82
83    if len <= 8 {
84        // Too short to safely reveal any part
85        "***".to_string()
86    } else {
87        // Show first 4 and last 4 characters
88        let prefix = &key[..4];
89        let suffix = &key[len - 4..];
90        format!("{}***{}", prefix, suffix)
91    }
92}
93
94/// Obfuscate a header value based on the header name.
95///
96/// Sensitive headers (Authorization, Cookie, etc.) are fully obfuscated.
97/// Other headers are returned as-is.
98///
99/// # Example
100///
101/// ```
102/// use allframe_core::security::obfuscate_header;
103///
104/// // Sensitive headers are obfuscated
105/// assert_eq!(
106///     obfuscate_header("Authorization", "Bearer sk_live_secret"),
107///     "Bearer ***"
108/// );
109/// assert_eq!(
110///     obfuscate_header("Cookie", "session=abc123"),
111///     "***"
112/// );
113///
114/// // Non-sensitive headers are passed through
115/// assert_eq!(
116///     obfuscate_header("Content-Type", "application/json"),
117///     "application/json"
118/// );
119/// ```
120pub fn obfuscate_header(name: &str, value: &str) -> String {
121    let name_lower = name.to_lowercase();
122
123    match name_lower.as_str() {
124        "authorization" => {
125            // Preserve the auth scheme but hide the token
126            if let Some(space_idx) = value.find(' ') {
127                let scheme = &value[..space_idx];
128                format!("{} ***", scheme)
129            } else {
130                "***".to_string()
131            }
132        }
133        "cookie" | "set-cookie" => "***".to_string(),
134        "x-api-key" | "api-key" | "apikey" => obfuscate_api_key(value),
135        "x-auth-token" | "x-access-token" | "x-refresh-token" => "***".to_string(),
136        "proxy-authorization" => "***".to_string(),
137        _ => value.to_string(),
138    }
139}
140
141/// Trait for types that can be obfuscated for safe logging.
142///
143/// Implement this trait for custom types that contain sensitive data.
144///
145/// # Example
146///
147/// ```
148/// use allframe_core::security::Obfuscate;
149///
150/// struct DatabaseConfig {
151///     host: String,
152///     password: String,
153/// }
154///
155/// impl Obfuscate for DatabaseConfig {
156///     fn obfuscate(&self) -> String {
157///         format!("DatabaseConfig {{ host: {}, password: *** }}", self.host)
158///     }
159/// }
160///
161/// let config = DatabaseConfig {
162///     host: "localhost".to_string(),
163///     password: "secret".to_string(),
164/// };
165///
166/// assert_eq!(config.obfuscate(), "DatabaseConfig { host: localhost, password: *** }");
167/// ```
168pub trait Obfuscate {
169    /// Return an obfuscated representation suitable for logging.
170    fn obfuscate(&self) -> String;
171}
172
173// Implement Obfuscate for common types
174
175impl Obfuscate for String {
176    fn obfuscate(&self) -> String {
177        if self.len() <= 8 {
178            "***".to_string()
179        } else {
180            obfuscate_api_key(self)
181        }
182    }
183}
184
185impl Obfuscate for &str {
186    fn obfuscate(&self) -> String {
187        if self.len() <= 8 {
188            "***".to_string()
189        } else {
190            obfuscate_api_key(self)
191        }
192    }
193}
194
195impl<T: Obfuscate> Obfuscate for Option<T> {
196    fn obfuscate(&self) -> String {
197        match self {
198            Some(v) => format!("Some({})", v.obfuscate()),
199            None => "None".to_string(),
200        }
201    }
202}
203
204/// Helper struct for wrapping sensitive values.
205///
206/// When formatted with `Debug` or `Display`, shows obfuscated value.
207///
208/// # Example
209///
210/// ```
211/// use allframe_core::security::Sensitive;
212///
213/// let password = Sensitive::new("super_secret_password");
214/// assert_eq!(format!("{}", password), "***");
215/// assert_eq!(format!("{:?}", password), "Sensitive(***)");
216/// ```
217#[derive(Clone)]
218#[allow(dead_code)]
219pub struct Sensitive<T>(T);
220
221#[allow(dead_code)]
222impl<T> Sensitive<T> {
223    /// Create a new sensitive wrapper.
224    pub fn new(value: T) -> Self {
225        Self(value)
226    }
227
228    /// Get the inner value (use with caution).
229    pub fn into_inner(self) -> T {
230        self.0
231    }
232
233    /// Get a reference to the inner value (use with caution).
234    pub fn as_inner(&self) -> &T {
235        &self.0
236    }
237}
238
239impl<T> std::fmt::Display for Sensitive<T> {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        write!(f, "***")
242    }
243}
244
245impl<T> std::fmt::Debug for Sensitive<T> {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        write!(f, "Sensitive(***)")
248    }
249}
250
251impl<T: Default> Default for Sensitive<T> {
252    fn default() -> Self {
253        Self(T::default())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_obfuscate_url_with_credentials() {
263        let url = "https://user:password@api.example.com/v1/users?key=secret";
264        assert_eq!(obfuscate_url(url), "https://api.example.com/***");
265    }
266
267    #[test]
268    fn test_obfuscate_url_simple() {
269        let url = "https://example.com";
270        assert_eq!(obfuscate_url(url), "https://example.com/***");
271    }
272
273    #[test]
274    fn test_obfuscate_url_with_port() {
275        let url = "http://localhost:8080/api";
276        assert_eq!(obfuscate_url(url), "http://localhost:8080/***");
277    }
278
279    #[test]
280    fn test_obfuscate_url_invalid() {
281        let url = "not-a-url";
282        assert_eq!(obfuscate_url(url), "***invalid-url***");
283    }
284
285    #[test]
286    fn test_obfuscate_redis_url_with_auth() {
287        let url = "redis://:password@localhost:6379/0";
288        assert_eq!(obfuscate_redis_url(url), "redis://***@localhost:6379/***");
289    }
290
291    #[test]
292    fn test_obfuscate_redis_url_with_user() {
293        let url = "redis://user:password@localhost:6379/0";
294        assert_eq!(obfuscate_redis_url(url), "redis://***@localhost:6379/***");
295    }
296
297    #[test]
298    fn test_obfuscate_redis_url_no_auth() {
299        let url = "redis://localhost:6379";
300        assert_eq!(obfuscate_redis_url(url), "redis://localhost:6379/***");
301    }
302
303    #[test]
304    fn test_obfuscate_api_key_long() {
305        let key = "sk_live_abcdefghijklmnop";
306        assert_eq!(obfuscate_api_key(key), "sk_l***mnop");
307    }
308
309    #[test]
310    fn test_obfuscate_api_key_short() {
311        let key = "short";
312        assert_eq!(obfuscate_api_key(key), "***");
313    }
314
315    #[test]
316    fn test_obfuscate_api_key_exactly_8() {
317        let key = "12345678";
318        assert_eq!(obfuscate_api_key(key), "***");
319    }
320
321    #[test]
322    fn test_obfuscate_api_key_9_chars() {
323        let key = "123456789";
324        assert_eq!(obfuscate_api_key(key), "1234***6789");
325    }
326
327    #[test]
328    fn test_obfuscate_header_authorization_bearer() {
329        assert_eq!(
330            obfuscate_header("Authorization", "Bearer token123"),
331            "Bearer ***"
332        );
333    }
334
335    #[test]
336    fn test_obfuscate_header_authorization_basic() {
337        assert_eq!(
338            obfuscate_header("Authorization", "Basic dXNlcjpwYXNz"),
339            "Basic ***"
340        );
341    }
342
343    #[test]
344    fn test_obfuscate_header_cookie() {
345        assert_eq!(obfuscate_header("Cookie", "session=abc123"), "***");
346    }
347
348    #[test]
349    fn test_obfuscate_header_api_key() {
350        assert_eq!(
351            obfuscate_header("X-API-Key", "sk_live_abcdefghij"),
352            "sk_l***ghij"
353        );
354    }
355
356    #[test]
357    fn test_obfuscate_header_content_type() {
358        assert_eq!(
359            obfuscate_header("Content-Type", "application/json"),
360            "application/json"
361        );
362    }
363
364    #[test]
365    fn test_obfuscate_header_case_insensitive() {
366        assert_eq!(
367            obfuscate_header("AUTHORIZATION", "Bearer token"),
368            "Bearer ***"
369        );
370        assert_eq!(
371            obfuscate_header("authorization", "Bearer token"),
372            "Bearer ***"
373        );
374    }
375
376    #[test]
377    fn test_obfuscate_trait_string() {
378        let s = "a_long_secret_string".to_string();
379        assert_eq!(s.obfuscate(), "a_lo***ring");
380    }
381
382    #[test]
383    fn test_obfuscate_trait_short_string() {
384        let s = "short".to_string();
385        assert_eq!(s.obfuscate(), "***");
386    }
387
388    #[test]
389    fn test_obfuscate_trait_option_some() {
390        let opt: Option<String> = Some("long_secret_value".to_string());
391        assert_eq!(opt.obfuscate(), "Some(long***alue)");
392    }
393
394    #[test]
395    fn test_obfuscate_trait_option_none() {
396        let opt: Option<String> = None;
397        assert_eq!(opt.obfuscate(), "None");
398    }
399
400    #[test]
401    fn test_sensitive_display() {
402        let s = Sensitive::new("secret");
403        assert_eq!(format!("{}", s), "***");
404    }
405
406    #[test]
407    fn test_sensitive_debug() {
408        let s = Sensitive::new("secret");
409        assert_eq!(format!("{:?}", s), "Sensitive(***)");
410    }
411
412    #[test]
413    fn test_sensitive_into_inner() {
414        let s = Sensitive::new("secret");
415        assert_eq!(s.into_inner(), "secret");
416    }
417
418    #[test]
419    fn test_sensitive_as_inner() {
420        let s = Sensitive::new("secret");
421        assert_eq!(s.as_inner(), &"secret");
422    }
423}