@anonympins/fingerprint 0.3.5 → 0.3.7

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.
@@ -1,91 +1,91 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint;
6
-
7
- /**
8
- * Représente le contexte d'une requête HTTP, fournissant un accès unifié
9
- * aux informations nécessaires pour l'analyse de l'empreinte.
10
- * Cette classe est conçue pour être créée à partir d'un objet de requête
11
- * standard (ex: PSR-7, Symfony, Laravel).
12
- */
13
- class RequestContext
14
- {
15
- public string $clientIp;
16
- public string $path;
17
- /** @var array<string, string> */
18
- public array $headers;
19
- /** @var array<string, mixed> */
20
- public array $query;
21
- /** @var array<string, mixed>|object|null */
22
- public $body;
23
- /** @var array<string, string> */
24
- public array $cookies;
25
- public ?string $httpVersion;
26
- public int $requestTimestamp;
27
-
28
- /** @var ?array{type: string, name: string} */
29
- public ?array $graphqlOperation = null;
30
-
31
- /** @var ?array<string, mixed> */
32
- public ?array $newCookieForResponse = null;
33
-
34
- // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
35
- public ?string $ja3;
36
- public ?string $ja4;
37
- public ?string $ja4s;
38
- public ?string $ja4h;
39
- public ?string $http2Fingerprint;
40
- public ?string $tcpFingerprint;
41
-
42
- /**
43
- * @param string $clientIp
44
- * @param string $path
45
- * @param array<string, string> $headers
46
- * @param array<string, mixed> $query
47
- * @param array<string, mixed>|object|null $body
48
- * @param array<string, string> $cookies
49
- * @param string|null $httpVersion
50
- * @param int|null $requestTimestamp
51
- */
52
- public function __construct(
53
- string $clientIp,
54
- string $path,
55
- array $headers,
56
- array $query,
57
- $body,
58
- array $cookies,
59
- ?string $httpVersion,
60
- ?int $requestTimestamp = null
61
- ) {
62
- $this->clientIp = $clientIp;
63
- $this->path = $path;
64
- // Normaliser les en-têtes en minuscules pour un accès cohérent
65
- $this->headers = array_change_key_case($headers, CASE_LOWER);
66
- $this->query = $query;
67
- $this->body = $body;
68
- $this->cookies = $cookies;
69
- $this->httpVersion = $httpVersion;
70
- $this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
71
-
72
- // Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
73
- $this->ja3 = $this->headers['x-ja3-hash'] ?? null;
74
- $this->ja4 = $this->headers['x-ja4-hash'] ?? null;
75
- $this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
76
- $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
77
- $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
78
- $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
79
- }
80
- /**
81
- * Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
82
- *
83
- * @param string $name Le nom de l'en-tête.
84
- * @return string|null La valeur de l'en-tête ou null si non trouvé.
85
- */
86
-
87
- public function getHeader(string $name): ?string
88
- {
89
- return $this->headers[strtolower($name)] ?? null;
90
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint;
6
+
7
+ /**
8
+ * Représente le contexte d'une requête HTTP, fournissant un accès unifié
9
+ * aux informations nécessaires pour l'analyse de l'empreinte.
10
+ * Cette classe est conçue pour être créée à partir d'un objet de requête
11
+ * standard (ex: PSR-7, Symfony, Laravel).
12
+ */
13
+ class RequestContext
14
+ {
15
+ public string $clientIp;
16
+ public string $path;
17
+ /** @var array<string, string> */
18
+ public array $headers;
19
+ /** @var array<string, mixed> */
20
+ public array $query;
21
+ /** @var array<string, mixed>|object|null */
22
+ public $body;
23
+ /** @var array<string, string> */
24
+ public array $cookies;
25
+ public ?string $httpVersion;
26
+ public int $requestTimestamp;
27
+
28
+ /** @var ?array{type: string, name: string} */
29
+ public ?array $graphqlOperation = null;
30
+
31
+ /** @var ?array<string, mixed> */
32
+ public ?array $newCookieForResponse = null;
33
+
34
+ // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
35
+ public ?string $ja3;
36
+ public ?string $ja4;
37
+ public ?string $ja4s;
38
+ public ?string $ja4h;
39
+ public ?string $http2Fingerprint;
40
+ public ?string $tcpFingerprint;
41
+
42
+ /**
43
+ * @param string $clientIp
44
+ * @param string $path
45
+ * @param array<string, string> $headers
46
+ * @param array<string, mixed> $query
47
+ * @param array<string, mixed>|object|null $body
48
+ * @param array<string, string> $cookies
49
+ * @param string|null $httpVersion
50
+ * @param int|null $requestTimestamp
51
+ */
52
+ public function __construct(
53
+ string $clientIp,
54
+ string $path,
55
+ array $headers,
56
+ array $query,
57
+ $body,
58
+ array $cookies,
59
+ ?string $httpVersion,
60
+ ?int $requestTimestamp = null
61
+ ) {
62
+ $this->clientIp = $clientIp;
63
+ $this->path = $path;
64
+ // Normaliser les en-têtes en minuscules pour un accès cohérent
65
+ $this->headers = array_change_key_case($headers, CASE_LOWER);
66
+ $this->query = $query;
67
+ $this->body = $body;
68
+ $this->cookies = $cookies;
69
+ $this->httpVersion = $httpVersion;
70
+ $this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
71
+
72
+ // Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
73
+ $this->ja3 = $this->headers['x-ja3-hash'] ?? null;
74
+ $this->ja4 = $this->headers['x-ja4-hash'] ?? null;
75
+ $this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
76
+ $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
77
+ $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
78
+ $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
79
+ }
80
+ /**
81
+ * Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
82
+ *
83
+ * @param string $name Le nom de l'en-tête.
84
+ * @return string|null La valeur de l'en-tête ou null si non trouvé.
85
+ */
86
+
87
+ public function getHeader(string $name): ?string
88
+ {
89
+ return $this->headers[strtolower($name)] ?? null;
90
+ }
91
91
  }
@@ -0,0 +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
+ }
105
+ }
@@ -0,0 +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
+ }
54
+ }
@@ -4,9 +4,9 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
7
  use Anonympins\Fingerprint\Challenge\ChallengeUtils;
