use hpx::tls::TlsOptions;
use rand::prelude::SliceRandom;
use crate::stealth::DeviceClass;
const CHROME_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
TLS_AES_256_GCM_SHA384:\
TLS_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
TLS_RSA_WITH_AES_128_GCM_SHA256:\
TLS_RSA_WITH_AES_256_GCM_SHA384:\
TLS_RSA_WITH_AES_128_CBC_SHA:\
TLS_RSA_WITH_AES_256_CBC_SHA";
const CHROME_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
rsa_pss_rsae_sha256:\
rsa_pkcs1_sha256:\
ecdsa_secp384r1_sha384:\
rsa_pss_rsae_sha384:\
rsa_pkcs1_sha384:\
rsa_pss_rsae_sha512:\
rsa_pkcs1_sha512";
const CHROME_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384";
const SAFARI_IOS_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
TLS_AES_256_GCM_SHA384:\
TLS_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
TLS_RSA_WITH_AES_256_GCM_SHA384:\
TLS_RSA_WITH_AES_128_GCM_SHA256:\
TLS_RSA_WITH_AES_256_CBC_SHA:\
TLS_RSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:\
TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:\
TLS_RSA_WITH_3DES_EDE_CBC_SHA";
const SAFARI_IOS_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
rsa_pss_rsae_sha256:\
rsa_pkcs1_sha256:\
ecdsa_secp384r1_sha384:\
rsa_pss_rsae_sha384:\
rsa_pss_rsae_sha384:\
rsa_pkcs1_sha384:\
rsa_pss_rsae_sha512:\
rsa_pkcs1_sha512:\
rsa_pkcs1_sha1";
const SAFARI_IOS_CURVES_LIST: &str = "X25519:P-256:P-384:P-521";
const FIREFOX_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
TLS_CHACHA20_POLY1305_SHA256:\
TLS_AES_256_GCM_SHA384:\
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
TLS_RSA_WITH_AES_128_GCM_SHA256:\
TLS_RSA_WITH_AES_256_GCM_SHA384:\
TLS_RSA_WITH_AES_128_CBC_SHA:\
TLS_RSA_WITH_AES_256_CBC_SHA";
const FIREFOX_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
ecdsa_secp384r1_sha384:\
ecdsa_secp521r1_sha512:\
rsa_pss_rsae_sha256:\
rsa_pss_rsae_sha384:\
rsa_pss_rsae_sha512:\
rsa_pkcs1_sha256:\
rsa_pkcs1_sha384:\
rsa_pkcs1_sha512:\
ecdsa_sha1:\
rsa_pkcs1_sha1";
const FIREFOX_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384:P-521:ffdhe2048:ffdhe3072";
const FIREFOX_DELEGATED_CREDENTIALS: &str = "ecdsa_secp256r1_sha256:\
ecdsa_secp384r1_sha384:\
ecdsa_secp521r1_sha512:\
ecdsa_sha1";
const FIREFOX_RECORD_SIZE_LIMIT: u16 = 0x4001;
const CHROME_EXTENSION_PERMUTATION: [u16; 16] = [
51, 65037, 10, 18, 45, 23, 17613, 27, 43, 0, 65281, 11, 5, 16, 35, 13, ];
const SAFARI_IOS_EXTENSION_PERMUTATION: [u16; 13] = [
0, 23, 65281, 10, 11, 16, 5, 13, 18, 51, 45, 43, 27, ];
const FIREFOX_EXTENSION_PERMUTATION: [u16; 15] = [
0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 65037, ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertCompression {
Brotli,
Zlib,
}
#[derive(Debug, Clone)]
pub struct DeviceFingerprint {
pub cipher_list: &'static str,
pub sigalgs_list: &'static str,
pub curves_list: &'static str,
pub extension_permutation: Vec<u16>,
pub cert_compression: Vec<CertCompression>,
pub min_tls_version: &'static str,
pub ech_grease: bool,
pub permute_extensions: bool,
pub no_session_ticket: bool,
pub grease_enabled: bool,
pub ocsp_stapling: bool,
pub signed_cert_timestamps: bool,
pub key_shares_limit: u8,
pub delegated_credentials: Option<&'static str>,
pub record_size_limit: Option<u16>,
pub alps_use_new_codepoint: bool,
}
impl DeviceFingerprint {
pub fn to_tls_options(&self) -> TlsOptions {
let min_ver = match self.min_tls_version {
"1.0" => hpx::tls::TlsVersion::TLS_1_0,
_ => hpx::tls::TlsVersion::TLS_1_2,
};
let mut builder = TlsOptions::builder()
.cipher_list(self.cipher_list)
.sigalgs_list(self.sigalgs_list)
.curves_list(self.curves_list)
.session_ticket(!self.no_session_ticket)
.enable_ech_grease(self.ech_grease)
.permute_extensions(Some(false))
.grease_enabled(Some(self.grease_enabled))
.enable_ocsp_stapling(self.ocsp_stapling)
.enable_signed_cert_timestamps(self.signed_cert_timestamps)
.key_shares_limit(Some(self.key_shares_limit))
.alps_use_new_codepoint(self.alps_use_new_codepoint)
.min_tls_version(min_ver)
.max_tls_version(hpx::tls::TlsVersion::TLS_1_3);
if let Some(dc) = self.delegated_credentials {
builder = builder.delegated_credentials(dc);
}
if let Some(limit) = self.record_size_limit {
builder = builder.record_size_limit(limit);
}
builder.build()
}
pub fn get_extension_permutation(&self) -> Vec<u16> {
if self.permute_extensions {
let mut rng = rand::rng();
let mut perm = self.extension_permutation.clone();
perm.shuffle(&mut rng);
perm
} else {
self.extension_permutation.clone()
}
}
pub fn shuffled_chrome_permutation() -> Vec<u16> {
let mut rng = rand::rng();
let mut perm = CHROME_EXTENSION_PERMUTATION.to_vec();
perm.shuffle(&mut rng);
perm
}
pub fn for_device(device_class: DeviceClass, browser_name: &str) -> Self {
if browser_name.eq_ignore_ascii_case("firefox") {
return Self::firefox_135();
}
match device_class {
DeviceClass::Desktop | DeviceClass::MobileAndroid => Self::chrome_147(),
DeviceClass::MobileIOS => Self::safari_ios_18(),
}
}
pub fn chrome_147() -> Self {
Self {
cipher_list: CHROME_CIPHER_LIST,
sigalgs_list: CHROME_SIGALGS_LIST,
curves_list: CHROME_CURVES_LIST,
extension_permutation: CHROME_EXTENSION_PERMUTATION.to_vec(),
cert_compression: vec![CertCompression::Brotli],
min_tls_version: "1.2",
ech_grease: true,
permute_extensions: true,
no_session_ticket: false,
grease_enabled: true,
ocsp_stapling: true,
signed_cert_timestamps: true,
key_shares_limit: 2,
delegated_credentials: None,
record_size_limit: None,
alps_use_new_codepoint: true,
}
}
pub fn safari_ios_18() -> Self {
Self {
cipher_list: SAFARI_IOS_CIPHER_LIST,
sigalgs_list: SAFARI_IOS_SIGALGS_LIST,
curves_list: SAFARI_IOS_CURVES_LIST,
extension_permutation: SAFARI_IOS_EXTENSION_PERMUTATION.to_vec(),
cert_compression: vec![CertCompression::Zlib],
min_tls_version: "1.0",
ech_grease: false,
permute_extensions: false,
no_session_ticket: true,
grease_enabled: true,
ocsp_stapling: true,
signed_cert_timestamps: true,
key_shares_limit: 2,
delegated_credentials: None,
record_size_limit: None,
alps_use_new_codepoint: false,
}
}
pub fn firefox_135() -> Self {
Self {
cipher_list: FIREFOX_CIPHER_LIST,
sigalgs_list: FIREFOX_SIGALGS_LIST,
curves_list: FIREFOX_CURVES_LIST,
extension_permutation: FIREFOX_EXTENSION_PERMUTATION.to_vec(),
cert_compression: vec![CertCompression::Zlib, CertCompression::Brotli],
min_tls_version: "1.2",
ech_grease: true,
permute_extensions: false,
no_session_ticket: false,
grease_enabled: false,
ocsp_stapling: true,
signed_cert_timestamps: true,
key_shares_limit: 2,
delegated_credentials: Some(FIREFOX_DELEGATED_CREDENTIALS),
record_size_limit: Some(FIREFOX_RECORD_SIZE_LIMIT),
alps_use_new_codepoint: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chrome_cipher_list_matches_reference() {
let expected = "TLS_AES_128_GCM_SHA256:\
TLS_AES_256_GCM_SHA384:\
TLS_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
TLS_RSA_WITH_AES_128_GCM_SHA256:\
TLS_RSA_WITH_AES_256_GCM_SHA384:\
TLS_RSA_WITH_AES_128_CBC_SHA:\
TLS_RSA_WITH_AES_256_CBC_SHA";
assert_eq!(CHROME_CIPHER_LIST, expected);
}
#[test]
fn chrome_sigalgs_matches_reference() {
let expected = "ecdsa_secp256r1_sha256:\
rsa_pss_rsae_sha256:\
rsa_pkcs1_sha256:\
ecdsa_secp384r1_sha384:\
rsa_pss_rsae_sha384:\
rsa_pkcs1_sha384:\
rsa_pss_rsae_sha512:\
rsa_pkcs1_sha512";
assert_eq!(CHROME_SIGALGS_LIST, expected);
}
#[test]
fn chrome_extension_count() {
assert_eq!(CHROME_EXTENSION_PERMUTATION.len(), 16);
}
#[test]
fn safari_extension_count() {
assert_eq!(SAFARI_IOS_EXTENSION_PERMUTATION.len(), 13);
}
#[test]
fn firefox_extension_count() {
assert_eq!(FIREFOX_EXTENSION_PERMUTATION.len(), 15);
}
#[test]
fn safari_has_20_ciphers() {
assert_eq!(SAFARI_IOS_CIPHER_LIST.matches(':').count() + 1, 20);
}
#[test]
fn firefox_has_17_ciphers() {
assert_eq!(FIREFOX_CIPHER_LIST.matches(':').count() + 1, 17);
}
#[test]
fn safari_sigalg_has_duplicate_rsa_pss_rsae_sha384() {
let count = SAFARI_IOS_SIGALGS_LIST
.split(':')
.filter(|s| *s == "rsa_pss_rsae_sha384")
.count();
assert_eq!(count, 2);
}
#[test]
fn firefox_curves_have_ffdhe() {
assert!(FIREFOX_CURVES_LIST.contains("ffdhe2048"));
assert!(FIREFOX_CURVES_LIST.contains("ffdhe3072"));
}
#[test]
fn chrome_curves_have_mlkem768() {
assert!(CHROME_CURVES_LIST.starts_with("X25519MLKEM768"));
}
#[test]
fn chrome_shuffle_preserves_set() {
let fp = DeviceFingerprint::chrome_147();
let p1 = fp.get_extension_permutation();
let p2 = fp.get_extension_permutation();
assert_eq!(p1.len(), 16);
assert_eq!(p2.len(), 16);
let mut sorted = p1.clone();
sorted.sort();
let mut expected = CHROME_EXTENSION_PERMUTATION.to_vec();
expected.sort();
assert_eq!(sorted, expected, "shuffle must preserve the set");
assert_ne!(p1, p2, "shuffle should be non-deterministic");
}
#[test]
fn chrome_preset_to_tls_options() {
let fp = DeviceFingerprint::chrome_147();
let opts = fp.to_tls_options();
assert!(opts.cipher_list.is_some());
assert!(opts.sigalgs_list.is_some());
assert!(opts.curves_list.is_some());
assert!(opts.session_ticket);
assert!(opts.enable_ech_grease);
assert_eq!(opts.grease_enabled, Some(true));
assert_eq!(opts.key_shares_limit, Some(2));
assert!(opts.alps_use_new_codepoint);
assert!(opts.delegated_credentials.is_none());
assert!(opts.record_size_limit.is_none());
assert_eq!(fp.extension_permutation.len(), 16);
}
#[test]
fn safari_preset_to_tls_options() {
let fp = DeviceFingerprint::safari_ios_18();
let opts = fp.to_tls_options();
assert!(!opts.session_ticket);
assert!(!opts.enable_ech_grease);
assert_eq!(opts.min_tls_version, Some(hpx::tls::TlsVersion::TLS_1_0));
assert!(opts.delegated_credentials.is_none());
}
#[test]
fn firefox_preset_to_tls_options() {
let fp = DeviceFingerprint::firefox_135();
let opts = fp.to_tls_options();
assert_eq!(opts.grease_enabled, Some(false));
assert!(opts.delegated_credentials.is_some());
assert_eq!(opts.record_size_limit, Some(FIREFOX_RECORD_SIZE_LIMIT));
}
#[test]
fn for_device_dispatches_correctly() {
let chrome = DeviceFingerprint::for_device(DeviceClass::Desktop, "Chrome");
assert_eq!(chrome.cipher_list, CHROME_CIPHER_LIST);
let safari = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Safari");
assert_eq!(safari.cipher_list, SAFARI_IOS_CIPHER_LIST);
let firefox = DeviceFingerprint::for_device(DeviceClass::Desktop, "Firefox");
assert_eq!(firefox.cipher_list, FIREFOX_CIPHER_LIST);
let ff_ios = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Firefox");
assert_eq!(ff_ios.cipher_list, FIREFOX_CIPHER_LIST);
}
}