use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
static SENSITIVE_FIELDS: &[&str] = &[
"password",
"token",
"secret",
"key",
"credential",
"auth",
"api_key",
"api_key_id",
"api_secret",
"access_key",
"access_key_id",
"secret_key",
"private_key",
"public_key",
"encryption_key",
"decryption_key",
"master_key",
"session_key",
"oauth",
"oauth_token",
"oauth_secret",
"bearer",
"bearer_token",
"jwt",
"session_id",
"session_token",
"aws_secret",
"aws_key",
"aws_token",
"aws_credentials",
"database_url",
"db_password",
"db_user",
"connection_string",
"credit_card",
"card_number",
"cvv",
"ssn",
"social_security",
"client_secret",
"client_id",
"refresh_token",
"pin",
"pin_code",
"two_factor",
"totp",
"backup_code",
"recovery_code",
];
#[derive(Debug, Clone, Default)]
pub struct DataMasker {
rules: Vec<MaskRule>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MaskRule {
name: String,
pattern: Regex,
replacement: String,
replace_count: usize,
}
impl DataMasker {
pub fn new() -> Self {
let rules = vec![
MaskRule::new_email_rule(),
MaskRule::new_phone_rule(),
MaskRule::new_id_card_rule(),
MaskRule::new_bank_card_rule(),
MaskRule::new_api_key_rule(),
MaskRule::new_aws_key_rule(),
MaskRule::new_jwt_rule(),
MaskRule::new_generic_secret_rule(),
];
Self { rules }
}
pub fn is_sensitive_field(field_name: &str) -> bool {
let lower_name = field_name.to_lowercase();
SENSITIVE_FIELDS
.iter()
.any(|sensitive| lower_name.contains(*sensitive))
}
pub fn mask(&self, text: &str) -> String {
let mut result = text.to_string();
for rule in &self.rules {
result = rule.apply(&result);
}
result
}
pub fn mask_value(&self, value: &mut Value) {
match value {
Value::String(s) => {
*s = self.mask(s);
}
Value::Array(arr) => {
for item in arr {
self.mask_value(item);
}
}
Value::Object(map) => {
for (_, v) in map {
self.mask_value(v);
}
}
_ => {}
}
}
pub fn mask_hashmap(&self, map: &mut HashMap<String, Value>) {
for (_, v) in map.iter_mut() {
self.mask_value(v);
}
}
}
use std::sync::LazyLock;
static EMAIL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+").expect("Invalid email regex"));
static PHONE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\b1[3-9]\d{9}\b").expect("Invalid phone regex"));
static ID_CARD_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\d{6})(\d{8})(\d{3}[\dX])$").expect("Invalid ID card regex"));
static BANK_CARD_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(\d{4})(\d+)(\d{4})").expect("Invalid bank card regex"));
static API_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(api[_-]?key[^\s:=]*\s*[=:]\s*[a-zA-Z0-9_-]{20,})")
.expect("Invalid API key regex")
});
static AWS_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}").expect("Invalid AWS key regex")
});
static JWT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*")
.expect("Invalid JWT regex")
});
static GENERIC_SECRET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)([^\s:=]*(?:token|secret|key|password|passwd|pwd|credential)s?[^\s:=]*\s*[=:]\s*)([a-zA-Z0-9_\-\+]{16,})")
.expect("Invalid generic secret regex")
});
impl MaskRule {
fn new_email_rule() -> Self {
Self {
name: "email".to_string(),
pattern: EMAIL_REGEX.clone(),
replacement: "**@**.***".to_string(),
replace_count: 1,
}
}
fn new_phone_rule() -> Self {
Self {
name: "phone".to_string(),
pattern: PHONE_REGEX.clone(),
replacement: "***-****-****".to_string(),
replace_count: 1,
}
}
fn new_id_card_rule() -> Self {
Self {
name: "id_card".to_string(),
pattern: ID_CARD_REGEX.clone(),
replacement: "MASK_ID_CARD".to_string(), replace_count: 1,
}
}
fn new_bank_card_rule() -> Self {
Self {
name: "bank_card".to_string(),
pattern: BANK_CARD_REGEX.clone(),
replacement: "MASK_BANK_CARD".to_string(), replace_count: 1,
}
}
fn new_api_key_rule() -> Self {
Self {
name: "api_key".to_string(),
pattern: API_KEY_REGEX.clone(),
replacement: "${1}***REDACTED***${3}".to_string(),
replace_count: 1,
}
}
fn new_aws_key_rule() -> Self {
Self {
name: "aws_key".to_string(),
pattern: AWS_KEY_REGEX.clone(),
replacement: "***REDACTED***".to_string(),
replace_count: 1,
}
}
fn new_jwt_rule() -> Self {
Self {
name: "jwt".to_string(),
pattern: JWT_REGEX.clone(),
replacement: "***REDACTED_JWT***".to_string(),
replace_count: 1,
}
}
fn new_generic_secret_rule() -> Self {
Self {
name: "generic_secret".to_string(),
pattern: GENERIC_SECRET_REGEX.clone(),
replacement: "${1}***REDACTED***${3}".to_string(),
replace_count: 1,
}
}
fn apply(&self, text: &str) -> String {
if self.name == "id_card" {
self.pattern.replace(text, "******$3").to_string()
} else if self.name == "bank_card" {
if text.len() >= 12 && text.chars().all(|c| c.is_ascii_digit()) {
let last_four = &text[text.len() - 4..];
format!("****-****-****-{}", last_four)
} else {
text.to_string()
}
} else if self.name == "api_key" || self.name == "generic_secret" {
self.pattern
.replace(text, self.replacement.as_str())
.to_string()
} else {
self.pattern
.replace(text, self.replacement.as_str())
.to_string()
}
}
}
pub fn mask_email(email: &str) -> String {
EMAIL_REGEX.replace(email, "**@**.***").to_string()
}
pub fn mask_phone(phone: &str) -> String {
PHONE_REGEX.replace(phone, "***-****-****").to_string()
}
#[allow(dead_code)]
fn mask_id_card(id_card: &str) -> String {
ID_CARD_REGEX
.replace(id_card, |caps: ®ex::Captures| {
let suffix = caps.get(3).map(|m| m.as_str()).unwrap_or("");
format!("******{}", suffix)
})
.to_string()
}
#[allow(dead_code)]
fn mask_bank_card(bank_card: &str) -> String {
if bank_card.len() > 4 {
let last_four = &bank_card[bank_card.len() - 4..];
format!("****-****-****-{}", last_four)
} else {
bank_card.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_email() {
let test_cases = vec![
("test@example.com", "**@**.***"),
("user.name@company.co.uk", "**@**.***"),
("admin@localhost", "**@**.***"),
];
for (input, expected) in test_cases {
let result = mask_email(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_phone() {
let test_cases = vec![
("13812345678", "***-****-****"),
("15987654321", "***-****-****"),
("Contact: 18655556666 now", "Contact: ***-****-**** now"),
];
for (input, expected) in test_cases {
let result = mask_phone(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_id_card() {
let test_cases = vec![
("110101199001011234", "******1234"),
("31011519880530218X", "******218X"),
];
for (input, expected) in test_cases {
let result = mask_id_card(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_mask_bank_card() {
let test_cases = vec![
("6222021234567890123", "****-****-****-0123"),
("4567890123456789", "****-****-****-6789"),
];
for (input, expected) in test_cases {
let result = mask_bank_card(input);
assert_eq!(result, expected, "Failed for: {}", input);
}
}
#[test]
fn test_data_masker() {
let masker = DataMasker::new();
let test_email = "user@example.com";
assert_eq!(masker.mask(test_email), "**@**.***");
let test_phone = "13912345678";
assert_eq!(masker.mask(test_phone), "***-****-****");
let mixed = "Contact user at test@example.com, phone: 13812345678";
let result = masker.mask(mixed);
assert!(!result.contains("test@example.com"));
assert!(!result.contains("13812345678"));
}
#[test]
fn test_mask_value() {
let masker = DataMasker::new();
let mut value = serde_json::json!({
"email": "user@example.com",
"phone": "13712345678",
"name": "John"
});
masker.mask_value(&mut value);
assert_eq!(value["email"], "**@**.***");
assert_eq!(value["phone"], "***-****-****");
assert_eq!(value["name"], "John");
}
#[test]
fn test_mask_nested_value() {
let masker = DataMasker::new();
let mut value = serde_json::json!({
"user": {
"email": "admin@company.org",
"contacts": ["test@email.com", "13811112222"]
}
});
masker.mask_value(&mut value);
let user = &value["user"];
assert_eq!(user["email"], "**@**.***");
let contacts = user["contacts"]
.as_array()
.expect("contacts should be an array");
assert_eq!(contacts[0], "**@**.***");
assert_eq!(contacts[1], "***-****-****");
}
}