use regex::Regex;
use std::sync::LazyLock;
static OPENAI_KEY_REGEX: LazyLock<Regex> = LazyLock::new(|| compile_regex(r"sk-[A-Za-z0-9_-]{16,}"));
static AWS_ACCESS_KEY_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| compile_regex(r"\bAKIA[0-9A-Z]{16}\b"));
static BEARER_TOKEN_REGEX: LazyLock<Regex> = LazyLock::new(|| compile_regex(r"(?i)\bBearer\s+[A-Za-z0-9.\-_]{16,}\b"));
static SECRET_ASSIGNMENT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
compile_regex(
r#"(?i)\b((?:[a-z0-9][a-z0-9_-]*?)?(?:api[\-_]?key|access[\-_]?key|client[\-_]?secret|credential|private[\-_]?key|token|secret|password|auth)[a-z0-9_-]*)\b(\s*[:=]\s*)(["']?)[^\s"']{8,}"#,
)
});
pub const PROVIDER_DIAGNOSTIC_MAX_BYTES: usize = 8 * 1024;
const PROVIDER_DIAGNOSTIC_TRUNCATION_MARKER: &str = "… [diagnostic truncated]";
pub fn redact_secrets(input: String) -> String {
let r1 = OPENAI_KEY_REGEX.replace_all(&input, "[REDACTED_SECRET]");
let r2 = AWS_ACCESS_KEY_ID_REGEX.replace_all(&r1, "[REDACTED_SECRET]");
let r3 = BEARER_TOKEN_REGEX.replace_all(&r2, "Bearer [REDACTED_SECRET]");
let r4 = SECRET_ASSIGNMENT_REGEX.replace_all(&r3, "$1$2$3[REDACTED_SECRET]");
r4.into_owned()
}
pub fn sanitize_provider_diagnostic(input: impl AsRef<[u8]>) -> String {
let input = input.as_ref();
let sample_len = input.len().min(PROVIDER_DIAGNOSTIC_MAX_BYTES + STREAMING_REDACTION_CARRY_BYTES);
let sample = String::from_utf8_lossy(input.get(..sample_len).unwrap_or(input));
let redacted = redact_secrets(sample.into_owned());
if redacted.len() <= PROVIDER_DIAGNOSTIC_MAX_BYTES {
return redacted;
}
let content_limit = PROVIDER_DIAGNOSTIC_MAX_BYTES.saturating_sub(PROVIDER_DIAGNOSTIC_TRUNCATION_MARKER.len());
let end = redacted.floor_char_boundary(content_limit);
format!("{}{}", redacted.get(..end).unwrap_or(&redacted), PROVIDER_DIAGNOSTIC_TRUNCATION_MARKER)
}
#[derive(Debug, Default)]
pub struct StreamingSecretRedactor {
pending: String,
}
const STREAMING_REDACTION_CARRY_BYTES: usize = 1_024;
impl StreamingSecretRedactor {
pub fn push(&mut self, chunk: &str) -> String {
self.pending.push_str(chunk);
if self.pending.len() <= STREAMING_REDACTION_CARRY_BYTES {
if !self.pending.contains('\n') {
return String::new();
}
}
let carry_split = self.pending.len().saturating_sub(STREAMING_REDACTION_CARRY_BYTES);
let line_split = self.pending.rfind('\n').map(|index| index + 1).unwrap_or(0);
let mut split_at = carry_split.max(line_split);
while split_at > 0 && !self.pending.is_char_boundary(split_at) {
split_at -= 1;
}
let prefix: String = self.pending.drain(..split_at).collect();
redact_secrets(prefix)
}
pub fn finish(self) -> String {
redact_secrets(self.pending)
}
}
#[allow(
clippy::panic,
reason = "Intentional compatibility, platform, or test-only suppression."
)]
fn compile_regex(pattern: &str) -> Regex {
match Regex::new(pattern) {
Ok(regex) => regex,
Err(err) => panic!("invalid regex pattern `{pattern}`: {err}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_regex() {
let _ = redact_secrets("test".to_string());
}
#[test]
fn redacts_openai_key() {
let input = "Found key: sk-test1234567890abcdefghij".to_string();
let output = redact_secrets(input);
assert_eq!(output, "Found key: [REDACTED_SECRET]");
}
#[test]
fn redacts_aws_access_key() {
let input = " creds: AKIAIOSFODNN7EXAMPLE ".to_string();
let output = redact_secrets(input);
assert_eq!(output, " creds: [REDACTED_SECRET] ");
}
#[test]
fn redacts_bearer_token() {
let input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9".to_string();
let output = redact_secrets(input);
assert_eq!(output, "Authorization: Bearer [REDACTED_SECRET]");
}
#[test]
fn redacts_api_key_assignment() {
let input = "api_key=sk-test12345678".to_string();
let output = redact_secrets(input);
assert_eq!(output, "api_key=[REDACTED_SECRET]");
}
#[test]
fn redacts_password_assignment() {
let input = "password: mysecretvalue".to_string();
let output = redact_secrets(input);
assert_eq!(output, "password: [REDACTED_SECRET]");
}
#[test]
fn redacts_token_in_quotes() {
let input = r#"token="abc123xyz789abcdef""#.to_string();
let output = redact_secrets(input);
assert_eq!(output, r#"token="[REDACTED_SECRET]""#);
}
#[test]
fn preserves_short_values() {
let input = "password: short".to_string();
let output = redact_secrets(input);
assert_eq!(output, "password: short");
}
#[test]
fn redacts_multiple_secrets() {
let input = "Keys: sk-test1234567890abcdefghij and AKIAIOSFODNN7EXAMPLE".to_string();
let output = redact_secrets(input);
assert!(output.contains("[REDACTED_SECRET]"));
assert!(!output.contains("AKIAIOSFODNN7EXAMPLE"));
assert!(!output.contains("sk-test1234567890abcdefghij"));
}
#[test]
fn preserves_non_secret_text() {
let input = "Hello world, this is normal text".to_string();
let output = redact_secrets(input);
assert_eq!(output, "Hello world, this is normal text");
}
#[test]
fn redacts_secrets_split_across_stream_chunks() {
let mut redactor = StreamingSecretRedactor::default();
let mut output = redactor.push("password=superse");
output.push_str(&redactor.push("cretvalue\n"));
output.push_str(&redactor.finish());
assert_eq!(output, "password=[REDACTED_SECRET]\n");
assert!(!output.contains("supersecretvalue"));
}
#[test]
fn provider_diagnostic_is_bounded_utf8_safe_and_redacted() {
let mut input = b"api_key=diagnostic-secret-value Bearer abcdefghijklmnop ".to_vec();
input.extend(std::iter::repeat_n(b'x', 20_000));
input.extend_from_slice("終端".as_bytes());
input.push(0xff);
let output = sanitize_provider_diagnostic(input);
assert!(output.len() <= PROVIDER_DIAGNOSTIC_MAX_BYTES);
assert!(output.is_char_boundary(output.len()));
assert!(!output.contains("diagnostic-secret-value"));
assert!(!output.contains("abcdefghijklmnop"));
}
}