1use hpx::tls::TlsOptions;
12use rand::prelude::SliceRandom;
13
14use crate::stealth::DeviceClass;
15
16const CHROME_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
21TLS_AES_256_GCM_SHA384:\
22TLS_CHACHA20_POLY1305_SHA256:\
23TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
24TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
25TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
26TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
27TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
28TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
29TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
30TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
31TLS_RSA_WITH_AES_128_GCM_SHA256:\
32TLS_RSA_WITH_AES_256_GCM_SHA384:\
33TLS_RSA_WITH_AES_128_CBC_SHA:\
34TLS_RSA_WITH_AES_256_CBC_SHA";
35
36const CHROME_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
41rsa_pss_rsae_sha256:\
42rsa_pkcs1_sha256:\
43ecdsa_secp384r1_sha384:\
44rsa_pss_rsae_sha384:\
45rsa_pkcs1_sha384:\
46rsa_pss_rsae_sha512:\
47rsa_pkcs1_sha512";
48
49const CHROME_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384";
54
55const SAFARI_IOS_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
60TLS_AES_256_GCM_SHA384:\
61TLS_CHACHA20_POLY1305_SHA256:\
62TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
63TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
64TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
65TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
66TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
67TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
68TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
69TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
70TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
71TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
72TLS_RSA_WITH_AES_256_GCM_SHA384:\
73TLS_RSA_WITH_AES_128_GCM_SHA256:\
74TLS_RSA_WITH_AES_256_CBC_SHA:\
75TLS_RSA_WITH_AES_128_CBC_SHA:\
76TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:\
77TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:\
78TLS_RSA_WITH_3DES_EDE_CBC_SHA";
79
80const SAFARI_IOS_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
86rsa_pss_rsae_sha256:\
87rsa_pkcs1_sha256:\
88ecdsa_secp384r1_sha384:\
89rsa_pss_rsae_sha384:\
90rsa_pss_rsae_sha384:\
91rsa_pkcs1_sha384:\
92rsa_pss_rsae_sha512:\
93rsa_pkcs1_sha512:\
94rsa_pkcs1_sha1";
95
96const SAFARI_IOS_CURVES_LIST: &str = "X25519:P-256:P-384:P-521";
101
102const FIREFOX_CIPHER_LIST: &str = "TLS_AES_128_GCM_SHA256:\
107TLS_CHACHA20_POLY1305_SHA256:\
108TLS_AES_256_GCM_SHA384:\
109TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
110TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
111TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
112TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
113TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
114TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
115TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:\
116TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:\
117TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
118TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
119TLS_RSA_WITH_AES_128_GCM_SHA256:\
120TLS_RSA_WITH_AES_256_GCM_SHA384:\
121TLS_RSA_WITH_AES_128_CBC_SHA:\
122TLS_RSA_WITH_AES_256_CBC_SHA";
123
124const FIREFOX_SIGALGS_LIST: &str = "ecdsa_secp256r1_sha256:\
130ecdsa_secp384r1_sha384:\
131ecdsa_secp521r1_sha512:\
132rsa_pss_rsae_sha256:\
133rsa_pss_rsae_sha384:\
134rsa_pss_rsae_sha512:\
135rsa_pkcs1_sha256:\
136rsa_pkcs1_sha384:\
137rsa_pkcs1_sha512:\
138ecdsa_sha1:\
139rsa_pkcs1_sha1";
140
141const FIREFOX_CURVES_LIST: &str = "X25519MLKEM768:X25519:P-256:P-384:P-521:ffdhe2048:ffdhe3072";
146
147const FIREFOX_DELEGATED_CREDENTIALS: &str = "ecdsa_secp256r1_sha256:\
152ecdsa_secp384r1_sha384:\
153ecdsa_secp521r1_sha512:\
154ecdsa_sha1";
155
156const FIREFOX_RECORD_SIZE_LIMIT: u16 = 0x4001;
161
162const CHROME_EXTENSION_PERMUTATION: [u16; 16] = [
176 51, 65037, 10, 18, 45, 23, 17613, 27, 43, 0, 65281, 11, 5, 16, 35, 13, ];
193
194const SAFARI_IOS_EXTENSION_PERMUTATION: [u16; 13] = [
196 0, 23, 65281, 10, 11, 16, 5, 13, 18, 51, 45, 43, 27, ];
210
211const FIREFOX_EXTENSION_PERMUTATION: [u16; 15] = [
213 0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 65037, ];
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CertCompression {
237 Brotli,
238 Zlib,
239}
240
241#[derive(Debug, Clone)]
251pub struct DeviceFingerprint {
252 pub cipher_list: &'static str,
254 pub sigalgs_list: &'static str,
256 pub curves_list: &'static str,
258 pub extension_permutation: Vec<u16>,
260 pub cert_compression: Vec<CertCompression>,
262 pub min_tls_version: &'static str,
264 pub ech_grease: bool,
266 pub permute_extensions: bool,
268 pub no_session_ticket: bool,
270 pub grease_enabled: bool,
272 pub ocsp_stapling: bool,
274 pub signed_cert_timestamps: bool,
276 pub key_shares_limit: u8,
278 pub delegated_credentials: Option<&'static str>,
280 pub record_size_limit: Option<u16>,
282 pub alps_use_new_codepoint: bool,
284}
285
286impl DeviceFingerprint {
287 pub fn to_tls_options(&self) -> TlsOptions {
294 let min_ver = match self.min_tls_version {
295 "1.0" => hpx::tls::TlsVersion::TLS_1_0,
296 _ => hpx::tls::TlsVersion::TLS_1_2,
297 };
298
299 let mut builder = TlsOptions::builder()
300 .cipher_list(self.cipher_list)
301 .sigalgs_list(self.sigalgs_list)
302 .curves_list(self.curves_list)
303 .session_ticket(!self.no_session_ticket)
304 .enable_ech_grease(self.ech_grease)
305 .permute_extensions(Some(false))
306 .grease_enabled(Some(self.grease_enabled))
307 .enable_ocsp_stapling(self.ocsp_stapling)
308 .enable_signed_cert_timestamps(self.signed_cert_timestamps)
309 .key_shares_limit(Some(self.key_shares_limit))
310 .alps_use_new_codepoint(self.alps_use_new_codepoint)
311 .min_tls_version(min_ver)
312 .max_tls_version(hpx::tls::TlsVersion::TLS_1_3);
313
314 if let Some(dc) = self.delegated_credentials {
315 builder = builder.delegated_credentials(dc);
316 }
317 if let Some(limit) = self.record_size_limit {
318 builder = builder.record_size_limit(limit);
319 }
320
321 builder.build()
322 }
323
324 pub fn get_extension_permutation(&self) -> Vec<u16> {
326 if self.permute_extensions {
327 let mut rng = rand::rng();
328 let mut perm = self.extension_permutation.clone();
329 perm.shuffle(&mut rng);
330 perm
331 } else {
332 self.extension_permutation.clone()
333 }
334 }
335
336 pub fn shuffled_chrome_permutation() -> Vec<u16> {
338 let mut rng = rand::rng();
339 let mut perm = CHROME_EXTENSION_PERMUTATION.to_vec();
340 perm.shuffle(&mut rng);
341 perm
342 }
343
344 pub fn for_device(device_class: DeviceClass, browser_name: &str) -> Self {
346 if browser_name.eq_ignore_ascii_case("firefox") {
347 return Self::firefox_135();
348 }
349 match device_class {
350 DeviceClass::Desktop | DeviceClass::MobileAndroid => Self::chrome_147(),
351 DeviceClass::MobileIOS => Self::safari_ios_18(),
352 }
353 }
354
355 pub fn chrome_147() -> Self {
357 Self {
358 cipher_list: CHROME_CIPHER_LIST,
359 sigalgs_list: CHROME_SIGALGS_LIST,
360 curves_list: CHROME_CURVES_LIST,
361 extension_permutation: CHROME_EXTENSION_PERMUTATION.to_vec(),
362 cert_compression: vec![CertCompression::Brotli],
363 min_tls_version: "1.2",
364 ech_grease: true,
365 permute_extensions: true,
366 no_session_ticket: false,
367 grease_enabled: true,
368 ocsp_stapling: true,
369 signed_cert_timestamps: true,
370 key_shares_limit: 2,
371 delegated_credentials: None,
372 record_size_limit: None,
373 alps_use_new_codepoint: true,
374 }
375 }
376
377 pub fn safari_ios_18() -> Self {
379 Self {
380 cipher_list: SAFARI_IOS_CIPHER_LIST,
381 sigalgs_list: SAFARI_IOS_SIGALGS_LIST,
382 curves_list: SAFARI_IOS_CURVES_LIST,
383 extension_permutation: SAFARI_IOS_EXTENSION_PERMUTATION.to_vec(),
384 cert_compression: vec![CertCompression::Zlib],
385 min_tls_version: "1.0",
386 ech_grease: false,
387 permute_extensions: false,
388 no_session_ticket: true,
389 grease_enabled: true,
390 ocsp_stapling: true,
391 signed_cert_timestamps: true,
392 key_shares_limit: 2,
393 delegated_credentials: None,
394 record_size_limit: None,
395 alps_use_new_codepoint: false,
396 }
397 }
398
399 pub fn firefox_135() -> Self {
401 Self {
402 cipher_list: FIREFOX_CIPHER_LIST,
403 sigalgs_list: FIREFOX_SIGALGS_LIST,
404 curves_list: FIREFOX_CURVES_LIST,
405 extension_permutation: FIREFOX_EXTENSION_PERMUTATION.to_vec(),
406 cert_compression: vec![CertCompression::Zlib, CertCompression::Brotli],
407 min_tls_version: "1.2",
408 ech_grease: true,
409 permute_extensions: false,
410 no_session_ticket: false,
411 grease_enabled: false,
412 ocsp_stapling: true,
413 signed_cert_timestamps: true,
414 key_shares_limit: 2,
415 delegated_credentials: Some(FIREFOX_DELEGATED_CREDENTIALS),
416 record_size_limit: Some(FIREFOX_RECORD_SIZE_LIMIT),
417 alps_use_new_codepoint: false,
418 }
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn chrome_cipher_list_matches_reference() {
428 let expected = "TLS_AES_128_GCM_SHA256:\
429 TLS_AES_256_GCM_SHA384:\
430 TLS_CHACHA20_POLY1305_SHA256:\
431 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:\
432 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:\
433 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:\
434 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:\
435 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:\
436 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:\
437 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:\
438 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:\
439 TLS_RSA_WITH_AES_128_GCM_SHA256:\
440 TLS_RSA_WITH_AES_256_GCM_SHA384:\
441 TLS_RSA_WITH_AES_128_CBC_SHA:\
442 TLS_RSA_WITH_AES_256_CBC_SHA";
443 assert_eq!(CHROME_CIPHER_LIST, expected);
444 }
445
446 #[test]
447 fn chrome_sigalgs_matches_reference() {
448 let expected = "ecdsa_secp256r1_sha256:\
449 rsa_pss_rsae_sha256:\
450 rsa_pkcs1_sha256:\
451 ecdsa_secp384r1_sha384:\
452 rsa_pss_rsae_sha384:\
453 rsa_pkcs1_sha384:\
454 rsa_pss_rsae_sha512:\
455 rsa_pkcs1_sha512";
456 assert_eq!(CHROME_SIGALGS_LIST, expected);
457 }
458
459 #[test]
460 fn chrome_extension_count() {
461 assert_eq!(CHROME_EXTENSION_PERMUTATION.len(), 16);
462 }
463
464 #[test]
465 fn safari_extension_count() {
466 assert_eq!(SAFARI_IOS_EXTENSION_PERMUTATION.len(), 13);
467 }
468
469 #[test]
470 fn firefox_extension_count() {
471 assert_eq!(FIREFOX_EXTENSION_PERMUTATION.len(), 15);
472 }
473
474 #[test]
475 fn safari_has_20_ciphers() {
476 assert_eq!(SAFARI_IOS_CIPHER_LIST.matches(':').count() + 1, 20);
477 }
478
479 #[test]
480 fn firefox_has_17_ciphers() {
481 assert_eq!(FIREFOX_CIPHER_LIST.matches(':').count() + 1, 17);
482 }
483
484 #[test]
485 fn safari_sigalg_has_duplicate_rsa_pss_rsae_sha384() {
486 let count = SAFARI_IOS_SIGALGS_LIST
488 .split(':')
489 .filter(|s| *s == "rsa_pss_rsae_sha384")
490 .count();
491 assert_eq!(count, 2);
492 }
493
494 #[test]
495 fn firefox_curves_have_ffdhe() {
496 assert!(FIREFOX_CURVES_LIST.contains("ffdhe2048"));
497 assert!(FIREFOX_CURVES_LIST.contains("ffdhe3072"));
498 }
499
500 #[test]
501 fn chrome_curves_have_mlkem768() {
502 assert!(CHROME_CURVES_LIST.starts_with("X25519MLKEM768"));
503 }
504
505 #[test]
506 fn chrome_shuffle_preserves_set() {
507 let fp = DeviceFingerprint::chrome_147();
508 let p1 = fp.get_extension_permutation();
509 let p2 = fp.get_extension_permutation();
510
511 assert_eq!(p1.len(), 16);
512 assert_eq!(p2.len(), 16);
513
514 let mut sorted = p1.clone();
515 sorted.sort();
516 let mut expected = CHROME_EXTENSION_PERMUTATION.to_vec();
517 expected.sort();
518 assert_eq!(sorted, expected, "shuffle must preserve the set");
519 assert_ne!(p1, p2, "shuffle should be non-deterministic");
520 }
521
522 #[test]
523 fn chrome_preset_to_tls_options() {
524 let fp = DeviceFingerprint::chrome_147();
525 let opts = fp.to_tls_options();
526 assert!(opts.cipher_list.is_some());
527 assert!(opts.sigalgs_list.is_some());
528 assert!(opts.curves_list.is_some());
529 assert!(opts.session_ticket);
530 assert!(opts.enable_ech_grease);
531 assert_eq!(opts.grease_enabled, Some(true));
532 assert_eq!(opts.key_shares_limit, Some(2));
533 assert!(opts.alps_use_new_codepoint);
534 assert!(opts.delegated_credentials.is_none());
535 assert!(opts.record_size_limit.is_none());
536 assert_eq!(fp.extension_permutation.len(), 16);
538 }
539
540 #[test]
541 fn safari_preset_to_tls_options() {
542 let fp = DeviceFingerprint::safari_ios_18();
543 let opts = fp.to_tls_options();
544 assert!(!opts.session_ticket);
545 assert!(!opts.enable_ech_grease);
546 assert_eq!(opts.min_tls_version, Some(hpx::tls::TlsVersion::TLS_1_0));
547 assert!(opts.delegated_credentials.is_none());
548 }
549
550 #[test]
551 fn firefox_preset_to_tls_options() {
552 let fp = DeviceFingerprint::firefox_135();
553 let opts = fp.to_tls_options();
554 assert_eq!(opts.grease_enabled, Some(false));
555 assert!(opts.delegated_credentials.is_some());
556 assert_eq!(opts.record_size_limit, Some(FIREFOX_RECORD_SIZE_LIMIT));
557 }
558
559 #[test]
560 fn for_device_dispatches_correctly() {
561 let chrome = DeviceFingerprint::for_device(DeviceClass::Desktop, "Chrome");
562 assert_eq!(chrome.cipher_list, CHROME_CIPHER_LIST);
563
564 let safari = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Safari");
565 assert_eq!(safari.cipher_list, SAFARI_IOS_CIPHER_LIST);
566
567 let firefox = DeviceFingerprint::for_device(DeviceClass::Desktop, "Firefox");
568 assert_eq!(firefox.cipher_list, FIREFOX_CIPHER_LIST);
569
570 let ff_ios = DeviceFingerprint::for_device(DeviceClass::MobileIOS, "Firefox");
572 assert_eq!(ff_ios.cipher_list, FIREFOX_CIPHER_LIST);
573 }
574}