@anonympins/fingerprint 0.7.5 → 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 +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/fingerprint.client.js +1784 -1772
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +198 -28
- 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/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +19 -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/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +382 -86
- package/src/php/Config/SecurityProfiles.php +75 -62
- 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 +2120 -1837
- 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 +2 -2
- 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 +327 -14
- 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
|
@@ -8,22 +8,36 @@ use Anonympins\Fingerprint\Store\StoreManager;
|
|
|
8
8
|
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
9
9
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
10
10
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
11
|
-
|
|
11
|
+
use Anonympins\Fingerprint\Utils\Env;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Utility class for generating and verifying Proof-of-Work challenges.
|
|
15
15
|
*/
|
|
16
16
|
class ChallengeUtils
|
|
17
17
|
{
|
|
18
|
-
/** @var array<string, float>
|
|
18
|
+
/** @var array<string, float> Local cache of converted floats to avoid repeated pack/unpack system calls */
|
|
19
19
|
private static array $froundCache = [];
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Emulates JavaScript's Math.fround: rounds a float to the nearest 32-bit
|
|
23
|
+
* single-precision float. Uses a local cache to avoid repeated pack/unpack calls.
|
|
24
|
+
*
|
|
25
|
+
* @param float $value The float value to round.
|
|
26
|
+
* @return float The value rounded to single precision.
|
|
27
|
+
*/
|
|
21
28
|
private static function fround(float $value): float
|
|
22
29
|
{
|
|
23
30
|
$key = (string)$value;
|
|
24
31
|
return self::$froundCache[$key] ?? (self::$froundCache[$key] = unpack('f', pack('f', $value))[1]);
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Hashes a string seed into a normalized float between 0 and 1
|
|
36
|
+
* using a simple 32-bit rolling hash (similar to Java's String.hashCode).
|
|
37
|
+
*
|
|
38
|
+
* @param string $seed The seed string to hash.
|
|
39
|
+
* @return float A pseudo-random float in the range [0, 1).
|
|
40
|
+
*/
|
|
27
41
|
public static function hashSeedToFloat(string $seed): float
|
|
28
42
|
{
|
|
29
43
|
$hash = 0;
|
|
@@ -36,6 +50,15 @@ class ChallengeUtils
|
|
|
36
50
|
return abs($hash % 1000000) / 1000000;
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Derives a set of 4 unique sample indices from the client IP and a secret,
|
|
55
|
+
* using an HMAC-SHA256 over the client IP and a 5-minute time window.
|
|
56
|
+
* This makes the indices deterministic but rotating over time.
|
|
57
|
+
*
|
|
58
|
+
* @param string $clientIp The client IP address.
|
|
59
|
+
* @param string $secret The shared secret used for the HMAC.
|
|
60
|
+
* @return array<int> An array of 4 unique indices in the range [0, 63].
|
|
61
|
+
*/
|
|
39
62
|
public static function deriveSampleIndices(string $clientIp, string $secret): array
|
|
40
63
|
{
|
|
41
64
|
$timeWindow = (int)floor(time() / (60 * 5));
|
|
@@ -56,6 +79,19 @@ class ChallengeUtils
|
|
|
56
79
|
return $indices;
|
|
57
80
|
}
|
|
58
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Verifies a GPU Proof-of-Work solution based on the logistic map
|
|
84
|
+
* (chaotic iteration x_{n+1} = r * x_n * (1 - x_n) with r = 3.9999).
|
|
85
|
+
* Only a subset of the 64 values is recomputed, using indices derived
|
|
86
|
+
* from the client IP and a shared secret.
|
|
87
|
+
*
|
|
88
|
+
* @param string $seed The challenge seed.
|
|
89
|
+
* @param int $iterations The number of logistic map iterations per value.
|
|
90
|
+
* @param string $solution A comma-separated string of 64 float values.
|
|
91
|
+
* @param string $clientIp The client IP used to derive the sample indices.
|
|
92
|
+
* @param string $secret The secret used to derive the sample indices.
|
|
93
|
+
* @return bool True if the sampled values match, false otherwise.
|
|
94
|
+
*/
|
|
59
95
|
public static function verifyGpuPow(string $seed, int $iterations, string $solution, string $clientIp = '127.0.0.1', string $secret = 'gpu-pow-salt'): bool
|
|
60
96
|
{
|
|
61
97
|
$values = explode(',', $solution);
|
|
@@ -82,6 +118,7 @@ class ChallengeUtils
|
|
|
82
118
|
return true;
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
/** Templates used to generate signed trap URLs that lure malicious crawlers. */
|
|
85
122
|
private const TRAP_URL_TEMPLATES = [
|
|
86
123
|
'/includes/config-{RANDOM}.php',
|
|
87
124
|
'/.env.{RANDOM}',
|
|
@@ -92,6 +129,14 @@ class ChallengeUtils
|
|
|
92
129
|
'/.git/config_{RANDOM}'
|
|
93
130
|
];
|
|
94
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Emulates a 32-bit signed integer multiplication (like Math.imul in JS),
|
|
134
|
+
* handling overflow correctly.
|
|
135
|
+
*
|
|
136
|
+
* @param int $a The first operand.
|
|
137
|
+
* @param int $b The second operand.
|
|
138
|
+
* @return int The 32-bit signed result of a * b.
|
|
139
|
+
*/
|
|
95
140
|
private static function imul(int $a, int $b): int
|
|
96
141
|
{
|
|
97
142
|
$ah = ($a >> 16) & 0xffff;
|
|
@@ -103,11 +148,21 @@ class ChallengeUtils
|
|
|
103
148
|
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
104
149
|
}
|
|
105
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Deterministically generates a 1024-byte block of pseudo-random data
|
|
153
|
+
* from a seed and a block index, using a rolling hash (cyrb53-based).
|
|
154
|
+
* This is the building block for the proof-of-space challenge.
|
|
155
|
+
*
|
|
156
|
+
* @param string $seed The seed string.
|
|
157
|
+
* @param int $blockIndex The index of the block to generate.
|
|
158
|
+
* @param int $blockSize The size of the block in bytes (default 1024).
|
|
159
|
+
* @return string The raw binary block content.
|
|
160
|
+
*/
|
|
106
161
|
private static function generateBlock(string $seed, int $blockIndex, int $blockSize = 1024): string
|
|
107
162
|
{
|
|
108
163
|
$block = str_repeat("\x00", $blockSize);
|
|
109
164
|
$h = FingerprintBuilder::cyrb53($seed . ":" . $blockIndex);
|
|
110
|
-
|
|
165
|
+
|
|
111
166
|
$h_int = (int)bcmod($h, '4294967296');
|
|
112
167
|
for ($i = 0; $i < $blockSize; $i++) {
|
|
113
168
|
$h_int = self::imul($h_int ^ $i, 1597334677);
|
|
@@ -116,6 +171,15 @@ class ChallengeUtils
|
|
|
116
171
|
return $block;
|
|
117
172
|
}
|
|
118
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Registers a cooperative node in the store, keyed by its IP subnet.
|
|
176
|
+
* Entries older than 2 minutes are pruned before adding the new node.
|
|
177
|
+
*
|
|
178
|
+
* @param string $clientIp The client IP (used to compute the subnet).
|
|
179
|
+
* @param string $nodeId The unique node identifier.
|
|
180
|
+
* @param string $seed The node's seed.
|
|
181
|
+
* @return void
|
|
182
|
+
*/
|
|
119
183
|
public static function registerCooperativeNode(string $clientIp, string $nodeId, string $seed): void
|
|
120
184
|
{
|
|
121
185
|
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
@@ -125,20 +189,28 @@ class ChallengeUtils
|
|
|
125
189
|
$store = StoreManager::getStore();
|
|
126
190
|
$key = "coop-pospace:subnet:{$subnet}";
|
|
127
191
|
$nodes = $store->get($key) ?? [];
|
|
128
|
-
|
|
192
|
+
|
|
129
193
|
$now = time();
|
|
130
|
-
//
|
|
194
|
+
// Clean up expired nodes (older than 2 minutes)
|
|
131
195
|
$nodes = array_filter($nodes, fn($n) => ($now - $n['timestamp']) < 120);
|
|
132
|
-
|
|
196
|
+
|
|
133
197
|
$nodes[$nodeId] = [
|
|
134
198
|
'nodeId' => $nodeId,
|
|
135
199
|
'seed' => $seed,
|
|
136
200
|
'timestamp' => $now
|
|
137
201
|
];
|
|
138
|
-
|
|
202
|
+
|
|
139
203
|
$store->set($key, $nodes, 120);
|
|
140
204
|
}
|
|
141
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Finds a random active peer node in the same subnet as the client,
|
|
208
|
+
* excluding a given node ID.
|
|
209
|
+
*
|
|
210
|
+
* @param string $clientIp The client IP (used to compute the subnet).
|
|
211
|
+
* @param string $excludeNodeId The node ID to exclude from the results.
|
|
212
|
+
* @return array|null The peer node data, or null if none is found.
|
|
213
|
+
*/
|
|
142
214
|
public static function findPeerInSubnet(string $clientIp, string $excludeNodeId): ?array
|
|
143
215
|
{
|
|
144
216
|
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
@@ -148,7 +220,7 @@ class ChallengeUtils
|
|
|
148
220
|
$store = StoreManager::getStore();
|
|
149
221
|
$key = "coop-pospace:subnet:{$subnet}";
|
|
150
222
|
$nodes = $store->get($key) ?? [];
|
|
151
|
-
|
|
223
|
+
|
|
152
224
|
$now = time();
|
|
153
225
|
$activePeers = [];
|
|
154
226
|
foreach ($nodes as $id => $node) {
|
|
@@ -156,14 +228,25 @@ class ChallengeUtils
|
|
|
156
228
|
$activePeers[] = $node;
|
|
157
229
|
}
|
|
158
230
|
}
|
|
159
|
-
|
|
231
|
+
|
|
160
232
|
if (empty($activePeers)) {
|
|
161
233
|
return null;
|
|
162
234
|
}
|
|
163
|
-
|
|
235
|
+
|
|
164
236
|
return $activePeers[array_rand($activePeers)];
|
|
165
237
|
}
|
|
166
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Handles cooperative peer-to-peer operations (federation threat intel sharing,
|
|
241
|
+
* node registration, peer discovery, WebRTC signaling, block requests/responses).
|
|
242
|
+
* Enforces cooperative signature verification for every operation except
|
|
243
|
+
* the federation threat intel sharing path.
|
|
244
|
+
*
|
|
245
|
+
* @param array $params The request parameters (includes `coop_op`).
|
|
246
|
+
* @param string $clientIp The client IP address.
|
|
247
|
+
* @param array $config Additional configuration (federated peers, thresholds, keys).
|
|
248
|
+
* @return array|null The response payload, or null if `coop_op` is missing.
|
|
249
|
+
*/
|
|
167
250
|
public static function handleCooperativeRequest(array $params, string $clientIp = '127.0.0.1', array $config = []): ?array
|
|
168
251
|
{
|
|
169
252
|
$op = $params['coop_op'] ?? null;
|
|
@@ -252,7 +335,7 @@ class ChallengeUtils
|
|
|
252
335
|
return ['error' => 'Missing node_id'];
|
|
253
336
|
}
|
|
254
337
|
|
|
255
|
-
// ---
|
|
338
|
+
// --- COOPERATIVE SIGNATURE VERIFICATION ---
|
|
256
339
|
$challengeContext = $store->get("secret:{$nodeId}");
|
|
257
340
|
if (!$challengeContext || empty($challengeContext['clientSecret'])) {
|
|
258
341
|
return ['error' => 'Invalid or expired node_id'];
|
|
@@ -295,7 +378,7 @@ class ChallengeUtils
|
|
|
295
378
|
if (!hash_equals($expectedSig, $coopSig)) {
|
|
296
379
|
return ['error' => 'Invalid cooperative signature'];
|
|
297
380
|
}
|
|
298
|
-
// ---
|
|
381
|
+
// --- END OF VERIFICATION ---
|
|
299
382
|
|
|
300
383
|
switch ($op) {
|
|
301
384
|
case 'register':
|
|
@@ -343,7 +426,7 @@ class ChallengeUtils
|
|
|
343
426
|
if (empty($peerId) || empty($requestId)) {
|
|
344
427
|
return ['error' => 'Invalid parameters'];
|
|
345
428
|
}
|
|
346
|
-
|
|
429
|
+
|
|
347
430
|
$queueKey = "coop-mailbox:queue:{$peerId}";
|
|
348
431
|
$requests = $store->get($queueKey) ?? [];
|
|
349
432
|
$requests[] = [
|
|
@@ -367,7 +450,7 @@ class ChallengeUtils
|
|
|
367
450
|
if (empty($requesterId) || empty($requestId)) {
|
|
368
451
|
return ['error' => 'Invalid parameters'];
|
|
369
452
|
}
|
|
370
|
-
|
|
453
|
+
|
|
371
454
|
$responseKey = "coop-mailbox:res:{$requesterId}:{$requestId}";
|
|
372
455
|
$store->set($responseKey, ['block_data' => $blockData], 30);
|
|
373
456
|
return ['status' => 'delivered'];
|
|
@@ -385,6 +468,18 @@ class ChallengeUtils
|
|
|
385
468
|
return null;
|
|
386
469
|
}
|
|
387
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Generates a proof-of-space challenge for a client, including random block
|
|
473
|
+
* queries. If a peer is found in the same subnet, a cooperative component is
|
|
474
|
+
* added to the challenge (peer ID and a random block index).
|
|
475
|
+
*
|
|
476
|
+
* @param string $clientIp The client IP address.
|
|
477
|
+
* @param string $nonce The challenge nonce.
|
|
478
|
+
* @param float $suspicionFactor The suspicion factor for the request.
|
|
479
|
+
* @param string $originalUrl The original requested URL (used as redirect path).
|
|
480
|
+
* @param array $securityConfig The security configuration array.
|
|
481
|
+
* @return array The challenge details.
|
|
482
|
+
*/
|
|
388
483
|
public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
389
484
|
{
|
|
390
485
|
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
@@ -408,12 +503,12 @@ class ChallengeUtils
|
|
|
408
503
|
'path' => $originalUrl
|
|
409
504
|
];
|
|
410
505
|
|
|
411
|
-
//
|
|
506
|
+
// Attempt cooperative coupling with a peer node in the same subnet
|
|
412
507
|
$peer = self::findPeerInSubnet($clientIp, $nonce);
|
|
413
508
|
if ($peer !== null) {
|
|
414
509
|
$challenge['peerId'] = $peer['nodeId'];
|
|
415
510
|
$challenge['peerBlockIdx'] = random_int(0, $maxBlocks - 1);
|
|
416
|
-
|
|
511
|
+
|
|
417
512
|
$store = StoreManager::getStore();
|
|
418
513
|
$store->set("coop-assoc:{$nonce}", [
|
|
419
514
|
'peerNodeId' => $peer['nodeId'],
|
|
@@ -425,14 +520,26 @@ class ChallengeUtils
|
|
|
425
520
|
return $challenge;
|
|
426
521
|
}
|
|
427
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Verifies a proof-of-space solution by reconstructing the expected hash
|
|
525
|
+
* from the queried blocks. If a cooperative association exists for the nonce,
|
|
526
|
+
* the peer block is also appended to the combined data.
|
|
527
|
+
*
|
|
528
|
+
* @param string $nonce The challenge nonce.
|
|
529
|
+
* @param string $solution The client-provided solution hash.
|
|
530
|
+
* @param array $queries The list of queried block indices.
|
|
531
|
+
* @param string $seed The seed used to generate the blocks.
|
|
532
|
+
* @param string $clientSecret The client secret.
|
|
533
|
+
* @return bool True if the solution matches, false otherwise.
|
|
534
|
+
*/
|
|
428
535
|
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
429
536
|
{
|
|
430
537
|
$combined = '';
|
|
431
538
|
foreach ($queries as $idx) {
|
|
432
539
|
$combined .= self::generateBlock($seed, (int)$idx);
|
|
433
540
|
}
|
|
434
|
-
|
|
435
|
-
//
|
|
541
|
+
|
|
542
|
+
// Cooperative proof verification
|
|
436
543
|
$store = StoreManager::getStore();
|
|
437
544
|
$assoc = $store->get("coop-assoc:{$nonce}");
|
|
438
545
|
if ($assoc !== null) {
|
|
@@ -449,6 +556,15 @@ class ChallengeUtils
|
|
|
449
556
|
return hash_equals($hash, $solution);
|
|
450
557
|
}
|
|
451
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Verifies a Schnorr-style Zero-Knowledge Proof using the secp256k1 prime.
|
|
561
|
+
* Checks that g^s ≡ t * y^c (mod p), where c = SHA-256(g, y, t) mod p.
|
|
562
|
+
*
|
|
563
|
+
* @param string $yStr The public key y (hex).
|
|
564
|
+
* @param string $tStr The commitment t (hex).
|
|
565
|
+
* @param string $sStr The response s (hex).
|
|
566
|
+
* @return bool True if the proof is valid, false otherwise.
|
|
567
|
+
*/
|
|
452
568
|
public static function verifyZkpProof(string $yStr, string $tStr, string $sStr): bool
|
|
453
569
|
{
|
|
454
570
|
try {
|
|
@@ -474,7 +590,11 @@ class ChallengeUtils
|
|
|
474
590
|
}
|
|
475
591
|
|
|
476
592
|
/**
|
|
477
|
-
*
|
|
593
|
+
* Retrieves the secret key for PoW tasks from environment variables.
|
|
594
|
+
* Throws an exception in production if the secret is missing.
|
|
595
|
+
*
|
|
596
|
+
* @return string The PoW secret.
|
|
597
|
+
* @throws \RuntimeException If missing in production.
|
|
478
598
|
*/
|
|
479
599
|
public static function getPowSecret(): string
|
|
480
600
|
{
|
|
@@ -487,9 +607,71 @@ class ChallengeUtils
|
|
|
487
607
|
}
|
|
488
608
|
|
|
489
609
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
610
|
+
* Programmatically generates an Ed25519 key pair in PEM format
|
|
611
|
+
* matching OpenSSL CLI:
|
|
612
|
+
* `openssl genpkey -algorithm ed25519 -out issuer-private.pem`
|
|
613
|
+
* `openssl pkey -in issuer-private.pem -pubout -out issuer-public.pem`
|
|
614
|
+
*
|
|
615
|
+
* @param string $outDir Target directory to store the PEM files.
|
|
616
|
+
* @param array $options Options including custom file names.
|
|
617
|
+
* @return array{privateKeyPath: string, publicKeyPath: string, privateKey: string, publicKey: string}
|
|
618
|
+
* @throws \RuntimeException If OpenSSL or Ed25519 is not supported or key generation fails.
|
|
619
|
+
*/
|
|
620
|
+
public static function generateIssuerPemKeys(string $outDir = '', array $options = []): array
|
|
621
|
+
{
|
|
622
|
+
if (!defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
623
|
+
throw new \RuntimeException("Ed25519 is not supported in this OpenSSL environment.");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
$pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
|
|
627
|
+
if (!$pkey || !@openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
628
|
+
throw new \RuntimeException("Failed to generate Ed25519 private key.");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
$details = openssl_pkey_get_details($pkey);
|
|
632
|
+
$publicKeyPem = $details['key'] ?? '';
|
|
633
|
+
if (empty($publicKeyPem)) {
|
|
634
|
+
throw new \RuntimeException("Failed to extract Ed25519 public key.");
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if ($outDir === '') {
|
|
638
|
+
$outDir = dirname(__DIR__, 2) . '/config';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!is_dir($outDir)) {
|
|
642
|
+
if (function_exists('wp_mkdir_p')) {
|
|
643
|
+
wp_mkdir_p($outDir);
|
|
644
|
+
} else {
|
|
645
|
+
mkdir($outDir, 0755, true);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
$privName = $options['privateKeyName'] ?? 'issuer-private.pem';
|
|
650
|
+
$pubName = $options['publicKeyName'] ?? 'issuer-public.pem';
|
|
651
|
+
|
|
652
|
+
$privateKeyPath = rtrim($outDir, '/\\') . '/' . $privName;
|
|
653
|
+
$publicKeyPath = rtrim($outDir, '/\\') . '/' . $pubName;
|
|
654
|
+
|
|
655
|
+
file_put_contents($privateKeyPath, $privateKeyPem);
|
|
656
|
+
@chmod($privateKeyPath, 0600);
|
|
657
|
+
file_put_contents($publicKeyPath, $publicKeyPem);
|
|
658
|
+
@chmod($publicKeyPath, 0644);
|
|
659
|
+
|
|
660
|
+
return [
|
|
661
|
+
'privateKeyPath' => $privateKeyPath,
|
|
662
|
+
'publicKeyPath' => $publicKeyPath,
|
|
663
|
+
'privateKey' => $privateKeyPem,
|
|
664
|
+
'publicKey' => $publicKeyPem,
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Generates an encrypted and signed stateless ticket containing the
|
|
670
|
+
* authorization context. Uses Ed25519 if a private key is available,
|
|
671
|
+
* otherwise falls back to AES-256-CBC + HMAC-SHA256.
|
|
672
|
+
*
|
|
673
|
+
* @param array $payload The authorization context to embed.
|
|
674
|
+
* @return string The encoded ticket.
|
|
493
675
|
*/
|
|
494
676
|
public static function generateStatelessTicket(array $payload): string
|
|
495
677
|
{
|
|
@@ -513,17 +695,19 @@ class ChallengeUtils
|
|
|
513
695
|
$iv = random_bytes(16);
|
|
514
696
|
$encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
515
697
|
$signature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
516
|
-
|
|
698
|
+
|
|
517
699
|
return rtrim(strtr(base64_encode($iv), '+/', '-_'), '=') . '.' .
|
|
518
|
-
|
|
519
|
-
|
|
700
|
+
rtrim(strtr(base64_encode($encrypted), '+/', '-_'), '=') . '.' .
|
|
701
|
+
rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
|
|
520
702
|
}
|
|
521
703
|
|
|
522
704
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* @
|
|
705
|
+
* Decodes and validates an encrypted and signed stateless ticket.
|
|
706
|
+
* Supports both Ed25519 and AES-256-CBC + HMAC-SHA256 formats.
|
|
707
|
+
*
|
|
708
|
+
* @param string $ticket The ticket to decode.
|
|
709
|
+
* @param string $secret Optional secret for HMAC verification.
|
|
710
|
+
* @return array|null The decoded payload, or null if invalid.
|
|
527
711
|
*/
|
|
528
712
|
public static function parseStatelessTicket(string $ticket, string $secret = ''): ?array
|
|
529
713
|
{
|
|
@@ -538,13 +722,13 @@ class ChallengeUtils
|
|
|
538
722
|
};
|
|
539
723
|
$payloadJson = $base64UrlDecode($parts[1]);
|
|
540
724
|
$signature = $base64UrlDecode($parts[2]);
|
|
541
|
-
|
|
725
|
+
|
|
542
726
|
$ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
|
|
543
727
|
if (!$ed25519PubKey) {
|
|
544
728
|
self::logError("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
545
729
|
return null;
|
|
546
730
|
}
|
|
547
|
-
|
|
731
|
+
|
|
548
732
|
$publicKey = openssl_pkey_get_public($ed25519PubKey);
|
|
549
733
|
if ($publicKey && openssl_verify($payloadJson, $signature, $publicKey, null) === 1) {
|
|
550
734
|
return json_decode($payloadJson, true);
|
|
@@ -579,8 +763,18 @@ class ChallengeUtils
|
|
|
579
763
|
}
|
|
580
764
|
|
|
581
765
|
/**
|
|
582
|
-
*
|
|
583
|
-
*
|
|
766
|
+
* Checks whether a pass ticket is valid. Supports opaque tickets (via the store)
|
|
767
|
+
* and the legacy stateless fallback. Handles greenlist, ZKP, same-IP, same-subnet,
|
|
768
|
+
* and cross-network roaming validations.
|
|
769
|
+
*
|
|
770
|
+
* @param string|null $ip The client IP.
|
|
771
|
+
* @param string|null $ticket The ticket to validate.
|
|
772
|
+
* @param string $deviceId The client device ID.
|
|
773
|
+
* @param string $deviceHash The client device hash.
|
|
774
|
+
* @param bool $allowCrossNetworkRoaming Whether to allow roaming across networks.
|
|
775
|
+
* @param string $secret Optional secret for legacy ticket validation.
|
|
776
|
+
* @param string $zkpProof Optional ZKP proof in the format "y:t:s".
|
|
777
|
+
* @return bool True if the ticket is valid, false otherwise.
|
|
584
778
|
*/
|
|
585
779
|
public static function isTicketValid(
|
|
586
780
|
?string $ip,
|
|
@@ -595,7 +789,7 @@ class ChallengeUtils
|
|
|
595
789
|
return false;
|
|
596
790
|
}
|
|
597
791
|
|
|
598
|
-
//
|
|
792
|
+
// Try stateless validation first
|
|
599
793
|
$ticketData = self::parseStatelessTicket($ticket, $secret);
|
|
600
794
|
if ($ticketData !== null) {
|
|
601
795
|
$expiry = $ticketData['expiry'] ?? null;
|
|
@@ -611,18 +805,18 @@ class ChallengeUtils
|
|
|
611
805
|
return true;
|
|
612
806
|
}
|
|
613
807
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
808
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
809
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
810
|
+
if (!empty($zkpProof)) {
|
|
811
|
+
$zkpParts = explode(':', $zkpProof);
|
|
812
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
813
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
814
|
+
return true;
|
|
622
815
|
}
|
|
623
816
|
}
|
|
624
|
-
return false;
|
|
625
817
|
}
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
626
820
|
if ($ip === $originalIp) {
|
|
627
821
|
return true;
|
|
628
822
|
}
|
|
@@ -655,18 +849,18 @@ class ChallengeUtils
|
|
|
655
849
|
return true;
|
|
656
850
|
}
|
|
657
851
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
852
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
853
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
854
|
+
if (!empty($zkpProof)) {
|
|
855
|
+
$zkpParts = explode(':', $zkpProof);
|
|
856
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
857
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
858
|
+
return true;
|
|
666
859
|
}
|
|
667
860
|
}
|
|
668
|
-
return false;
|
|
669
861
|
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
670
864
|
|
|
671
865
|
if ($ip === $originalIp) {
|
|
672
866
|
return true;
|
|
@@ -685,7 +879,7 @@ class ChallengeUtils
|
|
|
685
879
|
return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
|
|
686
880
|
}
|
|
687
881
|
|
|
688
|
-
//
|
|
882
|
+
// Backward-compatible fallback for legacy signed tickets (stateless)
|
|
689
883
|
if (!str_contains($ticket, ':')) {
|
|
690
884
|
return false;
|
|
691
885
|
}
|
|
@@ -701,7 +895,12 @@ class ChallengeUtils
|
|
|
701
895
|
}
|
|
702
896
|
|
|
703
897
|
/**
|
|
704
|
-
*
|
|
898
|
+
* Computes the difficulty target for a CPU challenge based on the
|
|
899
|
+
* suspicion factor. Higher suspicion yields a harder (smaller) target.
|
|
900
|
+
*
|
|
901
|
+
* @param float $suspicionFactor The suspicion factor (0.0 to 1.0).
|
|
902
|
+
* @param array $securityConfig The security configuration.
|
|
903
|
+
* @return string The target as a hex string.
|
|
705
904
|
*/
|
|
706
905
|
public static function calculateCpuTarget(float $suspicionFactor, array $securityConfig): string
|
|
707
906
|
{
|
|
@@ -712,7 +911,7 @@ class ChallengeUtils
|
|
|
712
911
|
$totalDifficultyBits = $minDifficultyBits + $suspicionFactor * ($maxDifficultyBits - $minDifficultyBits);
|
|
713
912
|
|
|
714
913
|
if ($totalDifficultyBits <= 0) {
|
|
715
|
-
//
|
|
914
|
+
// Maximum target (trivial challenge)
|
|
716
915
|
return (BigInt::pow(2, 256)->sub(new BigInt(1)))->toHex();
|
|
717
916
|
}
|
|
718
917
|
|
|
@@ -721,7 +920,15 @@ class ChallengeUtils
|
|
|
721
920
|
}
|
|
722
921
|
|
|
723
922
|
/**
|
|
724
|
-
*
|
|
923
|
+
* Creates the base data block used by the CPU challenge. The fingerprint
|
|
924
|
+
* parts are sorted to ensure deterministic ordering.
|
|
925
|
+
*
|
|
926
|
+
* @param string $nonce The challenge nonce.
|
|
927
|
+
* @param string $clientSecret The client secret.
|
|
928
|
+
* @param string $fingerprint The client fingerprint (pipe-separated).
|
|
929
|
+
* @param string $clientIp The client IP.
|
|
930
|
+
* @param string $tlsSessionId The TLS session ID.
|
|
931
|
+
* @return string The concatenated base block.
|
|
725
932
|
*/
|
|
726
933
|
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
|
|
727
934
|
{
|
|
@@ -729,13 +936,23 @@ class ChallengeUtils
|
|
|
729
936
|
$filteredParts = array_filter($parts);
|
|
730
937
|
sort($filteredParts);
|
|
731
938
|
$sortedFingerprint = implode('|', $filteredParts);
|
|
732
|
-
|
|
939
|
+
|
|
733
940
|
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
|
|
734
941
|
}
|
|
735
942
|
|
|
736
943
|
/**
|
|
737
|
-
*
|
|
738
|
-
*
|
|
944
|
+
* Verifies a CPU Proof-of-Work solution and, on success, generates a
|
|
945
|
+
* stateless ticket. In HTTP (insecure) mode, no SHA-256 computation is
|
|
946
|
+
* required and a ticket is issued directly.
|
|
947
|
+
*
|
|
948
|
+
* @param string $clientIp The client IP.
|
|
949
|
+
* @param int $ticketTtl The ticket time-to-live in milliseconds.
|
|
950
|
+
* @param string $nonce The challenge nonce.
|
|
951
|
+
* @param string $solution The client-provided solution.
|
|
952
|
+
* @param array $challengeContext The challenge context (cpuTarget, baseBlock, isHttp).
|
|
953
|
+
* @param string $deviceId The client device ID.
|
|
954
|
+
* @param string $deviceHash The client device hash.
|
|
955
|
+
* @return string|null The opaque ticket on success, or null on failure.
|
|
739
956
|
*/
|
|
740
957
|
public static function verifyCpuTargetPoWAndGenerateTicket(
|
|
741
958
|
string $clientIp,
|
|
@@ -746,9 +963,9 @@ class ChallengeUtils
|
|
|
746
963
|
string $deviceId = '',
|
|
747
964
|
string $deviceHash = ''
|
|
748
965
|
): ?string {
|
|
749
|
-
//
|
|
966
|
+
// In HTTP (insecure) mode, no SHA-256 computation is required
|
|
750
967
|
if (!empty($challengeContext['isHttp'])) {
|
|
751
|
-
self::logError('[FP Server Verify]
|
|
968
|
+
self::logError('[FP Server Verify] HTTP mode detected (insecure policy): validation without SHA-256 accepted.');
|
|
752
969
|
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
753
970
|
return self::generateStatelessTicket([
|
|
754
971
|
'expiry' => $expiry,
|
|
@@ -775,7 +992,7 @@ class ChallengeUtils
|
|
|
775
992
|
|
|
776
993
|
if ($isValid) {
|
|
777
994
|
self::logError('[FP Server Verify] CPU PoW verification PASSED.');
|
|
778
|
-
|
|
995
|
+
|
|
779
996
|
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
780
997
|
$payload = [
|
|
781
998
|
'expiry' => $expiry,
|
|
@@ -797,7 +1014,13 @@ class ChallengeUtils
|
|
|
797
1014
|
}
|
|
798
1015
|
|
|
799
1016
|
/**
|
|
800
|
-
*
|
|
1017
|
+
* Checks the Token Bucket rate limiter for challenge requests from a subnet.
|
|
1018
|
+
* Tokens are refilled at a constant rate up to a maximum capacity.
|
|
1019
|
+
*
|
|
1020
|
+
* @param string $clientIp The client IP (subnet is used as the rate-limit key).
|
|
1021
|
+
* @param float $capacity The bucket capacity (max tokens).
|
|
1022
|
+
* @param float $refillRate The token refill rate (tokens per second).
|
|
1023
|
+
* @return bool True if the request is allowed, false if rate-limited.
|
|
801
1024
|
*/
|
|
802
1025
|
public static function checkChallengeRateLimit(string $clientIp, float $capacity = 5.0, float $refillRate = 0.1): bool
|
|
803
1026
|
{
|
|
@@ -836,7 +1059,14 @@ class ChallengeUtils
|
|
|
836
1059
|
}
|
|
837
1060
|
|
|
838
1061
|
/**
|
|
839
|
-
*
|
|
1062
|
+
* Derives the set of challenged block indices for a memory PoW solution,
|
|
1063
|
+
* based on the seed, the solution, and the number of blocks.
|
|
1064
|
+
*
|
|
1065
|
+
* @param string $seed The challenge seed.
|
|
1066
|
+
* @param int $solution The client solution (used as a salt in the hash).
|
|
1067
|
+
* @param int $numBlocks The total number of blocks.
|
|
1068
|
+
* @param int $k The number of challenged indices to derive (default 4).
|
|
1069
|
+
* @return array<int> The list of challenged block indices.
|
|
840
1070
|
*/
|
|
841
1071
|
private static function getChallengedIndices(string $seed, int $solution, int $numBlocks, int $k = 4): array
|
|
842
1072
|
{
|
|
@@ -849,6 +1079,16 @@ class ChallengeUtils
|
|
|
849
1079
|
return $indices;
|
|
850
1080
|
}
|
|
851
1081
|
|
|
1082
|
+
/**
|
|
1083
|
+
* Verifies a Merkle proof for a given leaf hash, index, and expected root.
|
|
1084
|
+
* Recomputes the root by hashing pairs of siblings along the path.
|
|
1085
|
+
*
|
|
1086
|
+
* @param string $leafHash The hex hash of the leaf.
|
|
1087
|
+
* @param int $index The leaf index in the tree.
|
|
1088
|
+
* @param array $proof The list of sibling hashes (hex).
|
|
1089
|
+
* @param string $root The expected Merkle root (hex).
|
|
1090
|
+
* @return bool True if the proof is valid, false otherwise.
|
|
1091
|
+
*/
|
|
852
1092
|
private static function verifyMerkleProof(string $leafHash, int $index, array $proof, string $root): bool
|
|
853
1093
|
{
|
|
854
1094
|
$currentHash = $leafHash;
|
|
@@ -861,6 +1101,17 @@ class ChallengeUtils
|
|
|
861
1101
|
return $currentHash === $root;
|
|
862
1102
|
}
|
|
863
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* Legacy memory PoW verification, used for low-difficulty challenges with
|
|
1106
|
+
* a simple numeric solution. Rebuilds the whole buffer and replays the
|
|
1107
|
+
* random walk to compare with the provided solution.
|
|
1108
|
+
*
|
|
1109
|
+
* @param string $nonce The challenge nonce.
|
|
1110
|
+
* @param int $solution The client-provided solution.
|
|
1111
|
+
* @param int $difficulty The difficulty (in MB).
|
|
1112
|
+
* @param string $clientSecret The client secret.
|
|
1113
|
+
* @return bool True if the solution matches, false otherwise.
|
|
1114
|
+
*/
|
|
864
1115
|
private static function verifyMemoryPoWLegacy(string $nonce, int $solution, int $difficulty, string $clientSecret): bool
|
|
865
1116
|
{
|
|
866
1117
|
$size = $difficulty * 1024 * 1024;
|
|
@@ -887,6 +1138,17 @@ class ChallengeUtils
|
|
|
887
1138
|
return $finalHash === $solution;
|
|
888
1139
|
}
|
|
889
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Verifies a memory PoW solution. Supports both the modern JSON format
|
|
1143
|
+
* (with Merkle proofs) and the legacy numeric format for low difficulties.
|
|
1144
|
+
* Enforces a maximum allowed difficulty to prevent DoS.
|
|
1145
|
+
*
|
|
1146
|
+
* @param string $nonce The challenge nonce.
|
|
1147
|
+
* @param string $solution The client-provided solution (JSON or numeric string).
|
|
1148
|
+
* @param int $difficulty The difficulty (in MB). 0 means the challenge is skipped.
|
|
1149
|
+
* @param string $clientSecret The client secret.
|
|
1150
|
+
* @return bool True if the solution is valid, false otherwise.
|
|
1151
|
+
*/
|
|
890
1152
|
public static function verifyMemoryPoW(
|
|
891
1153
|
string $nonce,
|
|
892
1154
|
string $solution,
|
|
@@ -972,7 +1234,12 @@ class ChallengeUtils
|
|
|
972
1234
|
}
|
|
973
1235
|
|
|
974
1236
|
/**
|
|
975
|
-
*
|
|
1237
|
+
* Emulates JavaScript's 32-bit signed integer multiplication (Math.imul),
|
|
1238
|
+
* handling overflow correctly.
|
|
1239
|
+
*
|
|
1240
|
+
* @param int $a The first operand.
|
|
1241
|
+
* @param int $b The second operand.
|
|
1242
|
+
* @return int The 32-bit signed result of a * b.
|
|
976
1243
|
*/
|
|
977
1244
|
private static function gmp_imul(int $a, int $b): int
|
|
978
1245
|
{
|
|
@@ -984,9 +1251,11 @@ class ChallengeUtils
|
|
|
984
1251
|
}
|
|
985
1252
|
|
|
986
1253
|
/**
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
1254
|
+
* Generates a signed trap URL. The URL is picked from a template and a
|
|
1255
|
+
* signature is appended as a query parameter.
|
|
1256
|
+
*
|
|
1257
|
+
* @param string $nonce The nonce used to sign the URL.
|
|
1258
|
+
* @return string The trap URL.
|
|
990
1259
|
*/
|
|
991
1260
|
public static function generateTrapUrl(string $nonce): string
|
|
992
1261
|
{
|
|
@@ -999,11 +1268,13 @@ class ChallengeUtils
|
|
|
999
1268
|
}
|
|
1000
1269
|
|
|
1001
1270
|
/**
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
* @param string $
|
|
1006
|
-
* @
|
|
1271
|
+
* Verifies whether a given path and signature form a valid trap URL for
|
|
1272
|
+
* the given nonce. Uses hash_equals for timing-safe comparison.
|
|
1273
|
+
*
|
|
1274
|
+
* @param string $path The request path.
|
|
1275
|
+
* @param string $signature The signature from the query string.
|
|
1276
|
+
* @param string $nonce The nonce to verify against.
|
|
1277
|
+
* @return bool True if the trap URL is valid, false otherwise.
|
|
1007
1278
|
*/
|
|
1008
1279
|
public static function verifyTrapUrl(string $path, string $signature, string $nonce): bool
|
|
1009
1280
|
{
|
|
@@ -1011,13 +1282,15 @@ class ChallengeUtils
|
|
|
1011
1282
|
return false;
|
|
1012
1283
|
}
|
|
1013
1284
|
$expectedSignature = substr(hash_hmac('sha256', $nonce . $path, self::getPowSecret()), 0, 16);
|
|
1014
|
-
//
|
|
1285
|
+
// Use hash_equals for timing-safe comparison.
|
|
1015
1286
|
return hash_equals($expectedSignature, $signature);
|
|
1016
1287
|
}
|
|
1017
1288
|
|
|
1018
1289
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1290
|
+
* Loads the JavaScript solver code for inline injection into challenge pages.
|
|
1291
|
+
* Searches several candidate paths, including WordPress plugin directories.
|
|
1292
|
+
*
|
|
1293
|
+
* @return string The JavaScript solver code, or an empty string if not found.
|
|
1021
1294
|
*/
|
|
1022
1295
|
private static function getPowSolverCode(): string
|
|
1023
1296
|
{
|
|
@@ -1040,10 +1313,20 @@ class ChallengeUtils
|
|
|
1040
1313
|
return file_get_contents($solverPath) ?: '';
|
|
1041
1314
|
}
|
|
1042
1315
|
}
|
|
1043
|
-
self::logError("[ChallengeUtils]
|
|
1316
|
+
self::logError("[ChallengeUtils] Error: The pow.solver.inline.js file was not found at the expected location.");
|
|
1044
1317
|
return '';
|
|
1045
1318
|
}
|
|
1046
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Generates the HTML page for a proof-of-space challenge, embedding the
|
|
1322
|
+
* solver code and the challenge script. The page initializes local storage,
|
|
1323
|
+
* runs the proof-of-space solver, and redirects with the solution.
|
|
1324
|
+
*
|
|
1325
|
+
* @param array $challengeDetails The challenge details (nonce, sizeMb, queries, path, peerId, peerBlockIdx).
|
|
1326
|
+
* @param string $clientSecret The client secret.
|
|
1327
|
+
* @param array $securityConfig The security configuration.
|
|
1328
|
+
* @return string The generated HTML page.
|
|
1329
|
+
*/
|
|
1047
1330
|
public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
|
|
1048
1331
|
{
|
|
1049
1332
|
$nonce = $challengeDetails['nonce'];
|
|
@@ -1089,14 +1372,21 @@ class ChallengeUtils
|
|
|
1089
1372
|
}
|
|
1090
1373
|
|
|
1091
1374
|
/**
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
* @param array $
|
|
1097
|
-
* @param
|
|
1098
|
-
* @param string $
|
|
1099
|
-
* @
|
|
1375
|
+
* Generates the HTML page for a combined CPU + Memory proof-of-work challenge.
|
|
1376
|
+
* The page embeds the solver code, the challenge script, and hidden trap links
|
|
1377
|
+
* (rendered in random tags/positions) to lure malicious crawlers.
|
|
1378
|
+
*
|
|
1379
|
+
* @param array $cpuChallengeDetails The CPU challenge details (nonce, target, path).
|
|
1380
|
+
* @param int $memoryDifficulty The memory difficulty in MB (0 to skip).
|
|
1381
|
+
* @param string $clientSecret The client secret.
|
|
1382
|
+
* @param array $securityConfig The security configuration.
|
|
1383
|
+
* @param array $trapUrls The list of trap URLs to embed.
|
|
1384
|
+
* @param string $originalFingerprint The original client fingerprint.
|
|
1385
|
+
* @param string $clientIp The client IP.
|
|
1386
|
+
* @param string $tlsSessionId The TLS session ID.
|
|
1387
|
+
* @param string|null $baseBlock Optional pre-computed base block.
|
|
1388
|
+
* @param bool $isHttps Whether the request is over HTTPS.
|
|
1389
|
+
* @return string The generated HTML page.
|
|
1100
1390
|
*/
|
|
1101
1391
|
public static function generateCombinedPoWChallengePage(
|
|
1102
1392
|
array $cpuChallengeDetails,
|
|
@@ -1191,6 +1481,12 @@ class ChallengeUtils
|
|
|
1191
1481
|
);
|
|
1192
1482
|
}
|
|
1193
1483
|
|
|
1484
|
+
/**
|
|
1485
|
+
* Logs an error message using the PHP error log.
|
|
1486
|
+
*
|
|
1487
|
+
* @param string $message The message to log.
|
|
1488
|
+
* @return void
|
|
1489
|
+
*/
|
|
1194
1490
|
private static function logError(string $message): void
|
|
1195
1491
|
{
|
|
1196
1492
|
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Challenge diagnostic logging
|