@anonympins/fingerprint 0.4.4 → 0.4.5

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",
@@ -567,6 +568,50 @@ class RequestUtils
567
568
  return ['clientHintsInconsistencyScore' => 0.0];
568
569
  }
569
570
 
571
+ $fullVersionList = $context->getHeader('sec-ch-ua-full-version-list');
572
+ if (!empty($fullVersionList)) {
573
+ $chFullVersion = null;
574
+ $chFullBrowser = null;
575
+ if (preg_match_all('/"([^"]+)";v="([^"]+)"/', $fullVersionList, $matches, PREG_SET_ORDER)) {
576
+ foreach ($matches as $match) {
577
+ $brand = $match[1];
578
+ $version = $match[2];
579
+ if ($brand === 'Google Chrome' || $brand === 'Chromium' || $brand === 'Microsoft Edge') {
580
+ $chFullVersion = $version;
581
+ $chFullBrowser = $brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
582
+ if ($brand === 'Google Chrome' || $brand === 'Microsoft Edge') {
583
+ break;
584
+ }
585
+ }
586
+ }
587
+ }
588
+ if ($chFullVersion && $chFullBrowser) {
589
+ if (preg_match('/(Chrome|Edg)\/([\d\.]+)/', $ua, $uaFullMatches)) {
590
+ $uaBrowserMapped = $uaFullMatches[1] === 'Edg' ? 'Edge' : 'Chrome';
591
+ $uaFullVersion = $uaFullMatches[2];
592
+ if ($uaBrowserMapped === $chFullBrowser && $uaFullVersion !== $chFullVersion) {
593
+ $parts1 = array_map('intval', explode('.', $uaFullVersion));
594
+ $parts2 = array_map('intval', explode('.', $chFullVersion));
595
+ $diffIndex = -1;
596
+ $maxLen = max(count($parts1), count($parts2));
597
+ for ($i = 0; $i < $maxLen; $i++) {
598
+ $p1 = $parts1[$i] ?? 0;
599
+ $p2 = $parts2[$i] ?? 0;
600
+ if ($p1 !== $p2) {
601
+ $diffIndex = $i;
602
+ break;
603
+ }
604
+ }
605
+ $baseScores = [95.0, 90.0, 85.0, 80.0];
606
+ $baseScore = $baseScores[$diffIndex] ?? 80.0;
607
+ $delta = abs(($parts1[$diffIndex] ?? 0) - ($parts2[$diffIndex] ?? 0));
608
+ $finalFullScore = min(100.0, $baseScore + min(5.0, $delta * 5.0));
609
+ return ['clientHintsInconsistencyScore' => $finalFullScore];
610
+ }
611
+ }
612
+ }
613
+ }
614
+
570
615
  // 1. Extraire la version du navigateur depuis le User-Agent
571
616
  $uaVersion = null;
572
617
  if (preg_match('/(Chrome|Firefox|Edg|Safari)\/([\d\.]+)/', $ua, $uaMatches)) {
@@ -602,13 +647,19 @@ class RequestUtils
602
647
  return ['clientHintsInconsistencyScore' => 90.0];
603
648
  }
604
649
 
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];
650
+ $clientHintsInconsistencyScore = 0.0;
651
+ if ($versionDifference > 0) {
652
+ if ($versionDifference <= 2) {
653
+ $clientHintsInconsistencyScore = $versionDifference * 20.0;
654
+ } elseif ($versionDifference <= 7) {
655
+ $clientHintsInconsistencyScore = 40.0 + ($versionDifference - 2) * 8.0;
656
+ } else {
657
+ $clientHintsInconsistencyScore = min(100.0, 80.0 + ($versionDifference - 7) * 3.33);
658
+ }
659
+ $clientHintsInconsistencyScore = round($clientHintsInconsistencyScore, 1);
609
660
  }
610
661
 
611
- return ['clientHintsInconsistencyScore' => 0.0];
662
+ return ['clientHintsInconsistencyScore' => $clientHintsInconsistencyScore];
612
663
  }
