use crate::courierust_crypto::sha256::sha256_hex;
use crate::courierust_fingerprint::profile::{is_grease, TlsProfile};
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
pub fn ja4(p: &TlsProfile) -> String {
let mut out = String::with_capacity(80);
out.push(if p.protocol == 'q' {
'q'
} else if p.protocol == 'd' {
'd'
} else {
't'
});
out.push_str(&ja4_version(p));
out.push(if p.has_sni { 'd' } else { 'i' });
let ciphers: Vec<u16> = p
.ciphers
.iter()
.copied()
.filter(|&c| !is_grease(c))
.collect();
let exts: Vec<u16> = p
.extensions
.iter()
.copied()
.filter(|&e| !is_grease(e))
.collect();
out.push_str(&two_digits(ciphers.len()));
out.push_str(&two_digits(exts.len()));
out.push_str(&ja4_alpn(&p.alpn));
out.push('_');
if ciphers.is_empty() {
out.push_str("000000000000");
} else {
let mut sorted = ciphers.clone();
sorted.sort_unstable();
let s = hex_join(&sorted);
out.push_str(&sha256_hex(s.as_bytes())[..12]);
}
out.push('_');
let mut exts2: Vec<u16> = exts
.iter()
.copied()
.filter(|&e| e != 0 && e != 0x0010)
.collect();
exts2.sort_unstable();
if exts2.is_empty() {
out.push_str("000000000000");
} else {
let mut s = hex_join(&exts2);
if !p.signature_algorithms.is_empty() {
s.push('_');
s.push_str(&hex_join(&p.signature_algorithms));
}
out.push_str(&sha256_hex(s.as_bytes())[..12]);
}
out
}
pub fn ja4_version(p: &TlsProfile) -> String {
let v = p
.supported_versions
.iter()
.copied()
.filter(|&x| !is_grease(x))
.max()
.unwrap_or(p.tls_version);
version_to_str(v)
}
fn version_to_str(v: u16) -> String {
match v {
0x0304 => "13",
0x0303 => "12",
0x0302 => "11",
0x0301 => "10",
0x0300 => "s3",
0x0002 => "s2",
0xfeff => "d1",
0xfefd => "d2",
0xfefc => "d3",
_ => "00",
}
.to_string()
}
fn two_digits(n: usize) -> String {
format!("{:02}", n.min(99))
}
fn ja4_alpn(alpn: &[String]) -> String {
let first = match alpn.first() {
Some(s) => s,
None => return "00".to_string(),
};
let b = first.as_bytes();
if b.is_empty() {
return "00".to_string();
}
let first_alnum = b[0].is_ascii_alphanumeric();
let last_alnum = b[b.len() - 1].is_ascii_alphanumeric();
if first_alnum && last_alnum {
format!("{}{}", b[0] as char, b[b.len() - 1] as char)
} else {
const HEX: &[u8; 16] = b"0123456789abcdef";
let first = HEX[(b[0] >> 4) as usize] as char;
let last = HEX[(b[b.len() - 1] & 0x0f) as usize] as char;
format!("{first}{last}")
}
}
fn hex_join(v: &[u16]) -> String {
let mut s = String::new();
for (i, x) in v.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&format!("{:04x}", x));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::courierust_fingerprint::profile::chrome_tls_profile;
#[test]
fn matches_official_spec_example() {
let p = chrome_tls_profile();
assert_eq!(ja4(&p), "t13d1516h2_8daaf6152771_e5627efa2ab1");
}
#[test]
fn no_alpn_gives_00() {
let mut p = TlsProfile::default();
p.alpn.clear();
let fp = ja4(&p);
assert!(fp.starts_with("t12d00"), "{fp}");
}
#[test]
fn grease_is_filtered() {
let p = TlsProfile {
ciphers: vec![0x1301, 0x0a0a, 0x1302, 0x1a1a],
extensions: vec![0x0000, 0x0010, 0x0a0a, 0x002b],
alpn: vec!["h2".into()],
..Default::default()
};
let fp = ja4(&p);
assert!(fp.starts_with("t12d0203h2"), "{fp}");
}
}