@anonympins/fingerprint 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,6 +90,7 @@ class RequestUtils
90
90
  "ch_model" => "sec-ch-ua-model",
91
91
  "ch_arch" => "sec-ch-ua-arch",
92
92
  "ch_bitness" => "sec-ch-ua-bitness",
93
+ "ch_full_version_list" => "sec-ch-ua-full-version-list",
93
94
  "upgrade_req" => "upgrade-insecure-requests",
94
95
  "accept_lang" => "accept-language",
95
96
  "accept_enc" => "accept-encoding",
@@ -551,6 +552,51 @@ class RequestUtils
551
552
  }
552
553
  }
553
554
 
555
+ // 2. Incohérence de l'écran (si les Client Hints sont disponibles)
556
+ $clientScreenHash = $clientFpMap['scr'] ?? null;
557
+ $viewportWidth = $context->getHeader('sec-ch-viewport-width');
558
+ if ($clientScreenHash && $viewportWidth) {
559
+ $viewportWidthInt = (int)$viewportWidth;
560
+ $matchedScreenWidth = null;
561
+ $commonWidths = [320, 360, 375, 390, 412, 414, 768, 1024, 1280, 1366, 1440, 1536, 1600, 1920, 2560, 3840];
562
+ $commonHeights = [480, 568, 640, 667, 736, 800, 812, 844, 896, 900, 1024, 1080, 1200, 1440, 1600, 2160];
563
+ $commonDepths = [24, 30, 32];
564
+
565
+ foreach ($commonWidths as $w) {
566
+ foreach ($commonHeights as $h) {
567
+ foreach ($commonDepths as $d) {
568
+ $candidate = "{$w}x{$h}_{$d}";
569
+ $candidateHash = (string)FingerprintBuilder::cyrb53($candidate);
570
+ if ($clientScreenHash === $candidateHash) {
571
+ $matchedScreenWidth = $w;
572
+ break 3;
573
+ }
574
+ }
575
+ }
576
+ }
577
+
578
+ if ($matchedScreenWidth !== null && $viewportWidthInt > $matchedScreenWidth) {
579
+ $score += 20;
580
+ }
581
+ }
582
+
583
+ // 3. Incohérence du GPU/Canvas et JA3
584
+ $clientGpuHash = $clientFpMap['gpu'] ?? null;
585
+ $ja3 = $context->ja3;
586
+ if ($clientGpuHash && $ja3) {
587
+ $expectedClients = self::TLS_FINGERPRINT_DB[$ja3] ?? null;
588
+ if ($expectedClients) {
589
+ if (!is_array($expectedClients)) {
590
+ $expectedClients = [$expectedClients];
591
+ }
592
+ $nonBrowserLibraries = ['Python', 'Go', 'Java', 'curl'];
593
+ $isLibrary = !empty(array_intersect($expectedClients, $nonBrowserLibraries));
594
+ if ($isLibrary) {
595
+ $score += 30;
596
+ }
597
+ }
598
+ }
599
+
554
600
  return ['crossLayerInconsistencyScore' => min(100.0, $score)];
555
601
  }
556
602
 
@@ -567,6 +613,50 @@ class RequestUtils
567
613
  return ['clientHintsInconsistencyScore' => 0.0];
568
614
  }
569
615
 
