use std::borrow::Cow;
use std::sync::LazyLock;
use regex::Regex;
use zeph_common::secrets::{
AWS_SECRET_KEY_PATTERN, BEARER_TOKEN_PATTERN, JWT_PATTERN, PEM_PRIVATE_KEY_PATTERN,
PEM_PRIVATE_KEY_UNTERMINATED_PATTERN, SECRET_PREFIXES,
};
static SECRET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
let pattern = SECRET_PREFIXES
.iter()
.map(|p| regex::escape(p))
.collect::<Vec<_>>()
.join("|");
let full = format!("(?:{pattern})[^\\s\"'`,;{{}}\\[\\]]*");
Regex::new(&full).expect("secret shape regex is valid")
});
static BEARER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(BEARER_TOKEN_PATTERN).expect("bearer shape regex is valid"));
static JWT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(JWT_PATTERN).expect("jwt shape regex is valid"));
static PEM_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(PEM_PRIVATE_KEY_PATTERN).expect("pem shape regex is valid"));
static PEM_UNTERMINATED_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(PEM_PRIVATE_KEY_UNTERMINATED_PATTERN).expect("pem unterminated shape regex is valid")
});
static AWS_SECRET_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(AWS_SECRET_KEY_PATTERN).expect("aws secret shape regex is valid"));
#[must_use]
pub fn scrub_secret_shapes(text: &str) -> Cow<'_, str> {
let after_pem: Cow<'_, str> = PEM_REGEX.replace_all(text, "[REDACTED_PEM_KEY]");
let after_pem_fallback: Cow<'_, str> =
match PEM_UNTERMINATED_REGEX.replace_all(after_pem.as_ref(), "[REDACTED_PEM_KEY]") {
Cow::Borrowed(_) => after_pem,
Cow::Owned(s) => Cow::Owned(s),
};
let has_prefix_match = SECRET_PREFIXES
.iter()
.any(|p| after_pem_fallback.contains(*p));
let after_prefixes: Cow<'_, str> = if has_prefix_match {
match SECRET_REGEX.replace_all(after_pem_fallback.as_ref(), "[REDACTED]") {
Cow::Borrowed(_) => after_pem_fallback,
Cow::Owned(s) => Cow::Owned(s),
}
} else {
after_pem_fallback
};
let after_bearer: Cow<'_, str> =
match BEARER_REGEX.replace_all(after_prefixes.as_ref(), "${1}[REDACTED]") {
Cow::Borrowed(_) => after_prefixes,
Cow::Owned(s) => Cow::Owned(s),
};
let after_jwt: Cow<'_, str> =
match JWT_REGEX.replace_all(after_bearer.as_ref(), "[REDACTED_JWT]") {
Cow::Borrowed(_) => after_bearer,
Cow::Owned(s) => Cow::Owned(s),
};
match AWS_SECRET_REGEX.replace_all(after_jwt.as_ref(), "${1}[REDACTED]${2}") {
Cow::Borrowed(_) => after_jwt,
Cow::Owned(s) => Cow::Owned(s),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redacts_generic_api_key_shape() {
let text = "the key is sk-test-abc123def456, use it wisely";
let result = scrub_secret_shapes(text);
assert!(!result.contains("sk-test-abc123def456"));
assert!(result.contains("[REDACTED]"));
}
#[test]
fn redacts_bearer_token() {
let result =
scrub_secret_shapes("Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.payload.signature");
assert!(result.contains("[REDACTED]"));
assert!(!result.contains("eyJhbGciOiJSUzI1NiJ9"));
assert!(result.contains("Authorization:"));
}
#[test]
fn redacts_standalone_jwt() {
let jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.SflKxwRJSMeKKF2";
let text = format!("token value: {jwt} was found");
let result = scrub_secret_shapes(&text);
assert!(result.contains("[REDACTED_JWT]"));
assert!(!result.contains("eyJhbGci"));
}
#[test]
fn preserves_normal_text() {
let text = "This is a normal response with no secrets";
let result = scrub_secret_shapes(text);
assert_eq!(result, text);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn all_secret_prefixes_detected() {
for prefix in SECRET_PREFIXES {
let text = format!("token: {prefix}abc123");
let result = scrub_secret_shapes(&text);
assert!(result.contains("[REDACTED]"), "failed for prefix: {prefix}");
assert!(!result.contains(*prefix), "prefix not redacted: {prefix}");
}
}
#[test]
fn redacts_full_pem_block() {
let text = "here is my key:\n-----BEGIN RSA PRIVATE KEY-----\nMIIBVQIBADANBgkqhkiG9w0B\nAQEFAASCAT8wggE7AgEAAkEA\n-----END RSA PRIVATE KEY-----\nthanks";
let result = scrub_secret_shapes(text);
assert!(result.contains("[REDACTED_PEM_KEY]"));
assert!(!result.contains("MIIBVQIBADANBgkqhkiG9w0B"));
assert!(result.contains("here is my key:"));
assert!(result.contains("thanks"));
}
#[test]
fn redacts_multiple_pem_blocks_independently() {
let text = "-----BEGIN PRIVATE KEY-----\nfirstbody\n-----END PRIVATE KEY-----\nsome text in between\n-----BEGIN EC PRIVATE KEY-----\nsecondbody\n-----END EC PRIVATE KEY-----";
let result = scrub_secret_shapes(text);
assert_eq!(result.matches("[REDACTED_PEM_KEY]").count(), 2);
assert!(!result.contains("firstbody"));
assert!(!result.contains("secondbody"));
assert!(result.contains("some text in between"));
}
#[test]
fn no_false_positive_on_bare_begin_or_private_words() {
let text = "Let's BEGIN the PRIVATE discussion about keys without any PEM markers";
let result = scrub_secret_shapes(text);
assert_eq!(result, text);
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn redacts_pem_block_with_mismatched_header_footer_labels() {
let text = "-----BEGIN RSA PRIVATE KEY-----\nbody-material\n-----END EC PRIVATE KEY-----";
let result = scrub_secret_shapes(text);
assert!(result.contains("[REDACTED_PEM_KEY]"));
assert!(!result.contains("body-material"));
}
#[test]
fn redacts_footerless_pem_header_via_fallback() {
let text = "here is my key:\n-----BEGIN RSA PRIVATE KEY-----\nMIIBVQIBADANBgkqhkiG9w0B\nAQEFAASCAT8wggE7AgEAAkEA";
let result = scrub_secret_shapes(text);
assert!(result.contains("[REDACTED_PEM_KEY]"));
assert!(!result.contains("MIIBVQIBADANBgkqhkiG9w0B"));
assert!(!result.contains("-----BEGIN"));
assert!(result.contains("here is my key:"));
}
#[test]
fn adversarial_repeated_unterminated_pem_headers_are_bounded() {
use zeph_common::secrets::PEM_BODY_CAP;
let huge_body = "A".repeat(50_000);
let text = format!(
"-----BEGIN RSA PRIVATE KEY-----\n{huge_body}\n-----BEGIN EC PRIVATE KEY-----\n{huge_body}"
);
let result = scrub_secret_shapes(&text);
assert_eq!(result.matches("[REDACTED_PEM_KEY]").count(), 2);
let remaining_as = result.matches('A').count();
assert!(
remaining_as > huge_body.len() - PEM_BODY_CAP,
"expected most of the oversized body to remain visible past the cap, got only \
{remaining_as} 'A' chars remaining"
);
}
#[test]
fn redacts_marker_anchored_aws_secret_key() {
let text = "aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
let result = scrub_secret_shapes(text);
assert!(!result.contains("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"));
assert!(result.contains("[REDACTED]"));
assert!(result.contains("aws_secret_access_key="));
}
#[test]
fn preserves_unanchored_high_entropy_string() {
let text = "build hash: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY was produced";
let result = scrub_secret_shapes(text);
assert_eq!(result, text);
}
}