@anonympins/fingerprint 0.4.5 → 0.4.6
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 +18 -0
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.js +442 -59
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/fingerprint.test.js +59 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +97 -0
- package/src/php/FingerprintEngine.php +136 -8
- 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/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +45 -0
package/src/js/pow.solver.js
CHANGED
|
@@ -9,6 +9,98 @@
|
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
+
function cyrb53(str, seed = 0) {
|
|
13
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
14
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
15
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
16
|
+
ch = str.charCodeAt(i);
|
|
17
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
18
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
19
|
+
}
|
|
20
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
21
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
22
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function openDb() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const request = indexedDB.open("pospace-db", 1);
|
|
28
|
+
request.onupgradeneeded = (e) => {
|
|
29
|
+
const db = e.target.result;
|
|
30
|
+
if (!db.objectStoreNames.contains("blocks")) {
|
|
31
|
+
db.createObjectStore("blocks");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
35
|
+
request.onerror = (e) => reject(e.target.error);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
40
|
+
const block = new Uint8Array(blockSize);
|
|
41
|
+
let h = cyrb53(seed + ":" + blockIndex);
|
|
42
|
+
for (let i = 0; i < blockSize; i++) {
|
|
43
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
44
|
+
block[i] = h & 0xff;
|
|
45
|
+
}
|
|
46
|
+
return block;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function initializeSpace(seed, sizeMb) {
|
|
50
|
+
const db = await openDb();
|
|
51
|
+
const transaction = db.transaction("blocks", "readwrite");
|
|
52
|
+
const store = transaction.objectStore("blocks");
|
|
53
|
+
const numBlocks = sizeMb * 1024;
|
|
54
|
+
const metadataKey = "pospace-metadata";
|
|
55
|
+
|
|
56
|
+
const metaReq = store.get(metadataKey);
|
|
57
|
+
const meta = await new Promise((resolve) => {
|
|
58
|
+
metaReq.onsuccess = () => resolve(metaReq.result);
|
|
59
|
+
});
|
|
60
|
+
if (meta && meta.sizeMb === sizeMb && meta.seed === seed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CHUNK_SIZE = 1000;
|
|
65
|
+
for (let i = 0; i < numBlocks; i += CHUNK_SIZE) {
|
|
66
|
+
const end = Math.min(numBlocks, i + CHUNK_SIZE);
|
|
67
|
+
for (let j = i; j < end; j++) {
|
|
68
|
+
const block = generateBlock(seed, j);
|
|
69
|
+
store.put(block, j);
|
|
70
|
+
}
|
|
71
|
+
await new Promise(r => setTimeout(r, 0));
|
|
72
|
+
}
|
|
73
|
+
store.put({ sizeMb, seed }, metadataKey);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
77
|
+
const db = await openDb();
|
|
78
|
+
const transaction = db.transaction("blocks", "readonly");
|
|
79
|
+
const store = transaction.objectStore("blocks");
|
|
80
|
+
|
|
81
|
+
let combined = new Uint8Array(queries.length * 1024);
|
|
82
|
+
for (let i = 0; i < queries.length; i++) {
|
|
83
|
+
const idx = queries[i];
|
|
84
|
+
const getReq = store.get(idx);
|
|
85
|
+
let block = await new Promise((resolve) => {
|
|
86
|
+
getReq.onsuccess = () => resolve(getReq.result);
|
|
87
|
+
});
|
|
88
|
+
if (!block) {
|
|
89
|
+
block = generateBlock(seed, idx);
|
|
90
|
+
}
|
|
91
|
+
combined.set(block, i * 1024);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const encoder = new TextEncoder();
|
|
95
|
+
const nonceBytes = encoder.encode(nonce + ":" + clientSecret);
|
|
96
|
+
const finalBlock = new Uint8Array(combined.length + nonceBytes.length);
|
|
97
|
+
finalBlock.set(combined);
|
|
98
|
+
finalBlock.set(nonceBytes, combined.length);
|
|
99
|
+
|
|
100
|
+
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
101
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
102
|
+
}
|
|
103
|
+
|
|
12
104
|
/**
|
|
13
105
|
* Résout un challenge CPU basé sur une cible en utilisant un bloc de base binaire.
|
|
14
106
|
* @param {Uint8Array} baseBlock - Le bloc de données initial (nonce, secret, fp) fourni par le serveur.
|
|
@@ -458,7 +550,7 @@ class ChallengeSolution {
|
|
|
458
550
|
* @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
|
|
459
551
|
*/
|
|
460
552
|
export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
|
|
461
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
553
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask, queries, sizeMb } = challenge;
|
|
462
554
|
let rawSolution = {};
|
|
463
555
|
|
|
464
556
|
switch (type) {
|
|
@@ -515,6 +607,10 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
|
|
|
515
607
|
rawSolution.work_result = workResult;
|
|
516
608
|
rawSolution.problem_id = usefulWorkTask.problemId;
|
|
517
609
|
break;
|
|
610
|
+
case 'pospace':
|
|
611
|
+
await initializeSpace(nonce + ":" + clientSecret, sizeMb || 100);
|
|
612
|
+
rawSolution.hash = await solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
613
|
+
break;
|
|
518
614
|
default:
|
|
519
615
|
throw new Error(`Unknown challenge type: ${type}`);
|
|
520
616
|
}
|
|
@@ -66,6 +66,18 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
66
66
|
expect(FingerprintBuilder.compare(fp1, ''), "Comparison with empty string should be 0").toBe(0);
|
|
67
67
|
expect(FingerprintBuilder.compare(null, fp2), "Comparison with null should be 0").toBe(0);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it('should calculate a high crossLayerInconsistencyScore when viewport width exceeds screen width', () => {
|
|
71
|
+
const context = {
|
|
72
|
+
headers: {
|
|
73
|
+
'x-device-fingerprint': `scr:${cyrb53("1920x1080_24")}`,
|
|
74
|
+
'sec-ch-viewport-width': '2560'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { crossLayerInconsistencyScore } = __internal.getCrossLayerInconsistency(context);
|
|
79
|
+
expect(crossLayerInconsistencyScore).toBe(20);
|
|
80
|
+
});
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
describe('JA3 Fingerprinting', () => {
|
|
@@ -1635,6 +1647,53 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1635
1647
|
});
|
|
1636
1648
|
});
|
|
1637
1649
|
|
|
1650
|
+
describe('TLS Session Resumption (Cookieless Identity)', () => {
|
|
1651
|
+
const inMemoryStore = {
|
|
1652
|
+
_map: new Map(),
|
|
1653
|
+
async get(key) { return this._map.get(key); },
|
|
1654
|
+
async set(key, value) { this._map.set(key, value); },
|
|
1655
|
+
async has(key) { return this._map.has(key); },
|
|
1656
|
+
async delete(key) { this._map.delete(key); },
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
beforeEach(() => {
|
|
1660
|
+
inMemoryStore._map.clear();
|
|
1661
|
+
configureStore(inMemoryStore);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('should resume device ID using TLS Session ID even if cookies are cleared', async () => {
|
|
1665
|
+
const securityConfig = {
|
|
1666
|
+
weights: { historyScore: 1.0 },
|
|
1667
|
+
thresholds: { low: 20, block: 95 }
|
|
1668
|
+
};
|
|
1669
|
+
const engine = new FingerprintEngine(securityConfig);
|
|
1670
|
+
|
|
1671
|
+
const req1 = {
|
|
1672
|
+
clientIp: '1.2.3.4',
|
|
1673
|
+
path: '/',
|
|
1674
|
+
cookies: {},
|
|
1675
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1676
|
+
rawHeaders: [],
|
|
1677
|
+
httpVersion: '1.1'
|
|
1678
|
+
};
|
|
1679
|
+
const decision1 = await engine.processRequest(req1);
|
|
1680
|
+
const deviceId = decision1.newCookieForResponse.value;
|
|
1681
|
+
expect(deviceId).toBeDefined();
|
|
1682
|
+
|
|
1683
|
+
const req2 = {
|
|
1684
|
+
clientIp: '1.2.3.4',
|
|
1685
|
+
path: '/',
|
|
1686
|
+
cookies: {}, // Cookies cleared!
|
|
1687
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1688
|
+
rawHeaders: [],
|
|
1689
|
+
httpVersion: '1.1'
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
const decision2 = await engine.processRequest(req2);
|
|
1693
|
+
expect(decision2.newCookieForResponse).toBeUndefined(); // Resumed successfully, no new cookie needed
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1638
1697
|
describe('getTlsSpoofingScore', () => {
|
|
1639
1698
|
let getTlsFingerprintMock; // Renamed to reflect it's the mock function
|
|
1640
1699
|
let getTlsSpoofingScore;
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -77,7 +77,7 @@ class AutoTuner
|
|
|
77
77
|
|
|
78
78
|
echo sprintf("[AutoTuning] Démarrage du cycle d'optimisation complet avec %d points de données assainis.\n", count($sanitizedData));
|
|
79
79
|
|
|
80
|
-
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData]);
|
|
80
|
+
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
|
|
81
81
|
|
|
82
82
|
if (empty($paretoFront)) {
|
|
83
83
|
echo "[AutoTuning] L'optimisation n'a retourné aucune solution.\n";
|
|
@@ -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,65 @@ 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 generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
52
|
+
{
|
|
53
|
+
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
54
|
+
$sizeMb = $pospaceConfig['sizeMb'] ?? 100;
|
|
55
|
+
$numQueries = $pospaceConfig['numQueries'] ?? 10;
|
|
56
|
+
|
|
57
|
+
$queries = [];
|
|
58
|
+
$maxBlocks = $sizeMb * 1024;
|
|
59
|
+
while (count($queries) < $numQueries) {
|
|
60
|
+
$idx = random_int(0, $maxBlocks - 1);
|
|
61
|
+
if (!in_array($idx, $queries, true)) {
|
|
62
|
+
$queries[] = $idx;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
'type' => 'pospace',
|
|
68
|
+
'nonce' => $nonce,
|
|
69
|
+
'sizeMb' => $sizeMb,
|
|
70
|
+
'queries' => $queries,
|
|
71
|
+
'path' => $originalUrl
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
76
|
+
{
|
|
77
|
+
$combined = '';
|
|
78
|
+
foreach ($queries as $idx) {
|
|
79
|
+
$combined .= self::generateBlock($seed, (int)$idx);
|
|
80
|
+
}
|
|
81
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
82
|
+
$hash = hash('sha256', $finalBlock);
|
|
83
|
+
return hash_equals($hash, $solution);
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
/**
|
|
27
87
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
28
88
|
*/
|
|
@@ -401,6 +461,43 @@ class ChallengeUtils
|
|
|
401
461
|
return file_get_contents($solverPath) ?: '';
|
|
402
462
|
}
|
|
403
463
|
|
|
464
|
+
public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
|
|
465
|
+
{
|
|
466
|
+
$nonce = $challengeDetails['nonce'];
|
|
467
|
+
$sizeMb = $challengeDetails['sizeMb'];
|
|
468
|
+
$queries = $challengeDetails['queries'];
|
|
469
|
+
$path = $challengeDetails['path'];
|
|
470
|
+
|
|
471
|
+
$solverCode = self::getPowSolverCode();
|
|
472
|
+
$queriesJson = json_encode($queries);
|
|
473
|
+
|
|
474
|
+
$challengeScript = <<<JS
|
|
475
|
+
async function solve() {
|
|
476
|
+
const nonce = "{$nonce}";
|
|
477
|
+
const path = "{$path}";
|
|
478
|
+
const clientSecret = "{$clientSecret}";
|
|
479
|
+
const queries = {$queriesJson};
|
|
480
|
+
const sizeMb = {$sizeMb};
|
|
481
|
+
|
|
482
|
+
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
483
|
+
await new Promise(r => setTimeout(r, 10));
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
487
|
+
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
488
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
489
|
+
|
|
490
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
491
|
+
} catch(e) {
|
|
492
|
+
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
solve();
|
|
496
|
+
JS;
|
|
497
|
+
|
|
498
|
+
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>";
|
|
499
|
+
}
|
|
500
|
+
|
|
404
501
|
/**
|
|
405
502
|
* Génère le contenu HTML pour un challenge combiné CPU + Mémoire.
|
|
406
503
|
* @param array $cpuChallengeDetails
|
|
@@ -225,6 +225,16 @@
|
|
|
225
225
|
$deviceData = null;
|
|
226
226
|
$newCookie = null;
|
|
227
227
|
|
|
228
|
+
// Cookieless Identity Tracking: Attempt to restore device ID using TLS session resume ID
|
|
229
|
+
$tlsSessionId = $context->tlsSessionId;
|
|
230
|
+
if (!$deviceId && $tlsSessionId) {
|
|
231
|
+
$resumedDeviceId = $store->get("tls-session:{$tlsSessionId}");
|
|
232
|
+
if ($resumedDeviceId) {
|
|
233
|
+
$deviceId = $resumedDeviceId;
|
|
234
|
+
$this->log('Identity resumed via TLS Session ID', ['deviceId' => $deviceId, 'tlsSessionId' => $tlsSessionId]);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
228
238
|
if ($deviceId) {
|
|
229
239
|
$deviceData = $store->get("device:{$deviceId}");
|
|
230
240
|
}
|
|
@@ -275,6 +285,11 @@
|
|
|
275
285
|
}
|
|
276
286
|
}
|
|
277
287
|
|
|
288
|
+
// Bind the current TLS session ID to the device ID
|
|
289
|
+
if ($deviceId && $tlsSessionId) {
|
|
290
|
+
$store->set("tls-session:{$tlsSessionId}", $deviceId, 3600); // 1h cache duration
|
|
291
|
+
}
|
|
292
|
+
|
|
278
293
|
return [
|
|
279
294
|
'deviceId' => $deviceId,
|
|
280
295
|
'deviceData' => $deviceData,
|
|
@@ -515,6 +530,53 @@
|
|
|
515
530
|
];
|
|
516
531
|
}
|
|
517
532
|
|
|
533
|
+
|
|
534
|
+
private function decodePolymorphicFingerprint(string $fpString, array $mapping): string
|
|
535
|
+
{
|
|
536
|
+
if (empty($mapping['keys'])) {
|
|
537
|
+
return $fpString;
|
|
538
|
+
}
|
|
539
|
+
$reverseKeys = array_flip($mapping['keys']);
|
|
540
|
+
$parts = explode('|', $fpString);
|
|
541
|
+
$mappedParts = [];
|
|
542
|
+
foreach ($parts as $part) {
|
|
543
|
+
$pair = explode(':', $part, 2);
|
|
544
|
+
if (count($pair) === 2) {
|
|
545
|
+
$origKey = $reverseKeys[$pair[0]] ?? $pair[0];
|
|
546
|
+
$mappedParts[] = "{$origKey}:{$pair[1]}";
|
|
547
|
+
} else {
|
|
548
|
+
$mappedParts[] = $part;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return implode('|', $mappedParts);
|
|
552
|
+
}
|
|
553
|
+
private function translatePolymorphicHeaders(RequestContext $context): void
|
|
554
|
+
{
|
|
555
|
+
$store = StoreManager::getStore();
|
|
556
|
+
$activeMappings = $store->get('active-polymorphic-mappings') ?: [];
|
|
557
|
+
if (!is_array($activeMappings)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
foreach ($activeMappings as $mapping) {
|
|
562
|
+
$devFpHeader = strtolower($mapping['headers']['x-device-fingerprint'] ?? '');
|
|
563
|
+
$behaviorHeader = strtolower($mapping['headers']['x-behavior-metrics'] ?? '');
|
|
564
|
+
|
|
565
|
+
if (!empty($devFpHeader) && isset($context->headers[$devFpHeader])) {
|
|
566
|
+
$context->headers['x-device-fingerprint'] = $context->headers[$devFpHeader];
|
|
567
|
+
if (!empty($behaviorHeader) && isset($context->headers[$behaviorHeader])) {
|
|
568
|
+
$context->headers['x-behavior-metrics'] = $context->headers[$behaviorHeader];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
$clientFp = $context->headers['x-device-fingerprint'];
|
|
572
|
+
if ($clientFp && is_string($clientFp)) {
|
|
573
|
+
$context->headers['x-device-fingerprint'] = $this->decodePolymorphicFingerprint($clientFp, $mapping);
|
|
574
|
+
}
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
518
580
|
/**
|
|
519
581
|
* Traite une requête entrante et retourne une décision.
|
|
520
582
|
* @param RequestContext $context Le contexte de la requête.
|
|
@@ -522,8 +584,21 @@
|
|
|
522
584
|
*/
|
|
523
585
|
public function processRequest(RequestContext $context): array
|
|
524
586
|
{
|
|
587
|
+
$this->translatePolymorphicHeaders($context);
|
|
588
|
+
|
|
525
589
|
// Initialiser le vecteur de suspicion pour éviter les erreurs de type.
|
|
526
590
|
$suspicionVector = [];
|
|
591
|
+
|
|
592
|
+
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
593
|
+
|
|
594
|
+
// Parse GraphQL query if applicable
|
|
595
|
+
if ($context->path === '/graphql' && !empty($context->body)) {
|
|
596
|
+
$gqlInfo = RequestUtils::parseGraphQLQuery(is_array($context->body) ? $context->body : []);
|
|
597
|
+
if ($gqlInfo) {
|
|
598
|
+
$context->graphqlOperation = $gqlInfo;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
527
602
|
|
|
528
603
|
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
529
604
|
|
|
@@ -549,6 +624,7 @@
|
|
|
549
624
|
$isChallengeSubmission = $powNonce && (
|
|
550
625
|
isset($context->query['pow_solution']) ||
|
|
551
626
|
isset($context->query['pow_solution_cpu']) ||
|
|
627
|
+
isset($context->query['pow_solution_space']) ||
|
|
552
628
|
(isset($context->query['pow_type']) && $context->query['pow_type'] === 'useful_work_task')
|
|
553
629
|
);
|
|
554
630
|
if ($isChallengeSubmission) {
|
|
@@ -587,6 +663,28 @@
|
|
|
587
663
|
$isValid = $isValid && $isMemValid;
|
|
588
664
|
}
|
|
589
665
|
}
|
|
666
|
+
} elseif ($powType === 'pospace') {
|
|
667
|
+
$powSolutionSpace = $context->query['pow_solution_space'] ?? null;
|
|
668
|
+
if ($powSolutionSpace && isset($challengeContext['queries'])) {
|
|
669
|
+
$isSpaceValid = ChallengeUtils::verifySpacePoW(
|
|
670
|
+
$powNonce,
|
|
671
|
+
$powSolutionSpace,
|
|
672
|
+
$challengeContext['queries'],
|
|
673
|
+
$powNonce . ":" . $challengeContext['clientSecret'],
|
|
674
|
+
$challengeContext['clientSecret']
|
|
675
|
+
);
|
|
676
|
+
$isValid = $isSpaceValid;
|
|
677
|
+
if ($isValid) {
|
|
678
|
+
$ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000;
|
|
679
|
+
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
680
|
+
$ticket = ChallengeUtils::generateStatelessTicket([
|
|
681
|
+
'expiry' => $expiry,
|
|
682
|
+
'originalIp' => $context->clientIp,
|
|
683
|
+
'deviceId' => '',
|
|
684
|
+
'deviceHash' => ''
|
|
685
|
+
]);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
590
688
|
} elseif ($powType === 'useful_work_task') {
|
|
591
689
|
$problemId = $context->query['pow_problem_id'] ?? null;
|
|
592
690
|
$workResultJson = $context->query['pow_solution_work_result'] ?? null;
|
|
@@ -622,10 +720,6 @@
|
|
|
622
720
|
$this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
|
|
623
721
|
|
|
624
722
|
return [
|
|
625
|
-
// ... (le reste de la logique de redirection)
|
|
626
|
-
'action' => 'redirect',
|
|
627
|
-
'path' => RequestUtils::cleanUrlFromPowParams($challengeContext['originalPath'] ?? '/', $context->query),
|
|
628
|
-
'score' => 0.0,
|
|
629
723
|
'action' => 'redirect',
|
|
630
724
|
'path' => RequestUtils::cleanUrlFromPowParams($challengeContext['originalPath'] ?? '/', $context->query),
|
|
631
725
|
'score' => 0.0,
|
|
@@ -791,7 +885,11 @@
|
|
|
791
885
|
|
|
792
886
|
$nonce = bin2hex(random_bytes(16));
|
|
793
887
|
$clientSecret = bin2hex(random_bytes(16));
|
|
794
|
-
|
|
888
|
+
$highThreshold = $thresholds['high'] ?? 75;
|
|
889
|
+
|
|
890
|
+
$suspicionFactor = ($finalScore - $lowThreshold) / (($thresholds['high'] ?? 75) - $lowThreshold);
|
|
891
|
+
$suspicionFactor = max(0, min(1.5, $suspicionFactor));
|
|
892
|
+
|
|
795
893
|
// --- NOUVELLE LOGIQUE uPoW ---
|
|
796
894
|
$shouldUseUsefulWork = ($this->securityConfig['enableUsefulWork'] ?? false) && (
|
|
797
895
|
($this->securityConfig['forceUsefulWork'] ?? false) || (random_int(0, 255) / 255) > 0.5
|
|
@@ -843,9 +941,39 @@
|
|
|
843
941
|
|
|
844
942
|
// --- FIN DE LA LOGIQUE uPoW (le reste est le fallback) ---
|
|
845
943
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
944
|
+
if ($this->securityConfig['enableProofOfSpace'] ?? false) {
|
|
945
|
+
$spaceChallenge = ChallengeUtils::generateSpaceChallenge($context->clientIp, $nonce, $suspicionFactor, $context->path, $this->securityConfig);
|
|
946
|
+
$store->set("secret:{$nonce}", [
|
|
947
|
+
'clientSecret' => $clientSecret,
|
|
948
|
+
'suspicionScore' => $finalScore,
|
|
949
|
+
'queries' => $spaceChallenge['queries'],
|
|
950
|
+
'sizeMb' => $spaceChallenge['sizeMb'],
|
|
951
|
+
'fingerprint' => RequestUtils::getCompositeDeviceHash($context),
|
|
952
|
+
'originalPath' => $context->path,
|
|
953
|
+
], $this->securityConfig['challengeTtl'] ?? 300);
|
|
954
|
+
|
|
955
|
+
if ($deviceData) {
|
|
956
|
+
$deviceData['lastChallengeNonce'] = $nonce;
|
|
957
|
+
$store->set("device:{$deviceId}", $deviceData); // @phpstan-ignore-line
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if ($isApiRequest) {
|
|
961
|
+
$decision['body'] = [
|
|
962
|
+
'challenge' => [
|
|
963
|
+
'type' => 'pospace',
|
|
964
|
+
'nonce' => $nonce,
|
|
965
|
+
'clientSecret' => $clientSecret,
|
|
966
|
+
'queries' => $spaceChallenge['queries'],
|
|
967
|
+
'sizeMb' => $spaceChallenge['sizeMb'],
|
|
968
|
+
]
|
|
969
|
+
];
|
|
970
|
+
} else {
|
|
971
|
+
$page = ChallengeUtils::generateSpaceChallengePage($spaceChallenge, $clientSecret, $this->securityConfig);
|
|
972
|
+
$decision['body'] = $page;
|
|
973
|
+
}
|
|
974
|
+
return $decision;
|
|
975
|
+
}
|
|
976
|
+
|
|
849
977
|
$cpuChallengeDetails = [
|
|
850
978
|
'type' => 'cpu_target',
|
|
851
979
|
'nonce' => $nonce,
|