use std::sync::RwLock;
use log::warn;
pub const MASK: &str = "********";
const MIN_SECRET_LEN: usize = 4;
static SECRETS: RwLock<Vec<String>> = RwLock::new(Vec::new());
pub fn register_secret(value: &str) {
if value.is_empty() {
return;
}
if value.len() < MIN_SECRET_LEN {
warn!(
"protected value is too short to mask reliably ({} chars), it will not be redacted from logs",
value.len()
);
return;
}
let mut candidates = vec![value.to_string()];
if let Ok(escaped) = serde_json::to_string(value) {
let inner = escaped.trim_matches('"');
if inner != value {
candidates.push(inner.to_string());
}
}
let mut secrets = SECRETS.write().unwrap();
for candidate in candidates {
if candidate.len() < MIN_SECRET_LEN || secrets.contains(&candidate) {
continue;
}
let pos = secrets
.iter()
.position(|s| s.len() < candidate.len())
.unwrap_or(secrets.len());
secrets.insert(pos, candidate);
}
}
pub fn redact(text: &str) -> String {
let secrets = SECRETS.read().unwrap();
if secrets.is_empty() {
return text.to_string();
}
let mut out = text.to_string();
for secret in secrets.iter() {
if out.contains(secret.as_str()) {
out = out.replace(secret.as_str(), MASK);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_masks_registered_value() {
register_secret("Sup3r-S3cret-Passw0rd");
let out = redact("MasterUserPassword = 'Sup3r-S3cret-Passw0rd',");
assert_eq!(out, format!("MasterUserPassword = '{}',", MASK));
}
#[test]
fn test_redact_masks_multiple_occurrences() {
register_secret("mult1-Occurrence-secret");
let out = redact("a mult1-Occurrence-secret b mult1-Occurrence-secret c");
assert_eq!(out, format!("a {} b {} c", MASK, MASK));
}
#[test]
fn test_redact_leaves_other_text_alone() {
register_secret("unrelated-secret-xyz123");
let out = redact("no secrets here");
assert_eq!(out, "no secrets here");
}
#[test]
fn test_short_values_not_registered() {
register_secret("ab1");
let out = redact("value is ab1");
assert_eq!(out, "value is ab1");
}
#[test]
fn test_empty_value_not_registered() {
register_secret("");
let out = redact("some text");
assert_eq!(out, "some text");
}
#[test]
fn test_overlapping_secrets_longest_first() {
register_secret("overlap-secret");
register_secret("overlap-secret-longer-form");
let out = redact("x overlap-secret-longer-form y");
assert_eq!(out, format!("x {} y", MASK));
}
#[test]
fn test_json_escaped_form_registered() {
register_secret(r#"pa"ss\word-with-specials"#);
let json = serde_json::to_string(&serde_json::json!({
"password": r#"pa"ss\word-with-specials"#
}))
.unwrap();
let out = redact(&json);
assert!(
!out.contains("word-with-specials"),
"escaped secret leaked: {}",
out
);
}
#[test]
fn test_duplicate_registration_is_idempotent() {
register_secret("dup-registration-secret");
register_secret("dup-registration-secret");
let out = redact("dup-registration-secret");
assert_eq!(out, MASK);
}
}