616
+ $fullVersionList = $context->getHeader('sec-ch-ua-full-version-list');
617
+ if (!empty($fullVersionList)) {
618
+ $chFullVersion = null;
619
+ $chFullBrowser = null;
620
+ if (preg_match_all('/"([^"]+)";v="([^"]+)"/', $fullVersionList, $matches, PREG_SET_ORDER)) {
621
+ foreach ($matches as $match) {
622
+ $brand = $match[1];
623
+ $version = $match[2];
624
+ if ($brand === 'Google Chrome' || $brand === 'Chromium' || $brand === 'Microsoft Edge') {
625
+ $chFullVersion = $version;
626
+ $chFullBrowser = $brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
627
+ if ($brand === 'Google Chrome' || $brand === 'Microsoft Edge') {
628
+ break;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ if ($chFullVersion && $chFullBrowser) {
634
+ if (preg_match('/(Chrome|Edg)\/([\d\.]+)/', $ua, $uaFullMatches)) {
635
+ $uaBrowserMapped = $uaFullMatches[1] === 'Edg' ? 'Edge' : 'Chrome';
636
+ $uaFullVersion = $uaFullMatches[2];
637
+ if ($uaBrowserMapped === $chFullBrowser && $uaFullVersion !== $chFullVersion) {
638
+ $parts1 = array_map('intval', explode('.', $uaFullVersion));
639
+ $parts2 = array_map('intval', explode('.', $chFullVersion));
640
+ $diffIndex = -1;
641
+ $maxLen = max(count($parts1), count($parts2));
642
+ for ($i = 0; $i < $maxLen; $i++) {
643
+ $p1 = $parts1[$i] ?? 0;
644
+ $p2 = $parts2[$i] ?? 0;
645
+ if ($p1 !== $p2) {
646
+ $diffIndex = $i;
647
+ break;
648
+ }
649
+ }
650
+ $baseScores = [95.0, 90.0, 85.0, 80.0];
651
+ $baseScore = $baseScores[$diffIndex] ?? 80.0;
652
+ $delta = abs(($parts1[$diffIndex] ?? 0) - ($parts2[$diffIndex] ?? 0));
653
+ $finalFullScore = min(100.0, $baseScore + min(5.0, $delta * 5.0));
654
+ return ['clientHintsInconsistencyScore' => $finalFullScore];
655
+ }
656
+ }
657
+ }
658
+ }
659
+
570
660
  // 1. Extraire la version du navigateur depuis le User-Agent
571
661
  $uaVersion = null;
572
662
  if (preg_match('/(Chrome|Firefox|Edg|Safari)\/([\d\.]+)/', $ua, $uaMatches)) {
@@ -602,13 +692,19 @@ class RequestUtils
602
692
  return ['clientHintsInconsistencyScore' => 90.0];
603
693
  }
604
694
 
605
- if ($versionDifference > 5) { // Un écart de plus de 5 versions majeures est très suspect
606
- return ['clientHintsInconsistencyScore' => 80.0];
607
- } elseif ($versionDifference > 1) { // Un petit écart est légèrement suspect
608
- return ['clientHintsInconsistencyScore' => 40.0];
695
+ $clientHintsInconsistencyScore = 0.0;
696
+ if ($versionDifference > 0) {
697
+ if ($versionDifference <= 2) {
698
+ $clientHintsInconsistencyScore = $versionDifference * 20.0;
699
+ } elseif ($versionDifference <= 7) {
700
+ $clientHintsInconsistencyScore = 40.0 + ($versionDifference - 2) * 8.0;
701
+ } else {
702
+ $clientHintsInconsistencyScore = min(100.0, 80.0 + ($versionDifference - 7) * 3.33);
703
+ }
704
+ $clientHintsInconsistencyScore = round($clientHintsInconsistencyScore, 1);
609
705
  }
610
706
 
611
- return ['clientHintsInconsistencyScore' => 0.0];
707
+ return ['clientHintsInconsistencyScore' => $clientHintsInconsistencyScore];
612
708
  }
613
709
  /**
614
710
  * Calcule les indicateurs comportementaux liés à l'historique de l'appareil.
@@ -1314,6 +1410,183 @@ class RequestUtils
1314
1410
  return hash_equals($expectedSignature, $storedSignature);
1315
1411
  }
1316
1412
 
1413
+ /**
1414
+ * Parse une trame TCP SYN brute (IPv4 ou IPv6).
1415
+ */
1416
+ public static function parseTcpSyn(?string $binary): ?array
1417
+ {
1418
+ if (!$binary || strlen($binary) < 40) return null;
1419
+ $ttl = 64;
1420
+ $tcpOffset = 20;
1421
+ $version = ord($binary[0]) >> 4;
1422
+
1423
+ if ($version === 4) {
1424
+ $ttl = ord($binary[8]);
1425
+ $ihl = ord($binary[0]) & 0x0f;
1426
+ $tcpOffset = $ihl * 4;
1427
+ } elseif ($version === 6) {
1428
+ $ttl = ord($binary[7]); // Hop Limit
1429
+ $tcpOffset = 40;
1430
+ } else {
1431
+ $tcpOffset = 0;
1432
+ $ttl = 64;
1433
+ }
1434
+
1435
+ if (strlen($binary) < $tcpOffset + 20) return null;
1436
+
1437
+ $windowSize = (ord($binary[$tcpOffset + 14]) << 8) | ord($binary[$tcpOffset + 15]);
1438
+ $dataOffset = (ord($binary[$tcpOffset + 12]) >> 4) * 4;
1439
+ $optionsEnd = $tcpOffset + $dataOffset;
1440
+
1441
+ $mss = null;
1442
+ $ws = null;
1443
+ $sack = false;
1444
+
1445
+ $i = $tcpOffset + 20;
1446
+ while ($i < $optionsEnd && $i < strlen($binary)) {
1447
+ $optType = ord($binary[$i]);
1448
+ if ($optType === 0) break;
1449
+ if ($optType === 1) {
1450
+ $i++;
1451
+ continue;
1452
+ }
1453
+ if ($i + 1 >= strlen($binary)) break;
1454
+ $optLen = ord($binary[$i + 1]);
1455
+ if ($optLen < 2 || $i + $optLen > strlen($binary)) break;
1456
+
1457
+ if ($optType === 2 && $optLen === 4) {
1458
+ $mss = (ord($binary[$i + 2]) << 8) | ord($binary[$i + 3]);
1459
+ } elseif ($optType === 3 && $optLen === 3) {
1460
+ $ws = ord($binary[$i + 2]);
1461
+ } elseif ($optType === 4 && $optLen === 2) {
1462
+ $sack = true;
1463
+ }
1464
+ $i += $optLen;
1465
+ }
1466
+
1467
+ return ['ttl' => $ttl, 'windowSize' => $windowSize, 'mss' => $mss, 'ws' => $ws, 'sack' => $sack];
1468
+ }
1469
+
1470
+ /**
1471
+ * Classifie l'OS à partir du fingerprint de la pile TCP/IP.
1472
+ */
1473
+ public static function classifyTcpOs(?array $fingerprint): string
1474
+ {
1475
+ if (!$fingerprint) return 'unknown';
1476
+ $ttl = $fingerprint['ttl'] ?? 64;
1477
+ $windowSize = $fingerprint['windowSize'] ?? 0;
1478
+ $ws = $fingerprint['ws'] ?? null;
1479
+
1480
+ if ($ttl > 64 && $ttl <= 128) {
1481
+ return 'Windows';
1482
+ }
1483
+ if ($ttl > 32 && $ttl <= 64) {
1484
+ if ($windowSize === 29200 || $windowSize === 14600 || $windowSize === 5840) {
1485
+ return 'Linux';
1486
+ }
1487
+ return 'Linux';
1488
+ }
1489
+ if ($ttl <= 64) {
1490
+ if ($windowSize === 65535 && ($ws === 6 || $ws === 8 || $ws === 5)) {
1491
+ return 'macOS/iOS';
1492
+ }
1493
+ }
1494
+ if ($ttl > 64) return 'Windows';
1495
+ if ($ttl > 0) return 'Linux';
1496
+ return 'unknown';
1497
+ }
1498
+
1499
+ /**
1500
+ * Détecte les anomalies de pile réseau par rapport au User-Agent.
1501
+ */
1502
+ public static function getTcpAnomalyScore(RequestContext $context): array
1503
+ {
1504
+ $fp = null;
1505
+ $rawTcpBinary = $context->getHeader('x-raw-tcp-binary') ?? $context->headers['x-raw-tcp-binary'] ?? null;
1506
+ if ($rawTcpBinary) {
1507
+ $binary = @hex2bin($rawTcpBinary) ?: $rawTcpBinary;
1508
+ $fp = self::parseTcpSyn($binary);
1509
+ }
1510
+
1511
+ if (!$fp) {
1512
+ $tcpHeader = $context->getHeader('x-tcp-fingerprint') ?? $context->tcpFingerprint ?? null;
1513
+ if ($tcpHeader && is_string($tcpHeader)) {
1514
+ $parts = explode(':', $tcpHeader);
1515
+ if (count($parts) >= 2) {
1516
+ $fp = [
1517
+ 'ttl' => (int)$parts[0],
1518
+ 'windowSize' => (int)$parts[1],
1519
+ 'mss' => isset($parts[2]) ? (int)$parts[2] : null,
1520
+ 'ws' => isset($parts[3]) ? (int)$parts[3] : null,
1521
+ 'sack' => isset($parts[4]) && ($parts[4] === '1' || $parts[4] === 'true')
1522
+ ];
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ if (!$fp) {
1528
+ return ['tcpAnomalyScore' => 0.0];
1529
+ }
1530
+
1531
+ $tcpOs = self::classifyTcpOs($fp);
1532
+ $ua = $context->getHeader('user-agent') ?? '';
1533
+ $uaParts = self::parseUserAgent($ua);
1534
+ $uaOs = $uaParts['os'] ?? null;
1535
+
1536
+ if (!$uaOs || $tcpOs === 'unknown') {
1537
+ return ['tcpAnomalyScore' => 0.0];
1538
+ }
1539
+
1540
+ $mappedOs = null;
1541
+ if (str_starts_with($uaOs, 'Windows')) {
1542
+ $mappedOs = 'Windows';
1543
+ } elseif (str_starts_with($uaOs, 'Mac') || str_starts_with($uaOs, 'macOS')) {
1544
+ $mappedOs = 'macOS';
1545
+ } elseif (str_starts_with($uaOs, 'iOS')) {
1546
+ $mappedOs = 'iOS';
1547
+ } elseif (str_starts_with($uaOs, 'Linux')) {
1548
+ $mappedOs = 'Linux';
1549
+ }
1550
+
1551
+ if ($mappedOs === null) {
1552
+ return ['tcpAnomalyScore' => 0.0];
1553
+ }
1554
+
1555
+ $osExpectedTcp = [
1556
+ 'Windows' => ['ttl' => 128, 'windowSize' => 64240, 'ws' => 8, 'mss' => 1460, 'sack' => true],
1557
+ 'Linux' => ['ttl' => 64, 'windowSize' => 29200, 'ws' => 7, 'mss' => 1460, 'sack' => true],
1558
+ 'macOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true],
1559
+ 'iOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true]
1560
+ ];
1561
+
1562
+ $expected = $osExpectedTcp[$mappedOs];
1563
+ $ttlDiff = abs($fp['ttl'] - $expected['ttl']) / $expected['ttl'];
1564
+ $winDiff = abs($fp['windowSize'] - $expected['windowSize']) / $expected['windowSize'];
1565
+ $wsDiff = $expected['ws'] !== null && ($fp['ws'] ?? null) !== null ? abs($fp['ws'] - $expected['ws']) / $expected['ws'] : 0.0;
1566
+ $mssDiff = $expected['mss'] !== null && ($fp['mss'] ?? null) !== null ? abs($fp['mss'] - $expected['mss']) / $expected['mss'] : 0.0;
1567
+ $sackDiff = ($fp['sack'] ?? true) === ($expected['sack'] ?? true) ? 0.0 : 1.0;
1568
+
1569
+ $deviation = (
1570
+ min(1.0, $ttlDiff) * 0.50 +
1571
+ min(1.0, $winDiff) * 0.25 +
1572
+ min(1.0, $wsDiff) * 0.15 +
1573
+ min(1.0, $mssDiff) * 0.05 +
1574
+ $sackDiff * 0.05
1575
+ );
1576
+
1577
+ $tcpAnomalyScore = 0.0;
1578
+ if ($tcpOs !== $mappedOs && $tcpOs !== 'unknown') {
1579
+ $baseAnomaly = $mappedOs === 'Windows' ? 80.0 :
1580
+ (($mappedOs === 'macOS' || $mappedOs === 'iOS') ? 85.0 : 75.0);
1581
+ $tcpAnomalyScore = $baseAnomaly + ($deviation - 0.4) * 10.0;
1582
+ } else {
1583
+ $tcpAnomalyScore = $deviation * 40.0;
1584
+ }
1585
+
1586
+ $tcpAnomalyScore = max(0.0, min(100.0, round($tcpAnomalyScore, 1)));
1587
+ return ['tcpAnomalyScore' => $tcpAnomalyScore];
1588
+ }
1589
+
1317
1590
  /**
1318
1591
  * Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
1319
1592
  *
@@ -1330,6 +1603,47 @@ class RequestUtils
1330
1603
  return false;
1331
1604
  }
1332
1605
 
1606
+ if (str_contains($ticket, '.')) {
1607
+ $parts = explode('.', $ticket);
1608
+ if (count($parts) === 3) {
1609
+ $base64UrlDecode = function ($input) {
1610
+ return base64_decode(strtr($input, '-_', '+/'));
1611
+ };
1612
+ $iv = $base64UrlDecode($parts[0]);
1613
+ $encrypted = $base64UrlDecode($parts[1]);
1614
+ $signature = $base64UrlDecode($parts[2]);
1615
+ if ($iv && $encrypted && $signature && strlen($iv) === 16) {
1616
+ $key = hash('sha256', $secret ?: "fallback-dev-secret-32-chars-minimum", true);
1617
+ $expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
1618
+ if (hash_equals($expectedSignature, $signature)) {
1619
+ $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
1620
+ if ($decrypted !== false) {
1621
+ $ticketData = json_decode($decrypted, true);
1622
+ if ($ticketData) {
1623
+ $expiry = $ticketData['expiry'] ?? null;
1624
+ $originalIp = $ticketData['originalIp'] ?? null;
1625
+ $storedDeviceId = $ticketData['deviceId'] ?? '';
1626
+ $storedDeviceHash = $ticketData['deviceHash'] ?? '';
1627
+ if (!$expiry || (time() * 1000) > (int)$expiry) {
1628
+ return false;
1629
+ }
1630
+ if ($ip === $originalIp) {
1631
+ return true;
1632
+ }
1633
+ $currentSubnet = self::getIpSubnet($ip);
1634
+ $originalSubnet = self::getIpSubnet($originalIp);
1635
+ if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
1636
+ return true;
1637
+ }
1638
+ return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
1639
+ }
1640
+ }
1641
+ }
1642
+ }
1643
+ }
1644
+ return false;
1645
+ }
1646
+
1333
1647
  if (str_contains($ticket, '|')) {
1334
1648
  $parts = explode('|', $ticket);
1335
1649
  if (count($parts) < 3) return false;