@anonympins/fingerprint 0.5.1 → 0.5.4

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +118 -115
  3. package/package.json +1 -1
  4. package/src/js/dynamic-wasm.js +52 -13
  5. package/src/js/fingerprint.builder.js +169 -168
  6. package/src/js/fingerprint.client.js +1271 -1189
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +436 -162
  9. package/src/js/fingerprint.utils.js +30 -0
  10. package/src/js/gpu_pow.solver.js +96 -1
  11. package/src/js/library.js +40 -21
  12. package/src/js/mongodb-store.js +165 -79
  13. package/src/js/pow.solver.inline.js +627 -30
  14. package/src/js/pow.solver.js +453 -31
  15. package/src/js/problem-manager.js +1 -1
  16. package/src/js/redis-store.js +114 -42
  17. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  18. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  19. package/src/js/tests/fingerprint.client.test.js +128 -128
  20. package/src/js/tests/fingerprint.engine.test.js +370 -370
  21. package/src/js/tests/fingerprint.test.js +119 -5
  22. package/src/js/tests/gpu_pow.test.js +81 -0
  23. package/src/js/tests/ip-reputation.test.js +1 -1
  24. package/src/js/tests/pow.solver.test.js +9 -6
  25. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  26. package/src/php/Challenge/ChallengeUtils.php +137 -35
  27. package/src/php/FingerprintClient.php +22 -5
  28. package/src/php/FingerprintEngine.php +335 -58
  29. package/src/php/Optimization/Optimization.php +23 -16
  30. package/src/php/Optimization/OptimizationOperators.php +27 -0
  31. package/src/php/ProblemManager.php +1 -1
  32. package/src/php/Store/MongoDbStore.php +184 -104
  33. package/src/php/Store/RedisStore.php +91 -11
  34. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  35. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  36. package/src/php/Tests/IpReputationTest.php +1 -1
  37. package/src/php/Tests/PowTest.php +1 -1
  38. package/src/php/Tests/RequestUtilsTest.php +29 -0
  39. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  40. package/src/php/Utils/BigInt.php +202 -202
  41. package/src/php/Utils/RequestUtils.php +74 -21
  42. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -638,6 +638,55 @@ class ChallengeUtils
