@anonympins/fingerprint 0.4.6 → 0.5.1
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 +43 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/fingerprint.client.js +360 -23
- package/src/js/fingerprint.js +508 -200
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/gpu_pow.solver.js +217 -0
- package/src/js/library.js +1 -1
- package/src/js/pow.solver.inline.js +12 -4
- package/src/js/pow.solver.js +12 -4
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +195 -1
- package/src/js/tests/gpu_pow.test.js +118 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/AutoTuner.php +71 -0
- package/src/php/Challenge/ChallengeUtils.php +344 -9
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +61 -2
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +285 -81
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +120 -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);
|
|
@@ -140,23 +142,7 @@ function getActiveMappingForRequest(headers) {
|
|
|
140
142
|
return null;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
145
|
-
const reverseKeys = {};
|
|
146
|
-
for (const [orig, rand] of Object.entries(mapping.keys)) {
|
|
147
|
-
reverseKeys[rand] = orig;
|
|
148
|
-
}
|
|
149
|
-
const parts = fpString.split('|');
|
|
150
|
-
const mappedParts = parts.map(part => {
|
|
151
|
-
const pair = part.split(':');
|
|
152
|
-
if (pair.length === 2) {
|
|
153
|
-
const origKey = reverseKeys[pair[0]] || pair[0];
|
|
154
|
-
return `${origKey}:${pair[1]}`;
|
|
155
|
-
}
|
|
156
|
-
return part;
|
|
157
|
-
});
|
|
158
|
-
return mappedParts.join('|');
|
|
159
|
-
}
|
|
145
|
+
|
|
160
146
|
|
|
161
147
|
const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
162
148
|
const base64UrlDecode = (str) => {
|
|
@@ -168,6 +154,31 @@ const base64UrlDecode = (str) => {
|
|
|
168
154
|
};
|
|
169
155
|
|
|
170
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
|
+
|
|
171
182
|
const secret = getPowSecret();
|
|
172
183
|
const key = crypto.createHash('sha256').update(secret).digest();
|
|
173
184
|
const iv = crypto.randomBytes(16);
|
|
@@ -178,9 +189,118 @@ export function generateStatelessTicket(payload) {
|
|
|
178
189
|
const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
|
|
179
190
|
return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
|
|
180
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
|
|
194
|
+
* @private
|
|
195
|
+
* @param {object} context - Le contexte de la requête.
|
|
196
|
+
* @returns {{quicAnomalyScore: number}}
|
|
197
|
+
*/
|
|
198
|
+
function getQuicAnomalyScore(context) {
|
|
199
|
+
const quicFp = context.headers?.['x-quic-fp'] || context.quicFingerprint || null;
|
|
200
|
+
if (!quicFp || typeof quicFp !== 'string') {
|
|
201
|
+
return { quicAnomalyScore: 0.0 };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const parts = quicFp.split(';');
|
|
205
|
+
if (parts.length < 2) return { quicAnomalyScore: 0.0 };
|
|
181
206
|
|
|
207
|
+
const params = {};
|
|
208
|
+
parts[1].split(',').forEach(p => {
|
|
209
|
+
const kv = p.split('=');
|
|
210
|
+
if (kv.length === 2) params[kv[0]] = kv[1];
|
|
211
|
+
});
|
|
212
|
+
const priorityOrder = parts[2] || '';
|
|
213
|
+
|
|
214
|
+
const ua = context.headers?.['user-agent'] || '';
|
|
215
|
+
const uaParts = parseUserAgent(ua);
|
|
216
|
+
const browser = uaParts.browser;
|
|
217
|
+
|
|
218
|
+
if (!browser) return { quicAnomalyScore: 0.0 };
|
|
219
|
+
|
|
220
|
+
let anomaly = 0.0;
|
|
221
|
+
if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
|
|
222
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
223
|
+
const maxStreams = parseInt(params['4'] || '0', 10);
|
|
224
|
+
if (maxData > 0 && maxData < 1048576) anomaly += 40.0;
|
|
225
|
+
if (maxStreams > 0 && maxStreams !== 100) anomaly += 30.0;
|
|
226
|
+
if (priorityOrder && !priorityOrder.includes('u=')) anomaly += 30.0;
|
|
227
|
+
} else if (browser.startsWith('Firefox')) {
|
|
228
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
229
|
+
if (maxData > 0 && maxData > 5000000) anomaly += 40.0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { quicAnomalyScore: Math.max(0.0, Math.min(100.0, anomaly)) };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Détecte les anomalies de rendu (V-Sync, FPS, gigue) à partir des métriques d'affichage.
|
|
236
|
+
* @private
|
|
237
|
+
* @param {object} context - Le contexte de la requête.
|
|
238
|
+
* @returns {{renderingAnomalyScore: number}}
|
|
239
|
+
*/
|
|
240
|
+
function getRenderingAnomalyScore(context) {
|
|
241
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
242
|
+
if (!behaviorHeader) {
|
|
243
|
+
return { renderingAnomalyScore: 0.0 };
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
247
|
+
if (!metrics || !metrics.rendering) {
|
|
248
|
+
return { renderingAnomalyScore: 0.0 };
|
|
249
|
+
}
|
|
250
|
+
const rendering = metrics.rendering;
|
|
251
|
+
let score = 0.0;
|
|
252
|
+
if (rendering.offscreenAnom) {
|
|
253
|
+
score += 100.0;
|
|
254
|
+
}
|
|
255
|
+
const fps = parseFloat(rendering.fps || 0.0);
|
|
256
|
+
const jitter = parseFloat(rendering.jitter || 0.0);
|
|
257
|
+
if (fps > 250.0 || (fps > 0.0 && fps < 15.0)) {
|
|
258
|
+
score += 50.0;
|
|
259
|
+
}
|
|
260
|
+
if (jitter > 6.0) {
|
|
261
|
+
score += Math.min(80.0, (jitter - 6.0) * 10.0);
|
|
262
|
+
}
|
|
263
|
+
return { renderingAnomalyScore: Math.min(100.0, score) };
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return { renderingAnomalyScore: 0.0 };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
182
268
|
export function parseStatelessTicket(ticket) {
|
|
183
269
|
try {
|
|
270
|
+
if (ticket.startsWith('ed25519.')) {
|
|
271
|
+
const parts = ticket.split('.');
|
|
272
|
+
if (parts.length !== 3) return null;
|
|
273
|
+
const payloadBuffer = base64UrlDecode(parts[1]);
|
|
274
|
+
const signatureBuffer = base64UrlDecode(parts[2]);
|
|
275
|
+
let publicKey = process.env.ED25519_PUBLIC_KEY;
|
|
276
|
+
if (!publicKey) {
|
|
277
|
+
console.error('[Fingerprint] ED25519_PUBLIC_KEY is not defined in environment.');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
publicKey = publicKey.replace(/\\n/g, '\n');
|
|
281
|
+
|
|
282
|
+
let isVerified = false;
|
|
283
|
+
try {
|
|
284
|
+
isVerified = crypto.verify(undefined, payloadBuffer, {
|
|
285
|
+
key: publicKey,
|
|
286
|
+
format: 'pem',
|
|
287
|
+
type: 'spki'
|
|
288
|
+
}, signatureBuffer);
|
|
289
|
+
} catch (verifyErr) {
|
|
290
|
+
try {
|
|
291
|
+
isVerified = crypto.verify(null, payloadBuffer, {
|
|
292
|
+
key: publicKey,
|
|
293
|
+
format: 'pem',
|
|
294
|
+
type: 'spki'
|
|
295
|
+
}, signatureBuffer);
|
|
296
|
+
} catch (verifyErr2) {
|
|
297
|
+
isVerified = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!isVerified) return null;
|
|
301
|
+
return JSON.parse(payloadBuffer.toString('utf8'));
|
|
302
|
+
}
|
|
303
|
+
|
|
184
304
|
const parts = ticket.split('.');
|
|
185
305
|
if (parts.length !== 3) return null;
|
|
186
306
|
|
|
@@ -239,26 +359,6 @@ async function checkChallengeRateLimit(clientIp) {
|
|
|
239
359
|
return true;
|
|
240
360
|
}
|
|
241
361
|
|
|
242
|
-
/**
|
|
243
|
-
* @private
|
|
244
|
-
* Deep merges two objects. The `source` object's properties overwrite the `target`'s.
|
|
245
|
-
* @param {object} target - The target object.
|
|
246
|
-
* @param {object} source - The source object.
|
|
247
|
-
* @returns {object} The merged object.
|
|
248
|
-
*/
|
|
249
|
-
function deepMerge(target, source) {
|
|
250
|
-
const output = { ...target };
|
|
251
|
-
if (target && typeof target === 'object' && source && typeof source === 'object') {
|
|
252
|
-
Object.keys(source).forEach(key => {
|
|
253
|
-
if (source[key] && typeof source[key] === 'object' && key in target) {
|
|
254
|
-
output[key] = deepMerge(target[key], source[key]);
|
|
255
|
-
} else {
|
|
256
|
-
output[key] = source[key];
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
return output;
|
|
261
|
-
}
|
|
262
362
|
|
|
263
363
|
const securityProfiles = {
|
|
264
364
|
/**
|
|
@@ -279,7 +379,10 @@ const securityProfiles = {
|
|
|
279
379
|
tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
|
|
280
380
|
subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
|
|
281
381
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
282
|
-
botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
|
|
382
|
+
botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
383
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
384
|
+
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
385
|
+
renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
283
386
|
},
|
|
284
387
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
285
388
|
patterns: {
|
|
@@ -315,7 +418,8 @@ const securityProfiles = {
|
|
|
315
418
|
tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
|
|
316
419
|
subnetScore: 0.5,
|
|
317
420
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
318
|
-
botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
|
|
421
|
+
botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
422
|
+
renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
319
423
|
},
|
|
320
424
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
321
425
|
patterns: {
|
|
@@ -352,7 +456,9 @@ const securityProfiles = {
|
|
|
352
456
|
tlsSpoofingScore: 0.7, // Important pour les API
|
|
353
457
|
subnetScore: 0.4,
|
|
354
458
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
355
|
-
botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
|
|
459
|
+
botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
460
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
461
|
+
quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
|
|
356
462
|
},
|
|
357
463
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
358
464
|
patterns: {
|
|
@@ -390,7 +496,10 @@ const securityProfiles = {
|
|
|
390
496
|
tlsSpoofingScore: 0.6, // Moins critique pour les blogs
|
|
391
497
|
subnetScore: 0.2,
|
|
392
498
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
393
|
-
botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
|
|
499
|
+
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
500
|
+
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
501
|
+
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
502
|
+
renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
394
503
|
},
|
|
395
504
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
396
505
|
patterns: {
|
|
@@ -427,7 +536,10 @@ const securityProfiles = {
|
|
|
427
536
|
tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
|
|
428
537
|
subnetScore: 0.5,
|
|
429
538
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
430
|
-
botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
|
|
539
|
+
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
540
|
+
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
541
|
+
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
542
|
+
renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
431
543
|
},
|
|
432
544
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
433
545
|
patterns: {
|
|
@@ -593,44 +705,6 @@ function getTlsFingerprint(context) {
|
|
|
593
705
|
return { ja3, ja4 };
|
|
594
706
|
}
|
|
595
707
|
|
|
596
|
-
/**
|
|
597
|
-
* Analyses a raw JA3 string.
|
|
598
|
-
* Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
599
|
-
* @param {string} ja3String
|
|
600
|
-
* @returns {object|null}
|
|
601
|
-
*/
|
|
602
|
-
export function parseJa3(ja3String) {
|
|
603
|
-
if (!ja3String || typeof ja3String !== 'string') {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
const parts = ja3String.split(',');
|
|
607
|
-
if (parts.length !== 5) {
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
return {
|
|
611
|
-
tlsVersion: parseInt(parts[0], 10),
|
|
612
|
-
ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
|
|
613
|
-
extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
|
|
614
|
-
curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
|
|
615
|
-
points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
621
|
-
* This is our "level 2 fingerprint".
|
|
622
|
-
* @param {object} context - The request context.
|
|
623
|
-
* @returns {string} A hash representing the device.
|
|
624
|
-
*/
|
|
625
|
-
function getHeaderSignature(context) {
|
|
626
|
-
if (!context.rawHeaders) return '';
|
|
627
|
-
const headerKeys = [];
|
|
628
|
-
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
629
|
-
headerKeys.push(context.rawHeaders[i]);
|
|
630
|
-
}
|
|
631
|
-
return cyrb53(headerKeys.sort().join(','));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
708
|
/**
|
|
635
709
|
* Returns the client-side fingerprint if available, otherwise computes a server-side hash.
|
|
636
710
|
* This aligns with the test's expectation for prioritization.
|
|
@@ -766,79 +840,6 @@ function hasGrease(values) {
|
|
|
766
840
|
return values.some(val => GREASE_VALUES.includes(val));
|
|
767
841
|
}
|
|
768
842
|
|
|
769
|
-
// Fonctions utilitaires
|
|
770
|
-
function parseUserAgent(ua) {
|
|
771
|
-
// Parser basique du User-Agent
|
|
772
|
-
const result = {};
|
|
773
|
-
|
|
774
|
-
// Détection du navigateur
|
|
775
|
-
if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
|
776
|
-
result.browser = 'Chrome';
|
|
777
|
-
const match = ua.match(/Chrome\/(\d+)/);
|
|
778
|
-
if (match) result.browser += `/${match[1]}`;
|
|
779
|
-
} else if (ua.includes('Firefox')) {
|
|
780
|
-
result.browser = 'Firefox';
|
|
781
|
-
const match = ua.match(/Firefox\/(\d+)/);
|
|
782
|
-
if (match) result.browser += `/${match[1]}`;
|
|
783
|
-
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
|
784
|
-
result.browser = 'Safari';
|
|
785
|
-
const match = ua.match(/Version\/(\d+)/);
|
|
786
|
-
if (match) result.browser += `/${match[1]}`;
|
|
787
|
-
} else if (ua.includes('Edg')) {
|
|
788
|
-
result.browser = 'Edge';
|
|
789
|
-
const match = ua.match(/Edg\/(\d+)/);
|
|
790
|
-
if (match) result.browser += `/${match[1]}`;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Détection de l'OS
|
|
794
|
-
if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
|
|
795
|
-
else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
|
|
796
|
-
else if (ua.includes('Mac OS X')) result.os = 'macOS';
|
|
797
|
-
else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
|
|
798
|
-
else if (ua.includes('Android')) result.os = 'Android';
|
|
799
|
-
else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
|
|
800
|
-
|
|
801
|
-
// Détection du type d'appareil
|
|
802
|
-
if (ua.includes('Mobile')) result.device = 'mobile';
|
|
803
|
-
else if (ua.includes('Tablet')) result.device = 'tablet';
|
|
804
|
-
else result.device = 'desktop';
|
|
805
|
-
|
|
806
|
-
return result;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function normalizeReferer(referer) {
|
|
810
|
-
try {
|
|
811
|
-
const url = new URL(referer);
|
|
812
|
-
return `${url.protocol}//${url.hostname}`;
|
|
813
|
-
} catch {
|
|
814
|
-
return referer;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function isPrivateIp(ip) {
|
|
819
|
-
// Vérifier si l'IP est privée
|
|
820
|
-
const parts = ip.split('.');
|
|
821
|
-
if (parts.length !== 4) return false;
|
|
822
|
-
const first = parseInt(parts[0]);
|
|
823
|
-
return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function hashNetwork(ip, prefix = 24) {
|
|
827
|
-
// Hash du réseau (masque /24 ou /16)
|
|
828
|
-
const parts = ip.split('.');
|
|
829
|
-
if (parts.length !== 4) return null;
|
|
830
|
-
const maskBytes = prefix / 8;
|
|
831
|
-
const network = parts.slice(0, maskBytes).join('.');
|
|
832
|
-
// Hash simple
|
|
833
|
-
let hash = 0;
|
|
834
|
-
for (let i = 0; i < network.length; i++) {
|
|
835
|
-
const char = network.charCodeAt(i);
|
|
836
|
-
hash = ((hash << 5) - hash) + char;
|
|
837
|
-
hash = hash & hash;
|
|
838
|
-
}
|
|
839
|
-
return hash.toString(16);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
843
|
/**
|
|
843
844
|
* Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
|
|
844
845
|
* @param {string} nonce - Unique nonce for the challenge.
|
|
@@ -1091,19 +1092,30 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
|
|
|
1091
1092
|
return finalHash === parseInt(solution, 10);
|
|
1092
1093
|
};
|
|
1093
1094
|
|
|
1094
|
-
export function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1095
|
+
export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1096
|
+
let combined = [];
|
|
1097
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1098
|
+
const idx = queries[i];
|
|
1099
|
+
const block = generateBlock(seed, idx);
|
|
1100
|
+
combined.push(...block);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const assoc = await store.get(`coop-assoc:${nonce}`);
|
|
1104
|
+
if (assoc) {
|
|
1105
|
+
const peerSeed = assoc.peerSeed;
|
|
1106
|
+
const peerBlockIdx = assoc.peerBlockIdx;
|
|
1107
|
+
if (peerSeed !== undefined && peerBlockIdx !== undefined) {
|
|
1108
|
+
const peerBlock = generateBlock(peerSeed, peerBlockIdx);
|
|
1109
|
+
combined.push(...peerBlock);
|
|
1110
|
+
}
|
|
1111
|
+
await store.delete(`coop-assoc:${nonce}`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
|
|
1115
|
+
const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
|
|
1116
|
+
|
|
1117
|
+
const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
|
|
1118
|
+
return hash === solution;
|
|
1107
1119
|
}
|
|
1108
1120
|
|
|
1109
1121
|
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
@@ -1116,26 +1128,31 @@ function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
|
1116
1128
|
return block;
|
|
1117
1129
|
}
|
|
1118
1130
|
|
|
1119
|
-
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
|
|
1131
|
+
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
|
|
1120
1132
|
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
1121
|
-
|
|
1122
|
-
|
|
1133
|
+
if (typeof ticket !== 'string' || ticket.length === 0) return false;
|
|
1123
1134
|
// 1. Resolve stateless ticket first (zero database I/O cost)
|
|
1124
1135
|
const statelessData = parseStatelessTicket(ticket);
|
|
1125
1136
|
if (statelessData) {
|
|
1126
1137
|
const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
|
|
1127
|
-
|
|
1128
1138
|
if (!expiry || Date.now() > expiry) {
|
|
1129
1139
|
return false;
|
|
1130
1140
|
}
|
|
1131
|
-
|
|
1141
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1142
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1143
|
+
if (zkpProof) {
|
|
1144
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1145
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1132
1151
|
if (ip === originalIp) return true;
|
|
1133
1152
|
const currentSubnet = getIpSubnet(ip);
|
|
1134
1153
|
const originalSubnet = getIpSubnet(originalIp);
|
|
1135
1154
|
if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
|
|
1136
|
-
|
|
1137
1155
|
if (!allowCrossNetworkRoaming) return false;
|
|
1138
|
-
|
|
1139
1156
|
return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
|
|
1140
1157
|
}
|
|
1141
1158
|
|
|
@@ -1148,6 +1165,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1148
1165
|
await store.delete(`ticket:${ticket}`);
|
|
1149
1166
|
return false;
|
|
1150
1167
|
}
|
|
1168
|
+
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1169
|
+
const expectedY = storedDeviceHash.split(':')[1];
|
|
1170
|
+
if (zkpProof) {
|
|
1171
|
+
const [y, t, s] = zkpProof.split(':');
|
|
1172
|
+
if (y === expectedY && verifyZkpProof(y, t, s)) {
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1151
1178
|
|
|
1152
1179
|
if (ip === originalIp) return true;
|
|
1153
1180
|
const currentSubnet = getIpSubnet(ip);
|
|
@@ -1255,7 +1282,7 @@ function getHeaderAnomalies(context) {
|
|
|
1255
1282
|
};
|
|
1256
1283
|
}
|
|
1257
1284
|
|
|
1258
|
-
export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1285
|
+
export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1259
1286
|
const sizeMb = securityConfig?.pospace?.sizeMb || 100;
|
|
1260
1287
|
const numQueries = securityConfig?.pospace?.numQueries || 10;
|
|
1261
1288
|
|
|
@@ -1268,13 +1295,27 @@ export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, origina
|
|
|
1268
1295
|
}
|
|
1269
1296
|
}
|
|
1270
1297
|
|
|
1271
|
-
|
|
1298
|
+
const challenge = {
|
|
1272
1299
|
type: "pospace",
|
|
1273
1300
|
nonce: nonce,
|
|
1274
1301
|
sizeMb,
|
|
1275
1302
|
queries,
|
|
1276
1303
|
path: originalUrl
|
|
1277
1304
|
};
|
|
1305
|
+
|
|
1306
|
+
const peer = await findPeerInSubnet(clientIp, nonce);
|
|
1307
|
+
if (peer) {
|
|
1308
|
+
challenge.peerId = peer.nodeId;
|
|
1309
|
+
challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
|
|
1310
|
+
|
|
1311
|
+
await store.set(`coop-assoc:${nonce}`, {
|
|
1312
|
+
peerNodeId: peer.nodeId,
|
|
1313
|
+
peerSeed: peer.seed,
|
|
1314
|
+
peerBlockIdx: challenge.peerBlockIdx
|
|
1315
|
+
}, 120);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return challenge;
|
|
1278
1319
|
}
|
|
1279
1320
|
|
|
1280
1321
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
@@ -1418,6 +1459,20 @@ const injectionPatterns = {
|
|
|
1418
1459
|
traversal: /(\.\.\/|\.\.\\)/,
|
|
1419
1460
|
// Remote Command Execution (RCE)
|
|
1420
1461
|
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
1462
|
+
// Server-Side Request Forgery (SSRF) - Detects local/private IPs and hosts
|
|
1463
|
+
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,
|
|
1464
|
+
// Carriage Return Line Feed (CRLF) Injection / HTTP Response Splitting
|
|
1465
|
+
crlf: /[\r\n]|%0[ad]/i,
|
|
1466
|
+
// Cross-Site Scripting (XSS) - Fast native regex fallback
|
|
1467
|
+
xss: /(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i,
|
|
1468
|
+
// Open Redirect - Basic detection of external protocol/URLs
|
|
1469
|
+
openRedirect: /^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i,
|
|
1470
|
+
// Local/Remote File Inclusion (LFI/RFI)
|
|
1471
|
+
lfi: /(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i,
|
|
1472
|
+
// Shellshock (CVE-2014-6271)
|
|
1473
|
+
shellshock: /\(\)\s*\{\s*:\s*;\s*\}\s*/i,
|
|
1474
|
+
// NoSQL Injection (MongoDB query operators)
|
|
1475
|
+
nosql: /\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i
|
|
1421
1476
|
};
|
|
1422
1477
|
|
|
1423
1478
|
/**
|
|
@@ -1632,6 +1687,41 @@ function getBehaviorScore(context) {
|
|
|
1632
1687
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
1633
1688
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
1634
1689
|
|
|
1690
|
+
// NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
|
|
1691
|
+
const dwellTimes = metrics.keystrokeDwellTimes || [];
|
|
1692
|
+
const flightTimes = metrics.keystrokeFlightTimes || [];
|
|
1693
|
+
|
|
1694
|
+
if (dwellTimes.length >= 5) {
|
|
1695
|
+
const meanDwell = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
|
|
1696
|
+
const varDwell = dwellTimes.reduce((a, b) => a + Math.pow(b - meanDwell, 2), 0) / dwellTimes.length;
|
|
1697
|
+
const stdDevDwell = Math.sqrt(varDwell);
|
|
1698
|
+
|
|
1699
|
+
if (stdDevDwell < 2.0) {
|
|
1700
|
+
score += 35; // Suspicion d'automatisation (pas de variation humaine de pression)
|
|
1701
|
+
}
|
|
1702
|
+
if (meanDwell < 15.0) {
|
|
1703
|
+
score += 25; // Dwell time irréaliste
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (flightTimes.length >= 5) {
|
|
1708
|
+
const times = flightTimes.map(f => f.time);
|
|
1709
|
+
const meanFlight = times.reduce((a, b) => a + b, 0) / times.length;
|
|
1710
|
+
const varFlight = times.reduce((a, b) => a + Math.pow(b - meanFlight, 2), 0) / times.length;
|
|
1711
|
+
const stdDevFlight = Math.sqrt(varFlight);
|
|
1712
|
+
|
|
1713
|
+
if (stdDevFlight < 3.0) {
|
|
1714
|
+
score += 35; // Pas de variation de transition (flight time robotique)
|
|
1715
|
+
}
|
|
1716
|
+
if (meanFlight < 25.0) {
|
|
1717
|
+
score += 25; // Transitions trop rapides
|
|
1718
|
+
}
|
|
1719
|
+
const benfordDev = Optimization.Operators.benfordTest(times);
|
|
1720
|
+
if (benfordDev > 0.18) {
|
|
1721
|
+
score += 30; // Les intervalles ne suivent pas la loi de Benford
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1635
1725
|
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
1636
1726
|
if (segments.length > 10) {
|
|
1637
1727
|
const benfordDeviation = Optimization.Operators.benfordTest(segments);
|
|
@@ -2798,8 +2888,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2798
2888
|
const stableFp = extractStablePart(currentDeviceHash);
|
|
2799
2889
|
const stableFpHash = cyrb53(stableFp).toString();
|
|
2800
2890
|
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
2801
|
-
|
|
2802
|
-
|
|
2891
|
+
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
2892
|
+
const { quicAnomalyScore } = getQuicAnomalyScore(context);
|
|
2893
|
+
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
2803
2894
|
|
|
2804
2895
|
// Save the updated device state to the store
|
|
2805
2896
|
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
@@ -2811,7 +2902,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2811
2902
|
deviceData.ips = new Set(deviceData.ips);
|
|
2812
2903
|
}
|
|
2813
2904
|
// Le vecteur de suspicion est maintenant complet.
|
|
2814
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
|
|
2905
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
|
|
2815
2906
|
};
|
|
2816
2907
|
|
|
2817
2908
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -3147,12 +3238,45 @@ function parseGraphQLQuery(body) {
|
|
|
3147
3238
|
export class FingerprintEngine {
|
|
3148
3239
|
constructor(securityConfig) {
|
|
3149
3240
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
3150
|
-
|
|
3241
|
+
|
|
3242
|
+
// Dynamically bind Ed25519 keys if passed via config
|
|
3243
|
+
if (securityConfig && securityConfig.ed25519_private_key) {
|
|
3244
|
+
process.env.ED25519_PRIVATE_KEY = securityConfig.ed25519_private_key;
|
|
3245
|
+
}
|
|
3246
|
+
if (securityConfig && securityConfig.ed25519_public_key) {
|
|
3247
|
+
process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
let finalConfig = securityConfig;
|
|
3251
|
+
if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
|
|
3252
|
+
const sPath = securityConfig.autotuning.savePath;
|
|
3253
|
+
if (existsSync(sPath)) {
|
|
3254
|
+
try {
|
|
3255
|
+
const savedConfig = JSON.parse(readFileSync(sPath, 'utf-8'));
|
|
3256
|
+
finalConfig = deepMerge(securityConfig, savedConfig);
|
|
3257
|
+
} catch (e) {
|
|
3258
|
+
console.warn(`[Fingerprint] Failed to auto-load optimized config from ${sPath}:`, e.message);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
this.securityConfig = finalConfig;
|
|
3151
3263
|
this.isProduction = isProduction;
|
|
3152
3264
|
this._allowlist = this._buildAllowlist();
|
|
3153
|
-
this._validateConfig(
|
|
3154
|
-
this.verbose =
|
|
3155
|
-
this.dryRun =
|
|
3265
|
+
this._validateConfig(finalConfig); // Validate the configuration
|
|
3266
|
+
this.verbose = finalConfig.verbose || false;
|
|
3267
|
+
this.dryRun = finalConfig.dryRun || false;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
/**
|
|
3271
|
+
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
3272
|
+
* sans nécessiter de redémarrage.
|
|
3273
|
+
* @param {object} newConfig - La nouvelle configuration partielle ou complète.
|
|
3274
|
+
*/
|
|
3275
|
+
updateConfig(newConfig) {
|
|
3276
|
+
this._validateConfig(newConfig);
|
|
3277
|
+
this.securityConfig = deepMerge(this.securityConfig, newConfig);
|
|
3278
|
+
this.dryRun = this.securityConfig.dryRun || false;
|
|
3279
|
+
this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
|
|
3156
3280
|
}
|
|
3157
3281
|
|
|
3158
3282
|
/**
|
|
@@ -3173,7 +3297,8 @@ export class FingerprintEngine {
|
|
|
3173
3297
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
3174
3298
|
'trustedProxies',
|
|
3175
3299
|
'wasm',
|
|
3176
|
-
'similarityThreshold'
|
|
3300
|
+
'similarityThreshold',
|
|
3301
|
+
'ed25519_private_key', 'ed25519_public_key'
|
|
3177
3302
|
]);
|
|
3178
3303
|
|
|
3179
3304
|
// 1. Check for essential keys
|
|
@@ -3218,7 +3343,9 @@ export class FingerprintEngine {
|
|
|
3218
3343
|
(suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
|
|
3219
3344
|
(suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
|
|
3220
3345
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
3221
|
-
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0)
|
|
3346
|
+
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
3347
|
+
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
3348
|
+
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
3222
3349
|
|
|
3223
3350
|
return Math.min(100, score);
|
|
3224
3351
|
}
|
|
@@ -3477,7 +3604,17 @@ export class FingerprintEngine {
|
|
|
3477
3604
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
3478
3605
|
|
|
3479
3606
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3480
|
-
|
|
3607
|
+
|
|
3608
|
+
if (query.coop_op) {
|
|
3609
|
+
const result = await handleCooperativeRequest(query, clientIp);
|
|
3610
|
+
return {
|
|
3611
|
+
action: 'challenge',
|
|
3612
|
+
status: 200,
|
|
3613
|
+
body: result
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
3481
3618
|
|
|
3482
3619
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
3483
3620
|
|
|
@@ -3699,7 +3836,7 @@ export class FingerprintEngine {
|
|
|
3699
3836
|
isValid
|
|
3700
3837
|
});
|
|
3701
3838
|
} else if (pow_type === "pospace" && pow_solution_space) {
|
|
3702
|
-
const isSpaceValid = verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3839
|
+
const isSpaceValid = await verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3703
3840
|
isValid = isSpaceValid;
|
|
3704
3841
|
if (isValid) {
|
|
3705
3842
|
const ttl = finalTtl || 3600000;
|
|
@@ -3707,7 +3844,7 @@ export class FingerprintEngine {
|
|
|
3707
3844
|
expiry: Date.now() + ttl,
|
|
3708
3845
|
originalIp: clientIp,
|
|
3709
3846
|
deviceId,
|
|
3710
|
-
deviceHash
|
|
3847
|
+
deviceHash: currentDeviceHash
|
|
3711
3848
|
});
|
|
3712
3849
|
}
|
|
3713
3850
|
}
|
|
@@ -3980,7 +4117,8 @@ export class FingerprintEngine {
|
|
|
3980
4117
|
// 1. La requête est suspecte ET il n'y a pas de ticket valide.
|
|
3981
4118
|
// OU
|
|
3982
4119
|
// 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
3983
|
-
const
|
|
4120
|
+
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
4121
|
+
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
3984
4122
|
const mustReChallenge = isSuspiciousHigh && hasValidTicket;
|
|
3985
4123
|
|
|
3986
4124
|
if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
|
|
@@ -4081,7 +4219,7 @@ export class FingerprintEngine {
|
|
|
4081
4219
|
return decision;
|
|
4082
4220
|
}
|
|
4083
4221
|
if (this.securityConfig.enableProofOfSpace) {
|
|
4084
|
-
const spaceChallenge = generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4222
|
+
const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4085
4223
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4086
4224
|
await store.set(`secret:${nonce}`, {
|
|
4087
4225
|
clientSecret,
|
|
@@ -4262,6 +4400,117 @@ const staticExtensions = new RegExp(
|
|
|
4262
4400
|
);
|
|
4263
4401
|
const isStaticResource = (path) => staticExtensions.test(path);
|
|
4264
4402
|
|
|
4403
|
+
export async function registerCooperativeNode(clientIp, nodeId, seed) {
|
|
4404
|
+
const subnet = getIpSubnet(clientIp);
|
|
4405
|
+
if (!subnet) return;
|
|
4406
|
+
|
|
4407
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4408
|
+
const nodes = (await store.get(key)) || {};
|
|
4409
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4410
|
+
|
|
4411
|
+
// Clean up expired nodes (older than 120 seconds)
|
|
4412
|
+
const cleanedNodes = {};
|
|
4413
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4414
|
+
if (now - node.timestamp < 120) {
|
|
4415
|
+
cleanedNodes[id] = node;
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
cleanedNodes[nodeId] = {
|
|
4420
|
+
nodeId,
|
|
4421
|
+
seed,
|
|
4422
|
+
timestamp: now
|
|
4423
|
+
};
|
|
4424
|
+
|
|
4425
|
+
await store.set(key, cleanedNodes, 120);
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
4429
|
+
const subnet = getIpSubnet(clientIp);
|
|
4430
|
+
if (!subnet) return null;
|
|
4431
|
+
|
|
4432
|
+
const key = `coop-pospace:subnet:${subnet}`;
|
|
4433
|
+
const nodes = (await store.get(key)) || {};
|
|
4434
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4435
|
+
|
|
4436
|
+
const activePeers = [];
|
|
4437
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
4438
|
+
if (id !== excludeNodeId && now - node.timestamp < 120) {
|
|
4439
|
+
activePeers.push(node);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
if (activePeers.length === 0) return null;
|
|
4444
|
+
|
|
4445
|
+
// Select a random peer
|
|
4446
|
+
const randomIndex = Math.floor(Math.random() * activePeers.length);
|
|
4447
|
+
return activePeers[randomIndex];
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
|
|
4451
|
+
const op = params.coop_op;
|
|
4452
|
+
if (!op) return null;
|
|
4453
|
+
|
|
4454
|
+
const nodeId = params.node_id || '';
|
|
4455
|
+
if (!nodeId) {
|
|
4456
|
+
return { error: 'Missing node_id' };
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
switch (op) {
|
|
4460
|
+
case 'register':
|
|
4461
|
+
const seed = params.seed || '';
|
|
4462
|
+
await registerCooperativeNode(clientIp, nodeId, seed);
|
|
4463
|
+
return { status: 'registered' };
|
|
4464
|
+
|
|
4465
|
+
case 'request_peer_block':
|
|
4466
|
+
const peerId = params.peer_id || '';
|
|
4467
|
+
const blockIdx = parseInt(params.block_idx || '0', 10);
|
|
4468
|
+
const requestId = params.req_id || '';
|
|
4469
|
+
if (!peerId || !requestId) {
|
|
4470
|
+
return { error: 'Invalid parameters' };
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
const queueKey = `coop-mailbox:queue:${peerId}`;
|
|
4474
|
+
const requests = (await store.get(queueKey)) || [];
|
|
4475
|
+
requests.push({
|
|
4476
|
+
req_id: requestId,
|
|
4477
|
+
requester_id: nodeId,
|
|
4478
|
+
block_idx: blockIdx
|
|
4479
|
+
});
|
|
4480
|
+
await store.set(queueKey, requests, 30);
|
|
4481
|
+
return { status: 'queued' };
|
|
4482
|
+
|
|
4483
|
+
case 'poll_requests':
|
|
4484
|
+
const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
|
|
4485
|
+
const polledRequests = (await store.get(pollQueueKey)) || [];
|
|
4486
|
+
await store.delete(pollQueueKey);
|
|
4487
|
+
return { requests: polledRequests };
|
|
4488
|
+
|
|
4489
|
+
case 'respond_block':
|
|
4490
|
+
const requesterId = params.requester_id || '';
|
|
4491
|
+
const respondRequestId = params.req_id || '';
|
|
4492
|
+
const blockData = params.block_data || '';
|
|
4493
|
+
if (!requesterId || !respondRequestId) {
|
|
4494
|
+
return { error: 'Invalid parameters' };
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
const responseKey = `coop-mailbox:res:${requesterId}:${respondRequestId}`;
|
|
4498
|
+
await store.set(responseKey, { block_data: blockData }, 30);
|
|
4499
|
+
return { status: 'delivered' };
|
|
4500
|
+
|
|
4501
|
+
case 'poll_response':
|
|
4502
|
+
const pollResponseRequestId = params.req_id || '';
|
|
4503
|
+
const pollResponseKey = `coop-mailbox:res:${nodeId}:${pollResponseRequestId}`;
|
|
4504
|
+
const data = await store.get(pollResponseKey);
|
|
4505
|
+
if (data) {
|
|
4506
|
+
await store.delete(pollResponseKey);
|
|
4507
|
+
return { status: 'ready', block_data: data.block_data };
|
|
4508
|
+
}
|
|
4509
|
+
return { status: 'pending' };
|
|
4510
|
+
}
|
|
4511
|
+
return null;
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4265
4514
|
|
|
4266
4515
|
/** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
|
|
4267
4516
|
let optimizedTtlCache = new Map();
|
|
@@ -4630,7 +4879,7 @@ function getTcpAnomalyScore(context) {
|
|
|
4630
4879
|
* @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
|
|
4631
4880
|
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
4632
4881
|
*/
|
|
4633
|
-
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
|
|
4882
|
+
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
|
|
4634
4883
|
if (typeof str !== 'string') return false;
|
|
4635
4884
|
|
|
4636
4885
|
for (const type of typesToDetect) {
|
|
@@ -5040,6 +5289,7 @@ export const __internal = {
|
|
|
5040
5289
|
getCompositeDeviceHash,
|
|
5041
5290
|
getSuspicionVector,
|
|
5042
5291
|
getTlsSessionId,
|
|
5292
|
+
pruneTrafficData,
|
|
5043
5293
|
cyrb53, // Export for testing
|
|
5044
5294
|
FingerprintBuilder, // Export for testing
|
|
5045
5295
|
calculateTarget,
|
|
@@ -5068,9 +5318,16 @@ export const __internal = {
|
|
|
5068
5318
|
getIpReputationScore, // Expose for testing
|
|
5069
5319
|
updateIpReputationScore, // Expose for testing
|
|
5070
5320
|
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
5321
|
+
verifyZkpProof,
|
|
5322
|
+
modPow,
|
|
5071
5323
|
parseTcpSyn, // Expose for testing
|
|
5072
5324
|
classifyTcpOs, // Expose for testing
|
|
5073
|
-
getTcpAnomalyScore // Expose for testing
|
|
5325
|
+
getTcpAnomalyScore, // Expose for testing,
|
|
5326
|
+
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5327
|
+
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5328
|
+
registerCooperativeNode,
|
|
5329
|
+
findPeerInSubnet,
|
|
5330
|
+
handleCooperativeRequest
|
|
5074
5331
|
};
|
|
5075
5332
|
|
|
5076
5333
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
@@ -5132,13 +5389,55 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
5132
5389
|
|
|
5133
5390
|
return [...suspiciousLogs, ...selectedPassed];
|
|
5134
5391
|
}
|
|
5392
|
+
/**
|
|
5393
|
+
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|
|
5394
|
+
* @private
|
|
5395
|
+
*/
|
|
5396
|
+
function pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup) {
|
|
5397
|
+
if (!Array.isArray(trafficData)) return;
|
|
5398
|
+
const now = Date.now();
|
|
5399
|
+
const removed = [];
|
|
5400
|
+
|
|
5401
|
+
// 1. Politique temporelle d'expiration
|
|
5402
|
+
if (maxAgeMs && maxAgeMs > 0) {
|
|
5403
|
+
const threshold = now - maxAgeMs;
|
|
5404
|
+
let i = 0;
|
|
5405
|
+
while (i < trafficData.length) {
|
|
5406
|
+
const log = trafficData[i];
|
|
5407
|
+
const logTs = log.timestamp || log.requestTimestamp || now;
|
|
5408
|
+
if (logTs < threshold) {
|
|
5409
|
+
removed.push(trafficData.splice(i, 1)[0]);
|
|
5410
|
+
} else {
|
|
5411
|
+
i++;
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
// 2. Politique de taille maximale (conserver les plus récents)
|
|
5417
|
+
if (maxDataPoints && maxDataPoints > 0 && trafficData.length > maxDataPoints) {
|
|
5418
|
+
const overflowCount = trafficData.length - maxDataPoints;
|
|
5419
|
+
const spliced = trafficData.splice(0, overflowCount);
|
|
5420
|
+
removed.push(...spliced);
|
|
5421
|
+
}
|
|
5135
5422
|
|
|
5423
|
+
// 3. Callback de nettoyage
|
|
5424
|
+
if (onCleanup && typeof onCleanup === 'function' && removed.length > 0) {
|
|
5425
|
+
try {
|
|
5426
|
+
onCleanup(removed);
|
|
5427
|
+
} catch (e) {
|
|
5428
|
+
console.error('[AutoTuning] Error in onCleanup callback:', e);
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5136
5432
|
/**
|
|
5137
5433
|
* Executes a threshold optimization pass using collected traffic data.
|
|
5138
5434
|
* @private
|
|
5139
5435
|
*/
|
|
5140
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
|
|
5141
|
-
|
|
5436
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, tuningOptions = {}) {
|
|
5437
|
+
const { maxAgeMs, clearAfterTuning = false, onCleanup } = tuningOptions;
|
|
5438
|
+
|
|
5439
|
+
pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
|
|
5440
|
+
const sanitizedData = sanitizeTrafficData(trafficData);
|
|
5142
5441
|
|
|
5143
5442
|
const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
|
|
5144
5443
|
const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
|
|
@@ -5155,12 +5454,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5155
5454
|
}
|
|
5156
5455
|
return;
|
|
5157
5456
|
}
|
|
5158
|
-
|
|
5159
|
-
if (trafficData.length > maxDataPoints) {
|
|
5160
|
-
console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
|
|
5161
|
-
trafficData.splice(0, trafficData.length - maxDataPoints);
|
|
5162
|
-
}
|
|
5163
|
-
|
|
5164
5457
|
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
|
|
5165
5458
|
|
|
5166
5459
|
const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
|
|
@@ -5295,6 +5588,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5295
5588
|
console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
|
|
5296
5589
|
}
|
|
5297
5590
|
}
|
|
5591
|
+
|
|
5592
|
+
if (clearAfterTuning) {
|
|
5593
|
+
const cleared = trafficData.splice(0, trafficData.length);
|
|
5594
|
+
if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
|
|
5595
|
+
try {
|
|
5596
|
+
onCleanup(cleared);
|
|
5597
|
+
} catch (e) {
|
|
5598
|
+
console.error('[AutoTuning] Error in onCleanup callback after clearing:', e);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
|
|
5602
|
+
}
|
|
5298
5603
|
}
|
|
5299
5604
|
|
|
5300
5605
|
/**
|
|
@@ -5321,6 +5626,9 @@ export function startThresholdAutoTuning(options) {
|
|
|
5321
5626
|
minDataPoints = 200,
|
|
5322
5627
|
maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
|
|
5323
5628
|
savePath, // NOUVEAU: Chemin de sauvegarde optionnel
|
|
5629
|
+
maxAgeMs,
|
|
5630
|
+
clearAfterTuning = false,
|
|
5631
|
+
onCleanup,
|
|
5324
5632
|
} = options;
|
|
5325
5633
|
|
|
5326
5634
|
if (!securityConfig || !trafficData) {
|
|
@@ -5330,7 +5638,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
5330
5638
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
5331
5639
|
|
|
5332
5640
|
autoTuningJobId = setInterval(() => {
|
|
5333
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
|
|
5641
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
|
|
5334
5642
|
}, interval);
|
|
5335
5643
|
}
|
|
5336
5644
|
|