use sha2::{Digest, Sha256};
pub(crate) fn redact_secret(plaintext: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(plaintext.as_bytes());
let digest = hasher.finalize();
let hex = digest
.iter()
.take(8)
.map(|b| format!("{b:02x}"))
.collect::<String>();
format!("sha256:{hex}...truncated")
}
pub(crate) const SECRET_FIELD_TYPES: &[&str] = &[
"password",
"secret",
"token",
"api_key",
"private_key",
"encryption_key",
];
pub(crate) fn is_secret_field_type(declared_type: &str) -> bool {
SECRET_FIELD_TYPES.contains(&declared_type)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redact_is_deterministic() {
assert_eq!(redact_secret("hunter2"), redact_secret("hunter2"));
}
#[test]
fn redact_changes_with_input() {
assert_ne!(redact_secret("hunter2"), redact_secret("hunter3"));
}
#[test]
fn redact_format_matches_design() {
let out = redact_secret("any-token-value");
assert!(out.starts_with("sha256:"), "format prefix: {out}");
assert!(out.ends_with("...truncated"), "format suffix: {out}");
assert_eq!(out.len(), 35);
}
#[test]
fn no_4char_input_substring_leaks() {
let corpus = [
"hunter2",
"correct horse battery staple",
"sk-live-1234567890abcdef",
"Bearer eyJhbGciOiJIUzI1NiJ9",
"اختبار-سري", "🔒-emoji-secret-🔑", "line\nwith\nnewlines",
"tab\there\ttoo",
"", "a", "abc", ];
for input in corpus {
let out = redact_secret(input);
let chars: Vec<char> = input.chars().collect();
for window in chars.windows(4) {
let needle: String = window.iter().collect();
assert!(
!out.contains(&needle),
"redacted output {out:?} leaks 4-char substring {needle:?} from input {input:?}",
);
}
}
}
#[test]
fn secret_field_categories_are_closed() {
assert_eq!(
SECRET_FIELD_TYPES,
&[
"password",
"secret",
"token",
"api_key",
"private_key",
"encryption_key",
]
);
}
#[test]
fn is_secret_field_type_detects_category() {
assert!(is_secret_field_type("password"));
assert!(is_secret_field_type("api_key"));
assert!(!is_secret_field_type("text"));
assert!(!is_secret_field_type("integer"));
assert!(!is_secret_field_type("Password")); }
}