638
638
  /**
639
639
  * Vérifie une solution de PoW mémoire.
640
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
+
641
690
  public static function verifyMemoryPoW(
642
691
  string $nonce,
643
692
  string $solution,
@@ -654,37 +703,69 @@ class ChallengeUtils
654
703
  return false;
655
704
  }
656
705
 
657
- // If difficulty is high (production workloads), we treat memory PoW purely as a client-side cost.
658
- // Cryptographic integrity is already fully enforced by the chained CPU PoW verification.
659
- if ($difficulty > 4) {
660
- return true;
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;
661
712
  }
662
713
 
663
- $size = $difficulty * 1024 * 1024;
664
- if ($size <= 0) {
665
- return true; // Pas de challenge mémoire si la difficulté est nulle ou négative.
666
- }
667
- $iterations = (int)floor($size / 16);
668
- $buffer = new \SplFixedArray((int)floor($size / 4));
714
+ $sol = $data['solution'];
715
+ $merkleRoot = $data['merkleRoot'];
716
+ $proofs = $data['proofs'];
669
717
 
718
+ $numBlocks = $difficulty * 256;
670
719
  $seed = ":{$nonce}:{$clientSecret}";
671
- $h = 0;
672
- foreach (unpack('C*', $seed) as $byte) {
673
- $h += $byte;
674
- }
675
720
 
676
- for ($i = 0; $i < count($buffer); $i++) {
677
- $buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
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
+ }
678
739
  }
679
740
 
680
- $finalHash = 0;
681
- $addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
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;
682
760
  for ($i = 0; $i < $iterations; $i++) {
683
- $addr = $buffer[$addr] % count($buffer);
684
- $finalHash ^= $addr;
761
+ $blockIdx = (int)floor($addr / 1024);
762
+ $elementIdx = $addr % 1024;
763
+ $addr = $getBlockElement($blockIdx, $elementIdx) % $totalElements;
764
+ $addr = $addr & 0xffffffff;
765
+ $expectedSolution ^= $addr;
685
766
  }
686
767
 
687
- return $finalHash === (int)$solution;
768
+ return $expectedSolution === (int)$sol;
688
769
  }
689
770
 
690
771
  /**
@@ -758,17 +839,23 @@ class ChallengeUtils
758
839
 
759
840
  $solverCode = self::getPowSolverCode();
760
841
  $queriesJson = json_encode($queries);
842
+ $coopTimeout = $securityConfig['pospace']['coopTimeout'] ?? 15;
843
+
844
+ $safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
845
+ $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
846
+ $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
761
847
 
762
848
  $challengeScript = <<<JS
763
849
  async function solve() {
764
- const nonce = "{$nonce}";
765
- const path = "{$path}";
766
- const clientSecret = "{$clientSecret}";
850
+ const nonce = {$safeNonce};
851
+ const path = {$safePath};
852
+ const clientSecret = {$safeClientSecret};
767
853
  const queries = {$queriesJson};
768
854
  const sizeMb = {$sizeMb};
769
855
  const nodeId = "{$nodeId}";
770
856
  const peerId = "{$peerId}";
771
857
  const peerBlockIdx = {$peerBlockIdx};
858
+ const coopTimeout = {$coopTimeout};
772
859
 
773
860
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
774
861
  await new Promise(r => setTimeout(r, 10));
@@ -804,7 +891,7 @@ class ChallengeUtils
804
891
  await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
805
892
 
806
893
  let attempts = 0;
807
- while (attempts < 15) {
894
+ while (attempts < coopTimeout) {
808
895
  const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
809
896
  const data = await res.json();
810
897
  if (data.status === 'ready') {
@@ -859,18 +946,33 @@ JS;
859
946
  $baseBlock = self::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
860
947
  $baseBlockBytes = '[' . implode(',', array_values(unpack('C*', $baseBlock))) . ']';
861
948
 
862
- $trapLinksHtml = implode(' ', array_map(
863
- fn($url, $index) => "<a href=\"{$url}\" tabindex=\"-1\"><span>&gt; " . ($index + 1) . "</span></a>",
864
- $trapUrls,
865
- array_keys($trapUrls)
866
- ));
867
- $trapContainerHtml = "<div style=\"position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;\" aria-hidden=\"true\">{$trapLinksHtml}</div>";
949
+ $trapTags = ['div', 'span', 'p', 'section'];
950
+ $selectedTrapTag = $trapTags[array_rand($trapTags)];
951
+ $layoutProps = [
952
+ 'position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;',
953
+ 'position:fixed;left:-8888px;top:-8888px;opacity:0;pointer-events:none;width:0;height:0;overflow:hidden;',
954
+ 'display:none;visibility:hidden;pointer-events:none;'
955
+ ];
956
+ $selectedLayout = $layoutProps[array_rand($layoutProps)];
957
+
958
+ $trapLinks = [];
959
+ foreach ($trapUrls as $index => $url) {
960
+ $nestingType = rand(0, 2);
961
+ $innerHtml = ($nestingType === 1) ? "<b>&gt; " . ($index + 1) . "</b>" : (($nestingType === 2) ? "<i>&gt; " . ($index + 1) . "</i>" : "<span>&gt; " . ($index + 1) . "</span>");
962
+ $trapLinks[] = "<a href=\"{$url}\" tabindex=\"-1\">{$innerHtml}</a>";
963
+ }
964
+ $trapLinksHtml = implode(' ', $trapLinks);
965
+ $trapContainerHtml = "<{$selectedTrapTag} style=\"{$selectedLayout}\" aria-hidden=\"true\">{$trapLinksHtml}</{$selectedTrapTag}>";
966
+
967
+ $safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
968
+ $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
969
+ $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
868
970
 
869
971
  $challengeScript = <<<JS
870
972
  async function solve() {
871
- const nonce = "{$nonce}";
872
- const path = "{$path}";
873
- const clientSecret = "{$clientSecret}";
973
+ const nonce = {$safeNonce};
974
+ const path = {$safePath};
975
+ const clientSecret = {$safeClientSecret};
874
976
  const cpuTarget = BigInt("0x" + "{$target}");
875
977
  const memDifficulty = {$memoryDifficulty};
876
978
  const baseBlock = new Uint8Array({$baseBlockBytes});
@@ -891,7 +993,7 @@ JS;
891
993
  return;
892
994
  }
893
995
 
894
- const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
996
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
895
997
  window.location.href = finalUrl;
896
998
  }
897
999
  solve();
@@ -77,13 +77,30 @@ class FingerprintClient
77
77
  $this->clientConfig['honeypots'][] = $fieldName;
78
78
  }
79
79
 
80
- // Styles CSS pour cacher le champ de manière robuste.
81
- $styles = 'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;';
80
+ // Styles CSS polymorphiques pour cacher le champ de manière robuste.
81
+ $styleOptions = [
82
+ 'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;',
83
+ 'position:fixed; left:-8888px; top:-8888px; width:0; height:0; overflow:hidden; opacity:0; pointer-events:none;',
84
+ 'display:none; visibility:hidden; pointer-events:none;'
85
+ ];
86
+ $styles = $styleOptions[array_rand($styleOptions)];
87
+
88
+ $containerTags = ['div', 'span', 'p', 'section'];
89
+ $tag = $containerTags[array_rand($containerTags)];
90
+
91
+ $nestingType = rand(0, 1);
92
+ if ($nestingType === 1) {
93
+ return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
94
+ . '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
95
+ . '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
96
+ . '</label>'
97
+ . '</' . $tag . '>';
98
+ }
82
99
 
83
- return '<div style="' . $styles . '" aria-hidden="true">'
84
- . '<label for="' . htmlspecialchars($fieldName) . '">' . $fieldName . '</label>'
100
+ return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
101
+ . '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName) . '</label>'
85
102
  . '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
86
- . '</div>';
103
+ . '</' . $tag . '>';
87
104
  }
88
105
 
89
106
  /**