@anonympins/fingerprint 0.3.7 → 0.4.0

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 (58) hide show
  1. package/CHANGELOG.md +331 -244
  2. package/README.md +63 -1201
  3. package/composer.json +38 -38
  4. package/index.js +4 -4
  5. package/package.json +103 -103
  6. package/phpunit.xml +20 -20
  7. package/public/fp.js +1 -1
  8. package/public/fp.wasm +0 -0
  9. package/src/js/build-client.js +1 -1
  10. package/src/js/fingerprint.client.js +2 -0
  11. package/src/js/fingerprint.js +4429 -4220
  12. package/src/js/mongodb-store.js +79 -79
  13. package/src/js/pow.solver.inline.js +31 -0
  14. package/src/js/pow.solver.js +31 -0
  15. package/src/js/tests/fingerprint.builder.test.js +79 -0
  16. package/src/js/tests/fingerprint.client.init.test.js +120 -0
  17. package/src/js/tests/fingerprint.client.test.js +105 -0
  18. package/src/js/tests/fingerprint.engine.test.js +371 -0
  19. package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
  20. package/src/js/tests/fingerprint.test.js +2319 -0
  21. package/src/js/tests/ip-reputation.test.js +132 -0
  22. package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
  23. package/src/js/tests/library.test.js +96 -0
  24. package/src/js/tests/metrics.test.js +104 -0
  25. package/src/js/tests/pow.solver.test.js +198 -0
  26. package/src/js/tests/problem-manager.test.js +323 -0
  27. package/src/js/tests/stores.test.js +118 -0
  28. package/src/php/Challenge/ChallengeUtils.php +361 -305
  29. package/src/php/Config/SecurityProfiles.php +271 -266
  30. package/src/php/FingerprintBuilder.php +185 -185
  31. package/src/php/FingerprintClient.php +131 -131
  32. package/src/php/FingerprintEngine.php +1006 -1006
  33. package/src/php/Ja3AnomalyDetector.php +227 -227
  34. package/src/php/Optimization/FunctionRegistry.php +62 -62
  35. package/src/php/Optimization/Optimization.php +255 -255
  36. package/src/php/Optimization/OptimizationOperators.php +304 -304
  37. package/src/php/Store/InMemoryStore.php +66 -66
  38. package/src/php/Store/MongoDbStore.php +104 -104
  39. package/src/php/Store/RedisStore.php +53 -53
  40. package/src/php/Tests/ChallengeUtilsTest.php +81 -81
  41. package/src/php/Tests/FingerprintBuilderTest.php +57 -57
  42. package/src/php/Tests/FingerprintClientTest.php +71 -0
  43. package/src/php/Tests/FingerprintEngineTest.php +299 -299
  44. package/src/php/Tests/IpReputationTest.php +156 -156
  45. package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
  46. package/src/php/Tests/MetricsTest.php +45 -45
  47. package/src/php/Tests/PowTest.php +39 -39
  48. package/src/php/Tests/ProblemManagerTest.php +296 -296
  49. package/src/php/Tests/RequestUtilsTest.php +253 -145
  50. package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
  51. package/src/php/Tests/problems.config.json +8 -8
  52. package/src/php/Utils/BigInt.php +144 -144
  53. package/src/php/Utils/Logger.php +29 -29
  54. package/src/php/Utils/MaliciousPatterns.php +58 -58
  55. package/src/php/Utils/MetricsManager.php +166 -166
  56. package/src/php/Utils/RequestUtils.php +31 -4
  57. package/src/php/Utils/TLSClientHelloParser.php +117 -0
  58. package/src/php/bin/auto-tune.php +117 -117
