@anonympins/fingerprint 0.6.3 → 0.6.5
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 +22 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +7014 -6802
- package/src/js/gpu_pow.solver.js +34 -3
- package/src/js/library.js +285 -124
- package/src/js/mongodb-store.js +13 -0
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +2 -2
- package/src/js/tests/gpu_pow.test.js +9 -7
- package/src/php/AutoTuner.php +41 -0
- package/src/php/Challenge/ChallengeUtils.php +40 -6
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +113 -25
- package/src/php/Optimization/OptimizationOperators.php +127 -35
- package/src/php/RequestContext.php +4 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -2
- package/src/php/Tests/FingerprintEngineTest.php +46 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Utils/RequestUtils.php +42 -9
package/src/js/redis-store.js
CHANGED
|
@@ -111,5 +111,18 @@ export function createRedisStore(redisClient) {
|
|
|
111
111
|
triggerFallback();
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
+
async clear() {
|
|
115
|
+
localStore.clear();
|
|
116
|
+
for (const timeout of localTimeouts.values()) {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
}
|
|
119
|
+
localTimeouts.clear();
|
|
120
|
+
if (isDown) return;
|
|
121
|
+
try {
|
|
122
|
+
await redisClient.flushdb();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
triggerFallback();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
};
|
|
115
128
|
}
|
package/src/js/sql-store.js
CHANGED
|
@@ -1,78 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for Knex.js.
|
|
3
|
-
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a Knex.js client.
|
|
9
|
-
*
|
|
10
|
-
* **Note:** You must create the table yourself before using the store.
|
|
11
|
-
* The table should have at least the following columns:
|
|
12
|
-
* - `key` (string, primary key)
|
|
13
|
-
* - `value` (text or json/jsonb)
|
|
14
|
-
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
-
*
|
|
16
|
-
* Example schema for PostgreSQL:
|
|
17
|
-
* ```sql
|
|
18
|
-
* CREATE TABLE your_table_name (
|
|
19
|
-
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
-
* "value" TEXT NOT NULL,
|
|
21
|
-
* "expiresAt" TIMESTAMPTZ
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
-
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
-
*/
|
|
29
|
-
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
-
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
async get(key) {
|
|
36
|
-
const row = await knex(tableName).where('key', key).first();
|
|
37
|
-
if (!row) return null;
|
|
38
|
-
|
|
39
|
-
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
-
await this.delete(key); // Clean up expired key.
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(row.value, reviver);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// In case of malformed JSON, treat it as a miss.
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async set(key, value, ttl) {
|
|
54
|
-
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
-
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
-
|
|
57
|
-
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
-
await knex(tableName)
|
|
59
|
-
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
-
.onConflict('key')
|
|
61
|
-
.merge();
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
async has(key) {
|
|
65
|
-
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
-
if (!row) return false;
|
|
67
|
-
// Also check for expiration here.
|
|
68
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async delete(key) {
|
|
75
|
-
await knex(tableName).where('key', key).del();
|
|
76
|
-
},
|
|
77
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for Knex.js.
|
|
3
|
+
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
+
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a Knex.js client.
|
|
9
|
+
*
|
|
10
|
+
* **Note:** You must create the table yourself before using the store.
|
|
11
|
+
* The table should have at least the following columns:
|
|
12
|
+
* - `key` (string, primary key)
|
|
13
|
+
* - `value` (text or json/jsonb)
|
|
14
|
+
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
+
*
|
|
16
|
+
* Example schema for PostgreSQL:
|
|
17
|
+
* ```sql
|
|
18
|
+
* CREATE TABLE your_table_name (
|
|
19
|
+
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
+
* "value" TEXT NOT NULL,
|
|
21
|
+
* "expiresAt" TIMESTAMPTZ
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
+
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
+
*/
|
|
29
|
+
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
+
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async get(key) {
|
|
36
|
+
const row = await knex(tableName).where('key', key).first();
|
|
37
|
+
if (!row) return null;
|
|
38
|
+
|
|
39
|
+
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
+
await this.delete(key); // Clean up expired key.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(row.value, reviver);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// In case of malformed JSON, treat it as a miss.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async set(key, value, ttl) {
|
|
54
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
+
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
+
|
|
57
|
+
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
+
await knex(tableName)
|
|
59
|
+
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
+
.onConflict('key')
|
|
61
|
+
.merge();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async has(key) {
|
|
65
|
+
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
+
if (!row) return false;
|
|
67
|
+
// Also check for expiration here.
|
|
68
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await knex(tableName).where('key', key).del();
|
|
76
|
+
},
|
|
77
|
+
async clear() {
|
|
78
|
+
await knex(tableName).del();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
78
81
|
}
|
|
@@ -92,8 +92,7 @@ describe('FingerprintEngine Challenge Validation', () => {
|
|
|
92
92
|
|
|
93
93
|
// The client solves the challenge
|
|
94
94
|
// --- FIX: Simulate the client creating the baseBlock for the solver ---
|
|
95
|
-
const
|
|
96
|
-
const baseBlock = new TextEncoder().encode(messageBase);
|
|
95
|
+
const baseBlock = new Uint8Array(challengeContext.baseBlock);
|
|
97
96
|
|
|
98
97
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
99
98
|
// The memory challenge seed does not include the fingerprint.
|
|
@@ -153,7 +152,7 @@ describe('FingerprintEngine Challenge Validation', () => {
|
|
|
153
152
|
// --- 2. Second request: Solution is submitted from "Machine B" ---
|
|
154
153
|
// --- FIX: Simulate the client creating the baseBlock for the solver ---
|
|
155
154
|
// The solver uses the fingerprint of the machine it's running on.
|
|
156
|
-
const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B
|
|
155
|
+
const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:127.0.0.1::`;
|
|
157
156
|
const baseBlock = new TextEncoder().encode(messageBase);
|
|
158
157
|
|
|
159
158
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
@@ -739,7 +739,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
739
739
|
const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
|
|
740
740
|
|
|
741
741
|
// The client constructs the base block and solves the challenge
|
|
742
|
-
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}
|
|
742
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
|
|
743
743
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
|
|
744
744
|
const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
|
|
745
745
|
|
|
@@ -821,7 +821,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
821
821
|
const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
|
|
822
822
|
|
|
823
823
|
// The server stores the challenge context. We need to ensure the `baseBlock` is present for verification.
|
|
824
|
-
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}
|
|
824
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
|
|
825
825
|
await inMemoryStore.set(`secret:${nonce}`, {
|
|
826
826
|
clientSecret: clientSecret,
|
|
827
827
|
cpuTarget: cpuTargetHex,
|
|
@@ -27,7 +27,7 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
|
|
|
27
27
|
const solutions = [];
|
|
28
28
|
|
|
29
29
|
for (let idx = 0; idx < 64; idx++) {
|
|
30
|
-
let x = Math.fround(numericSeed + idx * 0.015);
|
|
30
|
+
let x = Math.fround((numericSeed + idx * 0.015) % 1); // Ensure x starts in [0, 1)
|
|
31
31
|
for (let i = 0; i < iterations; i++) {
|
|
32
32
|
x = Math.fround(rFloat * x * Math.fround(1.0 - x));
|
|
33
33
|
}
|
|
@@ -53,19 +53,21 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
|
|
|
53
53
|
const solutions = [];
|
|
54
54
|
|
|
55
55
|
for (let idx = 0; idx < 64; idx++) {
|
|
56
|
-
let x = Math.fround(numericSeed + idx * 0.015);
|
|
56
|
+
let x = Math.fround((numericSeed + idx * 0.015) % 1); // Ensure x starts in [0, 1)
|
|
57
57
|
for (let i = 0; i < iterations; i++) {
|
|
58
58
|
x = Math.fround(rFloat * x * Math.fround(1.0 - x));
|
|
59
59
|
}
|
|
60
60
|
solutions.push(x.toFixed(6));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// Tamper with
|
|
63
|
+
// Tamper with one of the dynamically derived sample indices
|
|
64
|
+
const sampleIndices = GpuPowSolver.deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
|
|
65
|
+
const tamperedIndex = sampleIndices[0];
|
|
64
66
|
const tamperedSolutions = [...solutions];
|
|
65
|
-
const originalVal = parseFloat(tamperedSolutions[
|
|
66
|
-
tamperedSolutions[
|
|
67
|
-
|
|
68
|
-
expect(
|
|
67
|
+
const originalVal = parseFloat(tamperedSolutions[tamperedIndex]);
|
|
68
|
+
tamperedSolutions[tamperedIndex] = (originalVal + 0.1).toFixed(6); // exceeding tolerance
|
|
69
|
+
const verificationResult = GpuPowSolver.verify(seed, iterations, tamperedSolutions.join(','), '127.0.0.1', 'gpu-pow-salt');
|
|
70
|
+
expect(verificationResult).toBe(false);
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
it('should fallback to WebGL2 if WebGPU is not supported', async () => {
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -53,6 +53,47 @@ class AutoTuner
|
|
|
53
53
|
$this->onCleanup = $options['onCleanup'] ?? null;
|
|
54
54
|
$this->savePath = $options['savePath'] ?? null;
|
|
55
55
|
$this->validationTolerance = $options['validationTolerance'] ?? 0.15;
|
|
56
|
+
|
|
57
|
+
if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger']) && !isset($this->securityConfig['logger_wrapped'])) {
|
|
58
|
+
$originalLogger = $this->securityConfig['logger'];
|
|
59
|
+
$maxPercentage = $this->securityConfig['autotuning']['maxDensityPercentage'] ?? 0.02;
|
|
60
|
+
|
|
61
|
+
$this->securityConfig['logger'] = function (array $log) use ($originalLogger, $maxPercentage) {
|
|
62
|
+
$ip = $log['clientIp'] ?? $log['ip'] ?? null;
|
|
63
|
+
$subnet = $ip ? RequestUtils::getIpSubnet($ip, 24, 48) : null;
|
|
64
|
+
$fp = $log['deviceHash'] ?? $log['fingerprint'] ?? $log['deviceFingerprint'] ?? null;
|
|
65
|
+
$stableFp = $fp ? RequestUtils::extractStablePart($fp) : null;
|
|
66
|
+
|
|
67
|
+
if (count($this->trafficData) > 0) {
|
|
68
|
+
$total = count($this->trafficData);
|
|
69
|
+
$ipMatchCount = 0;
|
|
70
|
+
$fpMatchCount = 0;
|
|
71
|
+
$matchedKey = null;
|
|
72
|
+
|
|
73
|
+
foreach ($this->trafficData as $key => $existingLog) {
|
|
74
|
+
$logIp = $existingLog['clientIp'] ?? $existingLog['ip'] ?? null;
|
|
75
|
+
$logSubnet = $logIp ? RequestUtils::getIpSubnet($logIp, 24, 48) : null;
|
|
76
|
+
$logFp = $existingLog['deviceHash'] ?? $existingLog['fingerprint'] ?? $existingLog['deviceFingerprint'] ?? null;
|
|
77
|
+
$logStableFp = $logFp ? RequestUtils::extractStablePart($logFp) : null;
|
|
78
|
+
|
|
79
|
+
$isIpMatch = $subnet && $logSubnet === $subnet;
|
|
80
|
+
$isFpMatch = $stableFp && $logStableFp === $stableFp;
|
|
81
|
+
if ($isIpMatch) $ipMatchCount++;
|
|
82
|
+
if ($isFpMatch) $fpMatchCount++;
|
|
83
|
+
if ($isIpMatch || $isFpMatch) $matchedKey = $key;
|
|
84
|
+
}
|
|
85
|
+
if (($subnet && ($ipMatchCount / $total) > $maxPercentage) || ($stableFp && ($fpMatchCount / $total) > $maxPercentage)) {
|
|
86
|
+
if ($matchedKey !== null) {
|
|
87
|
+
$this->trafficData[$matchedKey]['instancesCount'] = ($this->trafficData[$matchedKey]['instancesCount'] ?? 1) + 1;
|
|
88
|
+
$this->trafficData[$matchedKey]['weight'] = ($this->trafficData[$matchedKey]['weight'] ?? 1.0) + 1.0;
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
$originalLogger($log);
|
|
94
|
+
};
|
|
95
|
+
$this->securityConfig['logger_wrapped'] = true;
|
|
96
|
+
}
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
/**
|
|
@@ -32,12 +32,33 @@ class ChallengeUtils
|
|
|
32
32
|
return abs($hash % 1000000) / 1000000;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
public static function
|
|
35
|
+
public static function deriveSampleIndices(string $clientIp, string $secret): array
|
|
36
|
+
{
|
|
37
|
+
$timeWindow = (int)floor(time() / (60 * 5));
|
|
38
|
+
$message = "{$clientIp}:{$timeWindow}";
|
|
39
|
+
$hmacHex = hash_hmac('sha256', $message, $secret ?: 'gpu-pow-salt');
|
|
40
|
+
$hmacBytes = array_values(unpack('C*', hex2bin($hmacHex)));
|
|
41
|
+
|
|
42
|
+
$indices = [];
|
|
43
|
+
for ($i = 0; $i < 4; $i++) {
|
|
44
|
+
$offset = ($i * 2) % count($hmacBytes);
|
|
45
|
+
$value = ($hmacBytes[$offset] << 8) | $hmacBytes[($offset + 1) % count($hmacBytes)];
|
|
46
|
+
$idx = $value % 64;
|
|
47
|
+
while (in_array($idx, $indices, true)) {
|
|
48
|
+
$idx = ($idx + 1) % 64;
|
|
49
|
+
}
|
|
50
|
+
$indices[] = $idx;
|
|
51
|
+
}
|
|
52
|
+
return $indices;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public static function verifyGpuPow(string $seed, int $iterations, string $solution, string $clientIp = '127.0.0.1', string $secret = 'gpu-pow-salt'): bool
|
|
36
56
|
{
|
|
37
57
|
$values = explode(',', $solution);
|
|
38
58
|
if (count($values) !== 64) {
|
|
39
59
|
return false;
|
|
40
60
|
}
|
|
61
|
+
$sampleIndices = self::deriveSampleIndices($clientIp, $secret);
|
|
41
62
|
$numericSeed = self::hashSeedToFloat($seed);
|
|
42
63
|
$r = 3.9999;
|
|
43
64
|
foreach ($sampleIndices as $idx) {
|
|
@@ -203,9 +224,22 @@ class ChallengeUtils
|
|
|
203
224
|
return ['error' => 'Missing signature'];
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
|
|
207
|
-
$store->
|
|
208
|
-
|
|
227
|
+
$peersKey = "fed-peers:{$zkpY}";
|
|
228
|
+
$reportedPeers = $store->get($peersKey) ?: [];
|
|
229
|
+
if (!is_array($reportedPeers)) {
|
|
230
|
+
$reportedPeers = [];
|
|
231
|
+
}
|
|
232
|
+
if (!in_array($clientIp, $reportedPeers, true)) {
|
|
233
|
+
$reportedPeers[] = $clientIp;
|
|
234
|
+
$store->set($peersKey, $reportedPeers, 86400 * 30);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
$threshold = $config['federationConsensusThreshold'] ?? 3;
|
|
238
|
+
if (count($reportedPeers) >= $threshold) {
|
|
239
|
+
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
240
|
+
return ['status' => 'synchronized', 'banned' => true];
|
|
241
|
+
}
|
|
242
|
+
return ['status' => 'synchronized', 'banned' => false, 'reportsCount' => count($reportedPeers)];
|
|
209
243
|
}
|
|
210
244
|
|
|
211
245
|
$nodeId = $params['node_id'] ?? '';
|
|
@@ -633,14 +667,14 @@ class ChallengeUtils
|
|
|
633
667
|
/**
|
|
634
668
|
* Crée le bloc de données de base pour le challenge CPU.
|
|
635
669
|
*/
|
|
636
|
-
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
|
|
670
|
+
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
|
|
637
671
|
{
|
|
638
672
|
$parts = explode('|', $fingerprint);
|
|
639
673
|
$filteredParts = array_filter($parts);
|
|
640
674
|
sort($filteredParts);
|
|
641
675
|
$sortedFingerprint = implode('|', $filteredParts);
|
|
642
676
|
|
|
643
|
-
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
|
|
677
|
+
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
|
|
644
678
|
}
|
|
645
679
|
|
|
646
680
|
/**
|
|
@@ -58,6 +58,7 @@ class SecurityProfiles
|
|
|
58
58
|
'decayFactor' => 0.9,
|
|
59
59
|
'inactivityReset' => 5000,
|
|
60
60
|
],
|
|
61
|
+
'filterWhitelist' => 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
|
|
61
62
|
'wasm' => true,
|
|
62
63
|
],
|
|
63
64
|
|
|
@@ -108,6 +109,7 @@ class SecurityProfiles
|
|
|
108
109
|
],
|
|
109
110
|
'challengeNewDevices' => true, // Challenge all new devices
|
|
110
111
|
'wasm' => true,
|
|
112
|
+
'filterWhitelist' => true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
|
|
111
113
|
],
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -155,6 +157,7 @@ class SecurityProfiles
|
|
|
155
157
|
],
|
|
156
158
|
// This would be a callable in PHP, but for now, we represent its intent.
|
|
157
159
|
'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
|
|
160
|
+
'filterWhitelist' => 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
|
|
158
161
|
'wasm' => true,
|
|
159
162
|
],
|
|
160
163
|
|
|
@@ -204,6 +207,7 @@ class SecurityProfiles
|
|
|
204
207
|
'decayFactor' => 0.92,
|
|
205
208
|
'inactivityReset' => 10000,
|
|
206
209
|
],
|
|
210
|
+
'filterWhitelist' => 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
|
|
207
211
|
'wasm' => true,
|
|
208
212
|
],
|
|
209
213
|
|
|
@@ -257,6 +261,7 @@ class SecurityProfiles
|
|
|
257
261
|
// This would be a callable in PHP, but for now, we represent its intent.
|
|
258
262
|
'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
|
|
259
263
|
'wasm' => true,
|
|
264
|
+
'filterWhitelist' => true, // Tolérance zéro pour le scraping / scalping distribué
|
|
260
265
|
],
|
|
261
266
|
];
|
|
262
267
|
|
|
@@ -237,6 +237,9 @@
|
|
|
237
237
|
$this->logger = isset($securityConfig['logger']) && is_callable($securityConfig['logger']) ? new Logger($securityConfig['logger']) : null;
|
|
238
238
|
$this->dryRun = $securityConfig['dryRun'] ?? false;
|
|
239
239
|
$this->validateConfig($securityConfig);
|
|
240
|
+
if ($this->securityConfig['reset'] ?? false) {
|
|
241
|
+
$this->resetStore();
|
|
242
|
+
}
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/**
|
|
@@ -252,6 +255,18 @@
|
|
|
252
255
|
$this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Réinitialise le store de persistance actif.
|
|
260
|
+
*/
|
|
261
|
+
public function resetStore(): void
|
|
262
|
+
{
|
|
263
|
+
$store = StoreManager::getStore();
|
|
264
|
+
if (method_exists($store, 'clear')) {
|
|
265
|
+
$store->clear();
|
|
266
|
+
}
|
|
267
|
+
$this->log('Store has been reset/cleared.');
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
private function validateConfig(array $config): void
|
|
256
271
|
{
|
|
257
272
|
if (empty($config)) {
|
|
@@ -266,7 +281,7 @@
|
|
|
266
281
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
267
282
|
'similarityThreshold', 'summary', 'description',
|
|
268
283
|
'ed25519_private_key', 'ed25519_public_key',
|
|
269
|
-
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret'
|
|
284
|
+
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
|
|
270
285
|
];
|
|
271
286
|
|
|
272
287
|
if (empty($config['weights'])) {
|
|
@@ -497,28 +512,50 @@
|
|
|
497
512
|
*/
|
|
498
513
|
private function checkAllowlists(RequestContext $context): bool
|
|
499
514
|
{
|
|
515
|
+
$whitelisted = false;
|
|
516
|
+
$type = '';
|
|
517
|
+
|
|
500
518
|
if ($this->isIpInAllowlist($context->clientIp)) {
|
|
501
|
-
$
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
$
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
$
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if ($
|
|
518
|
-
$this->
|
|
519
|
+
$whitelisted = true;
|
|
520
|
+
$type = 'allowlist';
|
|
521
|
+
} elseif ($this->isPathInAllowlist($context->path)) {
|
|
522
|
+
$whitelisted = true;
|
|
523
|
+
$type = 'path_allowlist';
|
|
524
|
+
} elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
|
|
525
|
+
$whitelisted = true;
|
|
526
|
+
$type = 'host_path_allowlist';
|
|
527
|
+
} elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
|
|
528
|
+
$whitelisted = true;
|
|
529
|
+
$type = 'graphql_operation_allowlist';
|
|
530
|
+
} elseif ($this->verifyWhitelistedBot($context)) {
|
|
531
|
+
$whitelisted = true;
|
|
532
|
+
$type = 'bot';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if ($whitelisted) {
|
|
536
|
+
$filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
|
|
537
|
+
$bypassWhitelist = false;
|
|
538
|
+
if ($filterWhitelist === true) {
|
|
539
|
+
$bypassWhitelist = $this->hasCertainAttack($context);
|
|
540
|
+
} elseif (is_numeric($filterWhitelist)) {
|
|
541
|
+
if ($context->preCalculatedScore === null) {
|
|
542
|
+
$suspicionVector = [];
|
|
543
|
+
$context->preCalculatedVector = $this->getSuspicionVector($context, $suspicionVector);
|
|
544
|
+
$context->preCalculatedScore = $this->calculateFinalScore($context->preCalculatedVector);
|
|
545
|
+
}
|
|
546
|
+
if ($context->preCalculatedScore > $filterWhitelist) {
|
|
547
|
+
$bypassWhitelist = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if ($bypassWhitelist) {
|
|
552
|
+
$this->log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
$this->log("IP/Path in allowlist ({$type}) - allowing request", ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
519
556
|
return true;
|
|
520
557
|
}
|
|
521
|
-
|
|
558
|
+
|
|
522
559
|
return false;
|
|
523
560
|
}
|
|
524
561
|
|
|
@@ -908,6 +945,23 @@
|
|
|
908
945
|
}
|
|
909
946
|
}
|
|
910
947
|
|
|
948
|
+
/**
|
|
949
|
+
* Évalue si la requête présente des caractéristiques d'attaque flagrantes ou a déclenché un honeypot.
|
|
950
|
+
*/
|
|
951
|
+
private function hasCertainAttack(RequestContext $context): bool
|
|
952
|
+
{
|
|
953
|
+
$honeypotConfig = $this->securityConfig['honeypot'] ?? [];
|
|
954
|
+
$honeypot = RequestUtils::getHoneypotScore($context, $honeypotConfig);
|
|
955
|
+
if (($honeypot['honeypotScore'] ?? 0.0) >= 100.0) {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
$bot = RequestUtils::getBotScore($context);
|
|
959
|
+
if (($bot['botScore'] ?? 0.0) >= 100.0) {
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
911
965
|
/**
|
|
912
966
|
* Traite une requête entrante et retourne une décision.
|
|
913
967
|
* @param RequestContext $context Le contexte de la requête.
|
|
@@ -1032,6 +1086,28 @@
|
|
|
1032
1086
|
$problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
|
|
1033
1087
|
// FIX: La solution est directement le $workResult, pas une sous-propriété.
|
|
1034
1088
|
$problemManager->integrateSolution($problemId, $workResult);
|
|
1089
|
+
|
|
1090
|
+
// Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
|
|
1091
|
+
// on applique directement la meilleure solution calculée au moteur en direct.
|
|
1092
|
+
if ($problemId === 'security_auto_tuning' && ($this->securityConfig['autotuning']['enabled'] ?? false)) {
|
|
1093
|
+
$paretoFront = $workResult['paretoFront'] ?? null;
|
|
1094
|
+
if (is_array($paretoFront) && !empty($paretoFront)) {
|
|
1095
|
+
$bestSolution = $paretoFront[0];
|
|
1096
|
+
$minDistance = sqrt(pow((float)$bestSolution['objectives'][0], 2) + pow((float)$bestSolution['objectives'][1], 2));
|
|
1097
|
+
for ($i = 1; $i < count($paretoFront); $i++) {
|
|
1098
|
+
$distance = sqrt(pow((float)$paretoFront[$i]['objectives'][0], 2) + pow((float)$paretoFront[$i]['objectives'][1], 2));
|
|
1099
|
+
if ($distance < $minDistance) {
|
|
1100
|
+
$minDistance = $distance;
|
|
1101
|
+
$bestSolution = $paretoFront[$i];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (isset($bestSolution['solution'])) {
|
|
1105
|
+
$this->updateConfig($bestSolution['solution']);
|
|
1106
|
+
$this->log('Useful Work auto-tuning applied successfully to live config.');
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1035
1111
|
$isValid = true;
|
|
1036
1112
|
// FIX: Générer un vrai ticket pour uPoW, comme pour un PoW normal.
|
|
1037
1113
|
$ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000; // 1 heure par défaut
|
|
@@ -1113,8 +1189,8 @@
|
|
|
1113
1189
|
return $decision;
|
|
1114
1190
|
}
|
|
1115
1191
|
|
|
1116
|
-
$suspicionVector = $this->getSuspicionVector($context, $suspicionVector);
|
|
1117
|
-
$finalScore = $this->calculateFinalScore($suspicionVector);
|
|
1192
|
+
$suspicionVector = $context->preCalculatedVector ?? $this->getSuspicionVector($context, $suspicionVector);
|
|
1193
|
+
$finalScore = $context->preCalculatedScore ?? $this->calculateFinalScore($suspicionVector);
|
|
1118
1194
|
$this->log('Suspicion vector and final score calculated', [
|
|
1119
1195
|
'finalScore' => round($finalScore, 2),
|
|
1120
1196
|
'vector' => $suspicionVector
|
|
@@ -1173,10 +1249,21 @@
|
|
|
1173
1249
|
// 1. Valider cryptographiquement la preuve avant de bannir/diffuser
|
|
1174
1250
|
if (ChallengeUtils::verifyZkpProof($zkpY, $zkpT, $zkpS)) {
|
|
1175
1251
|
// 2. Dédoublonner : Ne diffuser que si la clé n'est pas déjà bannie
|
|
1176
|
-
|
|
1252
|
+
$peersKey = "fed-peers:{$zkpY}";
|
|
1253
|
+
$reportedPeers = $store->get($peersKey) ?: [];
|
|
1254
|
+
if (!is_array($reportedPeers)) {
|
|
1255
|
+
$reportedPeers = [];
|
|
1256
|
+
}
|
|
1257
|
+
if (!in_array('local', $reportedPeers, true)) {
|
|
1258
|
+
$reportedPeers[] = 'local';
|
|
1259
|
+
$store->set($peersKey, $reportedPeers, 86400 * 30);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
$threshold = $this->securityConfig['federationConsensusThreshold'] ?? 3;
|
|
1263
|
+
if (count($reportedPeers) >= $threshold) {
|
|
1177
1264
|
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
1178
|
-
$this->broadcastBannedZkp($zkpY);
|
|
1179
1265
|
}
|
|
1266
|
+
$this->broadcastBannedZkp($zkpY);
|
|
1180
1267
|
}
|
|
1181
1268
|
}
|
|
1182
1269
|
|
|
@@ -1354,7 +1441,8 @@
|
|
|
1354
1441
|
$memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
|
|
1355
1442
|
|
|
1356
1443
|
$originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
|
|
1357
|
-
$
|
|
1444
|
+
$tlsSessionId = $context->tlsSessionId ?? '';
|
|
1445
|
+
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
|
|
1358
1446
|
|
|
1359
1447
|
$challengeContext = [
|
|
1360
1448
|
'clientSecret' => $clientSecret,
|