use std::sync::LazyLock;
use regex::Regex;
static API_KEY_PATTERNS: LazyLock<Vec<(&'static str, Regex)>> = LazyLock::new(|| {
vec![
("AWS Access Key", Regex::new(r"AKIA[A-Z0-9]{16}")
.expect("AWS access key pattern is a valid regex")),
("AWS Secret Key", Regex::new(r#"(?i)aws[_-]?secret[_-]?access[_-]?key\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?"#)
.expect("AWS secret key pattern is a valid regex")),
("GitHub Token", Regex::new(r"gh[pousr]_[A-Za-z0-9_]{36,}")
.expect("GitHub token pattern is a valid regex")),
("Slack Token", Regex::new(r"xox[baprs]-[0-9a-zA-Z-]+")
.expect("Slack token pattern is a valid regex")),
("Google API Key", Regex::new(r"AIza[0-9A-Za-z_-]{35}")
.expect("Google API key pattern is a valid regex")),
("Stripe Key", Regex::new(r"sk_(?:live|test)_[0-9a-zA-Z]{24,}")
.expect("Stripe key pattern is a valid regex")),
("Generic API Key", Regex::new(r#"(?i)(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[:=]\s*['"]?([A-Za-z0-9_-]{16,})['"]?"#)
.expect("Generic API key pattern is a valid regex")),
]
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiKeyType {
AwsAccessKey,
AwsSecretKey,
GitHubToken,
SlackToken,
GoogleApiKey,
StripeKey,
Generic,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ApiKeyMatch {
pub key_type: ApiKeyType,
pub start: usize,
pub end: usize,
pub text: String,
}
#[must_use]
pub fn detect(text: &str) -> Vec<ApiKeyMatch> {
let mut matches = Vec::new();
for (name, pattern) in API_KEY_PATTERNS.iter() {
for m in pattern.find_iter(text) {
let key_type = match *name {
"AWS Access Key" => ApiKeyType::AwsAccessKey,
"AWS Secret Key" => ApiKeyType::AwsSecretKey,
"GitHub Token" => ApiKeyType::GitHubToken,
"Slack Token" => ApiKeyType::SlackToken,
"Google API Key" => ApiKeyType::GoogleApiKey,
"Stripe Key" => ApiKeyType::StripeKey,
"Generic API Key" => ApiKeyType::Generic,
_ => ApiKeyType::Unknown,
};
matches.push(ApiKeyMatch {
key_type,
start: m.start(),
end: m.end(),
text: m.as_str().to_string(),
});
}
}
matches.sort_by_key(|m| m.start);
matches
}
#[must_use]
pub fn contains_api_key(text: &str) -> bool {
!detect(text).is_empty()
}
#[must_use]
pub fn mask(key: &str) -> String {
if key.len() <= 8 {
return "*".repeat(key.len());
}
format!("{}...{}", &key[..4], &key[key.len() - 4..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_aws_key() {
let matches = detect("Key: AKIAIOSFODNN7EXAMPLE");
assert!(!matches.is_empty());
assert_eq!(matches[0].key_type, ApiKeyType::AwsAccessKey);
}
#[test]
fn detect_github_token() {
let matches = detect("Token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1234");
assert!(!matches.is_empty());
assert_eq!(matches[0].key_type, ApiKeyType::GitHubToken);
}
#[test]
fn mask_key() {
assert_eq!(mask("AKIAIOSFODNN7EXAMPLE"), "AKIA...MPLE");
}
#[test]
fn contains_key() {
assert!(contains_api_key("api_key=my_secret_key_12345678"));
assert!(!contains_api_key("no keys here"));
}
}