@anonympins/fingerprint 0.5.0 → 0.5.2
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.
- package/CHANGELOG.md +31 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +224 -24
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +382 -63
- package/src/js/fingerprint.utils.js +213 -183
- package/src/js/gpu_pow.solver.js +312 -0
- package/src/js/library.js +16 -13
- package/src/js/pow.solver.inline.js +138 -27
- package/src/js/pow.solver.js +141 -28
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +150 -3
- package/src/js/tests/gpu_pow.test.js +199 -0
- package/src/js/tests/pow.solver.test.js +4 -4
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +158 -27
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +124 -2
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +77 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/RequestUtils.php +120 -0
|
@@ -14,6 +14,48 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
|
14
14
|
*/
|
|
15
15
|
class ChallengeUtils
|
|
16
16
|
{
|
|
17
|
+
private static function fround(float $value): float
|
|
18
|
+
{
|
|
19
|
+
return unpack('f', pack('f', $value))[1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static function hashSeedToFloat(string $seed): float
|
|
23
|
+
{
|
|
24
|
+
$hash = 0;
|
|
25
|
+
for ($i = 0; $i < strlen($seed); $i++) {
|
|
26
|
+
$hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
|
|
27
|
+
if ($hash & 0x80000000) {
|
|
28
|
+
$hash = $hash - 0x100000000;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return abs($hash % 1000000) / 1000000;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public static function verifyGpuPow(string $seed, int $iterations, string $solution, array $sampleIndices = [0, 12, 35, 57]): bool
|
|
35
|
+
{
|
|
36
|
+
$values = explode(',', $solution);
|
|
37
|
+
if (count($values) !== 64) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
$numericSeed = self::hashSeedToFloat($seed);
|
|
41
|
+
$r = 3.9999;
|
|
42
|
+
foreach ($sampleIndices as $idx) {
|
|
43
|
+
if ($idx < 0 || $idx >= 64) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
$x = self::fround($numericSeed + $idx * 0.015);
|
|
47
|
+
$rFloat = self::fround($r);
|
|
48
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
49
|
+
$x = self::fround($rFloat * $x * self::fround(1.0 - $x));
|
|
50
|
+
}
|
|
51
|
+
$clientVal = (float)$values[$idx];
|
|
52
|
+
if (abs($clientVal - $x) > 1e-4) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
private const TRAP_URL_TEMPLATES = [
|
|
18
60
|
'/includes/config-{RANDOM}.php',
|
|
19
61
|
'/.env.{RANDOM}',
|
|
@@ -596,6 +638,55 @@ class ChallengeUtils
|
|
|
596
638
|
/**
|
|
597
639
|
* Vérifie une solution de PoW mémoire.
|
|
598
640
|
*/
|
|
641
|
+
private static function getChallengedIndices(string $seed, int $solution, int $numBlocks, int $k = 4): array
|
|
642
|
+
{
|
|
643
|
+
$indices = [];
|
|
644
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $solution), '4294967296');
|
|
645
|
+
for ($i = 0; $i < $k; $i++) {
|
|
646
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
647
|
+
$indices[] = abs($h) % $numBlocks;
|
|
648
|
+
}
|
|
649
|
+
return $indices;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private static function verifyMerkleProof(string $leafHash, int $index, array $proof, string $root): bool
|
|
653
|
+
{
|
|
654
|
+
$currentHash = $leafHash;
|
|
655
|
+
$idx = $index;
|
|
656
|
+
foreach ($proof as $sibling) {
|
|
657
|
+
$combined = ($idx % 2 === 0) ? $currentHash . $sibling : $sibling . $currentHash;
|
|
658
|
+
$currentHash = hash('sha256', hex2bin($combined));
|
|
659
|
+
$idx = (int)floor($idx / 2);
|
|
660
|
+
}
|
|
661
|
+
return $currentHash === $root;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private static function verifyMemoryPoWLegacy(string $nonce, int $solution, int $difficulty, string $clientSecret): bool
|
|
665
|
+
{
|
|
666
|
+
$size = $difficulty * 1024 * 1024;
|
|
667
|
+
$iterations = (int)floor($size / 16);
|
|
668
|
+
$buffer = new \SplFixedArray((int)floor($size / 4));
|
|
669
|
+
|
|
670
|
+
$seed = ":{$nonce}:{$clientSecret}";
|
|
671
|
+
$h = 0;
|
|
672
|
+
foreach (unpack('C*', $seed) as $byte) {
|
|
673
|
+
$h += $byte;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for ($i = 0; $i < count($buffer); $i++) {
|
|
677
|
+
$buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
$finalHash = 0;
|
|
681
|
+
$addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
|
|
682
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
683
|
+
$addr = $buffer[$addr] % count($buffer);
|
|
684
|
+
$finalHash ^= $addr;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return $finalHash === $solution;
|
|
688
|
+
}
|
|
689
|
+
|
|
599
690
|
public static function verifyMemoryPoW(
|
|
600
691
|
string $nonce,
|
|
601
692
|
string $solution,
|
|
@@ -612,37 +703,69 @@ class ChallengeUtils
|
|
|
612
703
|
return false;
|
|
613
704
|
}
|
|
614
705
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
706
|
+
$data = json_decode($solution, true);
|
|
707
|
+
if (json_last_error() !== JSON_ERROR_NONE || !isset($data['solution']) || !isset($data['merkleRoot']) || !isset($data['proofs'])) {
|
|
708
|
+
if ($difficulty <= 4 && preg_match('/^\d+$/', $solution)) {
|
|
709
|
+
return self::verifyMemoryPoWLegacy($nonce, (int)$solution, $difficulty, $clientSecret);
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
619
712
|
}
|
|
620
713
|
|
|
621
|
-
$
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
$iterations = (int)floor($size / 16);
|
|
626
|
-
$buffer = new \SplFixedArray((int)floor($size / 4));
|
|
714
|
+
$sol = $data['solution'];
|
|
715
|
+
$merkleRoot = $data['merkleRoot'];
|
|
716
|
+
$proofs = $data['proofs'];
|
|
627
717
|
|
|
718
|
+
$numBlocks = $difficulty * 256;
|
|
628
719
|
$seed = ":{$nonce}:{$clientSecret}";
|
|
629
|
-
$h = 0;
|
|
630
|
-
foreach (unpack('C*', $seed) as $byte) {
|
|
631
|
-
$h += $byte;
|
|
632
|
-
}
|
|
633
720
|
|
|
634
|
-
|
|
635
|
-
|
|
721
|
+
$challengedIndices = self::getChallengedIndices($seed, (int)$sol, $numBlocks, 4);
|
|
722
|
+
|
|
723
|
+
foreach ($challengedIndices as $b) {
|
|
724
|
+
$proof = $proofs[$b] ?? $proofs[(string)$b] ?? null;
|
|
725
|
+
if ($proof === null) return false;
|
|
726
|
+
|
|
727
|
+
$blockBytes = '';
|
|
728
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $b), '4294967296');
|
|
729
|
+
for ($i = 0; $i < 1024; $i++) {
|
|
730
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
731
|
+
$blockBytes .= pack('V', $h);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
$expectedLeaf = hash('sha256', $blockBytes);
|
|
735
|
+
|
|
736
|
+
if (!self::verifyMerkleProof($expectedLeaf, $b, $proof, $merkleRoot)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
636
739
|
}
|
|
637
740
|
|
|
638
|
-
$
|
|
639
|
-
$
|
|
741
|
+
$blockCache = [];
|
|
742
|
+
$getBlockElement = function (int $blockIdx, int $elementIdx) use (&$blockCache, $seed): int {
|
|
743
|
+
if (!isset($blockCache[$blockIdx])) {
|
|
744
|
+
$block = [];
|
|
745
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $blockIdx), '4294967296');
|
|
746
|
+
for ($i = 0; $i < 1024; $i++) {
|
|
747
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
748
|
+
$block[$i] = $h;
|
|
749
|
+
}
|
|
750
|
+
$blockCache[$blockIdx] = $block;
|
|
751
|
+
}
|
|
752
|
+
return $blockCache[$blockIdx][$elementIdx];
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
$totalElements = $numBlocks * 1024;
|
|
756
|
+
$addr = $totalElements > 0 ? $getBlockElement(0, 0) % $totalElements : 0;
|
|
757
|
+
$addr = $addr & 0xffffffff;
|
|
758
|
+
$expectedSolution = 0;
|
|
759
|
+
$iterations = 1024;
|
|
640
760
|
for ($i = 0; $i < $iterations; $i++) {
|
|
641
|
-
$
|
|
642
|
-
$
|
|
761
|
+
$blockIdx = (int)floor($addr / 1024);
|
|
762
|
+
$elementIdx = $addr % 1024;
|
|
763
|
+
$addr = $getBlockElement($blockIdx, $elementIdx) % $totalElements;
|
|
764
|
+
$addr = $addr & 0xffffffff;
|
|
765
|
+
$expectedSolution ^= $addr;
|
|
643
766
|
}
|
|
644
767
|
|
|
645
|
-
return $
|
|
768
|
+
return $expectedSolution === (int)$sol;
|
|
646
769
|
}
|
|
647
770
|
|
|
648
771
|
/**
|
|
@@ -717,11 +840,15 @@ class ChallengeUtils
|
|
|
717
840
|
$solverCode = self::getPowSolverCode();
|
|
718
841
|
$queriesJson = json_encode($queries);
|
|
719
842
|
|
|
843
|
+
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
844
|
+
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
845
|
+
$safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
846
|
+
|
|
720
847
|
$challengeScript = <<<JS
|
|
721
848
|
async function solve() {
|
|
722
|
-
const nonce =
|
|
723
|
-
const path =
|
|
724
|
-
const clientSecret =
|
|
849
|
+
const nonce = {$safeNonce};
|
|
850
|
+
const path = {$safePath};
|
|
851
|
+
const clientSecret = {$safeClientSecret};
|
|
725
852
|
const queries = {$queriesJson};
|
|
726
853
|
const sizeMb = {$sizeMb};
|
|
727
854
|
const nodeId = "{$nodeId}";
|
|
@@ -824,11 +951,15 @@ JS;
|
|
|
824
951
|
));
|
|
825
952
|
$trapContainerHtml = "<div style=\"position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;\" aria-hidden=\"true\">{$trapLinksHtml}</div>";
|
|
826
953
|
|
|
954
|
+
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
955
|
+
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
956
|
+
$safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
957
|
+
|
|
827
958
|
$challengeScript = <<<JS
|
|
828
959
|
async function solve() {
|
|
829
|
-
const nonce =
|
|
830
|
-
const path =
|
|
831
|
-
const clientSecret =
|
|
960
|
+
const nonce = {$safeNonce};
|
|
961
|
+
const path = {$safePath};
|
|
962
|
+
const clientSecret = {$safeClientSecret};
|
|
832
963
|
const cpuTarget = BigInt("0x" + "{$target}");
|
|
833
964
|
const memDifficulty = {$memoryDifficulty};
|
|
834
965
|
const baseBlock = new Uint8Array({$baseBlockBytes});
|
|
@@ -41,6 +41,8 @@ class SecurityProfiles
|
|
|
41
41
|
'subnetScore' => 0.5, // Pénalise les sous-réseaux IP avec une activité suspecte agrégée
|
|
42
42
|
'botnetClusterScore' => 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
43
43
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
44
|
+
'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
|
|
45
|
+
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
44
46
|
|
|
45
47
|
],
|
|
46
48
|
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
@@ -87,6 +89,8 @@ class SecurityProfiles
|
|
|
87
89
|
'subnetScore' => 0.7, // Poids plus élevé en mode strict
|
|
88
90
|
'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
89
91
|
'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
|
|
92
|
+
'quicAnomalyScore' => 1.0, // NEW: Anomalie QUIC
|
|
93
|
+
'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
|
|
90
94
|
|
|
91
95
|
],
|
|
92
96
|
'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
|
|
@@ -133,6 +137,7 @@ class SecurityProfiles
|
|
|
133
137
|
'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
|
|
134
138
|
'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
135
139
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
140
|
+
'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
|
|
136
141
|
|
|
137
142
|
],
|
|
138
143
|
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
@@ -181,6 +186,8 @@ class SecurityProfiles
|
|
|
181
186
|
'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
|
|
182
187
|
'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
183
188
|
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
189
|
+
'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
|
|
190
|
+
'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
|
|
184
191
|
|
|
185
192
|
],
|
|
186
193
|
'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
|
|
@@ -228,6 +235,8 @@ class SecurityProfiles
|
|
|
228
235
|
'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
|
|
229
236
|
'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
230
237
|
'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
|
|
238
|
+
'quicAnomalyScore' => 0.9, // NEW: Anomalie QUIC
|
|
239
|
+
'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
|
|
231
240
|
|
|
232
241
|
],
|
|
233
242
|
'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
private ?Logger $logger = null;
|
|
28
28
|
private bool $dryRun;
|
|
29
29
|
|
|
30
|
+
private static ?array $googlebotEntries = null;
|
|
31
|
+
private static ?array $yandexEntries = null;
|
|
32
|
+
private static ?array $bingbotEntries = null;
|
|
33
|
+
|
|
30
34
|
public function __construct(array $securityConfig)
|
|
31
35
|
{
|
|
32
36
|
$this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
|
|
@@ -61,6 +65,19 @@
|
|
|
61
65
|
$this->validateConfig($securityConfig);
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
70
|
+
* sans nécessiter de redémarrage.
|
|
71
|
+
*
|
|
72
|
+
* @param array $newConfig La nouvelle configuration (partielle ou complète).
|
|
73
|
+
*/
|
|
74
|
+
public function updateConfig(array $newConfig): void
|
|
75
|
+
{
|
|
76
|
+
$this->validateConfig($newConfig);
|
|
77
|
+
$this->securityConfig = SecurityProfiles::deepMerge($this->securityConfig, $newConfig);
|
|
78
|
+
$this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
private function validateConfig(array $config): void
|
|
65
82
|
{
|
|
66
83
|
if (empty($config)) {
|
|
@@ -90,7 +107,103 @@
|
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
}
|
|
110
|
+
private static function loadBotWhitelist(string $filename, array $fallbackEntries): array
|
|
111
|
+
{
|
|
112
|
+
$configDir = dirname(__DIR__, 2) . '/config';
|
|
113
|
+
$filePath = $configDir . '/' . $filename;
|
|
114
|
+
if (file_exists($filePath)) {
|
|
115
|
+
try {
|
|
116
|
+
$content = file_get_contents($filePath);
|
|
117
|
+
if ($content !== false) {
|
|
118
|
+
$decoded = json_decode($content, true);
|
|
119
|
+
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
120
|
+
return $decoded;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (\Throwable $e) {
|
|
124
|
+
error_log("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return $fallbackEntries;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public static function googlebot_whitelist(): array
|
|
131
|
+
{
|
|
132
|
+
if (self::$googlebotEntries === null) {
|
|
133
|
+
self::$googlebotEntries = self::loadBotWhitelist('googlebot.json', [
|
|
134
|
+
"2001:4860:4801:10::/64",
|
|
135
|
+
"2001:4860:4801:11::/64",
|
|
136
|
+
"2001:4860:4801:12::/64",
|
|
137
|
+
"66.249.79.64"
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
'type' => 'allowlist',
|
|
142
|
+
'entries' => self::$googlebotEntries
|
|
143
|
+
];
|
|
144
|
+
}
|
|
93
145
|
|
|
146
|
+
public static function yandex_whitelist(): array
|
|
147
|
+
{
|
|
148
|
+
if (self::$yandexEntries === null) {
|
|
149
|
+
self::$yandexEntries = self::loadBotWhitelist('yandex.json', [
|
|
150
|
+
"2a02:6b8::/29",
|
|
151
|
+
"5.45.192.0/18",
|
|
152
|
+
"213.180.192.0/19"
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
return [
|
|
156
|
+
'type' => 'allowlist',
|
|
157
|
+
'entries' => self::$yandexEntries
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public static function bingbot_whitelist(): array
|
|
162
|
+
{
|
|
163
|
+
if (self::$bingbotEntries === null) {
|
|
164
|
+
self::$bingbotEntries = self::loadBotWhitelist('bingbot.json', [
|
|
165
|
+
"157.55.39.0/24",
|
|
166
|
+
"207.46.13.0/24",
|
|
167
|
+
"40.77.178.0/23"
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
return [
|
|
171
|
+
'type' => 'allowlist',
|
|
172
|
+
'entries' => self::$bingbotEntries
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public static function default_whitelist(): array
|
|
177
|
+
{
|
|
178
|
+
return [
|
|
179
|
+
self::googlebot_whitelist(),
|
|
180
|
+
self::bingbot_whitelist(),
|
|
181
|
+
self::yandex_whitelist(),
|
|
182
|
+
['userAgent' => 'Googlebot', 'hostnameSuffix' => '.googlebot.com'],
|
|
183
|
+
['userAgent' => 'Google-Extended', 'hostnameSuffix' => '.google.com'],
|
|
184
|
+
['userAgent' => 'AdsBot-Google', 'hostnameSuffix' => '.googlebot.com'],
|
|
185
|
+
['userAgent' => 'Mediapartners-Google', 'hostnameSuffix' => '.google.com'],
|
|
186
|
+
['userAgent' => 'Google-InspectionTool', 'hostnameSuffix' => '.google.com'],
|
|
187
|
+
['userAgent' => '(bingbot|adidxbot)', 'hostnameSuffix' => '.search.msn.com'],
|
|
188
|
+
['userAgent' => 'DuckDuckBot', 'hostnameSuffix' => '.duckduckgo.com'],
|
|
189
|
+
['userAgent' => 'YandexBot', 'hostnameSuffix' => '.yandex.com'],
|
|
190
|
+
['userAgent' => 'YandexImages', 'hostnameSuffix' => '.yandex.com'],
|
|
191
|
+
['userAgent' => 'Baiduspider', 'hostnameSuffix' => '.crawl.baidu.com'],
|
|
192
|
+
['userAgent' => 'Slurp', 'hostnameSuffix' => '.crawl.yahoo.net'],
|
|
193
|
+
['userAgent' => 'Sogou web spider', 'hostnameSuffix' => '.sogou.com'],
|
|
194
|
+
['userAgent' => 'Exabot', 'hostnameSuffix' => '.exabot.com'],
|
|
195
|
+
['userAgent' => 'ia_archiver', 'hostnameSuffix' => '.alexa.com'],
|
|
196
|
+
['userAgent' => 'SeznamBot', 'hostnameSuffix' => '.seznam.cz'],
|
|
197
|
+
['userAgent' => 'Mail.RU_Bot', 'hostnameSuffix' => '.mail.ru'],
|
|
198
|
+
['userAgent' => 'Yeti', 'hostnameSuffix' => '.naver.com'],
|
|
199
|
+
['userAgent' => 'AhrefsBot', 'hostnameSuffix' => '.ahrefs.com'],
|
|
200
|
+
['userAgent' => 'SemrushBot', 'hostnameSuffix' => '.semrush.com'],
|
|
201
|
+
['userAgent' => 'MJ12bot', 'hostnameSuffix' => '.mj12bot.com'],
|
|
202
|
+
['userAgent' => 'rogerbot', 'hostnameSuffix' => '.moz.com'],
|
|
203
|
+
['userAgent' => 'DotBot', 'hostnameSuffix' => '.moz.com'],
|
|
204
|
+
['userAgent' => 'Screaming Frog SEO Spider', 'hostnameSuffix' => '.screamingfrog.co.uk'],
|
|
205
|
+
];
|
|
206
|
+
}
|
|
94
207
|
private function log(string $message, array $data = [], string $level = 'info'): void
|
|
95
208
|
{
|
|
96
209
|
if ($this->verbose) {
|
|
@@ -467,6 +580,12 @@
|
|
|
467
580
|
// Score d'anomalie de pile TCP/IP
|
|
468
581
|
$tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
|
|
469
582
|
|
|
583
|
+
// Score d'anomalie de flux QUIC/HTTP3
|
|
584
|
+
$quicAnomaly = RequestUtils::getQuicAnomalyScore($context);
|
|
585
|
+
|
|
586
|
+
// Score d'anomalie de rendu d'affichage (V-Sync)
|
|
587
|
+
$renderingAnomaly = RequestUtils::getRenderingAnomalyScore($context);
|
|
588
|
+
|
|
470
589
|
// Assemblage du vecteur de suspicion final
|
|
471
590
|
$suspicionVector = array_merge($suspicionVector, [
|
|
472
591
|
'inconsistencyScore' => $inconsistencyScore,
|
|
@@ -486,6 +605,8 @@
|
|
|
486
605
|
'subnetScore' => $subnetScore['subnetScore'],
|
|
487
606
|
'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
|
|
488
607
|
'tcpAnomalyScore' => $tcpAnomaly['tcpAnomalyScore'],
|
|
608
|
+
'quicAnomalyScore' => $quicAnomaly['quicAnomalyScore'],
|
|
609
|
+
'renderingAnomalyScore' => $renderingAnomaly['renderingAnomalyScore'],
|
|
489
610
|
]);
|
|
490
611
|
|
|
491
612
|
// Sauvegarder l'état mis à jour de l'appareil dans le store
|
|
@@ -1018,8 +1139,9 @@
|
|
|
1018
1139
|
'target' => ChallengeUtils::calculateCpuTarget($suspicionFactor, $this->securityConfig),
|
|
1019
1140
|
'path' => $context->path,
|
|
1020
1141
|
];
|
|
1021
|
-
|
|
1022
|
-
|
|
1142
|
+
|
|
1143
|
+
// Alignement linéaire parfait du ratio d'effort CPU/Mémoire
|
|
1144
|
+
$memActivationFactor = $suspicionFactor;
|
|
1023
1145
|
$memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
|
|
1024
1146
|
|
|
1025
1147
|
$originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
|
|
@@ -38,6 +38,7 @@ class RequestContext
|
|
|
38
38
|
public ?string $ja4s = null;
|
|
39
39
|
public ?string $ja4h = null;
|
|
40
40
|
public ?string $http2Fingerprint = null;
|
|
41
|
+
public ?string $quicFingerprint = null;
|
|
41
42
|
public ?string $tcpFingerprint = null;
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -77,6 +78,7 @@ class RequestContext
|
|
|
77
78
|
$this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
|
|
78
79
|
$this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
|
|
79
80
|
$this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
|
|
81
|
+
$this->quicFingerprint = $this->headers['x-quic-fp'] ?? null;
|
|
80
82
|
$this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
|
|
81
83
|
}
|
|
82
84
|
/**
|
|
@@ -10,6 +10,83 @@ use PHPUnit\Framework\TestCase;
|
|
|
10
10
|
|
|
11
11
|
class ChallengeUtilsTest extends TestCase
|
|
12
12
|
{
|
|
13
|
+
public function testGpuPowDeterministicHashing(): void
|
|
14
|
+
{
|
|
15
|
+
$seed = 'test-seed-determinism';
|
|
16
|
+
$f1 = ChallengeUtils::hashSeedToFloat($seed);
|
|
17
|
+
$f2 = ChallengeUtils::hashSeedToFloat($seed);
|
|
18
|
+
$f3 = ChallengeUtils::hashSeedToFloat('different-seed');
|
|
19
|
+
|
|
20
|
+
$this->assertEquals($f1, $f2, 'Seed hashing must be deterministic.');
|
|
21
|
+
$this->assertGreaterThanOrEqual(0, $f1);
|
|
22
|
+
$this->assertLessThan(1, $f1);
|
|
23
|
+
$this->assertNotEquals($f1, $f3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function testGpuPowVerificationRejectsTamperedSolution(): void
|
|
27
|
+
{
|
|
28
|
+
$seed = 'php-verification-seed';
|
|
29
|
+
$iterations = 100;
|
|
30
|
+
|
|
31
|
+
// Generate correct solutions mimicking the GPU solver trajectory
|
|
32
|
+
$numericSeed = ChallengeUtils::hashSeedToFloat($seed);
|
|
33
|
+
$r = 3.9999;
|
|
34
|
+
$solutions = [];
|
|
35
|
+
$fround = function (float $value): float {
|
|
36
|
+
return unpack('f', pack('f', $value))[1];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for ($idx = 0; $idx < 64; $idx++) {
|
|
40
|
+
$x = $fround($numericSeed + $idx * 0.015);
|
|
41
|
+
$rFloat = $fround($r);
|
|
42
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
43
|
+
$x = $fround($rFloat * $x * $fround(1.0 - $x));
|
|
44
|
+
}
|
|
45
|
+
$solutions[] = number_format($x, 6, '.', '');
|
|
46
|
+
}
|
|
47
|
+
$validSolution = implode(',', $solutions);
|
|
48
|
+
|
|
49
|
+
$this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $validSolution));
|
|
50
|
+
|
|
51
|
+
// Tamper with value on verified sample index 12
|
|
52
|
+
$tampered = $solutions;
|
|
53
|
+
$tampered[12] = (string)((float)$tampered[12] + 0.0002); // Out of tolerance
|
|
54
|
+
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public function testGpuPowVerification(): void
|
|
58
|
+
{
|
|
59
|
+
$seed = 'php-gpu-test-seed';
|
|
60
|
+
$iterations = 50;
|
|
61
|
+
|
|
62
|
+
$hash = 0;
|
|
63
|
+
for ($i = 0; $i < strlen($seed); $i++) {
|
|
64
|
+
$hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
|
|
65
|
+
if ($hash & 0x80000000) {
|
|
66
|
+
$hash = $hash - 0x100000000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
$numericSeed = abs($hash % 1000000) / 1000000;
|
|
70
|
+
$r = 3.9999;
|
|
71
|
+
$solutions = [];
|
|
72
|
+
$fround = function (float $value): float {
|
|
73
|
+
return unpack('f', pack('f', $value))[1];
|
|
74
|
+
};
|
|
75
|
+
for ($idx = 0; $idx < 64; $idx++) {
|
|
76
|
+
$x = $fround($numericSeed + $idx * 0.015);
|
|
77
|
+
$rFloat = $fround($r);
|
|
78
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
79
|
+
$x = $fround($rFloat * $x * $fround(1.0 - $x));
|
|
80
|
+
}
|
|
81
|
+
$solutions[] = number_format($x, 6, '.', '');
|
|
82
|
+
}
|
|
83
|
+
$solutionString = implode(',', $solutions);
|
|
84
|
+
$this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
|
|
85
|
+
$tamperedSolutions = $solutions;
|
|
86
|
+
$tamperedSolutions[0] = (string)((float)$tamperedSolutions[0] + 0.1);
|
|
87
|
+
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
|
|
88
|
+
}
|
|
89
|
+
|
|
13
90
|
protected function setUp(): void
|
|
14
91
|
{
|
|
15
92
|
// Définir une clé secrète pour les tests
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
8
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
|
+
|
|
11
|
+
class QuicFingerprintTest extends TestCase
|
|
12
|
+
{
|
|
13
|
+
public function testGetQuicAnomalyScoreReturnsZeroIfNoFp(): void
|
|
14
|
+
{
|
|
15
|
+
$context = $this->createMock(RequestContext::class);
|
|
16
|
+
$context->method('getHeader')->willReturn(null);
|
|
17
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
18
|
+
$this->assertEquals(0.0, $res['quicAnomalyScore']);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function testGetQuicAnomalyScoreDetectsSpoofedChrome(): void
|
|
22
|
+
{
|
|
23
|
+
$context = $this->createMock(RequestContext::class);
|
|
24
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
25
|
+
if ($name === 'user-agent') {
|
|
26
|
+
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
27
|
+
}
|
|
28
|
+
if ($name === 'x-quic-fp') {
|
|
29
|
+
return '1;1=65536,4=50;i=0';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
35
|
+
$this->assertEquals(100.0, $res['quicAnomalyScore']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testGetQuicAnomalyScoreAllowsLegitChrome(): void
|
|
39
|
+
{
|
|
40
|
+
$context = $this->createMock(RequestContext::class);
|
|
41
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
42
|
+
if ($name === 'user-agent') {
|
|
43
|
+
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
44
|
+
}
|
|
45
|
+
if ($name === 'x-quic-fp') {
|
|
46
|
+
return '1;1=1572864,4=100;u=2,i';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
52
|
+
$this->assertEquals(0.0, $res['quicAnomalyScore']);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -48,6 +48,28 @@ class RequestUtilsTest extends TestCase
|
|
|
48
48
|
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
49
|
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
50
|
}
|
|
51
|
+
public function testGetRenderingAnomalyScoreEmpty(): void
|
|
52
|
+
{
|
|
53
|
+
$context = $this->createRequestContext();
|
|
54
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
55
|
+
$this->assertEquals(0.0, $res['renderingAnomalyScore']);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function testGetRenderingAnomalyScoreSpoofed(): void
|
|
59
|
+
{
|
|
60
|
+
$metrics = [
|
|
61
|
+
'rendering' => [
|
|
62
|
+
'fps' => 60,
|
|
63
|
+
'jitter' => 12.5,
|
|
64
|
+
'offscreenAnom' => false
|
|
65
|
+
]
|
|
66
|
+
];
|
|
67
|
+
$context = $this->createRequestContext([
|
|
68
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
69
|
+
]);
|
|
70
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
71
|
+
$this->assertEquals(65.0, $res['renderingAnomalyScore']);
|
|
72
|
+
}
|
|
51
73
|
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
52
74
|
{
|
|
53
75
|
$context = $this->createRequestContext([
|
|
@@ -185,6 +207,27 @@ class RequestUtilsTest extends TestCase
|
|
|
185
207
|
$this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
public function testGetBehaviorScoreWithBotLikeKeystrokeDynamics(): void
|
|
211
|
+
{
|
|
212
|
+
$metrics = [
|
|
213
|
+
'honeypotInteraction' => false,
|
|
214
|
+
'keystrokeLatency' => 100.0,
|
|
215
|
+
'keystrokeDwellTimes' => [50, 50, 50, 50, 50],
|
|
216
|
+
'keystrokeFlightTimes' => [
|
|
217
|
+
['digraph' => 'ab', 'time' => 100],
|
|
218
|
+
['digraph' => 'bc', 'time' => 100],
|
|
219
|
+
['digraph' => 'cd', 'time' => 100],
|
|
220
|
+
['digraph' => 'de', 'time' => 100],
|
|
221
|
+
['digraph' => 'ef', 'time' => 100]
|
|
222
|
+
]
|
|
223
|
+
];
|
|
224
|
+
$context = $this->createRequestContext([
|
|
225
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
226
|
+
]);
|
|
227
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
228
|
+
$this->assertGreaterThanOrEqual(70.0, $score['behaviorScore']);
|
|
229
|
+
}
|
|
230
|
+
|
|
188
231
|
public function testGetBehaviorScoreWithBotLikeTouchMovements(): void
|
|
189
232
|
{
|
|
190
233
|
$metrics = [
|