9
8
  use Anonympins\Fingerprint\Utils\BigInt;
9
+ use PHPUnit\Framework\TestCase;
10
10
 
11
11
  class ChallengeUtilsTest extends TestCase
12
12
  {
@@ -4,8 +4,8 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
7
  use Anonympins\Fingerprint\FingerprintBuilder;
8
+ use PHPUnit\Framework\TestCase;
9
9
 
10
10
  class FingerprintBuilderTest extends TestCase
11
11
  {
@@ -4,13 +4,13 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
- use Anonympins\Fingerprint\FingerprintEngine;
7
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
9
8
  use Anonympins\Fingerprint\FingerprintBuilder;
9
+ use Anonympins\Fingerprint\FingerprintEngine;
10
10
  use Anonympins\Fingerprint\RequestContext;
11
- use Anonympins\Fingerprint\Store\StoreManager;
12
11
  use Anonympins\Fingerprint\Store\InMemoryStore;
13
- use Anonympins\Fingerprint\Config\SecurityProfiles;
12
+ use Anonympins\Fingerprint\Store\StoreManager;
13
+ use PHPUnit\Framework\TestCase;
14
14
 
15
15
  class FingerprintEngineTest extends TestCase
16
16
  {
@@ -4,11 +4,11 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
- use Anonympins\Fingerprint\Utils\RequestUtils;
9
- use Anonympins\Fingerprint\Store\StoreManager;
10
- use Anonympins\Fingerprint\Store\IStore;
11
7
  use Anonympins\Fingerprint\RequestContext;
8
+ use Anonympins\Fingerprint\Store\IStore;
9
+ use Anonympins\Fingerprint\Store\StoreManager;
10
+ use Anonympins\Fingerprint\Utils\RequestUtils;
11
+ use PHPUnit\Framework\TestCase;
12
12
 
13
13
  class IpReputationTest extends TestCase
14
14
  {
@@ -4,8 +4,8 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
7
  use Anonympins\Fingerprint\Ja3AnomalyDetector;
8
+ use PHPUnit\Framework\TestCase;
9
9
 
10
10
  /**
11
11
  * Tests unitaires pour la classe Ja3AnomalyDetector.
@@ -0,0 +1,46 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Tests;
6
+
7
+ use Anonympins\Fingerprint\RequestContext;
8
+ use Anonympins\Fingerprint\DirectFingerprint;
9
+ use Anonympins\Fingerprint\Utils\MetricsManager;
10
+ use Anonympins\Fingerprint\Store\InMemoryStore;
11
+ use Anonympins\Fingerprint\Store\StoreManager;
12
+ use PHPUnit\Framework\TestCase;
13
+
14
+ class MetricsTest extends TestCase
15
+ {
16
+ protected function setUp(): void
17
+ {
18
+ parent::setUp();
19
+ StoreManager::configureStore(new InMemoryStore());
20
+ }
21
+
22
+ public function testIncrementCounterAndGetPrometheusMetrics(): void
23
+ {
24
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
25
+ $metrics = MetricsManager::getPrometheusMetrics();
26
+
27
+ $this->assertStringContainsString('fingerprint_requests_total', $metrics);
28
+ $this->assertStringContainsString('status="passed"', $metrics);
29
+ }
30
+
31
+ public function testGetPrometheusMetricsViaDirectFingerprint(): void
32
+ {
33
+ $config = [
34
+ 'metricsAuthorizationCallback' => function (RequestContext $context) {
35
+ return true;
36
+ }
37
+ ];
38
+
39
+ $direct = new DirectFingerprint($config);
40
+ MetricsManager::incrementCounter('requests_total', ['status' => 'passed']);
41
+ $metrics = $direct->getPrometheusMetrics();
42
+
43
+ $this->assertNotNull($metrics);
44
+ $this->assertStringContainsString('fingerprint_requests_total', $metrics);
45
+ }
46
+ }
@@ -4,12 +4,12 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
7
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
8
8
  use Anonympins\Fingerprint\FingerprintEngine;
9
9
  use Anonympins\Fingerprint\ProblemManager;
10
10
  use Anonympins\Fingerprint\Store\InMemoryStore;
11
11
  use Anonympins\Fingerprint\Store\StoreManager;
12
- use Anonympins\Fingerprint\Config\SecurityProfiles;
12
+ use PHPUnit\Framework\TestCase;
13
13
 
14
14
  class PowTest extends TestCase
15
15
  {
@@ -4,11 +4,13 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
7
+ use Anonympins\Fingerprint\Optimization\FunctionRegistry;
8
8
  use Anonympins\Fingerprint\ProblemManager;
9
- use Anonympins\Fingerprint\Store\IStore;
10
9
  use Anonympins\Fingerprint\Store\InMemoryStore;
11
- use Anonympins\Fingerprint\Optimization\FunctionRegistry; // Assurez-vous que cette classe est autoloadable
10
+ use Anonympins\Fingerprint\Store\IStore;
11
+ use PHPUnit\Framework\TestCase;
12
+
13
+ // Assurez-vous que cette classe est autoloadable
12
14
 
13
15
  class ProblemManagerTest extends TestCase
14
16
  {
@@ -4,9 +4,9 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Anonympins\Fingerprint\Tests;
6
6
 
7
- use PHPUnit\Framework\TestCase;
8
7
  use Anonympins\Fingerprint\RequestContext;
9
8
  use Anonympins\Fingerprint\Utils\RequestUtils;
9
+ use PHPUnit\Framework\TestCase;
10
10
 
11
11
  class RequestUtilsTest extends TestCase
12
12
  {
@@ -78,4 +78,69 @@ class RequestUtilsTest extends TestCase
78
78
  $result = RequestUtils::getClickVarianceScore($context);
79
79
  $this->assertEquals(0.0, $result['clickVarianceScore']);
80
80
  }
81
+
82
+ public function testSanitizeTrafficDataFiltersSybilAttacks(): void
83
+ {
84
+ $trafficData = [];
85
+ // Ajout de 100 logs provenant d'un attaquant (Sybil)
86
+ for ($i = 0; $i < 100; $i++) {
87
+ $trafficData[] = ['deviceId' => 'attacker_device', 'type' => 'trap_triggered'];
88
+ }
89
+ // Ajout de 10 logs d'utilisateurs légitimes distincts
90
+ for ($i = 0; $i < 10; $i++) {
91
+ $trafficData[] = ['deviceId' => "legit_device_{$i}", 'type' => 'challenge_solved'];
92
+ }
93
+
94
+ $sanitized = RequestUtils::sanitizeTrafficData($trafficData);
95
+
96
+ $attackerLogs = array_filter($sanitized, fn($log) => $log['deviceId'] === 'attacker_device');
97
+
98
+ // Total de 110 logs. 2% de 110 est 2.2 -> max(3, 2) = 3 logs maximum autorisés pour l'attaquant.
99
+ $this->assertLessThanOrEqual(3, count($attackerLogs));
100
+ }
101
+
102
+ public function testChallengePayloadSigningAndVerification(): void
103
+ {
104
+ $secret = 'test-fallback-dev-secret-32-chars-minimum';
105
+ $clientIp = '203.0.113.42';
106
+ $payload = [
107
+ 'clientSecret' => 'some_client_secret',
108
+ 'cpuTarget' => '00000000ffffffff',
109
+ 'fingerprint' => 'os:hash|gpu:hash2',
110
+ 'memDifficulty' => '16',
111
+ 'originalPath' => '/submit',
112
+ ];
113
+
114
+ // 1. Cas nominal : Signature et vérification réussies
115
+ $signature = RequestUtils::signChallengePayload($secret, $payload, $clientIp);
116
+ $this->assertNotEmpty($signature);
117
+
118
+ $payloadWithSig = $payload;
119
+ $payloadWithSig['signature'] = $signature;
120
+
121
+ $isValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, $clientIp);
122
+ $this->assertTrue($isValid, "La signature valide doit être acceptée.");
123
+
124
+ // 2. Détection de modification (tampering) sur cpuTarget
125
+ $tamperedPayload = $payloadWithSig;
126
+ $tamperedPayload['cpuTarget'] = 'ffffffffffffffff'; // Tentative de baisse de la difficulté
127
+
128
+ $isTamperedValid = RequestUtils::verifyChallengePayload($secret, $tamperedPayload, $clientIp);
129
+ $this->assertFalse($isTamperedValid, "Un payload modifié doit être rejeté.");
130
+
131
+ // 3. Détection de modification sur le fingerprint
132
+ $tamperedFpPayload = $payloadWithSig;
133
+ $tamperedFpPayload['fingerprint'] = 'os:another_hash|gpu:hash2';
134
+
135
+ $isTamperedFpValid = RequestUtils::verifyChallengePayload($secret, $tamperedFpPayload, $clientIp);
136
+ $this->assertFalse($isTamperedFpValid, "Un fingerprint modifié doit être rejeté.");
137
+
138
+ // 4. Détection d'usurpation d'adresse IP (IP mismatch)
139
+ $isIpMismatchValid = RequestUtils::verifyChallengePayload($secret, $payloadWithSig, '198.51.100.1');
140
+ $this->assertFalse($isIpMismatchValid, "Le payload ne doit pas être valide pour une autre adresse IP.");
141
+
142
+ // 5. Absence de signature
143
+ $isMissingSigValid = RequestUtils::verifyChallengePayload($secret, $payload, $clientIp);
144
+ $this->assertFalse($isMissingSigValid, "Un payload sans signature doit être rejeté.");
145
+ }
81
146
  }