pub(crate) const RFC7519_EXAMPLE_JWT_PREFIX: &str =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkw";
pub(crate) fn looks_like_dashed_serial_key(credential: &str) -> bool {
if credential.len() != 29 {
return false;
}
let parts: Vec<&str> = credential.split('-').collect();
if parts.len() != 5 {
return false;
}
parts
.iter()
.all(|p| p.len() == 5 && p.chars().all(|c| c.is_ascii_alphanumeric()))
}
#[allow(dead_code)]
pub(crate) fn looks_like_pure_hash_digest_or_uuid(credential: &str) -> bool {
is_uuid_v4_shape(credential) || looks_like_hash_digest(credential)
}
pub(crate) fn looks_like_hash_digest(credential: &str) -> bool {
looks_like_prefixed_hash_digest(credential) || looks_like_bare_hex_digest(credential)
}
pub(crate) fn looks_like_prefixed_hash_digest(credential: &str) -> bool {
if let Some(body) = strip_hash_algo_prefix(credential) {
if body.len() == 64 && is_uniform_hex(body) {
return true;
}
if body.len() == 128 && is_uniform_hex(body) {
return true;
}
if body.len() == 40 && is_uniform_hex(body) {
return true;
}
if looks_like_base64_blob_with_padding(body) {
return true;
}
}
false
}
pub(crate) fn looks_like_bare_hex_digest(credential: &str) -> bool {
matches!(credential.len(), 32 | 40 | 48 | 56 | 64 | 72 | 128) && is_uniform_hex(credential)
}
fn strip_hash_algo_prefix(credential: &str) -> Option<&str> {
const LABELS: &[&str] = &["sha256:", "sha512:", "sha512-", "sha256-", "sha1:", "md5:"];
for label in LABELS {
if let Some(idx) = credential.find(label) {
return Some(&credential[idx + label.len()..]);
}
}
None
}
fn looks_like_base64_blob_with_padding(s: &str) -> bool {
if s.len() < 40 {
return false;
}
if !(s.ends_with("==") || s.ends_with('=')) {
return false;
}
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
}
pub fn looks_like_standard_base64_blob(credential: &str) -> bool {
crate::decode_structure::is_random_base64_blob(credential, 40, 80, 32)
}
fn is_uniform_hex(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let mut saw_lower = false;
let mut saw_upper = false;
for &b in bytes {
match b {
b'0'..=b'9' => {}
b'a'..=b'f' => saw_lower = true,
b'A'..=b'F' => saw_upper = true,
_ => return false,
}
}
!(saw_lower && saw_upper)
}
pub(crate) fn is_uuid_v4_shape(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 36 {
return false;
}
if b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
return false;
}
let mut saw_lower = false;
let mut saw_upper = false;
for (i, &c) in b.iter().enumerate() {
if matches!(i, 8 | 13 | 18 | 23) {
continue;
}
match c {
b'0'..=b'9' => {}
b'a'..=b'f' => saw_lower = true,
b'A'..=b'F' => saw_upper = true,
_ => return false,
}
}
!(saw_lower && saw_upper)
}
pub(crate) fn has_three_or_more_consecutive_identical(s: &str) -> bool {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
let mut run = 1usize;
while i + run < bytes.len() && bytes[i + run] == b {
run += 1;
}
if run >= 3 {
return true;
}
i += run;
}
false
}
pub(crate) fn known_prefix_body(credential: &str) -> Option<&str> {
crate::confidence::KNOWN_PREFIXES
.iter()
.find_map(|prefix| credential.strip_prefix(prefix))
}
pub(crate) fn looks_like_prefixed_masked_sequence(body: &str) -> bool {
if body.ends_with("...") || body.ends_with('…') {
return true;
}
let upper = body.to_ascii_uppercase();
let starts_with_mask = upper.starts_with("XXX") || upper.starts_with("***");
let contains_fake_sequence = ["1234567890", "0123456789", "ABCDEFGH", "ABCDEFGHIJ"]
.iter()
.any(|seq| upper.contains(seq));
starts_with_mask && contains_fake_sequence
}
pub(crate) fn has_repeated_block_mask(s: &str) -> bool {
let bytes = s.as_bytes();
let mut long_runs = 0usize;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
let mut run = 1usize;
while i + run < bytes.len() && bytes[i + run] == b {
run += 1;
}
if run >= 4 && b.is_ascii_alphanumeric() {
long_runs += 1;
if long_runs >= 3 {
return true;
}
}
i += run;
}
false
}
pub(crate) fn has_n_or_more_consecutive_identical(s: &str, n: usize) -> bool {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
let mut run = 1usize;
while i + run < bytes.len() && bytes[i + run] == b {
run += 1;
}
if run >= n && b != b'-' {
return true;
}
i += run;
}
false
}