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");
}
}