pub const ENTROPY_THRESHOLD: f64 = 4.5;
pub const MIN_ENTROPY_TOKEN_LEN: usize = 20;
pub fn calculate_entropy(bytes: &[u8]) -> f64 {
if bytes.is_empty() {
return 0.0;
}
let mut counts = [0u32; 256];
for &b in bytes {
counts[b as usize] += 1;
}
let len_f = bytes.len() as f64;
counts
.iter()
.copied()
.filter(|&c| c > 0)
.map(|c| {
let p = (c as f64) / len_f;
-p * p.log2()
})
.sum()
}
pub fn is_entropy_masked(token: &[u8]) -> bool {
if token.len() < MIN_ENTROPY_TOKEN_LEN {
return false;
}
if is_whitelisted_hash_or_pattern(token) {
return false;
}
calculate_entropy(token) > ENTROPY_THRESHOLD
}
#[inline]
pub fn is_entropy_token_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'+' | b'/')
}
fn is_whitelisted_hash_or_pattern(token: &[u8]) -> bool {
let is_pure_hex = token.iter().all(|&b| b.is_ascii_hexdigit());
if is_pure_hex && matches!(token.len(), 32 | 40 | 64 | 128) {
return true;
}
if token.len() == 36 && is_uuid_format(token) {
return true;
}
let is_pure_digits = token.iter().all(|&b| b.is_ascii_digit());
if is_pure_digits {
return true;
}
let is_pure_lowercase = token.iter().all(|&b| b.is_ascii_lowercase());
if is_pure_lowercase && calculate_entropy(token) < 4.6 {
return true;
}
false
}
fn is_uuid_format(token: &[u8]) -> bool {
if token.len() != 36 {
return false;
}
for (i, &b) in token.iter().enumerate() {
if matches!(i, 8 | 13 | 18 | 23) {
if b != b'-' {
return false;
}
} else if !b.is_ascii_hexdigit() {
return false;
}
}
true
}
pub fn mask_high_entropy_tokens(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0usize;
while i < bytes.len() {
if is_entropy_token_char(bytes[i]) {
let start = i;
while i < bytes.len() && is_entropy_token_char(bytes[i]) {
i += 1;
}
let candidate = &bytes[start..i];
if is_entropy_masked(candidate) {
out.push_str("[REDACTED_HIGH_ENTROPY]");
} else {
out.push_str(&s[start..i]);
}
continue;
}
let step = utf8_char_len(bytes[i]);
let end = (i + step).min(s.len());
out.push_str(&s[i..end]);
i = end;
}
out
}
pub fn mask_high_entropy_pad(bytes: &mut [u8]) {
let mut i = 0usize;
while i < bytes.len() {
if is_entropy_token_char(bytes[i]) {
let start = i;
while i < bytes.len() && is_entropy_token_char(bytes[i]) {
i += 1;
}
let candidate = &bytes[start..i];
if is_entropy_masked(candidate) {
for b in &mut bytes[start..i] {
*b = b'*';
}
}
continue;
}
i += 1;
}
}
fn utf8_char_len(b: u8) -> usize {
if b < 0x80 {
1
} else if b >> 5 == 0b110 {
2
} else if b >> 4 == 0b1110 {
3
} else if b >> 3 == 0b11110 {
4
} else {
1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shannon_entropy_calculation() {
let hex = b"0123456789abcdef";
let h = calculate_entropy(hex);
assert!((h - 4.0).abs() < 1e-6);
let b64 = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let h_b64 = calculate_entropy(b64);
assert!((h_b64 - 6.0).abs() < 1e-6);
}
#[test]
fn test_high_entropy_masking() {
let token = "aB39zKmP2qL8vX1yR4wT7jN_xY9ZaBc";
assert!(is_entropy_masked(token.as_bytes()));
let text = format!("key={token} ordinary");
let masked = mask_high_entropy_tokens(&text);
assert_eq!(masked, "key=[REDACTED_HIGH_ENTROPY] ordinary");
}
#[test]
fn test_whitelist_suppression() {
let git_sha = "e0d123456789abcdef0123456789abcdef012345";
assert!(!is_entropy_masked(git_sha.as_bytes()));
let uuid = "123e4567-e89b-12d3-a456-426614174000";
assert!(!is_entropy_masked(uuid.as_bytes()));
}
}