allframe_core/security/
obfuscation.rs1use url::Url;
7
8pub 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 "***invalid-url***".to_string()
33 }
34 }
35}
36
37pub 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 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
67pub fn obfuscate_api_key(key: &str) -> String {
81 let len = key.len();
82
83 if len <= 8 {
84 "***".to_string()
86 } else {
87 let prefix = &key[..4];
89 let suffix = &key[len - 4..];
90 format!("{}***{}", prefix, suffix)
91 }
92}
93
94pub 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 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
141pub trait Obfuscate {
169 fn obfuscate(&self) -> String;
171}
172
173impl 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#[derive(Clone)]
218#[allow(dead_code)]
219pub struct Sensitive<T>(T);
220
221#[allow(dead_code)]
222impl<T> Sensitive<T> {
223 pub fn new(value: T) -> Self {
225 Self(value)
226 }
227
228 pub fn into_inner(self) -> T {
230 self.0
231 }
232
233 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}