const ISSUER_PREFIXES: &[&str] = &[
"sk-",
"sk_live_",
"sk_test_",
"rk_live_",
"rk_test_",
"shpat_",
"shpss_",
"ghp_",
"gho_",
"ghu_",
"ghs_",
"ghr_",
"github_pat_",
"glpat-",
"gldt-",
"xoxb-",
"xoxa-",
"xoxp-",
"xoxr-",
"xoxs-",
"xapp-",
"npm_",
"dop_v1_",
"doo_v1_",
"hf_",
"AKIA",
"ASIA",
"AIza",
"ya29.",
];
const OPAQUE_MIN_LEN: usize = 16;
pub(super) fn is_credential_shaped(value: &str) -> bool {
let value = value.trim().trim_matches(['"', '\'']);
if value.is_empty() || value == super::REDACTED {
return false;
}
if has_issuer_prefix(value) || is_jwt(value) {
return true;
}
is_opaque_run(value)
}
pub(super) fn has_issuer_prefix(value: &str) -> bool {
ISSUER_PREFIXES.iter().any(|prefix| {
value.len() >= prefix.len() + 8
&& value.starts_with(prefix)
&& value[prefix.len()..].bytes().all(is_token_byte)
})
}
pub(super) fn issuer_prefix_start(token: &str) -> Option<usize> {
let bytes = token.as_bytes();
ISSUER_PREFIXES
.iter()
.filter_map(|prefix| {
token.match_indices(prefix).find_map(|(at, _)| {
let starts_a_word = at == 0 || !bytes[at - 1].is_ascii_alphanumeric();
(starts_a_word && has_issuer_prefix(&token[at..])).then_some(at)
})
})
.min()
}
pub(super) fn is_jwt(value: &str) -> bool {
let mut parts = value.split('.');
let (Some(header), Some(payload), Some(signature), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
header.starts_with("eyJ")
&& [header, payload, signature]
.iter()
.all(|p| p.len() >= 8 && p.bytes().all(is_base64url_byte))
}
fn is_opaque_run(value: &str) -> bool {
value.len() >= OPAQUE_MIN_LEN
&& value.bytes().all(is_token_byte)
&& value.bytes().any(|b| b.is_ascii_alphabetic())
&& value.bytes().any(|b| b.is_ascii_digit())
}
fn is_token_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'/' | b'=' | b'~')
}
fn is_base64url_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'=')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vendor_prefixed_tokens_are_credentials() {
assert!(is_credential_shaped("sk-azure-abcdef123456"));
assert!(is_credential_shaped(
"ghp_abcdefghijklmnopqrstuvwxyz0123456789"
));
assert!(is_credential_shaped("AKIAIOSFODNN7EXAMPLE"));
assert!(is_credential_shaped("\"sk-live-4242deadbeef\""));
}
#[test]
fn jwts_are_credentials() {
assert!(is_credential_shaped(
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVPmB92K27uhbUJU1p1r_wW1g"
));
assert!(!is_jwt("a.b.c"));
assert!(!is_jwt("not.a.jwt.at.all"));
}
#[test]
fn diagnostic_values_are_not_credentials() {
assert!(!is_credential_shaped("kind=0x09"));
assert!(!is_credential_shaped("page_id"));
assert!(!is_credential_shaped("7"));
assert!(!is_credential_shaped("none"));
assert!(!is_credential_shaped("oauth2"));
assert!(!is_credential_shaped("a long human sentence"));
assert!(!is_credential_shaped(""));
assert!(!is_credential_shaped(super::super::REDACTED));
}
#[test]
fn opaque_runs_need_both_letters_and_digits_and_length() {
assert!(is_credential_shaped("A1b2C3d4E5f6G7h8"));
assert!(!is_credential_shaped("abcdefghijklmnop"));
assert!(!is_credential_shaped("a1b2c3d4"));
}
}