@@ -1,67 +1,67 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Implémentation en mémoire de l'interface IStore.
9
- * Utile pour le développement ou les applications à instance unique sans persistance externe.
10
- */
11
- class InMemoryStore implements IStore
12
- {
13
- /**
14
- * @var array<string, array{value: mixed, expiresAt: int|null}>
15
- */
16
- private array $data = [];
17
-
18
- public function get(string $key)
19
- {
20
- if (!isset($this->data[$key])) {
21
- return null;
22
- }
23
-
24
- $item = $this->data[$key];
25
- if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) {
26
- $this->delete($key); // Supprime l'élément expiré
27
- return null;
28
- }
29
-
30
- return $item['value'];
31
- }
32
-
33
- public function set(string $key, $value, ?int $ttl = null): void
34
- {
35
- $expiresAt = $ttl !== null ? time() + $ttl : null;
36
-
37
- $this->data[$key] = ['value' => $value, 'expiresAt' => $expiresAt];
38
- }
39
-
40
- public function has(string $key): bool
41
- {
42
- if (!isset($this->data[$key])) {
43
- return false;
44
- }
45
-
46
- $item = $this->data[$key];
47
- if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) {
48
- $this->delete($key);
49
- return false;
50
- }
51
-
52
- return true;
53
- }
54
-
55
- public function delete(string $key): void
56
- {
57
- unset($this->data[$key]);
58
- }
59
-
60
- /**
61
- * Efface toutes les données du store. Utile pour les tests.
62
- */
63
- public function clear(): void
64
- {
65
- $this->data = [];
66
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Implémentation en mémoire de l'interface IStore.
9
+ * Utile pour le développement ou les applications à instance unique sans persistance externe.
10
+ */
11
+ class InMemoryStore implements IStore
12
+ {
13
+ /**
14
+ * @var array<string, array{value: mixed, expiresAt: int|null}>
15
+ */
16
+ private array $data = [];
17
+
18
+ public function get(string $key)
19
+ {
20
+ if (!isset($this->data[$key])) {
21
+ return null;
22
+ }
23
+
24
+ $item = $this->data[$key];
25
+ if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) {
26
+ $this->delete($key); // Supprime l'élément expiré
27
+ return null;
28
+ }
29
+
30
+ return $item['value'];
31
+ }
32
+
33
+ public function set(string $key, $value, ?int $ttl = null): void
34
+ {
35
+ $expiresAt = $ttl !== null ? time() + $ttl : null;
36
+
37
+ $this->data[$key] = ['value' => $value, 'expiresAt' => $expiresAt];
38
+ }
39
+
40
+ public function has(string $key): bool
41
+ {
42
+ if (!isset($this->data[$key])) {
43
+ return false;
44
+ }
45
+
46
+ $item = $this->data[$key];
47
+ if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) {
48
+ $this->delete($key);
49
+ return false;
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ public function delete(string $key): void
56
+ {
57
+ unset($this->data[$key]);
58
+ }
59
+
60
+ /**
61
+ * Efface toutes les données du store. Utile pour les tests.
62
+ */
63
+ public function clear(): void
64
+ {
65
+ $this->data = [];
66
+ }
67
67
  }
@@ -1,105 +1,105 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Adaptateur de stockage MongoDB pour le moteur Fingerprint.
9
- * Utilise la bibliothèque officielle mongodb/mongodb.
10
- */
11
- class MongoDbStore implements IStore
12
- {
13
- /**
14
- * @var \MongoDB\Collection
15
- */
16
- private $collection;
17
-
18
- /**
19
- * @param \MongoDB\Collection $collection Collection dédiée au stockage des empreintes
20
- */
21
- public function __construct($collection)
22
- {
23
- $this->collection = $collection;
24
- }
25
-
26
- public function get(string $key)
27
- {
28
- $doc = $this->collection->findOne(['_id' => $key]);
29
- if (!$doc) {
30
- return null;
31
- }
32
-
33
- // Vérification d'expiration active pour bypasser le délai de 60s du démon de MongoDB
34
- if (isset($doc['expiresAt'])) {
35
- $expiresAt = $doc['expiresAt'];
36
- if ($expiresAt instanceof \MongoDB\BSON\UTCDateTime) {
37
- $expiresAtMs = $expiresAt->toDateTime()->getTimestamp() * 1000;
38
- $nowMs = (int)(microtime(true) * 1000);
39
- if ($expiresAtMs < $nowMs) {
40
- $this->delete($key);
41
- return null;
42
- }
43
- }
44
- }
45
-
46
- return isset($doc['value']) ? json_decode($doc['value'], true) : null;
47
- }
48
-
49
- public function set(string $key, $value, ?int $ttl = null): void
50
- {
51
- $doc = [
52
- '_id' => $key,
53
- 'value' => json_encode($value),
54
- ];
55
-
56
- if ($ttl !== null && $ttl > 0) {
57
- $doc['expiresAt'] = new \MongoDB\BSON\UTCDateTime((time() + $ttl) * 1000);
58
- }
59
-
60
- $this->collection->replaceOne(
61
- ['_id' => $key],
62
- $doc,
63
- ['upsert' => true]
64
- );
65
- }
66
-
67
- public function has(string $key): bool
68
- {
69
- $doc = $this->collection->findOne(
70
- ['_id' => $key],
71
- ['projection' => ['expiresAt' => 1]]
72
- );
73
-
74
- if (!$doc) {
75
- return false;
76
- }
77
-
78
- if (isset($doc['expiresAt']) && $doc['expiresAt'] instanceof \MongoDB\BSON\UTCDateTime) {
79
- $expiresAtMs = $doc['expiresAt']->toDateTime()->getTimestamp() * 1000;
80
- $nowMs = (int)(microtime(true) * 1000);
81
- if ($expiresAtMs < $nowMs) {
82
- $this->delete($key);
83
- return false;
84
- }
85
- }
86
-
87
- return true;
88
- }
89
-
90
- public function delete(string $key): void
91
- {
92
- $this->collection->deleteOne(['_id' => $key]);
93
- }
94
-
95
- /**
96
- * Automatise la configuration de l'index TTL nécessaire dans MongoDB.
97
- */
98
- public function init(): void
99
- {
100
- $this->collection->createIndex(
101
- ['expiresAt' => 1],
102
- ['expireAfterSeconds' => 0]
103
- );
104
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Adaptateur de stockage MongoDB pour le moteur Fingerprint.
9
+ * Utilise la bibliothèque officielle mongodb/mongodb.
10
+ */
11
+ class MongoDbStore implements IStore
12
+ {
13
+ /**
14
+ * @var \MongoDB\Collection
15
+ */
16
+ private $collection;
17
+
18
+ /**
19
+ * @param \MongoDB\Collection $collection Collection dédiée au stockage des empreintes
20
+ */
21
+ public function __construct($collection)
22
+ {
23
+ $this->collection = $collection;
24
+ }
25
+
26
+ public function get(string $key)
27
+ {
28
+ $doc = $this->collection->findOne(['_id' => $key]);
29
+ if (!$doc) {
30
+ return null;
31
+ }
32
+
33
+ // Vérification d'expiration active pour bypasser le délai de 60s du démon de MongoDB
34
+ if (isset($doc['expiresAt'])) {
35
+ $expiresAt = $doc['expiresAt'];
36
+ if ($expiresAt instanceof \MongoDB\BSON\UTCDateTime) {
37
+ $expiresAtMs = $expiresAt->toDateTime()->getTimestamp() * 1000;
38
+ $nowMs = (int)(microtime(true) * 1000);
39
+ if ($expiresAtMs < $nowMs) {
40
+ $this->delete($key);
41
+ return null;
42
+ }
43
+ }
44
+ }
45
+
46
+ return isset($doc['value']) ? json_decode($doc['value'], true) : null;
47
+ }
48
+
49
+ public function set(string $key, $value, ?int $ttl = null): void
50
+ {
51
+ $doc = [
52
+ '_id' => $key,
53
+ 'value' => json_encode($value),
54
+ ];
55
+
56
+ if ($ttl !== null && $ttl > 0) {
57
+ $doc['expiresAt'] = new \MongoDB\BSON\UTCDateTime((time() + $ttl) * 1000);
58
+ }
59
+
60
+ $this->collection->replaceOne(
61
+ ['_id' => $key],
62
+ $doc,
63
+ ['upsert' => true]
64
+ );
65
+ }
66
+
67
+ public function has(string $key): bool
68
+ {
69
+ $doc = $this->collection->findOne(
70
+ ['_id' => $key],
71
+ ['projection' => ['expiresAt' => 1]]
72
+ );
73
+
74
+ if (!$doc) {
75
+ return false;
76
+ }
77
+
78
+ if (isset($doc['expiresAt']) && $doc['expiresAt'] instanceof \MongoDB\BSON\UTCDateTime) {
79
+ $expiresAtMs = $doc['expiresAt']->toDateTime()->getTimestamp() * 1000;
80
+ $nowMs = (int)(microtime(true) * 1000);
81
+ if ($expiresAtMs < $nowMs) {
82
+ $this->delete($key);
83
+ return false;
84
+ }
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ public function delete(string $key): void
91
+ {
92
+ $this->collection->deleteOne(['_id' => $key]);
93
+ }
94
+
95
+ /**
96
+ * Automatise la configuration de l'index TTL nécessaire dans MongoDB.
97
+ */
98
+ public function init(): void
99
+ {
100
+ $this->collection->createIndex(
101
+ ['expiresAt' => 1],
102
+ ['expireAfterSeconds' => 0]
103
+ );
104
+ }
105
105
  }
@@ -1,54 +1,54 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Adaptateur de stockage Redis pour le moteur Fingerprint.
9
- * Compatible avec phpredis et predis.
10
- */
11
- class RedisStore implements IStore
12
- {
13
- /**
14
- * @var mixed Une instance de \Redis ou de \Predis\Client
15
- */
16
- private $redis;
17
-
18
- /**
19
- * @param mixed $redis Client Redis déjà configuré et connecté
20
- */
21
- public function __construct($redis)
22
- {
23
- $this->redis = $redis;
24
- }
25
-
26
- public function get(string $key)
27
- {
28
- $value = $this->redis->get($key);
29
- if ($value === false || $value === null) {
30
- return null;
31
- }
32
- return json_decode($value, true);
33
- }
34
-
35
- public function set(string $key, $value, ?int $ttl = null): void
36
- {
37
- $stringValue = json_encode($value);
38
- if ($ttl !== null && $ttl > 0) {
39
- $this->redis->setex($key, $ttl, $stringValue);
40
- } else {
41
- $this->redis->set($key, $stringValue);
42
- }
43
- }
44
-
45
- public function has(string $key): bool
46
- {
47
- return (bool)$this->redis->exists($key);
48
- }
49
-
50
- public function delete(string $key): void
51
- {
52
- $this->redis->del($key);
53
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Adaptateur de stockage Redis pour le moteur Fingerprint.
9
+ * Compatible avec phpredis et predis.
10
+ */
11
+ class RedisStore implements IStore
12
+ {
13
+ /**
14
+ * @var mixed Une instance de \Redis ou de \Predis\Client
15
+ */
16
+ private $redis;
17
+
18
+ /**
19
+ * @param mixed $redis Client Redis déjà configuré et connecté
20
+ */
21
+ public function __construct($redis)
22
+ {
23
+ $this->redis = $redis;
24
+ }
25
+
26
+ public function get(string $key)
27
+ {
28
+ $value = $this->redis->get($key);
29
+ if ($value === false || $value === null) {
30
+ return null;
31
+ }
32
+ return json_decode($value, true);
33
+ }
34
+
35
+ public function set(string $key, $value, ?int $ttl = null): void
36
+ {
37
+ $stringValue = json_encode($value);
38
+ if ($ttl !== null && $ttl > 0) {
39
+ $this->redis->setex($key, $ttl, $stringValue);
40
+ } else {
41
+ $this->redis->set($key, $stringValue);
42
+ }
43
+ }
44
+
45
+ public function has(string $key): bool
46
+ {
47
+ return (bool)$this->redis->exists($key);
48
+ }
49
+
50
+ public function delete(string $key): void
51
+ {
52
+ $this->redis->del($key);
53
+ }
54
54
  }
@@ -1,82 +1,82 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Tests;
6
-
7
- use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
- use Anonympins\Fingerprint\Utils\BigInt;
9
- use PHPUnit\Framework\TestCase;
10
-
11
- class ChallengeUtilsTest extends TestCase
12
- {
13
- protected function setUp(): void
14
- {
15
- // Définir une clé secrète pour les tests
16
- $_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
17
- }
18
-
19
- public function testIsTicketValid(): void
20
- {
21
- $ip = '127.0.0.1';
22
- $expiry = (int)floor(microtime(true) * 1000) + 3600000; // 1 heure
23
- $signature = hash_hmac('sha256', "{$ip}:{$expiry}", $_ENV['POW_SECRET']);
24
- $validTicket = "{$expiry}:{$signature}";
25
-
26
- $this->assertTrue(ChallengeUtils::isTicketValid($ip, $validTicket));
27
- $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $validTicket), "Le ticket ne doit pas être valide pour une autre IP.");
28
-
29
- $expiredExpiry = (int)floor(microtime(true) * 1000) - 1000;
30
- $expiredSignature = hash_hmac('sha256', "{$ip}:{$expiredExpiry}", $_ENV['POW_SECRET']);
31
- $expiredTicket = "{$expiredExpiry}:{$expiredSignature}";
32
- $this->assertFalse(ChallengeUtils::isTicketValid($ip, $expiredTicket), "Un ticket expiré doit être invalide.");
33
-
34
- $this->assertFalse(ChallengeUtils::isTicketValid($ip, 'invalid-ticket-format'), "Un format de ticket invalide doit être rejeté.");
35
- }
36
-
37
- public function testCpuTargetCalculation(): void
38
- {
39
- $config = ['cpu' => ['minDifficultyBits' => 8, 'maxDifficultyBits' => 24]];
40
-
41
- // Low suspicion -> minimum difficulty
42
- $targetLow = ChallengeUtils::calculateCpuTarget(0.0, $config);
43
- $expectedTargetLow = (new BigInt(1))->shiftLeft(256 - 8);
44
- $this->assertEquals(0, BigInt::fromHex($targetLow)->compareTo($expectedTargetLow), "Target for 0.0 suspicion should correspond to 8 bits of difficulty.");
45
-
46
- // High suspicion -> maximum difficulty
47
- $targetHigh = ChallengeUtils::calculateCpuTarget(1.0, $config);
48
- $expectedTargetHigh = (new BigInt(1))->shiftLeft(256 - 24);
49
- $this->assertEquals(0, BigInt::fromHex($targetHigh)->compareTo($expectedTargetHigh), "Target for 1.0 suspicion should correspond to 24 bits of difficulty.");
50
-
51
- // Medium suspicion -> intermediate difficulty
52
- $targetMid = ChallengeUtils::calculateCpuTarget(0.5, $config);
53
- $expectedBits = 8 + 0.5 * (24 - 8); // 16 bits
54
- $expectedTargetMid = (new BigInt(1))->shiftLeft(256 - (int)$expectedBits);
55
- $this->assertEquals(0, BigInt::fromHex($targetMid)->compareTo($expectedTargetMid), "Target for 0.5 suspicion should correspond to 16 bits of difficulty.");
56
- }
57
-
58
- public function testVerifyCpuTargetPoW(): void
59
- {
60
- $ip = '127.0.0.1';
61
- $nonce = 'test-nonce';
62
- $fingerprint = 'ua:test-fp';
63
- $clientSecret = 'test-secret';
64
- $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $fingerprint);
65
-
66
- // Cible facile pour un test rapide (ex: 4 zéros hexadécimaux -> 16 bits)
67
- $targetHex = '0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
68
- $challengeContext = ['cpuTarget' => $targetHex, 'baseBlock' => $baseBlock];
69
-
70
- // Trouver une solution valide
71
- $solution = 0;
72
- while (true) {
73
- $hash = hash('sha256', $baseBlock . $solution);
74
- if (strcmp($hash, $targetHex) < 0) break;
75
- $solution++;
76
- }
77
-
78
- $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($ip, 3600, $nonce, (string)$solution, $challengeContext);
79
- $this->assertNotNull($ticket, "Un ticket valide aurait dû être généré.");
80
- $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket));
81
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
+ use Anonympins\Fingerprint\Utils\BigInt;
9
+ use PHPUnit\Framework\TestCase;
10
+
11
+ class ChallengeUtilsTest extends TestCase
12
+ {
13
+ protected function setUp(): void
14
+ {
15
+ // Définir une clé secrète pour les tests
16
+ $_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
17
+ }
18
+
19
+ public function testIsTicketValid(): void
20
+ {
21
+ $ip = '127.0.0.1';
22
+ $expiry = (int)floor(microtime(true) * 1000) + 3600000; // 1 heure
23
+ $signature = hash_hmac('sha256', "{$ip}:{$expiry}", $_ENV['POW_SECRET']);
24
+ $validTicket = "{$expiry}:{$signature}";
25
+
26
+ $this->assertTrue(ChallengeUtils::isTicketValid($ip, $validTicket));
27
+ $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $validTicket), "Le ticket ne doit pas être valide pour une autre IP.");
28
+
29
+ $expiredExpiry = (int)floor(microtime(true) * 1000) - 1000;
30
+ $expiredSignature = hash_hmac('sha256', "{$ip}:{$expiredExpiry}", $_ENV['POW_SECRET']);
31
+ $expiredTicket = "{$expiredExpiry}:{$expiredSignature}";
32
+ $this->assertFalse(ChallengeUtils::isTicketValid($ip, $expiredTicket), "Un ticket expiré doit être invalide.");
33
+
34
+ $this->assertFalse(ChallengeUtils::isTicketValid($ip, 'invalid-ticket-format'), "Un format de ticket invalide doit être rejeté.");
35
+ }
36
+
37
+ public function testCpuTargetCalculation(): void
38
+ {
39
+ $config = ['cpu' => ['minDifficultyBits' => 8, 'maxDifficultyBits' => 24]];
40
+
41
+ // Low suspicion -> minimum difficulty
42
+ $targetLow = ChallengeUtils::calculateCpuTarget(0.0, $config);
43
+ $expectedTargetLow = (new BigInt(1))->shiftLeft(256 - 8);
44
+ $this->assertEquals(0, BigInt::fromHex($targetLow)->compareTo($expectedTargetLow), "Target for 0.0 suspicion should correspond to 8 bits of difficulty.");
45
+
46
+ // High suspicion -> maximum difficulty
47
+ $targetHigh = ChallengeUtils::calculateCpuTarget(1.0, $config);
48
+ $expectedTargetHigh = (new BigInt(1))->shiftLeft(256 - 24);
49
+ $this->assertEquals(0, BigInt::fromHex($targetHigh)->compareTo($expectedTargetHigh), "Target for 1.0 suspicion should correspond to 24 bits of difficulty.");
50
+
51
+ // Medium suspicion -> intermediate difficulty
52
+ $targetMid = ChallengeUtils::calculateCpuTarget(0.5, $config);
53
+ $expectedBits = 8 + 0.5 * (24 - 8); // 16 bits
54
+ $expectedTargetMid = (new BigInt(1))->shiftLeft(256 - (int)$expectedBits);
55
+ $this->assertEquals(0, BigInt::fromHex($targetMid)->compareTo($expectedTargetMid), "Target for 0.5 suspicion should correspond to 16 bits of difficulty.");
56
+ }
57
+
58
+ public function testVerifyCpuTargetPoW(): void
59
+ {
60
+ $ip = '127.0.0.1';
61
+ $nonce = 'test-nonce';
62
+ $fingerprint = 'ua:test-fp';
63
+ $clientSecret = 'test-secret';
64
+ $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $fingerprint);
65
+
66
+ // Cible facile pour un test rapide (ex: 4 zéros hexadécimaux -> 16 bits)
67
+ $targetHex = '0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
68
+ $challengeContext = ['cpuTarget' => $targetHex, 'baseBlock' => $baseBlock];
69
+
70
+ // Trouver une solution valide
71
+ $solution = 0;
72
+ while (true) {
73
+ $hash = hash('sha256', $baseBlock . $solution);
74
+ if (strcmp($hash, $targetHex) < 0) break;
75
+ $solution++;
76
+ }
77
+
78
+ $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($ip, 3600, $nonce, (string)$solution, $challengeContext);
79
+ $this->assertNotNull($ticket, "Un ticket valide aurait dû être généré.");
80
+ $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket));
81
+ }
82
82
  }