613
664
  /**
614
665
  * Calcule les indicateurs comportementaux liés à l'historique de l'appareil.
@@ -1314,6 +1365,183 @@ class RequestUtils
1314
1365
  return hash_equals($expectedSignature, $storedSignature);
1315
1366
  }
1316
1367
 
1368
+ /**
1369
+ * Parse une trame TCP SYN brute (IPv4 ou IPv6).
1370
+ */
1371
+ public static function parseTcpSyn(?string $binary): ?array
1372
+ {
1373
+ if (!$binary || strlen($binary) < 40) return null;
1374
+ $ttl = 64;
1375
+ $tcpOffset = 20;
1376
+ $version = ord($binary[0]) >> 4;
1377
+
1378
+ if ($version === 4) {
1379
+ $ttl = ord($binary[8]);
1380
+ $ihl = ord($binary[0]) & 0x0f;
1381
+ $tcpOffset = $ihl * 4;
1382
+ } elseif ($version === 6) {
1383
+ $ttl = ord($binary[7]); // Hop Limit
1384
+ $tcpOffset = 40;
1385
+ } else {
1386
+ $tcpOffset = 0;
1387
+ $ttl = 64;
1388
+ }
1389
+
1390
+ if (strlen($binary) < $tcpOffset + 20) return null;
1391
+
1392
+ $windowSize = (ord($binary[$tcpOffset + 14]) << 8) | ord($binary[$tcpOffset + 15]);
1393
+ $dataOffset = (ord($binary[$tcpOffset + 12]) >> 4) * 4;
1394
+ $optionsEnd = $tcpOffset + $dataOffset;
1395
+
1396
+ $mss = null;
1397
+ $ws = null;
1398
+ $sack = false;
1399
+
1400
+ $i = $tcpOffset + 20;
1401
+ while ($i < $optionsEnd && $i < strlen($binary)) {
1402
+ $optType = ord($binary[$i]);
1403
+ if ($optType === 0) break;
1404
+ if ($optType === 1) {
1405
+ $i++;
1406
+ continue;
1407
+ }
1408
+ if ($i + 1 >= strlen($binary)) break;
1409
+ $optLen = ord($binary[$i + 1]);
1410
+ if ($optLen < 2 || $i + $optLen > strlen($binary)) break;
1411
+
1412
+ if ($optType === 2 && $optLen === 4) {
1413
+ $mss = (ord($binary[$i + 2]) << 8) | ord($binary[$i + 3]);
1414
+ } elseif ($optType === 3 && $optLen === 3) {
1415
+ $ws = ord($binary[$i + 2]);
1416
+ } elseif ($optType === 4 && $optLen === 2) {
1417
+ $sack = true;
1418
+ }
1419
+ $i += $optLen;
1420
+ }
1421
+
1422
+ return ['ttl' => $ttl, 'windowSize' => $windowSize, 'mss' => $mss, 'ws' => $ws, 'sack' => $sack];
1423
+ }
1424
+
1425
+ /**
1426
+ * Classifie l'OS à partir du fingerprint de la pile TCP/IP.
1427
+ */
1428
+ public static function classifyTcpOs(?array $fingerprint): string
1429
+ {
1430
+ if (!$fingerprint) return 'unknown';
1431
+ $ttl = $fingerprint['ttl'] ?? 64;
1432
+ $windowSize = $fingerprint['windowSize'] ?? 0;
1433
+ $ws = $fingerprint['ws'] ?? null;
1434
+
1435
+ if ($ttl > 64 && $ttl <= 128) {
1436
+ return 'Windows';
1437
+ }
1438
+ if ($ttl > 32 && $ttl <= 64) {
1439
+ if ($windowSize === 29200 || $windowSize === 14600 || $windowSize === 5840) {
1440
+ return 'Linux';
1441
+ }
1442
+ return 'Linux';
1443
+ }
1444
+ if ($ttl <= 64) {
1445
+ if ($windowSize === 65535 && ($ws === 6 || $ws === 8 || $ws === 5)) {
1446
+ return 'macOS/iOS';
1447
+ }
1448
+ }
1449
+ if ($ttl > 64) return 'Windows';
1450
+ if ($ttl > 0) return 'Linux';
1451
+ return 'unknown';
1452
+ }
1453
+
1454
+ /**
1455
+ * Détecte les anomalies de pile réseau par rapport au User-Agent.
1456
+ */
1457
+ public static function getTcpAnomalyScore(RequestContext $context): array
1458
+ {
1459
+ $fp = null;
1460
+ $rawTcpBinary = $context->getHeader('x-raw-tcp-binary') ?? $context->headers['x-raw-tcp-binary'] ?? null;
1461
+ if ($rawTcpBinary) {
1462
+ $binary = @hex2bin($rawTcpBinary) ?: $rawTcpBinary;
1463
+ $fp = self::parseTcpSyn($binary);
1464
+ }
1465
+
1466
+ if (!$fp) {
1467
+ $tcpHeader = $context->getHeader('x-tcp-fingerprint') ?? $context->tcpFingerprint ?? null;
1468
+ if ($tcpHeader && is_string($tcpHeader)) {
1469
+ $parts = explode(':', $tcpHeader);
1470
+ if (count($parts) >= 2) {
1471
+ $fp = [
1472
+ 'ttl' => (int)$parts[0],
1473
+ 'windowSize' => (int)$parts[1],
1474
+ 'mss' => isset($parts[2]) ? (int)$parts[2] : null,
1475
+ 'ws' => isset($parts[3]) ? (int)$parts[3] : null,
1476
+ 'sack' => isset($parts[4]) && ($parts[4] === '1' || $parts[4] === 'true')
1477
+ ];
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ if (!$fp) {
1483
+ return ['tcpAnomalyScore' => 0.0];
1484
+ }
1485
+
1486
+ $tcpOs = self::classifyTcpOs($fp);
1487
+ $ua = $context->getHeader('user-agent') ?? '';
1488
+ $uaParts = self::parseUserAgent($ua);
1489
+ $uaOs = $uaParts['os'] ?? null;
1490
+
1491
+ if (!$uaOs || $tcpOs === 'unknown') {
1492
+ return ['tcpAnomalyScore' => 0.0];
1493
+ }
1494
+
1495
+ $mappedOs = null;
1496
+ if (str_starts_with($uaOs, 'Windows')) {
1497
+ $mappedOs = 'Windows';
1498
+ } elseif (str_starts_with($uaOs, 'Mac') || str_starts_with($uaOs, 'macOS')) {
1499
+ $mappedOs = 'macOS';
1500
+ } elseif (str_starts_with($uaOs, 'iOS')) {
1501
+ $mappedOs = 'iOS';
1502
+ } elseif (str_starts_with($uaOs, 'Linux')) {
1503
+ $mappedOs = 'Linux';
1504
+ }
1505
+
1506
+ if ($mappedOs === null) {
1507
+ return ['tcpAnomalyScore' => 0.0];
1508
+ }
1509
+
1510
+ $osExpectedTcp = [
1511
+ 'Windows' => ['ttl' => 128, 'windowSize' => 64240, 'ws' => 8, 'mss' => 1460, 'sack' => true],
1512
+ 'Linux' => ['ttl' => 64, 'windowSize' => 29200, 'ws' => 7, 'mss' => 1460, 'sack' => true],
1513
+ 'macOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true],
1514
+ 'iOS' => ['ttl' => 64, 'windowSize' => 65535, 'ws' => 6, 'mss' => 1460, 'sack' => true]
1515
+ ];
1516
+
1517
+ $expected = $osExpectedTcp[$mappedOs];
1518
+ $ttlDiff = abs($fp['ttl'] - $expected['ttl']) / $expected['ttl'];
1519
+ $winDiff = abs($fp['windowSize'] - $expected['windowSize']) / $expected['windowSize'];
1520
+ $wsDiff = $expected['ws'] !== null && ($fp['ws'] ?? null) !== null ? abs($fp['ws'] - $expected['ws']) / $expected['ws'] : 0.0;
1521
+ $mssDiff = $expected['mss'] !== null && ($fp['mss'] ?? null) !== null ? abs($fp['mss'] - $expected['mss']) / $expected['mss'] : 0.0;
1522
+ $sackDiff = ($fp['sack'] ?? true) === ($expected['sack'] ?? true) ? 0.0 : 1.0;
1523
+
1524
+ $deviation = (
1525
+ min(1.0, $ttlDiff) * 0.50 +
1526
+ min(1.0, $winDiff) * 0.25 +
1527
+ min(1.0, $wsDiff) * 0.15 +
1528
+ min(1.0, $mssDiff) * 0.05 +
1529
+ $sackDiff * 0.05
1530
+ );
1531
+
1532
+ $tcpAnomalyScore = 0.0;
1533
+ if ($tcpOs !== $mappedOs && $tcpOs !== 'unknown') {
1534
+ $baseAnomaly = $mappedOs === 'Windows' ? 80.0 :
1535
+ (($mappedOs === 'macOS' || $mappedOs === 'iOS') ? 85.0 : 75.0);
1536
+ $tcpAnomalyScore = $baseAnomaly + ($deviation - 0.4) * 10.0;
1537
+ } else {
1538
+ $tcpAnomalyScore = $deviation * 40.0;
1539
+ }
1540
+
1541
+ $tcpAnomalyScore = max(0.0, min(100.0, round($tcpAnomalyScore, 1)));
1542
+ return ['tcpAnomalyScore' => $tcpAnomalyScore];
1543
+ }
1544
+
1317
1545
  /**
1318
1546
  * Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
1319
1547
  *
@@ -1330,6 +1558,47 @@ class RequestUtils
1330
1558
  return false;
1331
1559
  }
1332
1560
 
1561
+ if (str_contains($ticket, '.')) {
1562
+ $parts = explode('.', $ticket);
1563
+ if (count($parts) === 3) {
1564
+ $base64UrlDecode = function ($input) {
1565
+ return base64_decode(strtr($input, '-_', '+/'));
1566
+ };
1567
+ $iv = $base64UrlDecode($parts[0]);
1568
+ $encrypted = $base64UrlDecode($parts[1]);
1569
+ $signature = $base64UrlDecode($parts[2]);
1570
+ if ($iv && $encrypted && $signature && strlen($iv) === 16) {
1571
+ $key = hash('sha256', $secret ?: "fallback-dev-secret-32-chars-minimum", true);
1572
+ $expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
1573
+ if (hash_equals($expectedSignature, $signature)) {
1574
+ $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
1575
+ if ($decrypted !== false) {
1576
+ $ticketData = json_decode($decrypted, true);
1577
+ if ($ticketData) {
1578
+ $expiry = $ticketData['expiry'] ?? null;
1579
+ $originalIp = $ticketData['originalIp'] ?? null;
1580
+ $storedDeviceId = $ticketData['deviceId'] ?? '';
1581
+ $storedDeviceHash = $ticketData['deviceHash'] ?? '';
1582
+ if (!$expiry || (time() * 1000) > (int)$expiry) {
1583
+ return false;
1584
+ }
1585
+ if ($ip === $originalIp) {
1586
+ return true;
1587
+ }
1588
+ $currentSubnet = self::getIpSubnet($ip);
1589
+ $originalSubnet = self::getIpSubnet($originalIp);
1590
+ if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
1591
+ return true;
1592
+ }
1593
+ return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
1594
+ }
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ return false;
1600
+ }
1601
+
1333
1602
  if (str_contains($ticket, '|')) {
1334
1603
  $parts = explode('|', $ticket);
1335
1604
  if (count($parts) < 3) return false;