@anonympins/fingerprint 0.7.4 → 0.8.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 (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -5,7 +5,7 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint;
6
6
 
7
7
  /**
8
- * TLS (JA3) fingerprint anomaly and spoofing detector in PHP.
8
+ * TLS (JA3) fingerprint anomaly and spoofing detector.
9
9
  */
10
10
  class Ja3AnomalyDetector
11
11
  {
@@ -38,7 +38,7 @@ class Ja3AnomalyDetector
38
38
 
39
39
  /**
40
40
  * Parses a raw JA3 string.
41
- * Expected format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
41
+ * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
42
42
  */
43
43
  public static function parseJa3(string $ja3String): ?array
44
44
  {
@@ -74,7 +74,7 @@ class Ja3AnomalyDetector
74
74
  }
75
75
 
76
76
  /**
77
- * Extracts browser family from User-Agent string.
77
+ * Extracts the browser family from a User-Agent string.
78
78
  */
79
79
  public static function getBrowserFamily(string $userAgent): ?string
80
80
  {
@@ -95,13 +95,13 @@ class Ja3AnomalyDetector
95
95
  }
96
96
 
97
97
  /**
98
- * Evaluates comprehensive JA3 spoofing and anomaly suspicion score.
98
+ * Evaluates the comprehensive JA3 spoofing and anomaly suspicion score.
99
99
  *
100
100
  * @param string|null $ja3Hash MD5 hash of JA3 fingerprint (32 hex characters).
101
101
  * @param string|null $ja3Raw Raw unhashed JA3 string if available.
102
102
  * @param string $userAgent Request User-Agent header.
103
103
  * @param string $httpVersion HTTP protocol version string.
104
- * @param object|null $cacheInstance Cache store for multi-UA stagnation detection.
104
+ * @param object|null $cacheInstance A cache store for multi-UA stagnation detection.
105
105
  * @return int Suspicion score between 0 and 100.
106
106
  */
107
107
  public static function getJa3AnomalyScore(
@@ -120,13 +120,13 @@ class Ja3AnomalyDetector
120
120
  if (isset(self::TLS_FINGERPRINT_DB[$ja3Hash])) {
121
121
  $expectedBrowsers = self::TLS_FINGERPRINT_DB[$ja3Hash];
122
122
 
123
- // Case A: TLS matches known automated library but UA claims standard browser
123
+ // Case A: TLS matches a known automated library, but the UA claims to be a standard browser.
124
124
  $isLibrary = array_intersect($expectedBrowsers, ['Python', 'Go', 'Java', 'curl']);
125
125
  if (!empty($isLibrary) && $isHumanBrowser) {
126
126
  $score = max($score, 90); // High confidence spoofing
127
127
  }
128
128
 
129
- // Case B: Direct discrepancy between claimed browser and expected TLS stack
129
+ // Case B: Direct discrepancy between the claimed browser and the expected TLS stack.
130
130
  if ($claimedBrowser !== null) {
131
131
  $matched = false;
132
132
  foreach ($expectedBrowsers as $expected) {
@@ -141,7 +141,7 @@ class Ja3AnomalyDetector
141
141
  }
142
142
  }
143
143
 
144
- // Case C: Stateful multi-UA stagnation detection
144
+ // Case C: Stateful multi-UA stagnation detection.
145
145
  if ($cacheInstance && $claimedBrowser !== null && method_exists($cacheInstance, 'get') && method_exists($cacheInstance, 'set')) {
146
146
  $cacheKey = "ja3-browsers:" . $ja3Hash;
147
147
 
@@ -162,7 +162,7 @@ class Ja3AnomalyDetector
162
162
  }
163
163
  }
164
164
 
165
- // Multiple rotating browser UAs emitted from identical TLS stack
165
+ // Multiple rotating browser UAs are being emitted from an identical TLS stack.
166
166
  if (count($seenBrowsers) > 1) {
167
167
  $score = max($score, 85);
168
168
  }
@@ -172,22 +172,22 @@ class Ja3AnomalyDetector
172
172
  }
173
173
  }
174
174
 
175
- // --- ANALYSIS 2: DEEP INSPECTION ON RAW JA3 STRING ---
175
+ // --- ANALYSIS 2: DEEP INSPECTION OF THE RAW JA3 STRING ---
176
176
  if ($ja3Raw) {
177
177
  $parsed = self::parseJa3($ja3Raw);
178
178
  if ($parsed) {
179
- // Check A: GREASE presence for Chrome / Edge
179
+ // Check A: GREASE presence for Chrome/Edge.
180
180
  if ($claimedBrowser === 'Chrome' || $claimedBrowser === 'Edge') {
181
181
  $hasCiphersGrease = self::hasGrease($parsed['ciphers']);
182
182
  $hasExtensionsGrease = self::hasGrease($parsed['extensions']);
183
183
 
184
184
  if (!$hasCiphersGrease && !$hasExtensionsGrease) {
185
- // Modern Chromium missing GREASE indicates spoofing
185
+ // Modern Chromium missing GREASE indicates spoofing.
186
186
  $score = max($score, 75);
187
187
  }
188
188
  }
189
189
 
190
- // Check B: HTTP/2 or HTTP/3 without ALPN extension (Extension 16)
190
+ // Check B: HTTP/2 or HTTP/3 without ALPN extension (Extension 16).
191
191
  $isH2OrHigher = (
192
192
  strpos($httpVersion, '2.0') !== false ||
193
193
  strpos($httpVersion, 'HTTP/2') !== false ||
@@ -196,11 +196,11 @@ class Ja3AnomalyDetector
196
196
  $hasAlpnExtension = in_array(16, $parsed['extensions'], true);
197
197
 
198
198
  if ($isH2OrHigher && !$hasAlpnExtension) {
199
- // HTTP/2 active at server level but missing from client ALPN extension
199
+ // HTTP/2 is active at the server level but missing from the client's ALPN extension.
200
200
  $score = max($score, 70);
201
201
  }
202
202
 
203
- // Check C: Deprecated TLS version negotiated by claimed modern browser
203
+ // Check C: A deprecated TLS version was negotiated by a claimed modern browser.
204
204
  if ($isHumanBrowser && $parsed['tlsVersion'] < 771) {
205
205
  $score = max($score, 80);
206
206
  }
@@ -212,7 +212,7 @@ class Ja3AnomalyDetector
212
212
 
213
213
  /**
214
214
  * Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
215
- * Evaluates continuous physical transit discrepancies rather than arbitrary step numbers.
215
+ * Evaluates continuous physical transit discrepancies rather than arbitrary step numbers.
216
216
  *
217
217
  * @param RequestContext $context
218
218
  * @return float Suspicion score between 0.0 and 95.0
@@ -229,14 +229,14 @@ class Ja3AnomalyDetector
229
229
  $clientTimestamp = (int)$metrics['clientTimestamp'];
230
230
  $appLatency = $context->requestTimestamp - $clientTimestamp;
231
231
 
232
- if ($tcpRtt !== null && $tcpRtt > 0 && $appLatency > 0) {
233
- // Marge de tolérance défensive contre le jitter réseau et le scheduling JS / Garbage Collection
232
+ if ($tcpRtt !== null && $tcpRtt > 0 && $appLatency > 0) { // @phpstan-ignore-line
233
+ // Defensive tolerance margin against network jitter and JS scheduling/Garbage Collection.
234
234
  $jitterAllowance = 60.0;
235
235
  $effectiveRtt = max((float)$tcpRtt, 5.0);
236
236
  $tunnelDelta = $appLatency - ($tcpRtt + $jitterAllowance);
237
237
  $divergenceRatio = $appLatency / $effectiveRtt;
238
238
 
239
- // Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
239
+ // Disjunction condition: ultra-close edge link (< 40ms) + application transit at least 3x higher.
240
240
  if ($tcpRtt <= 40 && $divergenceRatio >= 3.0 && $tunnelDelta > 0) {
241
241
  $scaling = 120.0;
242
242
  $ratioWeight = min(1.0, ($divergenceRatio - 3.0) / 5.0);
@@ -250,16 +250,16 @@ class Ja3AnomalyDetector
250
250
  }
251
251
  }
252
252
 
253
- // --- EXEMPLE D'UTILISATION PRATIQUE ---
253
+ // --- PRACTICAL USAGE EXAMPLE ---
254
254
  /*
255
255
  $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
256
256
  $httpVersion = $_SERVER['SERVER_PROTOCOL'] ?? '';
257
257
 
258
- // Récupération des en-têtes injectés par votre Reverse-Proxy (Nginx, HAProxy, etc.)
258
+ // Retrieve headers injected by your Reverse-Proxy (Nginx, HAProxy, etc.)
259
259
  $ja3Hash = $_SERVER['HTTP_X_JA3_HASH'] ?? null;
260
260
  $ja3Raw = $_SERVER['HTTP_X_JA3_RAW'] ?? null;
261
261
 
262
- // Redis facultatif pour la détection stateful de rotation UA
262
+ // Optional Redis for stateful UA rotation detection
263
263
  $redis = new \Redis();
264
264
  $redis->connect('127.0.0.1', 6379);
265
265
 
@@ -5,7 +5,7 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint\Optimization;
6
6
 
7
7
  /**
8
- * Registry to safely expose optimization library routines to task runners.
8
+ * A registry to safely expose optimization library routines to task runners.
9
9
  */
10
10
  class FunctionRegistry
11
11
  {
@@ -42,7 +42,7 @@ class FunctionRegistry
42
42
  }
43
43
  /**
44
44
  * Registers a custom function. Primarily intended for testing.
45
- * @internal
45
+ * @internal For testing purposes only.
46
46
  * @param string $name
47
47
  * @param callable $function
48
48
  * @return void
@@ -54,8 +54,8 @@ class FunctionRegistry
54
54
  }
55
55
 
56
56
  /**
57
- * Resets function registry. Intended for testing.
58
- * @internal
57
+ * Resets the function registry. Intended for testing.
58
+ * @internal For testing purposes only.
59
59
  */
60
60
  public static function __internal_resetRegistry(): void
61
61
  {
@@ -5,7 +5,7 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint\Optimization;
6
6
 
7
7
  /**
8
- * Optimization algorithms library.
8
+ * A library of optimization algorithms.
9
9
  */
10
10
  class Optimization
11
11
  {
@@ -17,7 +17,7 @@ class Optimization
17
17
  try {
18
18
  return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
19
19
  } catch (\Exception $e) {
20
- if (function_exists('wp_rand')) {
20
+ if (function_exists('wp_rand')) { // WordPress fallback
21
21
  return (float)wp_rand(0, 1000000) / 1000000.0;
22
22
  }
23
23
  // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
@@ -133,7 +133,7 @@ class Optimization
133
133
  }
134
134
 
135
135
  /**
136
- * Sorts population into non-dominated Pareto fronts (NSGA-II).
136
+ * Sorts a population into non-dominated Pareto fronts (NSGA-II).
137
137
  * @param array<int, array> &$populationWithObjectives
138
138
  * @return array<int, array>
139
139
  */
@@ -186,7 +186,7 @@ class Optimization
186
186
  }
187
187
 
188
188
  /**
189
- * Computes crowding distance for a front to preserve diversity.
189
+ * Computes the crowding distance for a front to preserve diversity.
190
190
  * @param array<int, array> &$front
191
191
  */
192
192
  private static function calculateCrowdingDistance(array &$front): void
@@ -207,7 +207,7 @@ class Optimization
207
207
  $minObj = $front[0]['objectives'][$i];
208
208
  $maxObj = $front[$l - 1]['objectives'][$i];
209
209
 
210
- // Boundary solutions receive infinite distance to encourage spread
210
+ // Boundary solutions receive infinite distance to encourage population spread.
211
211
  $front[0]['crowdingDistance'] = INF;
212
212
  $front[$l - 1]['crowdingDistance'] = INF;
213
213
 
@@ -222,7 +222,7 @@ class Optimization
222
222
  }
223
223
 
224
224
  /**
225
- * Calculates deviation from Benford's Law distribution.
225
+ * Calculates the deviation from Benford's Law distribution.
226
226
  * Elevated values indicate synthetic or automated intervals.
227
227
  * @param array<int|float> $numbers
228
228
  */
@@ -5,10 +5,10 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint;
6
6
 
7
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).
8
+ * Represents the context of an HTTP request, providing unified access
9
+ * to information required for fingerprint analysis.
10
+ * This class is designed to be instantiated from a standard request object
11
+ * (e.g., PSR-7, Symfony, Laravel).
12
12
  */
13
13
  class RequestContext
14
14
  {
@@ -40,7 +40,7 @@ class RequestContext
40
40
  /** @var ?array<string, float> */
41
41
  public ?array $preCalculatedVector = null;
42
42
 
43
- // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
43
+ // Specific properties that may be provided by a reverse proxy
44
44
  public ?string $ja3 = null;
45
45
  public ?string $ja4 = null;
46
46
  public ?string $ja4s = null;
@@ -48,7 +48,7 @@ class RequestContext
48
48
  public ?string $http2Fingerprint = null;
49
49
  public ?string $quicFingerprint = null;
50
50
  public ?string $tcpFingerprint = null;
51
- public ?string $zkpY = null; // NOUVEAU: Clé publique ZKP du client
51
+ public ?string $zkpY = null; // Client ZKP public key
52
52
 
53
53
  /**
54
54
  * @param string $clientIp
@@ -73,7 +73,7 @@ class RequestContext
73
73
  ) {
74
74
  $this->clientIp = $clientIp;
75
75
  $this->path = $path;
76
- // Normaliser les en-têtes en minuscules pour un accès cohérent
76
+ // Normalize headers to lowercase for consistent access
77
77
  $this->headers = array_change_key_case($headers, CASE_LOWER);
78
78
  $this->query = $query;
79
79
  $this->body = $body;
@@ -86,7 +86,7 @@ class RequestContext
86
86
  ($this->getHeader('x-forwarded-proto') === 'https')
87
87
  );
88
88
 
89
- // Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
89
+ // Extract TLS/HTTP2/TCP fingerprints if provided by headers
90
90
  $this->ja3 = $this->headers['x-ja3-hash'] ?? null;
91
91
  $this->ja4 = $this->headers['x-ja4-hash'] ?? null;
92
92
  $this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
@@ -98,10 +98,10 @@ class RequestContext
98
98
  $this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
99
99
  }
100
100
  /**
101
- * Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
101
+ * Retrieves the value of an HTTP header in a case-insensitive manner.
102
102
  *
103
- * @param string $name Le nom de l'en-tête.
104
- * @return string|null La valeur de l'en-tête ou null si non trouvé.
103
+ * @param string $name Header name.
104
+ * @return string|null Header value or null if not found.
105
105
  */
106
106
 
107
107
  public function getHeader(string $name): ?string
@@ -1,67 +1,97 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * In-memory implementation of the IStore interface.
9
- * Suitable for local development, tests, or single-instance deployments.
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); // Delete expired item
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
- * Clears all data from the store. Useful for testing.
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
+ * In-memory implementation of the IStore interface.
9
+ * Suitable for local development, tests, or single-instance deployments.
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
+ /**
19
+ * Retrieves the value associated with the given key.
20
+ * If the stored item has expired, it is deleted and null is returned.
21
+ *
22
+ * @param string $key The key to look up.
23
+ * @return mixed The stored value, or null if the key is missing or expired.
24
+ */
25
+ public function get(string $key)
26
+ {
27
+ if (!isset($this->data[$key])) {
28
+ return null;
29
+ }
30
+
31
+ $item = $this->data[$key];
32
+ if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) { // Active expiration check
33
+ $this->delete($key); // Delete expired item
34
+ return null;
35
+ }
36
+
37
+ return $item['value'];
38
+ }
39
+
40
+ /**
41
+ * Stores a value under the given key, with an optional TTL (in seconds).
42
+ * If `$ttl` is null, the item never expires.
43
+ *
44
+ * @param string $key The key to store the value under.
45
+ * @param mixed $value The value to store.
46
+ * @param int|null $ttl Optional time-to-live in seconds.
47
+ * @return void
48
+ */
49
+ public function set(string $key, $value, ?int $ttl = null): void
50
+ {
51
+ $expiresAt = $ttl !== null ? time() + $ttl : null;
52
+
53
+ $this->data[$key] = ['value' => $value, 'expiresAt' => $expiresAt];
54
+ }
55
+
56
+ /**
57
+ * Checks whether the given key exists and has not expired.
58
+ * Expired items are deleted on access.
59
+ *
60
+ * @param string $key The key to check.
61
+ * @return bool True if the key exists and is valid, false otherwise.
62
+ */
63
+ public function has(string $key): bool
64
+ {
65
+ if (!isset($this->data[$key])) {
66
+ return false;
67
+ }
68
+
69
+ $item = $this->data[$key];
70
+ if ($item['expiresAt'] !== null && $item['expiresAt'] < time()) {
71
+ $this->delete($key);
72
+ return false;
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Deletes the value associated with the given key.
80
+ * Does nothing if the key does not exist.
81
+ *
82
+ * @param string $key The key to delete.
83
+ * @return void
84
+ */
85
+ public function delete(string $key): void
86
+ {
87
+ unset($this->data[$key]);
88
+ }
89
+
90
+ /**
91
+ * Clears all data from the store. Useful for test isolation.
92
+ */
93
+ public function clear(): void
94
+ {
95
+ $this->data = [];
96
+ }
67
97
  }
@@ -69,7 +69,7 @@ class MongoDbStore implements IStore
69
69
  return null;
70
70
  }
71
71
 
72
- // Active TTL check to bypass MongoDB daemon 60-second cleanup latency
72
+ // Active TTL check to bypass MongoDB's 60-second TTL cleanup daemon latency.
73
73
  if (isset($doc['expiresAt'])) {
74
74
  $expiresAt = $doc['expiresAt'];
75
75
  if ($expiresAt instanceof \MongoDB\BSON\UTCDateTime) {
@@ -168,7 +168,7 @@ class MongoDbStore implements IStore
168
168
  }
169
169
 
170
170
  /**
171
- * Automatically initializes required TTL index in MongoDB collection.
171
+ * Automatically initializes the required TTL index in the MongoDB collection.
172
172
  */
173
173
  public function init(): void
174
174
  {
@@ -5,7 +5,7 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint\Store;
6
6
 
7
7
  /**
8
- * Redis storage adapter for the Fingerprint engine.
8
+ * A Redis storage adapter for the Fingerprint engine.
9
9
  * Compatible with phpredis and predis clients.
10
10
  */
11
11
  class RedisStore implements IStore
@@ -1,36 +1,49 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Manages the global store singleton instance.
9
- */
10
- class StoreManager
11
- {
12
- private static ?IStore $store = null;
13
-
14
- public static function getStore(): IStore
15
- {
16
- if (self::$store === null) {
17
- self::$store = new InMemoryStore();
18
- }
19
- return self::$store;
20
- }
21
-
22
- public static function configureStore(IStore $externalStore): void
23
- {
24
- self::$store = $externalStore;
25
- }
26
-
27
- /**
28
- * Sets the active store instance (useful for dependency injection and tests).
29
- *
30
- * @param mixed $store
31
- */
32
- public static function setStore($store): void
33
- {
34
- self::$store = $store;
35
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Manages the global store singleton instance for the application.
9
+ */
10
+ class StoreManager
11
+ {
12
+ private static ?IStore $store = null;
13
+
14
+ /**
15
+ * Returns the current global store instance.
16
+ * Lazily instantiates an InMemoryStore if no store has been configured yet.
17
+ *
18
+ * @return IStore The active store instance.
19
+ */
20
+ public static function getStore(): IStore
21
+ {
22
+ if (self::$store === null) {
23
+ self::$store = new InMemoryStore();
24
+ }
25
+ return self::$store;
26
+ }
27
+
28
+ /**
29
+ * Replaces the active store with an externally provided IStore implementation.
30
+ * Typically used at bootstrap to plug in a persistent store (Redis, database, etc.).
31
+ *
32
+ * @param IStore $externalStore The external store implementation to use.
33
+ * @return void
34
+ */
35
+ public static function configureStore(IStore $externalStore): void
36
+ {
37
+ self::$store = $externalStore;
38
+ }
39
+
40
+ /**
41
+ * Sets the active store instance. Useful for dependency injection and testing.
42
+ *
43
+ * @param mixed $store
44
+ */
45
+ public static function setStore($store): void
46
+ {
47
+ self::$store = $store;
48
+ }
36
49
  }
@@ -263,6 +263,37 @@ class ChallengeUtilsTest extends TestCase
263
263
  Env::clear('ED25519_PUBLIC_KEY');
264
264
  }
265
265
 
266
+ public function testGenerateIssuerPemKeys(): void
267
+ {
268
+ if (!extension_loaded('openssl') || !defined('OPENSSL_KEYTYPE_ED25519')) {
269
+ $this->markTestSkipped('OpenSSL Ed25519 is not supported in this environment.');
270
+ }
271
+
272
+ $pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
273
+ if (!$pkey || !@openssl_pkey_export($pkey, $testPem)) {
274
+ $this->markTestSkipped('Ed25519 key generation or export is not supported in this PHP/OpenSSL environment.');
275
+ }
276
+
277
+ $tempDir = sys_get_temp_dir() . '/fp-issuer-keys-' . time();
278
+ try {
279
+ $result = ChallengeUtils::generateIssuerPemKeys($tempDir);
280
+ $this->assertFileExists($result['privateKeyPath']);
281
+ $this->assertFileExists($result['publicKeyPath']);
282
+ $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $result['privateKey']);
283
+ $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $result['publicKey']);
284
+ } finally {
285
+ if (file_exists($tempDir . '/issuer-private.pem')) {
286
+ unlink($tempDir . '/issuer-private.pem');
287
+ }
288
+ if (file_exists($tempDir . '/issuer-public.pem')) {
289
+ unlink($tempDir . '/issuer-public.pem');
290
+ }
291
+ if (is_dir($tempDir)) {
292
+ rmdir($tempDir);
293
+ }
294
+ }
295
+ }
296
+
266
297
  public function testCooperativePoSpaceWorkflow(): void
267
298
  {
268
299
  $clientIp = '127.0.0.1';