#[must_use]
pub fn constant_time_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[must_use]
pub fn redact(value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
if chars.len() <= 8 {
return "****".to_string();
}
format!(
"****{}",
chars[chars.len() - 4..].iter().collect::<String>()
)
}
#[must_use]
pub fn is_secret_header(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
["auth", "key", "token", "secret", "cookie", "credential"]
.iter()
.any(|hint| lower.contains(hint))
}
const CHILD_ENV_ALLOWLIST: &[&str] = &[
"PATH",
"HOME",
"SHELL",
"USER",
"LOGNAME",
"TMPDIR",
"TEMP",
"TMP",
"LANG",
"LANGUAGE",
"TERM",
"TZ",
"COLORTERM",
"NO_COLOR",
"SYSTEMROOT",
"WINDIR",
"COMSPEC",
"PATHEXT",
"APPDATA",
"LOCALAPPDATA",
"PROGRAMFILES",
"PROGRAMDATA",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"NUMBER_OF_PROCESSORS",
"PROCESSOR_ARCHITECTURE",
"OS",
];
pub fn child_env_allowed(name: &str) -> bool {
CHILD_ENV_ALLOWLIST
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(name))
}
const SECRET_NAME_HINTS: &[&str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"PASSPHRASE",
"CREDENTIAL",
"APIKEY",
"API_KEY",
"ACCESS_KEY",
"PRIVATE_KEY",
"KEY",
"_PAT",
"SESSION",
"COOKIE",
"AUTH",
"BEARER",
"SIGNATURE",
"SIGNING",
"DSN",
"NETRC",
"KUBECONFIG",
];
const SECRET_NAME_EXACT: &[&str] = &[
"DATABASE_URL",
"DATABASE_DSN",
"REDIS_URL",
"MONGO_URL",
"MONGODB_URI",
"AMQP_URL",
"SSH_AUTH_SOCK",
];
const SECRET_NAME_PREFIXES: &[&str] = &[
"LEVIATH_", "AWS_", "AZURE_", "GOOGLE_", "GCP_",
];
pub fn is_sensitive_env_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
SECRET_NAME_HINTS.iter().any(|h| upper.contains(h))
|| SECRET_NAME_EXACT.iter().any(|e| upper == *e)
|| SECRET_NAME_PREFIXES.iter().any(|p| upper.starts_with(p))
}
pub fn script_env_allowed(name: &str, allowlist: &[String]) -> bool {
!is_sensitive_env_name(name)
|| allowlist
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_time_eq_matches_ordinary_equality() {
assert!(constant_time_eq("secret", "secret"));
assert!(constant_time_eq("", ""));
assert!(!constant_time_eq("secret", "secreu"));
assert!(!constant_time_eq("Xecret", "secret"));
assert!(!constant_time_eq("secreX", "secret"));
assert!(!constant_time_eq("secret", "secretx"));
assert!(!constant_time_eq("secretx", "secret"));
}
#[test]
fn redact_keeps_only_a_short_suffix() {
assert_eq!(redact("sk-ant-api-key-12345"), "****2345");
assert!(!redact("sk-ant-api-key-12345").contains("sk-ant"));
assert!(!redact("ghp_realgithubtoken").contains("ghp_"));
}
#[test]
fn redact_hides_short_values_completely() {
for value in ["", "a", "abcd", "12345678"] {
assert_eq!(redact(value), "****", "{value:?}");
}
}
#[test]
fn redact_counts_characters_not_bytes() {
assert_eq!(redact("日本語日本語日本語"), "****語日本語");
assert_eq!(redact("日本"), "****");
}
#[test]
fn secret_headers_are_matched_by_shape_not_an_exact_list() {
for name in [
"authorization",
"Authorization",
"x-api-key",
"api-key",
"x-goog-api-key",
"proxy-authorization",
"cookie",
"set-cookie",
"x-auth-token",
"x-amz-security-token",
] {
assert!(is_secret_header(name), "{name} must be redacted");
}
}
#[test]
fn ordinary_headers_are_not_redacted() {
for name in [
"content-type",
"user-agent",
"accept",
"content-length",
"anthropic-version",
] {
assert!(!is_secret_header(name), "{name} should log verbatim");
}
}
#[test]
fn catches_what_the_old_denylist_missed() {
for name in [
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"GITHUB_TOKEN",
"GH_TOKEN",
"NPM_TOKEN",
"HF_TOKEN",
"SLACK_TOKEN",
"DATABASE_URL",
"LEVIATH_API_TOKEN",
"SSH_AUTH_SOCK",
] {
assert!(is_sensitive_env_name(name), "{name} should be sensitive");
assert!(!child_env_allowed(name), "{name} must not reach a child");
}
}
#[test]
fn catches_the_provider_keys() {
for name in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GOOGLE_API_KEY",
"OPENROUTER_API_KEY",
] {
assert!(is_sensitive_env_name(name), "{name}");
}
}
#[test]
fn common_secret_names_are_all_recognised() {
for name in [
"OPENAI_KEY",
"ENCRYPTION_KEY",
"MASTER_KEY",
"DEPLOY_KEY",
"ANTHROPIC_API_KEY",
"AWS_SECRET_ACCESS_KEY",
"GITHUB_TOKEN",
"GITHUB_PAT",
"SENTRY_DSN",
"NETRC",
"KUBECONFIG",
"npm_password",
] {
assert!(
is_sensitive_env_name(name),
"{name} must be treated as a secret"
);
}
for name in [
"PATH",
"HOME",
"LANG",
"TERM",
"TZ",
"EDITOR",
"OLLAMA_HOST",
] {
assert!(!is_sensitive_env_name(name), "{name} is not a secret");
}
}
#[test]
fn matching_is_case_insensitive() {
assert!(is_sensitive_env_name("github_token"));
assert!(is_sensitive_env_name("MyApp_Password"));
assert!(child_env_allowed("path"));
assert!(child_env_allowed("Path"));
}
#[test]
fn ordinary_names_are_not_sensitive() {
for name in ["PATH", "HOME", "TZ", "TERM", "EDITOR", "MY_APP_REGION"] {
assert!(!is_sensitive_env_name(name), "{name}");
}
}
#[test]
fn child_allowlist_is_the_short_list_not_the_environment() {
assert!(child_env_allowed("PATH"));
assert!(child_env_allowed("LANG"));
assert!(!child_env_allowed("MY_APP_REGION"));
assert!(!child_env_allowed("EDITOR"));
}
#[test]
fn script_reads_pass_unless_credential_shaped() {
let none: &[String] = &[];
assert!(script_env_allowed("PATH", none));
assert!(script_env_allowed("MY_APP_REGION", none));
assert!(!script_env_allowed("ANTHROPIC_API_KEY", none));
}
#[test]
fn allowlisting_a_credential_permits_exactly_that_one() {
let allow = vec!["MY_PROVIDER_KEY".to_string()];
assert!(script_env_allowed("MY_PROVIDER_KEY", &allow));
assert!(script_env_allowed("my_provider_key", &allow), "case");
assert!(!script_env_allowed("ANTHROPIC_API_KEY", &allow));
assert!(!script_env_allowed("ANTHROPIC_API_KEY", &["*".to_string()]));
}
}