@anonympins/fingerprint 0.4.5 → 0.5.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 +39 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +767 -215
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +389 -285
- package/src/js/pow.solver.js +105 -1
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +191 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +72 -1
- package/src/php/Challenge/ChallengeUtils.php +395 -5
- package/src/php/FingerprintEngine.php +176 -10
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +45 -0
|
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Challenge;
|
|
6
6
|
|
|
7
7
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
8
|
+
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
8
9
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
9
10
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
10
11
|
|
|
@@ -23,6 +24,235 @@ class ChallengeUtils
|
|
|
23
24
|
'/.git/config_{RANDOM}'
|
|
24
25
|
];
|
|
25
26
|
|
|
27
|
+
private static function imul(int $a, int $b): int
|
|
28
|
+
{
|
|
29
|
+
$ah = ($a >> 16) & 0xffff;
|
|
30
|
+
$al = $a & 0xffff;
|
|
31
|
+
$bh = ($b >> 16) & 0xffff;
|
|
32
|
+
$bl = $b & 0xffff;
|
|
33
|
+
$lo = $al * $bl;
|
|
34
|
+
$hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
|
|
35
|
+
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private static function generateBlock(string $seed, int $blockIndex, int $blockSize = 1024): string
|
|
39
|
+
{
|
|
40
|
+
$block = str_repeat("\x00", $blockSize);
|
|
41
|
+
$h = FingerprintBuilder::cyrb53($seed . ":" . $blockIndex);
|
|
42
|
+
|
|
43
|
+
$h_int = (int)bcmod($h, '4294967296');
|
|
44
|
+
for ($i = 0; $i < $blockSize; $i++) {
|
|
45
|
+
$h_int = self::imul($h_int ^ $i, 1597334677);
|
|
46
|
+
$block[$i] = chr($h_int & 0xff);
|
|
47
|
+
}
|
|
48
|
+
return $block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static function registerCooperativeNode(string $clientIp, string $nodeId, string $seed): void
|
|
52
|
+
{
|
|
53
|
+
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
54
|
+
if ($subnet === null) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
$store = StoreManager::getStore();
|
|
58
|
+
$key = "coop-pospace:subnet:{$subnet}";
|
|
59
|
+
$nodes = $store->get($key) ?? [];
|
|
60
|
+
|
|
61
|
+
$now = time();
|
|
62
|
+
// Nettoyage des nœuds expirés (vieux de plus de 2 minutes)
|
|
63
|
+
$nodes = array_filter($nodes, fn($n) => ($now - $n['timestamp']) < 120);
|
|
64
|
+
|
|
65
|
+
$nodes[$nodeId] = [
|
|
66
|
+
'nodeId' => $nodeId,
|
|
67
|
+
'seed' => $seed,
|
|
68
|
+
'timestamp' => $now
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
$store->set($key, $nodes, 120);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static function findPeerInSubnet(string $clientIp, string $excludeNodeId): ?array
|
|
75
|
+
{
|
|
76
|
+
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
77
|
+
if ($subnet === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
$store = StoreManager::getStore();
|
|
81
|
+
$key = "coop-pospace:subnet:{$subnet}";
|
|
82
|
+
$nodes = $store->get($key) ?? [];
|
|
83
|
+
|
|
84
|
+
$now = time();
|
|
85
|
+
$activePeers = [];
|
|
86
|
+
foreach ($nodes as $id => $node) {
|
|
87
|
+
if ($id !== $excludeNodeId && ($now - $node['timestamp']) < 120) {
|
|
88
|
+
$activePeers[] = $node;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (empty($activePeers)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return $activePeers[array_rand($activePeers)];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public static function handleCooperativeRequest(array $params): ?array
|
|
100
|
+
{
|
|
101
|
+
$op = $params['coop_op'] ?? null;
|
|
102
|
+
if (!$op) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$store = StoreManager::getStore();
|
|
107
|
+
$nodeId = $params['node_id'] ?? '';
|
|
108
|
+
if (empty($nodeId)) {
|
|
109
|
+
return ['error' => 'Missing node_id'];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
switch ($op) {
|
|
113
|
+
case 'register':
|
|
114
|
+
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
115
|
+
$seed = $params['seed'] ?? '';
|
|
116
|
+
self::registerCooperativeNode($clientIp, $nodeId, $seed);
|
|
117
|
+
return ['status' => 'registered'];
|
|
118
|
+
|
|
119
|
+
case 'request_peer_block':
|
|
120
|
+
$peerId = $params['peer_id'] ?? '';
|
|
121
|
+
$blockIdx = (int)($params['block_idx'] ?? 0);
|
|
122
|
+
$requestId = $params['req_id'] ?? '';
|
|
123
|
+
if (empty($peerId) || empty($requestId)) {
|
|
124
|
+
return ['error' => 'Invalid parameters'];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$queueKey = "coop-mailbox:queue:{$peerId}";
|
|
128
|
+
$requests = $store->get($queueKey) ?? [];
|
|
129
|
+
$requests[] = [
|
|
130
|
+
'req_id' => $requestId,
|
|
131
|
+
'requester_id' => $nodeId,
|
|
132
|
+
'block_idx' => $blockIdx
|
|
133
|
+
];
|
|
134
|
+
$store->set($queueKey, $requests, 30);
|
|
135
|
+
return ['status' => 'queued'];
|
|
136
|
+
|
|
137
|
+
case 'poll_requests':
|
|
138
|
+
$queueKey = "coop-mailbox:queue:{$nodeId}";
|
|
139
|
+
$requests = $store->get($queueKey) ?? [];
|
|
140
|
+
$store->delete($queueKey);
|
|
141
|
+
return ['requests' => $requests];
|
|
142
|
+
|
|
143
|
+
case 'respond_block':
|
|
144
|
+
$requesterId = $params['requester_id'] ?? '';
|
|
145
|
+
$requestId = $params['req_id'] ?? '';
|
|
146
|
+
$blockData = $params['block_data'] ?? '';
|
|
147
|
+
if (empty($requesterId) || empty($requestId)) {
|
|
148
|
+
return ['error' => 'Invalid parameters'];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$responseKey = "coop-mailbox:res:{$requesterId}:{$requestId}";
|
|
152
|
+
$store->set($responseKey, ['block_data' => $blockData], 30);
|
|
153
|
+
return ['status' => 'delivered'];
|
|
154
|
+
|
|
155
|
+
case 'poll_response':
|
|
156
|
+
$requestId = $params['req_id'] ?? '';
|
|
157
|
+
$responseKey = "coop-mailbox:res:{$nodeId}:{$requestId}";
|
|
158
|
+
$data = $store->get($responseKey);
|
|
159
|
+
if ($data) {
|
|
160
|
+
$store->delete($responseKey);
|
|
161
|
+
return ['status' => 'ready', 'block_data' => $data['block_data']];
|
|
162
|
+
}
|
|
163
|
+
return ['status' => 'pending'];
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
169
|
+
{
|
|
170
|
+
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
171
|
+
$sizeMb = $pospaceConfig['sizeMb'] ?? 100;
|
|
172
|
+
$numQueries = $pospaceConfig['numQueries'] ?? 10;
|
|
173
|
+
|
|
174
|
+
$queries = [];
|
|
175
|
+
$maxBlocks = $sizeMb * 1024;
|
|
176
|
+
while (count($queries) < $numQueries) {
|
|
177
|
+
$idx = random_int(0, $maxBlocks - 1);
|
|
178
|
+
if (!in_array($idx, $queries, true)) {
|
|
179
|
+
$queries[] = $idx;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
$challenge = [
|
|
184
|
+
'type' => 'pospace',
|
|
185
|
+
'nonce' => $nonce,
|
|
186
|
+
'sizeMb' => $sizeMb,
|
|
187
|
+
'queries' => $queries,
|
|
188
|
+
'path' => $originalUrl
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
// Tentative de couplage coopératif avec un nœud du même sous-réseau
|
|
192
|
+
$peer = self::findPeerInSubnet($clientIp, $nonce);
|
|
193
|
+
if ($peer !== null) {
|
|
194
|
+
$challenge['peerId'] = $peer['nodeId'];
|
|
195
|
+
$challenge['peerBlockIdx'] = random_int(0, $maxBlocks - 1);
|
|
196
|
+
|
|
197
|
+
$store = StoreManager::getStore();
|
|
198
|
+
$store->set("coop-assoc:{$nonce}", [
|
|
199
|
+
'peerNodeId' => $peer['nodeId'],
|
|
200
|
+
'peerSeed' => $peer['seed'],
|
|
201
|
+
'peerBlockIdx' => $challenge['peerBlockIdx']
|
|
202
|
+
], 120);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return $challenge;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
209
|
+
{
|
|
210
|
+
$combined = '';
|
|
211
|
+
foreach ($queries as $idx) {
|
|
212
|
+
$combined .= self::generateBlock($seed, (int)$idx);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Vérification de la preuve coopérative
|
|
216
|
+
$store = StoreManager::getStore();
|
|
217
|
+
$assoc = $store->get("coop-assoc:{$nonce}");
|
|
218
|
+
if ($assoc !== null) {
|
|
219
|
+
$peerSeed = $assoc['peerSeed'] ?? null;
|
|
220
|
+
$peerBlockIdx = $assoc['peerBlockIdx'] ?? null;
|
|
221
|
+
if ($peerSeed !== null && $peerBlockIdx !== null) {
|
|
222
|
+
$combined .= self::generateBlock($peerSeed, (int)$peerBlockIdx);
|
|
223
|
+
}
|
|
224
|
+
$store->delete("coop-assoc:{$nonce}");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
228
|
+
$hash = hash('sha256', $finalBlock);
|
|
229
|
+
return hash_equals($hash, $solution);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public static function verifyZkpProof(string $yStr, string $tStr, string $sStr): bool
|
|
233
|
+
{
|
|
234
|
+
try {
|
|
235
|
+
$p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); // secp256k1 prime
|
|
236
|
+
$g = new BigInt(2);
|
|
237
|
+
|
|
238
|
+
$y = BigInt::fromHex($yStr);
|
|
239
|
+
$t = BigInt::fromHex($tStr);
|
|
240
|
+
$s = BigInt::fromHex($sStr);
|
|
241
|
+
|
|
242
|
+
$cStr = (string)$g . (string)$y . (string)$t;
|
|
243
|
+
$cHex = hash('sha256', $cStr);
|
|
244
|
+
$c = BigInt::fromHex($cHex)->mod($p);
|
|
245
|
+
|
|
246
|
+
$left = $g->modPow($s, $p);
|
|
247
|
+
$y_c = $y->modPow($c, $p);
|
|
248
|
+
$right = $t->mul($y_c)->mod($p);
|
|
249
|
+
|
|
250
|
+
return $left->compareTo($right) === 0;
|
|
251
|
+
} catch (\Throwable $e) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
26
256
|
/**
|
|
27
257
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
28
258
|
*/
|
|
@@ -42,6 +272,22 @@ class ChallengeUtils
|
|
|
42
272
|
*/
|
|
43
273
|
public static function generateStatelessTicket(array $payload): string
|
|
44
274
|
{
|
|
275
|
+
$ed25519Key = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
|
|
276
|
+
if ($ed25519Key) {
|
|
277
|
+
try {
|
|
278
|
+
$serialized = json_encode($payload);
|
|
279
|
+
$privateKey = openssl_pkey_get_private($ed25519Key);
|
|
280
|
+
if ($privateKey && openssl_sign($serialized, $signature, $privateKey, null)) {
|
|
281
|
+
$base64UrlEncode = function ($input) {
|
|
282
|
+
return rtrim(strtr(base64_encode($input), '+/', '-_'), '=');
|
|
283
|
+
};
|
|
284
|
+
return 'ed25519.' . $base64UrlEncode($serialized) . '.' . $base64UrlEncode($signature);
|
|
285
|
+
}
|
|
286
|
+
} catch (\Throwable $e) {
|
|
287
|
+
error_log("[ChallengeUtils] Ed25519 signing failed: " . $e->getMessage());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
45
291
|
$key = hash('sha256', self::getPowSecret(), true);
|
|
46
292
|
$iv = random_bytes(16);
|
|
47
293
|
$encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
@@ -55,10 +301,40 @@ class ChallengeUtils
|
|
|
55
301
|
/**
|
|
56
302
|
* Décode et valide un ticket stateless chiffré et signé.
|
|
57
303
|
* @param string $ticket
|
|
304
|
+
* @param string $secret
|
|
58
305
|
* @return array|null
|
|
59
306
|
*/
|
|
60
|
-
public static function parseStatelessTicket(string $ticket): ?array
|
|
307
|
+
public static function parseStatelessTicket(string $ticket, string $secret = ''): ?array
|
|
61
308
|
{
|
|
309
|
+
try {
|
|
310
|
+
if (str_starts_with($ticket, 'ed25519.')) {
|
|
311
|
+
$parts = explode('.', $ticket);
|
|
312
|
+
if (count($parts) !== 3) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
$base64UrlDecode = function ($input) {
|
|
316
|
+
return base64_decode(strtr($input, '-_', '+/'));
|
|
317
|
+
};
|
|
318
|
+
$payloadJson = $base64UrlDecode($parts[1]);
|
|
319
|
+
$signature = $base64UrlDecode($parts[2]);
|
|
320
|
+
|
|
321
|
+
$ed25519PubKey = $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
|
|
322
|
+
if (!$ed25519PubKey) {
|
|
323
|
+
error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
$publicKey = openssl_pkey_get_public($ed25519PubKey);
|
|
328
|
+
if ($publicKey && openssl_verify($payloadJson, $signature, $publicKey, null) === 1) {
|
|
329
|
+
return json_decode($payloadJson, true);
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
} catch (\Throwable $e) {
|
|
334
|
+
error_log("[ChallengeUtils] Ed25519 verification failed: " . $e->getMessage());
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
62
338
|
$parts = explode('.', $ticket);
|
|
63
339
|
if (count($parts) !== 3) {
|
|
64
340
|
return null;
|
|
@@ -72,7 +348,7 @@ class ChallengeUtils
|
|
|
72
348
|
if (!$iv || !$encrypted || !$signature || strlen($iv) !== 16) {
|
|
73
349
|
return null;
|
|
74
350
|
}
|
|
75
|
-
$key = hash('sha256', self::getPowSecret(), true);
|
|
351
|
+
$key = hash('sha256', !empty($secret) ? $secret : self::getPowSecret(), true);
|
|
76
352
|
$expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
77
353
|
if (!hash_equals($expectedSignature, $signature)) {
|
|
78
354
|
return null;
|
|
@@ -83,20 +359,23 @@ class ChallengeUtils
|
|
|
83
359
|
|
|
84
360
|
/**
|
|
85
361
|
* Vérifie si un ticket de passage est valide (supporte les tickets opaques via store et le fallback legacy).
|
|
362
|
+
* Supporte une clé secrète optionnelle passée en paramètre pour la compatibilité avec les tests.
|
|
86
363
|
*/
|
|
87
364
|
public static function isTicketValid(
|
|
88
365
|
?string $ip,
|
|
89
366
|
?string $ticket,
|
|
90
367
|
string $deviceId = '',
|
|
91
368
|
string $deviceHash = '',
|
|
92
|
-
bool $allowCrossNetworkRoaming = false
|
|
369
|
+
bool $allowCrossNetworkRoaming = false,
|
|
370
|
+
string $secret = '',
|
|
371
|
+
string $zkpProof = ''
|
|
93
372
|
): bool {
|
|
94
373
|
if (empty($ip) || empty($ticket)) {
|
|
95
374
|
return false;
|
|
96
375
|
}
|
|
97
376
|
|
|
98
377
|
// Tentative de validation stateless d'abord
|
|
99
|
-
$ticketData = self::parseStatelessTicket($ticket);
|
|
378
|
+
$ticketData = self::parseStatelessTicket($ticket, $secret);
|
|
100
379
|
if ($ticketData !== null) {
|
|
101
380
|
$expiry = $ticketData['expiry'] ?? null;
|
|
102
381
|
$originalIp = $ticketData['originalIp'] ?? null;
|
|
@@ -106,6 +385,18 @@ class ChallengeUtils
|
|
|
106
385
|
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
107
386
|
return false;
|
|
108
387
|
}
|
|
388
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
389
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
390
|
+
if (!empty($zkpProof)) {
|
|
391
|
+
$zkpParts = explode(':', $zkpProof);
|
|
392
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
393
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
109
400
|
if ($ip === $originalIp) {
|
|
110
401
|
return true;
|
|
111
402
|
}
|
|
@@ -133,6 +424,18 @@ class ChallengeUtils
|
|
|
133
424
|
$store->delete("ticket:{$ticket}");
|
|
134
425
|
return false;
|
|
135
426
|
}
|
|
427
|
+
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
428
|
+
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
429
|
+
if (!empty($zkpProof)) {
|
|
430
|
+
$zkpParts = explode(':', $zkpProof);
|
|
431
|
+
if (count($zkpParts) === 3 && $zkpParts[0] === $expectedY) {
|
|
432
|
+
if (self::verifyZkpProof($zkpParts[0], $zkpParts[1], $zkpParts[2])) {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
136
439
|
|
|
137
440
|
if ($ip === $originalIp) {
|
|
138
441
|
return true;
|
|
@@ -161,7 +464,7 @@ class ChallengeUtils
|
|
|
161
464
|
return false;
|
|
162
465
|
}
|
|
163
466
|
|
|
164
|
-
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", self::getPowSecret());
|
|
467
|
+
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", !empty($secret) ? $secret : self::getPowSecret());
|
|
165
468
|
|
|
166
469
|
return hash_equals($expectedSig, $sig);
|
|
167
470
|
}
|
|
@@ -401,6 +704,93 @@ class ChallengeUtils
|
|
|
401
704
|
return file_get_contents($solverPath) ?: '';
|
|
402
705
|
}
|
|
403
706
|
|
|
707
|
+
public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
|
|
708
|
+
{
|
|
709
|
+
$nonce = $challengeDetails['nonce'];
|
|
710
|
+
$sizeMb = $challengeDetails['sizeMb'];
|
|
711
|
+
$queries = $challengeDetails['queries'];
|
|
712
|
+
$path = $challengeDetails['path'];
|
|
713
|
+
$peerId = $challengeDetails['peerId'] ?? '';
|
|
714
|
+
$peerBlockIdx = $challengeDetails['peerBlockIdx'] ?? -1;
|
|
715
|
+
$nodeId = $nonce;
|
|
716
|
+
|
|
717
|
+
$solverCode = self::getPowSolverCode();
|
|
718
|
+
$queriesJson = json_encode($queries);
|
|
719
|
+
|
|
720
|
+
$challengeScript = <<<JS
|
|
721
|
+
async function solve() {
|
|
722
|
+
const nonce = "{$nonce}";
|
|
723
|
+
const path = "{$path}";
|
|
724
|
+
const clientSecret = "{$clientSecret}";
|
|
725
|
+
const queries = {$queriesJson};
|
|
726
|
+
const sizeMb = {$sizeMb};
|
|
727
|
+
const nodeId = "{$nodeId}";
|
|
728
|
+
const peerId = "{$peerId}";
|
|
729
|
+
const peerBlockIdx = {$peerBlockIdx};
|
|
730
|
+
|
|
731
|
+
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
732
|
+
await new Promise(r => setTimeout(r, 10));
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
736
|
+
|
|
737
|
+
// Enregistrement coopératif
|
|
738
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
|
|
739
|
+
|
|
740
|
+
// Écoute des requêtes entrantes de nos pairs suspects
|
|
741
|
+
setInterval(async () => {
|
|
742
|
+
try {
|
|
743
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId);
|
|
744
|
+
const data = await res.json();
|
|
745
|
+
if (data.requests && data.requests.length > 0) {
|
|
746
|
+
for (const req of data.requests) {
|
|
747
|
+
document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
|
|
748
|
+
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
749
|
+
await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
} catch (e) {
|
|
753
|
+
console.error("Cooperative polling error", e);
|
|
754
|
+
}
|
|
755
|
+
}, 1000);
|
|
756
|
+
|
|
757
|
+
// Téléchargement du bloc du pair si configuré
|
|
758
|
+
let peerBlock = "";
|
|
759
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
760
|
+
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
761
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
762
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
|
|
763
|
+
|
|
764
|
+
let attempts = 0;
|
|
765
|
+
while (attempts < 15) {
|
|
766
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
|
|
767
|
+
const data = await res.json();
|
|
768
|
+
if (data.status === 'ready') {
|
|
769
|
+
peerBlock = data.block_data;
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
773
|
+
attempts++;
|
|
774
|
+
}
|
|
775
|
+
if (!peerBlock) {
|
|
776
|
+
document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
document.getElementById('loader').innerText = '⚙️ Génération de la Preuve d\\'Espace...';
|
|
781
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
|
|
782
|
+
|
|
783
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
|
|
784
|
+
} catch(e) {
|
|
785
|
+
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
solve();
|
|
789
|
+
JS;
|
|
790
|
+
|
|
791
|
+
return "<html><head><title>Security Check</title></head><body style=\"font-family:sans-serif; text-align:center; padding-top:50px;\"><h1>Security Check (Level 2)</h1><p>We are verifying your storage allocation. This may take a few seconds on first load.</p><div id=\"loader\" style=\"margin:20px;\">⚙️ Initializing storage space...</div><script>{$solverCode}</script><script>{$challengeScript}</script></body></html>";
|
|
792
|
+
}
|
|
793
|
+
|
|
404
794
|
/**
|
|
405
795
|
* Génère le contenu HTML pour un challenge combiné CPU + Mémoire.
|
|
406
796
|
* @param array $cpuChallengeDetails
|