use std::fmt;
pub fn redact_value(value: &str, visible_chars: usize) -> String {
if value.len() <= visible_chars {
"*".repeat(value.len())
} else {
format!("{}{}", "*".repeat(4), &value[value.len() - visible_chars..])
}
}
pub fn redact_connection_string(connection_string: &str) -> String {
if let Some(at_idx) = connection_string.find('@') {
let auth_part = &connection_string[..at_idx];
let host_part = &connection_string[at_idx..];
let protocol_end = if let Some(protocol_idx) = auth_part.find("://") {
protocol_idx + 3 } else {
0
};
if let Some(colon_idx) = auth_part[protocol_end..].rfind(':') {
let colon_idx = protocol_end + colon_idx;
let user_part = &auth_part[..colon_idx];
return format!("{}:****{}", user_part, host_part);
} else {
return format!("{}:****{}", auth_part, host_part);
}
}
connection_string.to_string()
}
pub fn redact_cache_key(key: &str) -> String {
let sensitive_patterns = [
"token",
"password",
"secret",
"api_key",
"apikey",
"auth",
"credential",
"session",
"cookie",
"jwt",
];
let key_lower = key.to_lowercase();
for pattern in &sensitive_patterns {
if key_lower.contains(pattern) {
return redact_value(key, 4);
}
}
if key.len() > 100 {
format!("{}...", &key[..97])
} else {
key.to_string()
}
}
pub fn redact_field(field_name: &str, value: &str) -> String {
let sensitive_fields = [
"password",
"secret",
"token",
"api_key",
"apikey",
"auth",
"credential",
"private_key",
"access_token",
"refresh_token",
"session_key",
"cookie",
];
let field_lower = field_name.to_lowercase();
for sensitive in &sensitive_fields {
if field_lower.contains(sensitive) {
return redact_value(value, 4);
}
}
value.to_string()
}
pub struct Redacted<T: fmt::Display> {
value: T,
visible_chars: usize,
}
impl<T: fmt::Display> Redacted<T> {
pub fn new(value: T) -> Self {
Self {
value,
visible_chars: 4,
}
}
pub fn with_visible_chars(mut self, visible_chars: usize) -> Self {
self.visible_chars = visible_chars;
self
}
}
impl<T: fmt::Display> fmt::Display for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.value.to_string();
write!(f, "{}", redact_value(&value, self.visible_chars))
}
}
impl<T: fmt::Display> fmt::Debug for Redacted<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\"{}\"", self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_value() {
assert_eq!(redact_value("password123", 3), "****123");
assert_eq!(redact_value("abc", 4), "***");
assert_eq!(redact_value("a", 1), "*");
assert_eq!(redact_value("longpassword", 5), "****sword");
}
#[test]
fn test_redact_connection_string() {
assert_eq!(
redact_connection_string("redis://:mypassword@localhost:6379"),
"redis://:****@localhost:6379"
);
assert_eq!(
redact_connection_string("redis://user:mypassword@localhost:6379"),
"redis://user:****@localhost:6379"
);
assert_eq!(
redact_connection_string("redis://user@localhost:6379"),
"redis://user:****@localhost:6379"
);
assert_eq!(
redact_connection_string("redis://localhost:6379"),
"redis://localhost:6379"
);
}
#[test]
fn test_redact_cache_key() {
assert_eq!(redact_cache_key("user_token_abc123"), "****c123");
assert_eq!(redact_cache_key("user_profile_123"), "user_profile_123");
assert_eq!(
redact_cache_key("very_long_cache_key_that_exceeds_normal_length_limit"),
"very_long_cache_key_that_exceeds_normal_length_limit"
);
}
#[test]
fn test_redacted_wrapper() {
let redacted = Redacted::new("secret_value");
assert_eq!(redacted.to_string(), "****alue");
let redacted = Redacted::new("secret_value").with_visible_chars(6);
assert_eq!(redacted.to_string(), "****_value");
}
#[test]
fn test_redact_connection_string_no_protocol() {
let result = redact_connection_string("user:password@host:6379");
assert!(result.contains("****"));
assert!(!result.contains("password"));
}
#[test]
fn test_redact_connection_string_only_user_no_colon() {
let result = redact_connection_string("redis://user@host:6379");
assert_eq!(result, "redis://user:****@host:6379");
}
#[test]
fn test_redact_connection_string_empty() {
let result = redact_connection_string("");
assert_eq!(result, "");
}
#[test]
fn test_redact_cache_key_long_key_truncation() {
let long_key = "a".repeat(150);
let result = redact_cache_key(&long_key);
assert!(result.ends_with("..."));
assert_eq!(result.len(), 100); }
#[test]
fn test_redact_cache_key_exact_100_chars() {
let key = "a".repeat(100);
let result = redact_cache_key(&key);
assert_eq!(result, key);
}
#[test]
fn test_redact_cache_key_101_chars() {
let key = "a".repeat(101);
let result = redact_cache_key(&key);
assert!(result.ends_with("..."));
}
#[test]
fn test_redact_field_password() {
let result = redact_field("password", "secret123");
assert_eq!(result, "****t123");
}
#[test]
fn test_redact_field_secret() {
let result = redact_field("client_secret", "mysecret");
assert_eq!(result, "****cret");
}
#[test]
fn test_redact_field_token() {
let result = redact_field("access_token", "tok_abc123");
assert_eq!(result, "****c123");
}
#[test]
fn test_redact_field_api_key() {
let result = redact_field("api_key", "key_abc123");
assert_eq!(result, "****c123");
}
#[test]
fn test_redact_field_apikey() {
let result = redact_field("apikey", "key_value");
assert_eq!(result, "****alue");
}
#[test]
fn test_redact_field_auth() {
let result = redact_field("authorization", "bearer xyz");
assert_eq!(result, "**** xyz");
}
#[test]
fn test_redact_field_credential() {
let result = redact_field("credentials", "user:pass");
assert_eq!(result, "****pass");
}
#[test]
fn test_redact_field_private_key() {
let result = redact_field("private_key", "-----BEGIN...");
assert_eq!(result, "****N...");
}
#[test]
fn test_redact_field_access_token() {
let result = redact_field("access_token", "tok123");
assert_eq!(result, "****k123");
}
#[test]
fn test_redact_field_refresh_token() {
let result = redact_field("refresh_token", "ref123");
assert_eq!(result, "****f123");
}
#[test]
fn test_redact_field_session_key() {
let result = redact_field("session_key", "sess123");
assert_eq!(result, "****s123");
}
#[test]
fn test_redact_field_cookie() {
let result = redact_field("cookie", "session=abc");
assert_eq!(result, "****=abc");
}
#[test]
fn test_redact_field_non_sensitive() {
let result = redact_field("username", "alice");
assert_eq!(result, "alice");
}
#[test]
fn test_redact_field_non_sensitive_long() {
let result = redact_field("description", "some long description");
assert_eq!(result, "some long description");
}
#[test]
fn test_redact_field_case_insensitive() {
let result = redact_field("PASSWORD", "secret123");
assert_eq!(result, "****t123");
}
#[test]
fn test_redact_value_empty() {
assert_eq!(redact_value("", 4), "");
}
#[test]
fn test_redact_value_exact_visible_chars() {
assert_eq!(redact_value("abcd", 4), "****");
}
#[test]
fn test_redact_value_zero_visible() {
assert_eq!(redact_value("test", 0), "****");
}
#[test]
fn test_redacted_debug() {
let redacted = Redacted::new("secret_value");
let debug_str = format!("{:?}", redacted);
assert!(debug_str.starts_with("\""));
assert!(debug_str.ends_with("\""));
assert!(debug_str.contains("****"));
}
#[test]
fn test_redacted_with_visible_chars_zero() {
let redacted = Redacted::new("secret_value").with_visible_chars(0);
assert_eq!(redacted.to_string(), "****");
}
#[test]
fn test_redacted_short_value() {
let redacted = Redacted::new("ab").with_visible_chars(4);
assert_eq!(redacted.to_string(), "**");
}
#[test]
fn test_redacted_with_numeric_value() {
let redacted = Redacted::new(123456789);
let s = redacted.to_string();
assert!(s.starts_with("****"));
}
#[test]
fn test_redact_cache_key_session() {
let result = redact_cache_key("session_abc123");
assert_eq!(result, "****c123");
}
#[test]
fn test_redact_cache_key_cookie() {
let result = redact_cache_key("cookie_xyz789");
assert_eq!(result, "****z789");
}
#[test]
fn test_redact_cache_key_jwt() {
let result = redact_cache_key("jwt_token_abc");
assert_eq!(result, "****_abc");
}
#[test]
fn test_redact_cache_key_credential() {
let result = redact_cache_key("credential_123");
assert_eq!(result, "****_123");
}
#[test]
fn test_redact_cache_key_apikey() {
let result = redact_cache_key("apikey_test");
assert_eq!(result, "****test");
}
#[test]
fn test_redact_cache_key_api_key() {
let result = redact_cache_key("api_key_test");
assert_eq!(result, "****test");
}
#[test]
fn test_redact_cache_key_normal() {
let result = redact_cache_key("user_profile_123");
assert_eq!(result, "user_profile_123");
}
}