@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
package/src/js/fingerprint.js
CHANGED
|
@@ -8,6 +8,8 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
|
8
8
|
import {readFileSync, existsSync} from "node:fs";
|
|
9
9
|
import {fileURLToPath} from "node:url";
|
|
10
10
|
import {dirname, join, resolve} from "node:path";
|
|
11
|
+
import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "./fingerprint.utils.js";
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = dirname(__filename);
|
|
@@ -15,6 +17,132 @@ const __dirname = dirname(__filename);
|
|
|
15
17
|
export { createRedisStore } from "./redis-store.js";
|
|
16
18
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
17
19
|
|
|
20
|
+
let activeMappings = [];
|
|
21
|
+
let lastMappingTime = 0;
|
|
22
|
+
let isCompilingMapping = false;
|
|
23
|
+
const MAPPING_ROTATION_INTERVAL = 60000; // 60 seconds
|
|
24
|
+
|
|
25
|
+
function generateSessionMapping() {
|
|
26
|
+
const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
|
|
27
|
+
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
headers: {
|
|
31
|
+
'x-device-fingerprint': randomHeader(),
|
|
32
|
+
'x-behavior-metrics': randomHeader(),
|
|
33
|
+
},
|
|
34
|
+
globals: {
|
|
35
|
+
'ClientLibrary': `ClientLib_${randomStr(6)}`,
|
|
36
|
+
'getDeviceFingerprint': `getFP_${randomStr(6)}`,
|
|
37
|
+
'getClientBehaviorMetrics': `getMetrics_${randomStr(6)}`,
|
|
38
|
+
},
|
|
39
|
+
keys: {
|
|
40
|
+
'ua': randomStr(4),
|
|
41
|
+
'hw': randomStr(4),
|
|
42
|
+
'geo': randomStr(4),
|
|
43
|
+
'scr': randomStr(4),
|
|
44
|
+
'os': randomStr(4),
|
|
45
|
+
'gpu': randomStr(4),
|
|
46
|
+
'cvs': randomStr(4),
|
|
47
|
+
'cdp': randomStr(4),
|
|
48
|
+
'bot': randomStr(4),
|
|
49
|
+
'wasm': randomStr(4),
|
|
50
|
+
},
|
|
51
|
+
wasmConstants: {
|
|
52
|
+
seed: crypto.randomBytes(4).readInt32LE(0),
|
|
53
|
+
multiplier: crypto.randomBytes(4).readInt32LE(0) | 1,
|
|
54
|
+
adder: crypto.randomBytes(4).readInt32LE(0)
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function compilePolymorphicJs(mapping) {
|
|
60
|
+
const clientScriptPath = join(__dirname, 'fingerprint.client.js');
|
|
61
|
+
let jsCode = '';
|
|
62
|
+
try {
|
|
63
|
+
jsCode = readFileSync(clientScriptPath, 'utf-8');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('[Fingerprint] Could not read fingerprint.client.js for dynamic obfuscation. Fallback to obfuscated build.');
|
|
66
|
+
try {
|
|
67
|
+
return readFileSync(join(__dirname, 'fingerprint.client.obfuscated.js'), 'utf-8');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
jsCode = jsCode.replace(/X-Device-Fingerprint/g, mapping.headers['x-device-fingerprint']);
|
|
74
|
+
jsCode = jsCode.replace(/X-Behavior-Metrics/g, mapping.headers['x-behavior-metrics']);
|
|
75
|
+
jsCode = jsCode.replace(/ClientLibrary/g, mapping.globals['ClientLibrary']);
|
|
76
|
+
jsCode = jsCode.replace(/getDeviceFingerprint/g, mapping.globals['getDeviceFingerprint']);
|
|
77
|
+
jsCode = jsCode.replace(/getClientBehaviorMetrics/g, mapping.globals['getClientBehaviorMetrics']);
|
|
78
|
+
|
|
79
|
+
for (const [origKey, randKey] of Object.entries(mapping.keys)) {
|
|
80
|
+
const regex1 = new RegExp(`add\\(["']${origKey}["']`, 'g');
|
|
81
|
+
jsCode = jsCode.replace(regex1, `add("${randKey}"`);
|
|
82
|
+
|
|
83
|
+
const regex2 = new RegExp(`addRaw\\(["']${origKey}["']`, 'g');
|
|
84
|
+
jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const obfuscationResult = JavaScriptObfuscator.obfuscate(jsCode, {
|
|
88
|
+
compact: true,
|
|
89
|
+
controlFlowFlattening: true,
|
|
90
|
+
deadCodeInjection: true,
|
|
91
|
+
stringArray: true,
|
|
92
|
+
stringArrayRotate: true,
|
|
93
|
+
stringArrayShuffle: true,
|
|
94
|
+
seed: Math.abs(mapping.wasmConstants.seed),
|
|
95
|
+
selfDefending: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return obfuscationResult.getObfuscatedCode();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function ensureLatestMapping() {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if ((now - lastMappingTime > MAPPING_ROTATION_INTERVAL || activeMappings.length === 0) && !isCompilingMapping) {
|
|
104
|
+
isCompilingMapping = true;
|
|
105
|
+
try {
|
|
106
|
+
const mapping = generateSessionMapping();
|
|
107
|
+
const polymorphicJs = await compilePolymorphicJs(mapping);
|
|
108
|
+
const polymorphicWasm = DynamicWasmGenerator.generate(mapping.wasmConstants);
|
|
109
|
+
|
|
110
|
+
mapping.jsBuffer = Buffer.from(polymorphicJs, 'utf8');
|
|
111
|
+
mapping.wasmBuffer = polymorphicWasm;
|
|
112
|
+
mapping.timestamp = now;
|
|
113
|
+
|
|
114
|
+
activeMappings.unshift(mapping);
|
|
115
|
+
if (activeMappings.length > 5) {
|
|
116
|
+
activeMappings.pop();
|
|
117
|
+
}
|
|
118
|
+
lastMappingTime = now;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await store.set('active-polymorphic-mappings', activeMappings.map(m => ({
|
|
122
|
+
headers: m.headers,
|
|
123
|
+
keys: m.keys
|
|
124
|
+
})));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Ignore
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
isCompilingMapping = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getActiveMappingForRequest(headers) {
|
|
135
|
+
if (!headers) return null;
|
|
136
|
+
for (const mapping of activeMappings) {
|
|
137
|
+
const headerName = mapping.headers['x-device-fingerprint'].toLowerCase();
|
|
138
|
+
if (headers[headerName]) {
|
|
139
|
+
return mapping;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
18
146
|
|
|
19
147
|
const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
20
148
|
const base64UrlDecode = (str) => {
|
|
@@ -26,6 +154,31 @@ const base64UrlDecode = (str) => {
|
|
|
26
154
|
};
|
|
27
155
|
|
|
28
156
|
export function generateStatelessTicket(payload) {
|
|
157
|
+
let ed25519Key = process.env.ED25519_PRIVATE_KEY;
|
|
158
|
+
if (ed25519Key) {
|
|
159
|
+
try {
|
|
160
|
+
ed25519Key = ed25519Key.replace(/\\n/g, '\n');
|
|
161
|
+
const serialized = JSON.stringify(payload);
|
|
162
|
+
let signature;
|
|
163
|
+
try {
|
|
164
|
+
signature = crypto.sign(undefined, Buffer.from(serialized), {
|
|
165
|
+
key: ed25519Key,
|
|
166
|
+
format: 'pem',
|
|
167
|
+
type: 'pkcs8'
|
|
168
|
+
});
|
|
169
|
+
} catch (signErr) {
|
|
170
|
+
signature = crypto.sign(null, Buffer.from(serialized), {
|
|
171
|
+
key: ed25519Key,
|
|
172
|
+
format: 'pem',
|
|
173
|
+
type: 'pkcs8'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return `ed25519.${base64UrlEncode(Buffer.from(serialized))}.${base64UrlEncode(signature)}`;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error('[Fingerprint] Ed25519 signing failed, falling back to symmetric:', e.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
29
182
|
const secret = getPowSecret();
|
|
30
183
|
const key = crypto.createHash('sha256').update(secret).digest();
|
|
31
184
|
const iv = crypto.randomBytes(16);
|
|
@@ -39,6 +192,40 @@ export function generateStatelessTicket(payload) {
|
|
|
39
192
|
|
|
40
193
|
export function parseStatelessTicket(ticket) {
|
|
41
194
|
try {
|
|
195
|
+
if (ticket.startsWith('ed25519.')) {
|
|
196
|
+
const parts = ticket.split('.');
|
|
197
|
+
if (parts.length !== 3) return null;
|
|
198
|
+
const payloadBuffer = base64UrlDecode(parts[1]);
|
|
199
|
+
const signatureBuffer = base64UrlDecode(parts[2]);
|
|
200
|
+
let publicKey = process.env.ED25519_PUBLIC_KEY;
|
|
201
|
+
if (!publicKey) {
|
|
202
|
+
console.error('[Fingerprint] ED25519_PUBLIC_KEY is not defined in environment.');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
publicKey = publicKey.replace(/\\n/g, '\n');
|
|
206
|
+
|
|
207
|
+
let isVerified = false;
|
|
208
|
+
try {
|
|
209
|
+
isVerified = crypto.verify(undefined, payloadBuffer, {
|
|
210
|
+
key: publicKey,
|
|
211
|
+
format: 'pem',
|
|
212
|
+
type: 'spki'
|
|
213
|
+
}, signatureBuffer);
|
|
214
|
+
} catch (verifyErr) {
|
|
215
|
+
try {
|
|
216
|
+
isVerified = crypto.verify(null, payloadBuffer, {
|
|
217
|
+
key: publicKey,
|
|
218
|
+
format: 'pem',
|
|
219
|
+
type: 'spki'
|
|
220
|
+
}, signatureBuffer);
|
|
221
|
+
} catch (verifyErr2) {
|
|
222
|
+
isVerified = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!isVerified) return null;
|
|
226
|
+
return JSON.parse(payloadBuffer.toString('utf8'));
|
|
227
|
+
}
|
|
228
|
+
|
|
42
229
|
const parts = ticket.split('.');
|
|
43
230
|
if (parts.length !== 3) return null;
|
|
44
231
|
|
|
@@ -97,26 +284,6 @@ async function checkChallengeRateLimit(clientIp) {
|
|
|
97
284
|
return true;
|
|
98
285
|
}
|
|
99
286
|
|
|
100
|
-
/**
|
|
101
|
-
* @private
|
|
102
|
-
* Deep merges two objects. The `source` object's properties overwrite the `target`'s.
|
|
103
|
-
* @param {object} target - The target object.
|
|
104
|
-
* @param {object} source - The source object.
|
|
105
|
-
* @returns {object} The merged object.
|
|
106
|
-
*/
|
|
107
|
-
function deepMerge(target, source) {
|
|
108
|
-
const output = { ...target };
|
|
109
|
-
if (target && typeof target === 'object' && source && typeof source === 'object') {
|
|
110
|
-
Object.keys(source).forEach(key => {
|
|
111
|
-
if (source[key] && typeof source[key] === 'object' && key in target) {
|
|
112
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
113
|
-
} else {
|
|
114
|
-
output[key] = source[key];
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
return output;
|
|
119
|
-
}
|
|
120
287
|
|
|
121
288
|
const securityProfiles = {
|
|
122
289
|
/**
|
|
@@ -451,44 +618,6 @@ function getTlsFingerprint(context) {
|
|
|
451
618
|
return { ja3, ja4 };
|
|
452
619
|
}
|
|
453
620
|
|
|
454
|
-
/**
|
|
455
|
-
* Analyses a raw JA3 string.
|
|
456
|
-
* Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
457
|
-
* @param {string} ja3String
|
|
458
|
-
* @returns {object|null}
|
|
459
|
-
*/
|
|
460
|
-
export function parseJa3(ja3String) {
|
|
461
|
-
if (!ja3String || typeof ja3String !== 'string') {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
const parts = ja3String.split(',');
|
|
465
|
-
if (parts.length !== 5) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
return {
|
|
469
|
-
tlsVersion: parseInt(parts[0], 10),
|
|
470
|
-
ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
|
|
471
|
-
extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
|
|
472
|
-
curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
|
|
473
|
-
points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
479
|
-
* This is our "level 2 fingerprint".
|
|
480
|
-
* @param {object} context - The request context.
|
|
481
|
-
* @returns {string} A hash representing the device.
|
|
482
|
-
*/
|
|
483
|
-
function getHeaderSignature(context) {
|
|
484
|
-
if (!context.rawHeaders) return '';
|
|
485
|
-
const headerKeys = [];
|
|
486
|
-
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
487
|
-
headerKeys.push(context.rawHeaders[i]);
|
|
488
|
-
}
|
|
489
|
-
return cyrb53(headerKeys.sort().join(','));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
621
|
/**
|
|
493
622
|
* Returns the client-side fingerprint if available, otherwise computes a server-side hash.
|
|
494
623
|
* This aligns with the test's expectation for prioritization.
|
|
@@ -624,79 +753,6 @@ function hasGrease(values) {
|
|
|
624
753
|
return values.some(val => GREASE_VALUES.includes(val));
|
|
625
754
|
}
|
|
626
755
|
|
|
627
|
-
// Fonctions utilitaires
|
|
628
|
-
function parseUserAgent(ua) {
|
|
629
|
-
// Parser basique du User-Agent
|
|
630
|
-
const result = {};
|
|
631
|
-
|
|
632
|
-
// Détection du navigateur
|
|
633
|
-
if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
|
634
|
-
result.browser = 'Chrome';
|
|
635
|
-
const match = ua.match(/Chrome\/(\d+)/);
|
|
636
|
-
if (match) result.browser += `/${match[1]}`;
|
|
637
|
-
} else if (ua.includes('Firefox')) {
|
|
638
|
-
result.browser = 'Firefox';
|
|
639
|
-
const match = ua.match(/Firefox\/(\d+)/);
|
|
640
|
-
if (match) result.browser += `/${match[1]}`;
|
|
641
|
-
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
|
642
|
-
result.browser = 'Safari';
|
|
643
|
-
const match = ua.match(/Version\/(\d+)/);
|
|
644
|
-
if (match) result.browser += `/${match[1]}`;
|
|
645
|
-
} else if (ua.includes('Edg')) {
|
|
646
|
-
result.browser = 'Edge';
|
|
647
|
-
const match = ua.match(/Edg\/(\d+)/);
|
|
648
|
-
if (match) result.browser += `/${match[1]}`;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Détection de l'OS
|
|
652
|
-
if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
|
|
653
|
-
else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
|
|
654
|
-
else if (ua.includes('Mac OS X')) result.os = 'macOS';
|
|
655
|
-
else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
|
|
656
|
-
else if (ua.includes('Android')) result.os = 'Android';
|
|
657
|
-
else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
|
|
658
|
-
|
|
659
|
-
// Détection du type d'appareil
|
|
660
|
-
if (ua.includes('Mobile')) result.device = 'mobile';
|
|
661
|
-
else if (ua.includes('Tablet')) result.device = 'tablet';
|
|
662
|
-
else result.device = 'desktop';
|
|
663
|
-
|
|
664
|
-
return result;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function normalizeReferer(referer) {
|
|
668
|
-
try {
|
|
669
|
-
const url = new URL(referer);
|
|
670
|
-
return `${url.protocol}//${url.hostname}`;
|
|
671
|
-
} catch {
|
|
672
|
-
return referer;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function isPrivateIp(ip) {
|
|
677
|
-
// Vérifier si l'IP est privée
|
|
678
|
-
const parts = ip.split('.');
|
|
679
|
-
if (parts.length !== 4) return false;
|
|
680
|
-
const first = parseInt(parts[0]);
|
|
681
|
-
return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function hashNetwork(ip, prefix = 24) {
|
|
685
|
-
// Hash du réseau (masque /24 ou /16)
|
|
686
|
-
const parts = ip.split('.');
|
|
687
|
-
if (parts.length !== 4) return null;
|
|
688
|
-
const maskBytes = prefix / 8;
|
|
689
|
-
const network = parts.slice(0, maskBytes).join('.');
|
|
690
|
-
// Hash simple
|
|
691
|
-
let hash = 0;
|
|
692
|
-
for (let i = 0; i < network.length; i++) {
|
|
693
|
-
const char = network.charCodeAt(i);
|
|
694
|
-
hash = ((hash << 5) - hash) + char;
|
|
695
|
-
hash = hash & hash;
|
|
696
|
-
}
|
|
697
|
-
return hash.toString(16);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
756
|
/**
|
|
701
757
|
* Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
|
|
702
758
|
* @param {string} nonce - Unique nonce for the challenge.
|
|
@@ -948,26 +1004,68 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
|
|
|
948
1004
|
}
|
|
949
1005
|
return finalHash === parseInt(solution, 10);
|
|
950
1006
|
};
|
|
951
|
-
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
|
|
952
|
-
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
953
|
-
if (typeof ticket !== 'string' || ticket.length === 0) return false;
|
|
954
1007
|
|
|
1008
|
+
export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1009
|
+
let combined = [];
|
|
1010
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1011
|
+
const idx = queries[i];
|
|
1012
|
+
const block = generateBlock(seed, idx);
|
|
1013
|
+
combined.push(...block);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const assoc = await store.get(`coop-assoc:${nonce}`);
|
|
1017
|
+
if (assoc) {
|
|
1018
|
+
const peerSeed = assoc.peerSeed;
|
|
1019
|
+
const peerBlockIdx = assoc.peerBlockIdx;
|
|
1020
|
+
if (peerSeed !== undefined && peerBlockIdx !== undefined) {
|
|
1021
|
+
const peerBlock = generateBlock(peerSeed, peerBlockIdx);
|
|
1022
|
+
combined.push(...peerBlock);
|
|
1023
|
+
}
|
|
1024
|
+
await store.delete(`coop-assoc:${nonce}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
|
|
1028
|
+
const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
|
|
1029
|
+
|
|
1030
|
+
const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
|
|
1031
|
+
return hash === solution;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
1035
|
+
const block = new Uint8Array(blockSize);
|
|
1036
|
+
let h = cyrb53(seed + ":" + blockIndex);
|
|
1037
|
+
for (let i = 0; i < blockSize; i++) {
|
|
1038
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
1039
|
+
block[i] = h & 0xff;
|
|
1040
|
+
}
|
|
1041
|
+
return block;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
|
|
1045
|
+
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
1046
|
+
if (typeof ticket !== 'string' || ticket.length === 0) return false;
|
|
955
1047
|
// 1. Resolve stateless ticket first (zero database I/O cost)
|
|
956
1048
|
const statelessData = parseStatelessTicket(ticket);
|
|
957
1049
|
if (statelessData) {
|
|
958
1050
|
const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
|
|
959
|
-
|
|
960
1051
|
if (!expiry || Date.now() > expiry) {
|
|
961
1052
|
return false;
|
|
962
1053
|
}
|
|
963
|
-
|
|
1054
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1055
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1056
|
+
if (zkpProof) {
|
|
1057
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1058
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
964
1064
|
if (ip === originalIp) return true;
|
|
965
1065
|
const currentSubnet = getIpSubnet(ip);
|
|
966
1066
|
const originalSubnet = getIpSubnet(originalIp);
|
|
967
1067
|
if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
|
|
968
|
-
|
|
969
1068
|
if (!allowCrossNetworkRoaming) return false;
|
|
970
|
-
|
|
971
1069
|
return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
|
|
972
1070
|
}
|
|
973
1071
|
|
|
@@ -980,6 +1078,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
980
1078
|
await store.delete(`ticket:${ticket}`);
|
|
981
1079
|
return false;
|
|
982
1080
|
}
|
|
1081
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1082
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1083
|
+
if (zkpProof) {
|
|
1084
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1085
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
983
1091
|
|
|
984
1092
|
if (ip === originalIp) return true;
|
|
985
1093
|
const currentSubnet = getIpSubnet(ip);
|
|
@@ -1087,6 +1195,80 @@ function getHeaderAnomalies(context) {
|
|
|
1087
1195
|
};
|
|
1088
1196
|
}
|
|
1089
1197
|
|
|
1198
|
+
export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1199
|
+
const sizeMb = securityConfig?.pospace?.sizeMb || 100;
|
|
1200
|
+
const numQueries = securityConfig?.pospace?.numQueries || 10;
|
|
1201
|
+
|
|
1202
|
+
const queries = [];
|
|
1203
|
+
const maxBlocks = sizeMb * 1024;
|
|
1204
|
+
while (queries.length < numQueries) {
|
|
1205
|
+
const idx = Math.floor(Math.random() * maxBlocks);
|
|
1206
|
+
if (!queries.includes(idx)) {
|
|
1207
|
+
queries.push(idx);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const challenge = {
|
|
1212
|
+
type: "pospace",
|
|
1213
|
+
nonce: nonce,
|
|
1214
|
+
sizeMb,
|
|
1215
|
+
queries,
|
|
1216
|
+
path: originalUrl
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
const peer = await findPeerInSubnet(clientIp, nonce);
|
|
1220
|
+
if (peer) {
|
|
1221
|
+
challenge.peerId = peer.nodeId;
|
|
1222
|
+
challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
|
|
1223
|
+
|
|
1224
|
+
await store.set(`coop-assoc:${nonce}`, {
|
|
1225
|
+
peerNodeId: peer.nodeId,
|
|
1226
|
+
peerSeed: peer.seed,
|
|
1227
|
+
peerBlockIdx: challenge.peerBlockIdx
|
|
1228
|
+
}, 120);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return challenge;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1235
|
+
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1236
|
+
const solverCode = getPowSolverCode();
|
|
1237
|
+
|
|
1238
|
+
const challengeScript = `
|
|
1239
|
+
async function solve() {
|
|
1240
|
+
const nonce = ${JSON.stringify(nonce)};
|
|
1241
|
+
const path = ${JSON.stringify(path)};
|
|
1242
|
+
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1243
|
+
const queries = ${JSON.stringify(queries)};
|
|
1244
|
+
const sizeMb = ${sizeMb};
|
|
1245
|
+
|
|
1246
|
+
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1247
|
+
await new Promise(r => setTimeout(r, 10));
|
|
1248
|
+
|
|
1249
|
+
try {
|
|
1250
|
+
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
1251
|
+
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1252
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
1253
|
+
|
|
1254
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
1255
|
+
} catch(e) {
|
|
1256
|
+
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
solve();
|
|
1260
|
+
`;
|
|
1261
|
+
|
|
1262
|
+
return `<html><head><title>Security Check</title></head>
|
|
1263
|
+
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
1264
|
+
<h1>Security Check (Level 2)</h1>
|
|
1265
|
+
<p>We are verifying your storage allocation. This may take a few seconds on first load.</p>
|
|
1266
|
+
<div id="loader" style="margin:20px;">⚙️ Initializing storage space...</div>
|
|
1267
|
+
<script>${solverCode}</script>
|
|
1268
|
+
<script>${challengeScript}</script>
|
|
1269
|
+
</body></html>`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1090
1272
|
/**
|
|
1091
1273
|
* Checks for submitted honeypot fields to detect bots.
|
|
1092
1274
|
* @param {object} context - The request context.
|
|
@@ -1190,6 +1372,20 @@ const injectionPatterns = {
|
|
|
1190
1372
|
traversal: /(\.\.\/|\.\.\\)/,
|
|
1191
1373
|
// Remote Command Execution (RCE)
|
|
1192
1374
|
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
1375
|
+
// Server-Side Request Forgery (SSRF) - Detects local/private IPs and hosts
|
|
1376
|
+
ssrf: /((?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+\b|169\.254\.169\.254\b|10\.\d+\.\d+\.\d+\b|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+\b|192\.168\.\d+\.\d+\b|localhost\b|0\.0\.0\.0\b|\[[0:]+1\](?=\W|$)))/i,
|
|
1377
|
+
// Carriage Return Line Feed (CRLF) Injection / HTTP Response Splitting
|
|
1378
|
+
crlf: /[\r\n]|%0[ad]/i,
|
|
1379
|
+
// Cross-Site Scripting (XSS) - Fast native regex fallback
|
|
1380
|
+
xss: /(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i,
|
|
1381
|
+
// Open Redirect - Basic detection of external protocol/URLs
|
|
1382
|
+
openRedirect: /^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i,
|
|
1383
|
+
// Local/Remote File Inclusion (LFI/RFI)
|
|
1384
|
+
lfi: /(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i,
|
|
1385
|
+
// Shellshock (CVE-2014-6271)
|
|
1386
|
+
shellshock: /\(\)\s*\{\s*:\s*;\s*\}\s*/i,
|
|
1387
|
+
// NoSQL Injection (MongoDB query operators)
|
|
1388
|
+
nosql: /\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i
|
|
1193
1389
|
};
|
|
1194
1390
|
|
|
1195
1391
|
/**
|
|
@@ -1469,11 +1665,27 @@ function getCrossLayerInconsistency(context) {
|
|
|
1469
1665
|
const clientScreenHash = clientFpMap.get('scr');
|
|
1470
1666
|
const viewportWidth = context.headers['sec-ch-viewport-width'];
|
|
1471
1667
|
if (clientScreenHash && viewportWidth) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1668
|
+
const viewportWidthInt = parseInt(viewportWidth, 10);
|
|
1669
|
+
let matchedScreenWidth = null;
|
|
1670
|
+
const commonWidths = [320, 360, 375, 390, 412, 414, 768, 1024, 1280, 1366, 1440, 1536, 1600, 1920, 2560, 3840];
|
|
1671
|
+
const commonHeights = [480, 568, 640, 667, 736, 800, 812, 844, 896, 900, 1024, 1080, 1200, 1440, 1600, 2160];
|
|
1672
|
+
const commonDepths = [24, 30, 32];
|
|
1673
|
+
|
|
1674
|
+
for (const w of commonWidths) {
|
|
1675
|
+
for (const h of commonHeights) {
|
|
1676
|
+
for (const d of commonDepths) {
|
|
1677
|
+
const candidate = `${w}x${h}_${d}`;
|
|
1678
|
+
if (clientScreenHash === String(cyrb53(candidate))) {
|
|
1679
|
+
matchedScreenWidth = w;
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (matchedScreenWidth !== null) break;
|
|
1684
|
+
}
|
|
1685
|
+
if (matchedScreenWidth !== null) break;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (matchedScreenWidth !== null && viewportWidthInt > matchedScreenWidth) {
|
|
1477
1689
|
score += 20;
|
|
1478
1690
|
}
|
|
1479
1691
|
}
|
|
@@ -1484,10 +1696,17 @@ function getCrossLayerInconsistency(context) {
|
|
|
1484
1696
|
const clientGpuHash = clientFpMap.get('gpu');
|
|
1485
1697
|
const ja3 = getTlsFingerprint(context)?.ja3;
|
|
1486
1698
|
if (clientGpuHash && ja3) {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1699
|
+
let expectedBrowsers = tlsFingerprintDb[ja3];
|
|
1700
|
+
if (expectedBrowsers) {
|
|
1701
|
+
if (!Array.isArray(expectedBrowsers)) {
|
|
1702
|
+
expectedBrowsers = [expectedBrowsers];
|
|
1703
|
+
}
|
|
1704
|
+
const nonBrowserLibraries = ['Python', 'Go', 'Java', 'curl'];
|
|
1705
|
+
const isLibrary = expectedBrowsers.some(lib => nonBrowserLibraries.includes(lib));
|
|
1706
|
+
if (isLibrary) {
|
|
1707
|
+
score += 30;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1491
1710
|
}
|
|
1492
1711
|
|
|
1493
1712
|
return { crossLayerInconsistencyScore: Math.min(100, score) };
|
|
@@ -2345,11 +2564,20 @@ export const configureStore = (externalStore) => {
|
|
|
2345
2564
|
async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
2346
2565
|
const existingDeviceId = context.cookies?.device_id;
|
|
2347
2566
|
const currentDeviceHash = getCompositeDeviceHash(context); // Use the composite hash for consistency checks
|
|
2348
|
-
|
|
2567
|
+
const tlsSessionId = getTlsSessionId(context);
|
|
2568
|
+
let deviceId = existingDeviceId;
|
|
2349
2569
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
2350
2570
|
let deviceData = null;
|
|
2351
2571
|
let newCookie = null;
|
|
2352
|
-
|
|
2572
|
+
|
|
2573
|
+
if (!deviceId && tlsSessionId) {
|
|
2574
|
+
const resumedDeviceId = await store.get(`tls-session:${tlsSessionId}`);
|
|
2575
|
+
if (resumedDeviceId) {
|
|
2576
|
+
deviceId = resumedDeviceId;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (deviceId) {
|
|
2353
2581
|
deviceData = await store.get(`device:${deviceId}`);
|
|
2354
2582
|
}
|
|
2355
2583
|
|
|
@@ -2392,6 +2620,9 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
2392
2620
|
// The write will happen in getSuspicionVector after all modifications.
|
|
2393
2621
|
}
|
|
2394
2622
|
|
|
2623
|
+
if (deviceId && tlsSessionId) {
|
|
2624
|
+
await store.set(`tls-session:${tlsSessionId}`, deviceId, 3600); // Bind TLS session for 1 hour
|
|
2625
|
+
}
|
|
2395
2626
|
return { deviceId, deviceData, consistencyScore, newCookie };
|
|
2396
2627
|
}
|
|
2397
2628
|
|
|
@@ -2884,12 +3115,33 @@ function parseGraphQLQuery(body) {
|
|
|
2884
3115
|
export class FingerprintEngine {
|
|
2885
3116
|
constructor(securityConfig) {
|
|
2886
3117
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
2887
|
-
|
|
3118
|
+
|
|
3119
|
+
// Dynamically bind Ed25519 keys if passed via config
|
|
3120
|
+
if (securityConfig && securityConfig.ed25519_private_key) {
|
|
3121
|
+
process.env.ED25519_PRIVATE_KEY = securityConfig.ed25519_private_key;
|
|
3122
|
+
}
|
|
3123
|
+
if (securityConfig && securityConfig.ed25519_public_key) {
|
|
3124
|
+
process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
let finalConfig = securityConfig;
|
|
3128
|
+
if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
|
|
3129
|
+
const sPath = securityConfig.autotuning.savePath;
|
|
3130
|
+
if (existsSync(sPath)) {
|
|
3131
|
+
try {
|
|
3132
|
+
const savedConfig = JSON.parse(readFileSync(sPath, 'utf-8'));
|
|
3133
|
+
finalConfig = deepMerge(securityConfig, savedConfig);
|
|
3134
|
+
} catch (e) {
|
|
3135
|
+
console.warn(`[Fingerprint] Failed to auto-load optimized config from ${sPath}:`, e.message);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
this.securityConfig = finalConfig;
|
|
2888
3140
|
this.isProduction = isProduction;
|
|
2889
3141
|
this._allowlist = this._buildAllowlist();
|
|
2890
|
-
this._validateConfig(
|
|
2891
|
-
this.verbose =
|
|
2892
|
-
this.dryRun =
|
|
3142
|
+
this._validateConfig(finalConfig); // Validate the configuration
|
|
3143
|
+
this.verbose = finalConfig.verbose || false;
|
|
3144
|
+
this.dryRun = finalConfig.dryRun || false;
|
|
2893
3145
|
}
|
|
2894
3146
|
|
|
2895
3147
|
/**
|
|
@@ -2910,7 +3162,8 @@ export class FingerprintEngine {
|
|
|
2910
3162
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
2911
3163
|
'trustedProxies',
|
|
2912
3164
|
'wasm',
|
|
2913
|
-
'similarityThreshold'
|
|
3165
|
+
'similarityThreshold',
|
|
3166
|
+
'ed25519_private_key', 'ed25519_public_key'
|
|
2914
3167
|
]);
|
|
2915
3168
|
|
|
2916
3169
|
// 1. Check for essential keys
|
|
@@ -3213,8 +3466,18 @@ export class FingerprintEngine {
|
|
|
3213
3466
|
async processRequest(requestContext) {
|
|
3214
3467
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
3215
3468
|
|
|
3216
|
-
const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3217
|
-
|
|
3469
|
+
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3470
|
+
|
|
3471
|
+
if (query.coop_op) {
|
|
3472
|
+
const result = await handleCooperativeRequest(query, clientIp);
|
|
3473
|
+
return {
|
|
3474
|
+
action: 'challenge',
|
|
3475
|
+
status: 200,
|
|
3476
|
+
body: result
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
3218
3481
|
|
|
3219
3482
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
3220
3483
|
|
|
@@ -3293,6 +3556,13 @@ export class FingerprintEngine {
|
|
|
3293
3556
|
decision.action = 'next';
|
|
3294
3557
|
delete decision.status;
|
|
3295
3558
|
delete decision.body;
|
|
3559
|
+
|
|
3560
|
+
if (requestContext._newCookies) {
|
|
3561
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3562
|
+
if (deviceCookie) {
|
|
3563
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3296
3566
|
}
|
|
3297
3567
|
return decision;
|
|
3298
3568
|
}
|
|
@@ -3332,8 +3602,8 @@ export class FingerprintEngine {
|
|
|
3332
3602
|
// --- NOUVELLE LOGIQUE DE PRIORITÉ ---
|
|
3333
3603
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
3334
3604
|
// avant même de recalculer le score de suspicion.
|
|
3335
|
-
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
3336
|
-
if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
3605
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id, pow_solution_space } = query;
|
|
3606
|
+
if (pow_nonce && (pow_solution || pow_solution_cpu || pow_solution_space)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
3337
3607
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
3338
3608
|
|
|
3339
3609
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
@@ -3411,13 +3681,13 @@ export class FingerprintEngine {
|
|
|
3411
3681
|
} else {
|
|
3412
3682
|
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
3413
3683
|
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
3414
|
-
this._log('Challenge context found, verifying solution', {
|
|
3684
|
+
this._log('Challenge context found, verifying solution', {optimalTtl, finalTtl});
|
|
3415
3685
|
|
|
3416
3686
|
if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
|
|
3417
3687
|
const cpuSolution = pow_solution_cpu || pow_solution;
|
|
3418
3688
|
ticket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext, deviceId, currentDeviceHash);
|
|
3419
3689
|
isValid = ticket !== null;
|
|
3420
|
-
this._log('CPU target challenge verification', {
|
|
3690
|
+
this._log('CPU target challenge verification', {isValid});
|
|
3421
3691
|
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
3422
3692
|
const cpuTicket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext, deviceId, currentDeviceHash);
|
|
3423
3693
|
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
|
|
@@ -3428,6 +3698,18 @@ export class FingerprintEngine {
|
|
|
3428
3698
|
memValid: isMemValid,
|
|
3429
3699
|
isValid
|
|
3430
3700
|
});
|
|
3701
|
+
} else if (pow_type === "pospace" && pow_solution_space) {
|
|
3702
|
+
const isSpaceValid = await verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3703
|
+
isValid = isSpaceValid;
|
|
3704
|
+
if (isValid) {
|
|
3705
|
+
const ttl = finalTtl || 3600000;
|
|
3706
|
+
ticket = generateStatelessTicket({
|
|
3707
|
+
expiry: Date.now() + ttl,
|
|
3708
|
+
originalIp: clientIp,
|
|
3709
|
+
deviceId,
|
|
3710
|
+
deviceHash: currentDeviceHash
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3431
3713
|
}
|
|
3432
3714
|
}
|
|
3433
3715
|
} else {
|
|
@@ -3464,6 +3746,7 @@ export class FingerprintEngine {
|
|
|
3464
3746
|
finalSearchParams.delete('pow_solution_cpu');
|
|
3465
3747
|
finalSearchParams.delete('pow_solution_mem');
|
|
3466
3748
|
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
3749
|
+
finalSearchParams.delete('pow_solution_space');
|
|
3467
3750
|
// NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
|
|
3468
3751
|
finalSearchParams.delete('pow_solution_population');
|
|
3469
3752
|
finalSearchParams.delete('pow_solution_work_result');
|
|
@@ -3501,6 +3784,12 @@ export class FingerprintEngine {
|
|
|
3501
3784
|
decision.action = 'next';
|
|
3502
3785
|
delete decision.status;
|
|
3503
3786
|
delete decision.body;
|
|
3787
|
+
if (requestContext._newCookies) {
|
|
3788
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3789
|
+
if (deviceCookie) {
|
|
3790
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3504
3793
|
}
|
|
3505
3794
|
return decision;
|
|
3506
3795
|
}
|
|
@@ -3593,6 +3882,12 @@ export class FingerprintEngine {
|
|
|
3593
3882
|
decision.action = 'next';
|
|
3594
3883
|
delete decision.status;
|
|
3595
3884
|
delete decision.body;
|
|
3885
|
+
if (requestContext._newCookies) {
|
|
3886
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3887
|
+
if (deviceCookie) {
|
|
3888
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3596
3891
|
}
|
|
3597
3892
|
return decision;
|
|
3598
3893
|
}
|
|
@@ -3640,6 +3935,12 @@ export class FingerprintEngine {
|
|
|
3640
3935
|
decision.action = 'next';
|
|
3641
3936
|
delete decision.status;
|
|
3642
3937
|
delete decision.body;
|
|
3938
|
+
if (requestContext._newCookies) {
|
|
3939
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3940
|
+
if (deviceCookie) {
|
|
3941
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3643
3944
|
}
|
|
3644
3945
|
return decision;
|
|
3645
3946
|
}
|
|
@@ -3664,6 +3965,12 @@ export class FingerprintEngine {
|
|
|
3664
3965
|
decision.action = 'next';
|
|
3665
3966
|
delete decision.status;
|
|
3666
3967
|
delete decision.body;
|
|
3968
|
+
if (requestContext._newCookies) {
|
|
3969
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3970
|
+
if (deviceCookie) {
|
|
3971
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3667
3974
|
}
|
|
3668
3975
|
return decision;
|
|
3669
3976
|
}
|
|
@@ -3673,7 +3980,8 @@ export class FingerprintEngine {
|
|
|
3673
3980
|
// 1. La requête est suspecte ET il n'y a pas de ticket valide.
|
|
3674
3981
|
// OU
|
|
3675
3982
|
// 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
3676
|
-
const
|
|
3983
|
+
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
3984
|
+
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
3677
3985
|
const mustReChallenge = isSuspiciousHigh && hasValidTicket;
|
|
3678
3986
|
|
|
3679
3987
|
if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
|
|
@@ -3698,6 +4006,12 @@ export class FingerprintEngine {
|
|
|
3698
4006
|
decision.action = 'next';
|
|
3699
4007
|
delete decision.status;
|
|
3700
4008
|
delete decision.body;
|
|
4009
|
+
if (requestContext._newCookies) {
|
|
4010
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
4011
|
+
if (deviceCookie) {
|
|
4012
|
+
decision.newCookieForResponse = deviceCookie;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
3701
4015
|
}
|
|
3702
4016
|
return decision;
|
|
3703
4017
|
}
|
|
@@ -3707,6 +4021,7 @@ export class FingerprintEngine {
|
|
|
3707
4021
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
3708
4022
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
3709
4023
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4024
|
+
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3710
4025
|
|
|
3711
4026
|
// Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
|
|
3712
4027
|
// Cela rend l'automatisation plus difficile pour un attaquant.
|
|
@@ -3749,7 +4064,6 @@ export class FingerprintEngine {
|
|
|
3749
4064
|
}
|
|
3750
4065
|
|
|
3751
4066
|
if (isSuspicious && usefulWorkDispatched) {
|
|
3752
|
-
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3753
4067
|
if (isApi) {
|
|
3754
4068
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
3755
4069
|
} else {
|
|
@@ -3767,6 +4081,33 @@ export class FingerprintEngine {
|
|
|
3767
4081
|
delete decision.status;
|
|
3768
4082
|
return decision;
|
|
3769
4083
|
}
|
|
4084
|
+
if (this.securityConfig.enableProofOfSpace) {
|
|
4085
|
+
const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4086
|
+
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4087
|
+
await store.set(`secret:${nonce}`, {
|
|
4088
|
+
clientSecret,
|
|
4089
|
+
suspicionScore: finalScore,
|
|
4090
|
+
queries: spaceChallenge.queries,
|
|
4091
|
+
sizeMb: spaceChallenge.sizeMb,
|
|
4092
|
+
originalPath: path,
|
|
4093
|
+
}, this.securityConfig.challengeTtl || 300);
|
|
4094
|
+
|
|
4095
|
+
if (isApi) {
|
|
4096
|
+
decision.body = {
|
|
4097
|
+
challenge: {
|
|
4098
|
+
type: 'pospace',
|
|
4099
|
+
nonce: nonce,
|
|
4100
|
+
clientSecret,
|
|
4101
|
+
queries: spaceChallenge.queries,
|
|
4102
|
+
sizeMb: spaceChallenge.sizeMb,
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
} else {
|
|
4106
|
+
const page = generateSpaceChallengePage(spaceChallenge, clientSecret, this.securityConfig);
|
|
4107
|
+
decision.body = page;
|
|
4108
|
+
}
|
|
4109
|
+
return decision;
|
|
4110
|
+
}
|
|
3770
4111
|
// Generate some trap URLs to embed in the challenge page.
|
|
3771
4112
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
3772
4113
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
|
|
@@ -3825,9 +4166,6 @@ export class FingerprintEngine {
|
|
|
3825
4166
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
3826
4167
|
}
|
|
3827
4168
|
|
|
3828
|
-
// Check if the request is an API request to return a JSON challenge
|
|
3829
|
-
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3830
|
-
|
|
3831
4169
|
if (isApi) {
|
|
3832
4170
|
// For API clients, send a JSON response with challenge details.
|
|
3833
4171
|
const challengePayload = {
|
|
@@ -3861,8 +4199,14 @@ export class FingerprintEngine {
|
|
|
3861
4199
|
if (logger) {
|
|
3862
4200
|
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
3863
4201
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
4202
|
+
const response = { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
|
|
4203
|
+
if (requestContext._newCookies) {
|
|
4204
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
4205
|
+
if (deviceCookie) {
|
|
4206
|
+
response.newCookieForResponse = deviceCookie;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return response;
|
|
3866
4210
|
}
|
|
3867
4211
|
|
|
3868
4212
|
/**
|
|
@@ -3919,6 +4263,117 @@ const staticExtensions = new RegExp(
|
|
|
3919
4263
|
);
|
|
3920
4264
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
3921
4265
|
|
|
4266
|
+
export async function registerCooperativeNode(clientIp, nodeId, seed) {
|
|
4267
|
+
const subnet = getIpSubnet(clientIp);
|
|
4268
|
+
if (!subnet) return;
|
|
4269
|
+
|
|
4270
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4271
|
+
const nodes = (await store.get(key)) || {};
|
|
4272
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4273
|
+
|
|
4274
|
+
// Clean up expired nodes (older than 120 seconds)
|
|
4275
|
+
const cleanedNodes = {};
|
|
4276
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4277
|
+
if (now - node.timestamp < 120) {
|
|
4278
|
+
cleanedNodes[id] = node;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
cleanedNodes[nodeId] = {
|
|
4283
|
+
nodeId,
|
|
4284
|
+
seed,
|
|
4285
|
+
timestamp: now
|
|
4286
|
+
};
|
|
4287
|
+
|
|
4288
|
+
await store.set(key, cleanedNodes, 120);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
4292
|
+
const subnet = getIpSubnet(clientIp);
|
|
4293
|
+
if (!subnet) return null;
|
|
4294
|
+
|
|
4295
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4296
|
+
const nodes = (await store.get(key)) || {};
|
|
4297
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4298
|
+
|
|
4299
|
+
const activePeers = [];
|
|
4300
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4301
|
+
if (id !== excludeNodeId && now - node.timestamp < 120) {
|
|
4302
|
+
activePeers.push(node);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
if (activePeers.length === 0) return null;
|
|
4307
|
+
|
|
4308
|
+
// Select a random peer
|
|
4309
|
+
const randomIndex = Math.floor(Math.random() * activePeers.length);
|
|
4310
|
+
return activePeers[randomIndex];
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
|
|
4314
|
+
const op = params.coop_op;
|
|
4315
|
+
if (!op) return null;
|
|
4316
|
+
|
|
4317
|
+
const nodeId = params.node_id || '';
|
|
4318
|
+
if (!nodeId) {
|
|
4319
|
+
return { error: 'Missing node_id' };
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
switch (op) {
|
|
4323
|
+
case 'register':
|
|
4324
|
+
const seed = params.seed || '';
|
|
4325
|
+
await registerCooperativeNode(clientIp, nodeId, seed);
|
|
4326
|
+
return { status: 'registered' };
|
|
4327
|
+
|
|
4328
|
+
case 'request_peer_block':
|
|
4329
|
+
const peerId = params.peer_id || '';
|
|
4330
|
+
const blockIdx = parseInt(params.block_idx || '0', 10);
|
|
4331
|
+
const requestId = params.req_id || '';
|
|
4332
|
+
if (!peerId || !requestId) {
|
|
4333
|
+
return { error: 'Invalid parameters' };
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
const queueKey = `coop-mailbox:queue:${peerId}`;
|
|
4337
|
+
const requests = (await store.get(queueKey)) || [];
|
|
4338
|
+
requests.push({
|
|
4339
|
+
req_id: requestId,
|
|
4340
|
+
requester_id: nodeId,
|
|
4341
|
+
block_idx: blockIdx
|
|
4342
|
+
});
|
|
4343
|
+
await store.set(queueKey, requests, 30);
|
|
4344
|
+
return { status: 'queued' };
|
|
4345
|
+
|
|
4346
|
+
case 'poll_requests':
|
|
4347
|
+
const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
|
|
4348
|
+
const polledRequests = (await store.get(pollQueueKey)) || [];
|
|
4349
|
+
await store.delete(pollQueueKey);
|
|
4350
|
+
return { requests: polledRequests };
|
|
4351
|
+
|
|
4352
|
+
case 'respond_block':
|
|
4353
|
+
const requesterId = params.requester_id || '';
|
|
4354
|
+
const respondRequestId = params.req_id || '';
|
|
4355
|
+
const blockData = params.block_data || '';
|
|
4356
|
+
if (!requesterId || !respondRequestId) {
|
|
4357
|
+
return { error: 'Invalid parameters' };
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
const responseKey = `coop-mailbox:res:${requesterId}:${respondRequestId}`;
|
|
4361
|
+
await store.set(responseKey, { block_data: blockData }, 30);
|
|
4362
|
+
return { status: 'delivered' };
|
|
4363
|
+
|
|
4364
|
+
case 'poll_response':
|
|
4365
|
+
const pollResponseRequestId = params.req_id || '';
|
|
4366
|
+
const pollResponseKey = `coop-mailbox:res:${nodeId}:${pollResponseRequestId}`;
|
|
4367
|
+
const data = await store.get(pollResponseKey);
|
|
4368
|
+
if (data) {
|
|
4369
|
+
await store.delete(pollResponseKey);
|
|
4370
|
+
return { status: 'ready', block_data: data.block_data };
|
|
4371
|
+
}
|
|
4372
|
+
return { status: 'pending' };
|
|
4373
|
+
}
|
|
4374
|
+
return null;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
3922
4377
|
|
|
3923
4378
|
/** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
|
|
3924
4379
|
let optimizedTtlCache = new Map();
|
|
@@ -4287,7 +4742,7 @@ function getTcpAnomalyScore(context) {
|
|
|
4287
4742
|
* @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
|
|
4288
4743
|
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
4289
4744
|
*/
|
|
4290
|
-
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
|
|
4745
|
+
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
|
|
4291
4746
|
if (typeof str !== 'string') return false;
|
|
4292
4747
|
|
|
4293
4748
|
for (const type of typesToDetect) {
|
|
@@ -4489,6 +4944,32 @@ export const default_whitelist = () => [
|
|
|
4489
4944
|
];
|
|
4490
4945
|
|
|
4491
4946
|
|
|
4947
|
+
/**
|
|
4948
|
+
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
4949
|
+
* Prioritizes proxy-provided headers and falls back to Node's native socket session.
|
|
4950
|
+
* @private
|
|
4951
|
+
* @param {object} context - The request context.
|
|
4952
|
+
* @returns {string|null}
|
|
4953
|
+
*/
|
|
4954
|
+
function getTlsSessionId(context) {
|
|
4955
|
+
if (!context) return null;
|
|
4956
|
+
const fromHeader = context.headers ? (context.headers['x-tls-session-id'] || context.headers['x-ssl-session-id']) : null;
|
|
4957
|
+
if (fromHeader) return fromHeader;
|
|
4958
|
+
|
|
4959
|
+
const socket = context.rawReq?.socket;
|
|
4960
|
+
if (socket) {
|
|
4961
|
+
if (socket.sessionId) {
|
|
4962
|
+
return socket.sessionId.toString('hex');
|
|
4963
|
+
}
|
|
4964
|
+
if (typeof socket.getSession === 'function') {
|
|
4965
|
+
const session = socket.getSession();
|
|
4966
|
+
if (session) {
|
|
4967
|
+
return crypto.createHash('sha256').update(session).digest('hex');
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
return null;
|
|
4972
|
+
}
|
|
4492
4973
|
|
|
4493
4974
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
4494
4975
|
export const powMiddleware = (securityConfig) => {
|
|
@@ -4521,6 +5002,24 @@ export const powMiddleware = (securityConfig) => {
|
|
|
4521
5002
|
}
|
|
4522
5003
|
|
|
4523
5004
|
return async (req, res, next) => {
|
|
5005
|
+
if (!req.headers_translated) {
|
|
5006
|
+
req.headers_translated = true;
|
|
5007
|
+
const matchedMapping = getActiveMappingForRequest(req.headers);
|
|
5008
|
+
if (matchedMapping) {
|
|
5009
|
+
const devFpHeader = matchedMapping.headers['x-device-fingerprint'].toLowerCase();
|
|
5010
|
+
const behaviorHeader = matchedMapping.headers['x-behavior-metrics'].toLowerCase();
|
|
5011
|
+
|
|
5012
|
+
if (req.headers[devFpHeader]) {
|
|
5013
|
+
req.headers['x-device-fingerprint'] = req.headers[devFpHeader];
|
|
5014
|
+
}
|
|
5015
|
+
if (req.headers[behaviorHeader]) {
|
|
5016
|
+
req.headers['x-behavior-metrics'] = req.headers[behaviorHeader];
|
|
5017
|
+
}
|
|
5018
|
+
if (req.headers['x-device-fingerprint']) {
|
|
5019
|
+
req.headers['x-device-fingerprint'] = decodePolymorphicFingerprint(req.headers['x-device-fingerprint'], matchedMapping);
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
4524
5023
|
if (securityConfig?.wasm) {
|
|
4525
5024
|
const wasmConfig = securityConfig.wasm;
|
|
4526
5025
|
let jsPath = '/fp.js';
|
|
@@ -4542,45 +5041,40 @@ export const powMiddleware = (securityConfig) => {
|
|
|
4542
5041
|
wasmFile = wasmConfig.wasmFile ? resolve(wasmConfig.wasmFile) : resolve(__dirname, '..', '..', 'public', 'fp.wasm');
|
|
4543
5042
|
}
|
|
4544
5043
|
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
} catch (e) {
|
|
4562
|
-
// Fallback
|
|
5044
|
+
if (req.path === jsPath) {
|
|
5045
|
+
try {
|
|
5046
|
+
if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
|
|
5047
|
+
await ensureLatestMapping();
|
|
5048
|
+
const latest = activeMappings[0];
|
|
5049
|
+
if (latest && latest.jsBuffer) {
|
|
5050
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
5051
|
+
return res.send(latest.jsBuffer);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
if (jsFile && existsSync(jsFile)) {
|
|
5055
|
+
const fileContent = readFileSync(jsFile);
|
|
5056
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
5057
|
+
return res.send(fileContent);
|
|
5058
|
+
}
|
|
5059
|
+
} catch (e) {}
|
|
4563
5060
|
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
} catch (e) {
|
|
4581
|
-
// Fallback
|
|
5061
|
+
if (req.path === wasmPath) {
|
|
5062
|
+
try {
|
|
5063
|
+
if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
|
|
5064
|
+
await ensureLatestMapping();
|
|
5065
|
+
const latest = activeMappings[0];
|
|
5066
|
+
if (latest && latest.wasmBuffer) {
|
|
5067
|
+
res.setHeader('Content-Type', 'application/wasm');
|
|
5068
|
+
return res.send(latest.wasmBuffer);
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
if (wasmFile && existsSync(wasmFile)) {
|
|
5072
|
+
const fileContent = readFileSync(wasmFile);
|
|
5073
|
+
res.setHeader('Content-Type', 'application/wasm');
|
|
5074
|
+
return res.send(fileContent);
|
|
5075
|
+
}
|
|
5076
|
+
} catch (e) {}
|
|
4582
5077
|
}
|
|
4583
|
-
}
|
|
4584
5078
|
}
|
|
4585
5079
|
|
|
4586
5080
|
const requestContext = {
|
|
@@ -4657,6 +5151,8 @@ export const __internal = {
|
|
|
4657
5151
|
getDeviceHash,
|
|
4658
5152
|
getCompositeDeviceHash,
|
|
4659
5153
|
getSuspicionVector,
|
|
5154
|
+
getTlsSessionId,
|
|
5155
|
+
pruneTrafficData,
|
|
4660
5156
|
cyrb53, // Export for testing
|
|
4661
5157
|
FingerprintBuilder, // Export for testing
|
|
4662
5158
|
calculateTarget,
|
|
@@ -4685,9 +5181,14 @@ export const __internal = {
|
|
|
4685
5181
|
getIpReputationScore, // Expose for testing
|
|
4686
5182
|
updateIpReputationScore, // Expose for testing
|
|
4687
5183
|
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
5184
|
+
verifyZkpProof,
|
|
5185
|
+
modPow,
|
|
4688
5186
|
parseTcpSyn, // Expose for testing
|
|
4689
5187
|
classifyTcpOs, // Expose for testing
|
|
4690
|
-
getTcpAnomalyScore // Expose for testing
|
|
5188
|
+
getTcpAnomalyScore, // Expose for testing,
|
|
5189
|
+
registerCooperativeNode,
|
|
5190
|
+
findPeerInSubnet,
|
|
5191
|
+
handleCooperativeRequest
|
|
4691
5192
|
};
|
|
4692
5193
|
|
|
4693
5194
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -4749,13 +5250,55 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
4749
5250
|
|
|
4750
5251
|
return [...suspiciousLogs, ...selectedPassed];
|
|
4751
5252
|
}
|
|
5253
|
+
/**
|
|
5254
|
+
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|
|
5255
|
+
* @private
|
|
5256
|
+
*/
|
|
5257
|
+
function pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup) {
|
|
5258
|
+
if (!Array.isArray(trafficData)) return;
|
|
5259
|
+
const now = Date.now();
|
|
5260
|
+
const removed = [];
|
|
5261
|
+
|
|
5262
|
+
// 1. Politique temporelle d'expiration
|
|
5263
|
+
if (maxAgeMs && maxAgeMs > 0) {
|
|
5264
|
+
const threshold = now - maxAgeMs;
|
|
5265
|
+
let i = 0;
|
|
5266
|
+
while (i < trafficData.length) {
|
|
5267
|
+
const log = trafficData[i];
|
|
5268
|
+
const logTs = log.timestamp || log.requestTimestamp || now;
|
|
5269
|
+
if (logTs < threshold) {
|
|
5270
|
+
removed.push(trafficData.splice(i, 1)[0]);
|
|
5271
|
+
} else {
|
|
5272
|
+
i++;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
4752
5276
|
|
|
5277
|
+
// 2. Politique de taille maximale (conserver les plus récents)
|
|
5278
|
+
if (maxDataPoints && maxDataPoints > 0 && trafficData.length > maxDataPoints) {
|
|
5279
|
+
const overflowCount = trafficData.length - maxDataPoints;
|
|
5280
|
+
const spliced = trafficData.splice(0, overflowCount);
|
|
5281
|
+
removed.push(...spliced);
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// 3. Callback de nettoyage
|
|
5285
|
+
if (onCleanup && typeof onCleanup === 'function' && removed.length > 0) {
|
|
5286
|
+
try {
|
|
5287
|
+
onCleanup(removed);
|
|
5288
|
+
} catch (e) {
|
|
5289
|
+
console.error('[AutoTuning] Error in onCleanup callback:', e);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
4753
5293
|
/**
|
|
4754
5294
|
* Executes a threshold optimization pass using collected traffic data.
|
|
4755
5295
|
* @private
|
|
4756
5296
|
*/
|
|
4757
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
|
|
4758
|
-
|
|
5297
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, tuningOptions = {}) {
|
|
5298
|
+
const { maxAgeMs, clearAfterTuning = false, onCleanup } = tuningOptions;
|
|
5299
|
+
|
|
5300
|
+
pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
|
|
5301
|
+
const sanitizedData = sanitizeTrafficData(trafficData);
|
|
4759
5302
|
|
|
4760
5303
|
const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
4761
5304
|
const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
|
|
@@ -4772,12 +5315,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
4772
5315
|
}
|
|
4773
5316
|
return;
|
|
4774
5317
|
}
|
|
4775
|
-
|
|
4776
|
-
if (trafficData.length > maxDataPoints) {
|
|
4777
|
-
console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
|
|
4778
|
-
trafficData.splice(0, trafficData.length - maxDataPoints);
|
|
4779
|
-
}
|
|
4780
|
-
|
|
4781
5318
|
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
|
|
4782
5319
|
|
|
4783
5320
|
const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
|
|
@@ -4912,6 +5449,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
4912
5449
|
console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
|
|
4913
5450
|
}
|
|
4914
5451
|
}
|
|
5452
|
+
|
|
5453
|
+
if (clearAfterTuning) {
|
|
5454
|
+
const cleared = trafficData.splice(0, trafficData.length);
|
|
5455
|
+
if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
|
|
5456
|
+
try {
|
|
5457
|
+
onCleanup(cleared);
|
|
5458
|
+
} catch (e) {
|
|
5459
|
+
console.error('[AutoTuning] Error in onCleanup callback after clearing:', e);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
|
|
5463
|
+
}
|
|
4915
5464
|
}
|
|
4916
5465
|
|
|
4917
5466
|
/**
|
|
@@ -4938,6 +5487,9 @@ export function startThresholdAutoTuning(options) {
|
|
|
4938
5487
|
minDataPoints = 200,
|
|
4939
5488
|
maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
|
|
4940
5489
|
savePath, // NOUVEAU: Chemin de sauvegarde optionnel
|
|
5490
|
+
maxAgeMs,
|
|
5491
|
+
clearAfterTuning = false,
|
|
5492
|
+
onCleanup,
|
|
4941
5493
|
} = options;
|
|
4942
5494
|
|
|
4943
5495
|
if (!securityConfig || !trafficData) {
|
|
@@ -4947,7 +5499,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
4947
5499
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
4948
5500
|
|
|
4949
5501
|
autoTuningJobId = setInterval(() => {
|
|
4950
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
|
|
5502
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
|
|
4951
5503
|
}, interval);
|
|
4952
5504
|
}
|
|
4953
5505
|
|