@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.
- package/CHANGELOG.md +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2122 -1690
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +362 -1
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- 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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
// ---
|
|
253
|
+
// --- PRACTICAL USAGE EXAMPLE ---
|
|
254
254
|
/*
|
|
255
255
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
256
256
|
$httpVersion = $_SERVER['SERVER_PROTOCOL'] ?? '';
|
|
257
257
|
|
|
258
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
101
|
+
* Retrieves the value of an HTTP header in a case-insensitive manner.
|
|
102
102
|
*
|
|
103
|
-
* @param string $name
|
|
104
|
-
* @return string|null
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
$
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
*/
|
|
63
|
-
public function
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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';
|