@anonympins/fingerprint 0.1.4 → 0.2.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/fingerprint.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { BlockList } from "node:net";
3
3
  import dns from "node:dns/promises";
4
+ import { problemManager } from "./problem-manager.js";
4
5
  import { Optimization } from "./library.js";
5
6
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
6
7
  import { readFileSync } from "node:fs";
@@ -26,111 +27,12 @@ const getPowSecret = () => {
26
27
  * @returns {string} The solver JavaScript code.
27
28
  */
28
29
  const getPowSolverCode = () => {
29
- try {
30
- const __filename = fileURLToPath(import.meta.url);
31
- const __dirname = dirname(__filename);
32
- const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
33
- return readFileSync(solverPath, 'utf-8');
34
- } catch (error) {
35
- console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
36
- // Fallback inline code if file cannot be loaded
37
- return `(function(global){
38
- async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
39
- const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
40
- let cpuSolution = 0;
41
- const ipPart = clientIp || '';
42
- while(true){
43
- // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
44
- const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
45
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
46
- const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
47
- if(BigInt('0x'+hashHex) < cpuTarget) break;
48
- cpuSolution++;
49
- if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
50
- }
51
- return cpuSolution;
52
- }
53
- async function solveMemory(seed, difficulty){
54
- const size = difficulty * 1024 * 1024;
55
- const buffer = new Uint32Array(size / 4);
56
- let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
57
- for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
58
- let solution = 0;
59
- const iterations = size / 16;
60
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
61
- for(let i=0;i<iterations;i++){
62
- addr = buffer[addr] % buffer.length;
63
- solution ^= addr;
64
- }
65
- return solution;
66
- }
67
- async function solveTsp(cities, targetMaxDistance){
68
- function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
69
- function evaluatePathDistance(cities,path){
70
- let total=0;
71
- for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
72
- total+=distance(cities[path[path.length-1]],cities[path[0]]);
73
- return total;
74
- }
75
- function solveTspNearestNeighbor(cities){
76
- const n=cities.length;
77
- if(n===0)return[];
78
- let path=[0];
79
- let visited=new Array(n).fill(false);
80
- visited[0]=true;
81
- for(let i=1;i<n;i++){
82
- let nearest=-1, minDist=Infinity;
83
- for(let j=0;j<n;j++){
84
- if(!visited[j]){
85
- const d=distance(cities[path[i-1]],cities[j]);
86
- if(d<minDist){minDist=d;nearest=j;}
87
- }
88
- }
89
- path.push(nearest);
90
- visited[nearest]=true;
91
- }
92
- return path;
93
- }
94
- await new Promise(r=>setTimeout(r,10));
95
- const solutionPath=solveTspNearestNeighbor(cities);
96
- const solutionDistance=evaluatePathDistance(cities,solutionPath);
97
- return{path:solutionPath,distance:solutionDistance};
98
- }
99
- async function solveChallenge(challenge) {
100
- const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
101
- const solutions = {};
102
-
103
- switch (type) {
104
- case 'cpu_target':
105
- solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
106
- break;
107
- case 'cpu_mem':
108
- case 'cpu_mem_inline':
109
- const memSeed = nonce + ":" + clientSecret;
110
- const [cpuSol, memSol] = await Promise.all([
111
- solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
112
- solveMemory(memSeed, memDifficulty)
113
- ]);
114
- solutions.cpu = cpuSol;
115
- solutions.mem = memSol;
116
- break;
117
- case 'tsp':
118
- const tspResult = await solveTsp(cities, targetMaxDistance);
119
- solutions.tsp = tspResult.path;
120
- solutions.distance = tspResult.distance;
121
- break;
122
- default:
123
- throw new Error(\`Unknown challenge type: \${type}\`);
124
- }
125
-
126
- return solutions;
127
- }
128
- global.solveCpuChallengeInline=solveCpuTargetInline;
129
- global.solveMemoryChallenge=solveMemory;
130
- global.solveTspChallenge=solveTsp;
131
- global.solveChallenge=solveChallenge;
132
- })(typeof window!=='undefined'?window:global);`;
133
- }
30
+ // On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
31
+ // ce qui est préférable à servir un code de secours potentiellement désynchronisé.
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
35
+ return readFileSync(solverPath, 'utf-8');
134
36
  };
135
37
 
136
38
  /**
@@ -189,7 +91,7 @@ function getHeaderSignature(context) {
189
91
  for (let i = 0; i < context.rawHeaders.length; i += 2) {
190
92
  headerKeys.push(context.rawHeaders[i]);
191
93
  }
192
- return cyrb53(headerKeys.join(','));
94
+ return cyrb53(headerKeys.sort().join(','));
193
95
  }
194
96
 
195
97
  /**
@@ -226,11 +128,6 @@ export function getCompositeDeviceHash(context) {
226
128
  const ua = context.headers["user-agent"];
227
129
  if (ua) {
228
130
  srv.add("ua", ua);
229
- // Extraire des infos supplémentaires du UA
230
- const uaParts = parseUserAgent(ua);
231
- if (uaParts.browser) srv.add("browser", uaParts.browser);
232
- if (uaParts.os) srv.add("os_version", uaParts.os);
233
- if (uaParts.device) srv.add("device_type", uaParts.device);
234
131
  }
235
132
 
236
133
  // 2. SIGNAL FORT: JA3 TLS Fingerprint
@@ -265,9 +162,6 @@ export function getCompositeDeviceHash(context) {
265
162
  srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
266
163
  }
267
164
 
268
- // 10. SIGNAL FORT: Ordonnancement des headers
269
- srv.add("h_ord", getHeaderSignature(context));
270
-
271
165
  // 11. SIGNAL AVANCÉ: Cookies (si disponible)
272
166
  if (context.cookies) {
273
167
  const cookieKeys = Object.keys(context.cookies).sort().join(',');
@@ -613,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
613
507
  * Verifies a memory PoW solution.
614
508
  * The server performs the same calculation to validate.
615
509
  */
616
- export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
510
+ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
617
511
  const size = difficulty * 1024 * 1024;
618
512
  const iterations = size / 16;
619
513
  const buffer = new Uint32Array(size / 4);
620
- const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
514
+ const seed = `:${nonce}:${clientSecret}`;
621
515
  let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
622
516
 
623
517
  for (let i = 0; i < buffer.length; i++) {
@@ -762,6 +656,25 @@ function getHoneypotScore(context, honeypotConfig = {}) {
762
656
  return { honeypotScore: 0 };
763
657
  }
764
658
 
659
+ /**
660
+ * @private
661
+ * Map of malicious patterns grouped by type.
662
+ */
663
+ const injectionPatterns = {
664
+ // SQL/NoSQL injections, including time-based attacks
665
+ sql: /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i,
666
+ // Log4Shell (JNDI injection)
667
+ log4shell: /\$\{jndi:(ldap|rmi|dns):/i,
668
+ // Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
669
+ ssti: /\{\{.*\}\}|\{%.*%\}/,
670
+ // XML External Entity (XXE) injection
671
+ xxe: /<!ENTITY\s+.*SYSTEM/i,
672
+ // Path Traversal
673
+ traversal: /(\.\.\/|\.\.\\)/,
674
+ // Remote Command Execution (RCE)
675
+ rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
676
+ };
677
+
765
678
  /**
766
679
  * Calcule un score basé sur les métriques comportementales envoyées par le client.
767
680
  * @param {object} context - Le contexte de la requête, contenant les en-têtes.
@@ -821,6 +734,29 @@ function getBehaviorScore(context) {
821
734
  }
822
735
  }
823
736
 
737
+ /**
738
+ * Calcule un score basé sur l'incohérence temporelle entre le client et le serveur pour détecter les attaques par rejeu.
739
+ * @param {object} context - Le contexte de la requête, contenant le timestamp de la requête.
740
+ * @param {object} metrics - Les métriques comportementales parsées depuis le client.
741
+ * @returns {{timeInconsistencyScore: number}}
742
+ */
743
+ function getTimeInconsistencyScore(context, metrics) {
744
+ const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
745
+ let score = 0;
746
+
747
+ if (metrics.clientTimestamp && context.requestTimestamp) {
748
+ const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
749
+
750
+ // Un delta très grand est un signal fort d'attaque par rejeu.
751
+ // Un delta négatif peut arriver si l'horloge du client est en avance, on l'ignore.
752
+ if (timeDelta > REPLAY_THRESHOLD_MS) {
753
+ // La pénalité est proportionnelle au dépassement du seuil.
754
+ score = Math.min(100, (timeDelta / REPLAY_THRESHOLD_MS - 1) * 50);
755
+ }
756
+ }
757
+ return { timeInconsistencyScore: score };
758
+ }
759
+
824
760
  /**
825
761
  * Calcule un score d'incohérence entre les données du fingerprint client et les en-têtes serveur.
826
762
  * @param {object} context - Le contexte de la requête.
@@ -903,8 +839,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
903
839
  const now = Date.now();
904
840
  const currentPath = context.path;
905
841
  // Make the function robust to handle both URLSearchParams and plain objects for query.
906
- // Ensure query parameters are consistently handled, whether they come from a URLSearchParams object or a plain object.
907
- const params = context.query instanceof URLSearchParams ? context.query : new URLSearchParams(context.query);
842
+ const params =
843
+ context.query instanceof URLSearchParams
844
+ ? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
845
+ : new URLSearchParams(context.query || {});
908
846
  params.sort(); // Sort for deterministic order
909
847
  const currentQueryString = params.toString();
910
848
 
@@ -960,8 +898,7 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
960
898
  // 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
961
899
  if (deviceData.timingHistory.length >= benfordMinSamples) {
962
900
  // On concatène tous les délais en une seule chaîne de chiffres.
963
- const timingString = deviceData.timingHistory.join('');
964
- const benfordDeviation = Optimization.Operators.benfordTest(timingString);
901
+ const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
965
902
 
966
903
  // Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
967
904
  // Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
@@ -1242,6 +1179,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
1242
1179
  // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1243
1180
  const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1244
1181
 
1182
+ // NOUVEAU: On calcule le score d'incohérence temporelle.
1183
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
1184
+
1245
1185
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
1246
1186
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
1247
1187
 
@@ -1257,7 +1197,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
1257
1197
  deviceData.ips = new Set(deviceData.ips);
1258
1198
  }
1259
1199
  // Le vecteur de suspicion est maintenant complet.
1260
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore };
1200
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore };
1261
1201
  };
1262
1202
 
1263
1203
  // A residential user can change networks (home, 4G, public wifi).
@@ -1320,20 +1260,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
1320
1260
  * @param {number} suspicionFactor - A number from 0 to 1.
1321
1261
  * @returns {BigInt} The target number.
1322
1262
  */
1323
- function calculateTarget(suspicionFactor) {
1263
+ function calculateTarget(suspicionFactor, securityConfig = {}) {
1324
1264
  // Difficulty range adjusted to be realistic.
1325
1265
  // MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
1326
1266
  // MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
1327
- const MIN_DIFFICULTY_BITS = 18; // Default value, should be configurable
1328
- const MAX_DIFFICULTY_BITS = 26; // Default value, should be configurable
1267
+ // NOUVEAU: La difficulté est maintenant configurable.
1268
+ const { cpu: cpuConfig = {} } = securityConfig;
1269
+ const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
1270
+ const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
1329
1271
 
1330
1272
  // Use linear interpolation between min and max difficulty.
1331
1273
  const totalDifficultyBits =
1332
1274
  MIN_DIFFICULTY_BITS +
1333
1275
  suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
1276
+
1277
+ if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
1278
+
1279
+ // The correct way to calculate the target is to define the number of leading zero bits required.
1280
+ // A target for N bits of difficulty is 2^(256-N).
1281
+ // We can calculate this with a left-shift on 1.
1282
+ const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
1283
+ return 1n << shift;
1284
+ }
1334
1285
 
1335
- // The target is max / 2^bits
1336
- return MAX_DIFFICULTY_TARGET >> BigInt(Math.floor(totalDifficultyBits));
1286
+ /**
1287
+ * @private
1288
+ * Crée le bloc de base pour le challenge CPU.
1289
+ * Ce buffer contient toutes les données sauf la solution.
1290
+ * @param {string} nonce
1291
+ * @param {string} clientSecret
1292
+ * @param {string} fingerprint
1293
+ * @returns {Buffer}
1294
+ */
1295
+ function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
1296
+ const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
1297
+ // On concatène les chaînes, puis on les convertit en buffer une seule fois.
1298
+ // Cela garantit que le client et le serveur travaillent sur la même base binaire.
1299
+ const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
1300
+ return Buffer.from(messageBase, 'utf8');
1337
1301
  }
1338
1302
 
1339
1303
  /**
@@ -1344,12 +1308,15 @@ export function generateCpuTargetChallenge(
1344
1308
  nonce,
1345
1309
  suspicionFactor,
1346
1310
  originalUrl,
1311
+ securityConfig,
1347
1312
  ) {
1348
- const target = calculateTarget(suspicionFactor);
1313
+ const target = calculateTarget(suspicionFactor, securityConfig);
1314
+ // Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
1315
+ const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
1349
1316
  return {
1350
1317
  type: "cpu_target",
1351
1318
  nonce: nonce,
1352
- target: target.toString(16), // Send the target in hexadecimal
1319
+ target: target.toString(16),
1353
1320
  path: originalUrl,
1354
1321
  };
1355
1322
  }
@@ -1374,10 +1341,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1374
1341
  async function solve() {
1375
1342
  const clientIp = ${JSON.stringify(clientIp)};
1376
1343
  const nonce = ${JSON.stringify(nonce)};
1377
- const cpuTarget = BigInt("0x${target}");
1378
- const solution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, null, (progress) => {
1379
- // Optional progress callback
1380
- });
1344
+ const cpuTarget = BigInt("0x" + "${target}");
1345
+ // La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
1346
+ // car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
1347
+ const baseBlockBytes = new TextEncoder().encode(nonce + ":");
1348
+ const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
1381
1349
  window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
1382
1350
  }
1383
1351
  solve();
@@ -1392,9 +1360,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1392
1360
  * @param {string} clientIp - The client's IP address.
1393
1361
  * @returns {string} HTML content.
1394
1362
  */
1395
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
1363
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
1396
1364
  const { nonce, target, path } = cpuChallengeDetails;
1397
1365
  const solverCode = getPowSolverCode();
1366
+ // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
1367
+ // Le fingerprint est maintenant passé directement en paramètre.
1368
+ const fingerprint = originalFingerprint;
1369
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
1370
+ const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
1398
1371
 
1399
1372
  const challengeScript = `
1400
1373
  async function solve() {
@@ -1402,19 +1375,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1402
1375
  const path = ${JSON.stringify(path)};
1403
1376
  const clientSecret = ${JSON.stringify(clientSecret)};
1404
1377
  const clientIp = ${JSON.stringify(clientIp)};
1405
- const cpuTarget = BigInt("0x${target}");
1378
+ const cpuTarget = BigInt("0x" + "${target}");
1406
1379
  const memDifficulty = ${memoryDifficulty};
1380
+ // Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
1381
+ // Il n'a plus besoin de construire le message lui-même.
1382
+ const baseBlock = new Uint8Array(${baseBlockBytes});
1407
1383
 
1408
1384
  // --- CPU Challenge ---
1409
- document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1410
- const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
1411
- // Optional progress callback
1412
- });
1385
+ document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
1413
1386
 
1414
1387
  // --- Memory Challenge ---
1415
1388
  document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1416
- await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1417
-
1389
+ await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1418
1390
  let memSolution = 0;
1419
1391
  try {
1420
1392
  const memSeed = nonce + ":" + clientSecret;
@@ -1423,7 +1395,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1423
1395
  document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1424
1396
  return;
1425
1397
  }
1426
- window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1398
+
1399
+ // Redirect with both solutions and the fingerprint used to solve.
1400
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1401
+ window.location.href = finalUrl;
1427
1402
  }
1428
1403
  solve();
1429
1404
  `;
@@ -1454,22 +1429,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1454
1429
  */
1455
1430
  export function verifyCpuTargetPoWAndGenerateTicket(
1456
1431
  clientIp, // This parameter is crucial and must be the actual client IP
1457
- ticketTtl, // NOUVEAU: Durée de validité du ticket (TTL en ms) configurable
1432
+ ticketTtl,
1458
1433
  nonce,
1459
1434
  solution,
1460
- clientSecret, // Le secret est maintenant requis
1461
- target, // La cible est maintenant passée directement en hexadécimal
1435
+ challengeContext = {}, // Le contexte complet du challenge est maintenant passé
1462
1436
  ) {
1463
- const message = clientSecret
1464
- ? `${nonce}:${solution}:${clientSecret}` // FIX: Ne pas inclure l'IP si un secret client est utilisé
1465
- : `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
1437
+ const { cpuTarget, baseBlock } = challengeContext;
1438
+ if (!cpuTarget || !baseBlock) {
1439
+ console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
1440
+ return null;
1441
+ }
1442
+
1443
+ // Le baseBlock est déjà un Buffer ou un tableau d'octets.
1444
+ // On s'assure que c'est un Buffer pour la concaténation.
1445
+ const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
1446
+ const solutionBuffer = Buffer.from(String(solution), 'utf8');
1447
+
1448
+ // Concaténation binaire directe. C'est la garantie de cohérence.
1449
+ const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
1450
+
1466
1451
  const hash = crypto
1467
1452
  .createHash("sha256")
1468
- .update(message)
1453
+ .update(finalBlock)
1469
1454
  .digest("hex");
1470
1455
  const hashAsInt = BigInt("0x" + hash);
1456
+ const targetAsInt = BigInt("0x" + cpuTarget);
1457
+
1458
+ // --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
1459
+ console.log('[FP Server Verify] Intermediate values:', {
1460
+ hashCalculated: `0x${hash}`,
1461
+ hashAsInt: hashAsInt.toString(), // Log as string to see full value
1462
+ target: `0x${cpuTarget}`,
1463
+ targetAsInt: targetAsInt.toString(), // Log as string to see full value
1464
+ });
1465
+ // --- FIN DES NOUVEAUX LOGS ---
1466
+
1467
+ const isValid = hashAsInt < targetAsInt;
1471
1468
 
1472
- if (hashAsInt < BigInt("0x" + target)) {
1469
+ // --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
1470
+ if (!isValid) {
1471
+ console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
1472
+ hashCalculated: `0x${hash}`,
1473
+ target: `0x${cpuTarget}`,
1474
+
1475
+ });
1476
+ }
1477
+ // --- FIN DES LOGS ---
1478
+
1479
+ if (isValid) {
1480
+ console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
1481
+ });
1473
1482
  // The comparison is direct with native BigInts
1474
1483
  // The proof is valid, generate the ticket
1475
1484
  const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
@@ -1483,106 +1492,6 @@ export function verifyCpuTargetPoWAndGenerateTicket(
1483
1492
  return null;
1484
1493
  }
1485
1494
 
1486
- const staticExtensions = new RegExp(
1487
- "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
1488
- "i",
1489
- );
1490
- const isStaticResource = (path) => staticExtensions.test(path);
1491
-
1492
-
1493
- /**
1494
- * Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
1495
- * @param {number} suspicionScore - Le score de suspicion de la requête.
1496
- * @returns {number} Le TTL optimal calculé en millisecondes.
1497
- */
1498
- function determineOptimalTicketTtl(suspicionScore) {
1499
- // Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
1500
- const MIN_TTL = 300000;
1501
- const MAX_TTL = 86400000;
1502
-
1503
- const solverFunction = () => {
1504
- const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
1505
-
1506
- // Un "individu" est simplement une valeur de TTL en millisecondes.
1507
- const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
1508
- const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
1509
- const mutate = (ttl) => {
1510
- const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
1511
- return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
1512
- };
1513
-
1514
- const paretoFront = Optimization.geneticAlgorithmMultiObjective(
1515
- createIndividual,
1516
- fitnessFunction,
1517
- crossover,
1518
- mutate,
1519
- {
1520
- generations: 40,
1521
- populationSize: 30,
1522
- }
1523
- );
1524
-
1525
- // Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
1526
- // Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
1527
- // en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
1528
- if (!paretoFront || paretoFront.length === 0) {
1529
- return { solution: null, fitness: Infinity };
1530
- }
1531
-
1532
- // Stratégie de sélection :
1533
- // Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
1534
- // Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
1535
- let bestSolutionInFront;
1536
- if (suspicionScore < 50) {
1537
- bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
1538
- } else {
1539
- bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
1540
- }
1541
- return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
1542
- };
1543
-
1544
- // On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
1545
- const { bestResult } = Optimization.runMultiple(solverFunction, 20);
1546
-
1547
- if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
1548
- // Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
1549
- return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
1550
- }
1551
-
1552
- // runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
1553
- // La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
1554
- return Math.round(bestResult.solution);
1555
- }
1556
-
1557
- /**
1558
- * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
1559
- * @param {string} str - La chaîne à vérifier.
1560
- * @returns {boolean} - True si un pattern malveillant est détecté.
1561
- * @private
1562
- */
1563
- function isMalicious(str) {
1564
- if (typeof str !== 'string') return false;
1565
- // Regex pour les injections SQL et NoSQL de base
1566
- // Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
1567
- const injectionRegex = /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
1568
- // Regex pour les injections plus avancées
1569
- const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
1570
- const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
1571
- const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
1572
- const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
1573
- // Regex pour les injections de commandes.
1574
- // Elle détecte :
1575
- // 1. L'utilisation de backticks ``.
1576
- // 2. Des commandes dangereuses (rm, whoami...) qui sont soit au début de la chaîne,
1577
- // soit précédées par un séparateur de commande (;, &&, ||, |) suivi d'espaces.
1578
- const commandInjectionRegex = /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
1579
-
1580
- return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
1581
- }
1582
-
1583
- // --- Middleware Proof-of-Work (Le péage) ---
1584
- export { isMalicious };
1585
-
1586
1495
  export class FingerprintEngine {
1587
1496
  constructor(securityConfig) {
1588
1497
  const isProduction = process.env.NODE_ENV === 'production';
@@ -1609,7 +1518,8 @@ export class FingerprintEngine {
1609
1518
  (suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
1610
1519
  (suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
1611
1520
  (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
1612
- (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0);
1521
+ (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
1522
+ (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
1613
1523
 
1614
1524
  return Math.min(100, score);
1615
1525
  }
@@ -1746,7 +1656,7 @@ export class FingerprintEngine {
1746
1656
  this._log('Whitelisted bot verified - allowing request', { clientIp });
1747
1657
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
1748
1658
  }
1749
-
1659
+
1750
1660
  // Resolve identity and check for persisted "condemned" status early.
1751
1661
  const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
1752
1662
  const isNewDevice = !!newCookie;
@@ -1756,7 +1666,7 @@ export class FingerprintEngine {
1756
1666
  if (deviceData?.condemned) {
1757
1667
  this._log('Device condemned - blocking request', { deviceId });
1758
1668
  if (onDeviceCompromised) {
1759
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1669
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1760
1670
  }
1761
1671
  return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1762
1672
  }
@@ -1792,6 +1702,7 @@ export class FingerprintEngine {
1792
1702
  const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
1793
1703
  const isSuspiciousMedium = finalScore >= thresholds.medium;
1794
1704
  const isSuspicious = finalScore >= thresholds.low;
1705
+ const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
1795
1706
 
1796
1707
  // Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
1797
1708
  const suspicionFactor = isSuspicious
@@ -1816,10 +1727,10 @@ export class FingerprintEngine {
1816
1727
  // --- NOUVELLE LOGIQUE DE PRIORITÉ ---
1817
1728
  // Si une solution de challenge est soumise, on la traite en priorité absolue,
1818
1729
  // avant même de recalculer le score de suspicion.
1819
- const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
1820
- if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1730
+ const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
1731
+ if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
1821
1732
  this._log('Challenge solution submitted', { pow_type, pow_nonce });
1822
-
1733
+
1823
1734
  // On doit calculer le score de suspicion *avant* de valider le ticket,
1824
1735
  // car le TTL optimal en dépend.
1825
1736
  const preliminaryVector = suspicionVector; // Use the already calculated vector
@@ -1842,27 +1753,57 @@ export class FingerprintEngine {
1842
1753
  const probationaryTtl = 30000; // 30 secondes
1843
1754
 
1844
1755
  if (challengeContext) {
1845
- optimalTtl = determineOptimalTicketTtl(preliminaryScore);
1846
- finalTtl = isProbationary ? probationaryTtl : optimalTtl;
1847
- this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
1848
-
1849
- if (pow_type === "cpu_target" && pow_solution) {
1850
- // On passe la durée de vie du ticket configurée
1851
- console.log("ticket");
1852
- ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget);
1853
- console.log(ticket !== null)
1854
- isValid = ticket !== null;
1855
- this._log('CPU target challenge verification', { isValid });
1856
- } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
1857
- const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget);
1858
- const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
1859
- isValid = cpuTicket !== null && isMemValid;
1860
- if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
1861
- this._log('Combined CPU+Memory challenge verification', {
1862
- cpuValid: cpuTicket !== null,
1863
- memValid: isMemValid,
1864
- isValid
1756
+ // *** NOUVELLE VÉRIFICATION CRUCIALE ***
1757
+ // On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
1758
+ // lors de l'émission du challenge.
1759
+ // --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
1760
+ // This handles API clients that might not use the full client-side library but still solve the challenge.
1761
+ const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
1762
+ const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
1763
+
1764
+ let similarity;
1765
+ const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
1766
+
1767
+ // If fingerprints are simple strings (like test placeholders 'fp-probation')
1768
+ // and don't contain the typical structure, fall back to a strict equality check.
1769
+ if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
1770
+ similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
1771
+ } else {
1772
+ // Use the weighted comparison for structured fingerprints.
1773
+ // We compare the fingerprint of the request that triggered the challenge
1774
+ // with the fingerprint of the request that is submitting the solution.
1775
+ // They should be very similar.
1776
+ similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
1777
+ }
1778
+
1779
+ if (similarity < similarityThreshold) {
1780
+ this._log('Fingerprint mismatch - challenge solved on a different machine!', {
1781
+ original: originalFingerprint,
1782
+ solver: solverFingerprint,
1783
+ similarity: similarity.toFixed(4),
1784
+ threshold: similarityThreshold
1865
1785
  });
1786
+ isValid = false;
1787
+ } else {
1788
+ optimalTtl = determineOptimalTicketTtl(preliminaryScore);
1789
+ finalTtl = isProbationary ? probationaryTtl : optimalTtl;
1790
+ this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
1791
+
1792
+ if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
1793
+ const cpuSolution = pow_solution_cpu || pow_solution;
1794
+ ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
1795
+ isValid = ticket !== null;
1796
+ this._log('CPU target challenge verification', { isValid });
1797
+ } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
1798
+ const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
1799
+ isValid = cpuTicket !== null && isMemValid;
1800
+ if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
1801
+ this._log('Combined CPU+Memory challenge verification', {
1802
+ cpuValid: cpuTicket !== null,
1803
+ memValid: isMemValid,
1804
+ isValid
1805
+ });
1806
+ }
1866
1807
  }
1867
1808
  } else {
1868
1809
  this._log('Challenge context not found or expired', { pow_nonce });
@@ -1878,7 +1819,7 @@ export class FingerprintEngine {
1878
1819
 
1879
1820
  // NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
1880
1821
  // 1. On part du chemin original stocké, qui peut contenir des query params.
1881
- const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
1822
+ const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
1882
1823
  // 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
1883
1824
  const finalSearchParams = new URLSearchParams(requestContext.query);
1884
1825
 
@@ -1888,11 +1829,16 @@ export class FingerprintEngine {
1888
1829
  finalSearchParams.delete('pow_solution');
1889
1830
  finalSearchParams.delete('pow_solution_cpu');
1890
1831
  finalSearchParams.delete('pow_solution_mem');
1832
+ finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
1833
+ // NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
1834
+ finalSearchParams.delete('pow_solution_population');
1835
+ finalSearchParams.delete('pow_solution_work_result');
1836
+ finalSearchParams.delete('pow_problem_id');
1891
1837
 
1892
1838
  // 4. On reconstruit le chemin final.
1893
1839
  const finalQueryString = finalSearchParams.toString();
1894
1840
  const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
1895
- this._log('Redirecting to clean path', { finalRedirectPath });
1841
+ this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
1896
1842
  return {
1897
1843
  action: 'redirect',
1898
1844
  path: finalRedirectPath,
@@ -1900,8 +1846,8 @@ export class FingerprintEngine {
1900
1846
  vector: { challenge_solved: 100 },
1901
1847
  cookie: {
1902
1848
  name: 'pow_clearance',
1903
- value: ticket,
1904
- options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl }
1849
+ value: ticket, // The ticket itself
1850
+ options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl } // Options for setting the cookie
1905
1851
  }
1906
1852
  };
1907
1853
  } else {
@@ -1910,7 +1856,68 @@ export class FingerprintEngine {
1910
1856
  this._log('Challenge solution invalid', { pow_nonce });
1911
1857
  suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
1912
1858
  finalScore = this.calculateFinalScore(suspicionVector);
1859
+ // --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
1860
+ const newBlockThreshold = thresholds.block ?? 95;
1861
+ if (finalScore >= newBlockThreshold) {
1862
+ this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
1863
+ return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1864
+ }
1865
+ // If not blocked, the request will proceed to be re-challenged.
1866
+ }
1867
+ } else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
1868
+ this._log('Optimization task solution submitted', { pow_nonce });
1869
+ const challengeContext = await store.get(`secret:${pow_nonce}`);
1870
+ let isValid = false;
1871
+
1872
+ if (challengeContext?.optimizationProblem) {
1873
+ try {
1874
+ const submittedChromosomes = JSON.parse(pow_solution_population);
1875
+ // Vérification simple : le client a-t-il renvoyé le bon nombre de solutions ?
1876
+ if (Array.isArray(submittedChromosomes) && submittedChromosomes.length === challengeContext.optimizationProblem.population.length) {
1877
+ // Le serveur recalcule la fitness pour la nouvelle population.
1878
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: challengeContext.optimizationProblem.trafficData });
1879
+ const newPopulation = submittedChromosomes.map(chromosome => ({ chromosome, fitness: fitnessFunction(chromosome) }));
1880
+
1881
+ // On met à jour le problème principal avec la nouvelle population.
1882
+ challengeContext.optimizationProblem.population = newPopulation;
1883
+ await store.set(`device:${deviceId}`, deviceData); // Sauvegarde l'état mis à jour
1884
+ isValid = true;
1885
+ }
1886
+ } catch (e) {
1887
+ this._log('Error parsing optimization solution', { error: e.message });
1888
+ }
1889
+ }
1890
+
1891
+ if (isValid) {
1892
+ await store.delete(`secret:${pow_nonce}`);
1893
+ // La solution est valide, on accorde un ticket et on redirige.
1894
+ const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
1895
+ return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
1896
+ } else {
1897
+ this._log('Optimization task solution invalid', { pow_nonce });
1898
+ suspicionVector.honeypotScore = 100;
1899
+ finalScore = this.calculateFinalScore(suspicionVector);
1913
1900
  }
1901
+ } else if (pow_nonce && pow_type === 'useful_work_task' && pow_solution_work_result && pow_problem_id) {
1902
+ this._log('Useful work solution submitted', { problemId: pow_problem_id });
1903
+ const challengeContext = await store.get(`secret:${pow_nonce}`);
1904
+ if (challengeContext) {
1905
+ try {
1906
+ const workResult = JSON.parse(pow_solution_work_result);
1907
+ problemManager.integrateSolution(pow_problem_id, workResult);
1908
+
1909
+ await store.delete(`secret:${pow_nonce}`);
1910
+ // Accorder un ticket de passage comme pour un PoW normal
1911
+ const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
1912
+ return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
1913
+
1914
+ } catch (e) {
1915
+ this._log('Error parsing useful work solution', { error: e.message });
1916
+ }
1917
+ }
1918
+ // Si la validation échoue, on pénalise fortement
1919
+ suspicionVector.honeypotScore = 100;
1920
+ finalScore = this.calculateFinalScore(suspicionVector);
1914
1921
  }
1915
1922
  // --- FIN DE LA LOGIQUE DE PRIORITÉ ---
1916
1923
 
@@ -1918,7 +1925,7 @@ export class FingerprintEngine {
1918
1925
  if (isBlocked) {
1919
1926
  this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
1920
1927
  if (onDeviceCompromised) {
1921
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1928
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1922
1929
  }
1923
1930
  return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1924
1931
  }
@@ -1930,7 +1937,7 @@ export class FingerprintEngine {
1930
1937
  this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
1931
1938
  deviceData.condemned = true; // This device is a bot. Condemn it.
1932
1939
  if (onDeviceCompromised) {
1933
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1940
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1934
1941
  }
1935
1942
  if (logger) {
1936
1943
  logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
@@ -1961,20 +1968,32 @@ export class FingerprintEngine {
1961
1968
  const nonce = crypto.randomBytes(16).toString("hex");
1962
1969
  const clientSecret = crypto.randomBytes(16).toString("hex");
1963
1970
 
1964
- // LEVEL 3: CAPTCHA (the highest)
1965
- if (isSuspiciousHigh) {
1966
- // ... logic for TSP/Captcha challenge
1967
- }
1971
+ // Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
1972
+ // Cela rend l'automatisation plus difficile pour un attaquant.
1973
+ if (isSuspicious && this.securityConfig.enableUsefulWork && Math.random() > 0.5) {
1974
+ this._log('Issuing a useful work challenge', { finalScore });
1975
+
1976
+ const { problemId, task } = problemManager.dispatchWork(suspicionFactor);
1977
+
1978
+ await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
1968
1979
 
1969
- // UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
1970
- if (isSuspicious) { // Couvre à la fois low et medium
1980
+ const challengePayload = {
1981
+ challenge: {
1982
+ type: 'useful_work_task',
1983
+ nonce: nonce,
1984
+ clientSecret: clientSecret,
1985
+ usefulWorkTask: { problemId, task }
1986
+ }
1987
+ };
1988
+ return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
1989
+ } else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
1971
1990
  // Generate some trap URLs to embed in the challenge page.
1972
1991
  // These links are visually hidden but present in the DOM to trap bots.
1973
1992
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
1974
1993
  const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
1975
1994
 
1976
-
1977
- const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
1995
+ // On passe la configuration pour que la difficulté soit calculée correctement.
1996
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
1978
1997
 
1979
1998
  // La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
1980
1999
  // Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
@@ -1990,13 +2009,18 @@ export class FingerprintEngine {
1990
2009
  memDifficulty,
1991
2010
  cpuTarget: cpuChallengeDetails.target
1992
2011
  });
2012
+ // (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
2013
+ const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
1993
2014
 
1994
2015
  // Store the entire challenge context with a short TTL (e.g., 5 minutes)
2016
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
1995
2017
  await store.set(`secret:${nonce}`, {
1996
2018
  clientSecret,
1997
2019
  cpuTarget: cpuChallengeDetails.target,
1998
2020
  suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
2021
+ fingerprint: originalFingerprint, // *** NOUVEAU ***
1999
2022
  memDifficulty: memDifficulty,
2023
+ baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
2000
2024
  originalPath: path, // *** FIX: Store the original path ***
2001
2025
  }, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
2002
2026
 
@@ -2017,7 +2041,7 @@ export class FingerprintEngine {
2017
2041
  }
2018
2042
 
2019
2043
  // Check if the request is an API request to return a JSON challenge
2020
- const isApi = requestContext.rawReq && this.securityConfig.thresholds?.isApiRequest?.(requestContext.rawReq);
2044
+ const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
2021
2045
 
2022
2046
  if (isApi) {
2023
2047
  // For API clients, send a JSON response with challenge details.
@@ -2028,13 +2052,14 @@ export class FingerprintEngine {
2028
2052
  clientSecret: clientSecret, // The client needs this to solve the challenge
2029
2053
  cpuTarget: cpuChallengeDetails.target,
2030
2054
  memDifficulty: memDifficulty,
2055
+ baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
2031
2056
  }
2032
2057
  };
2033
2058
  this._log('API challenge response generated', { challengePayload });
2034
2059
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2035
2060
  } else {
2036
2061
  // For browsers, send the HTML page.
2037
- const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
2062
+ const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
2038
2063
  this._log('Browser challenge page generated', {
2039
2064
  pageLength: page.length,
2040
2065
  hasTrapContainer: true
@@ -2110,6 +2135,176 @@ export class FingerprintEngine {
2110
2135
  }
2111
2136
  }
2112
2137
 
2138
+ const staticExtensions = new RegExp(
2139
+ "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
2140
+ "i",
2141
+ );
2142
+ const isStaticResource = (path) => staticExtensions.test(path);
2143
+
2144
+
2145
+ /**
2146
+ * Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
2147
+ * @param {number} suspicionScore - Le score de suspicion de la requête.
2148
+ * @returns {number} Le TTL optimal calculé en millisecondes.
2149
+ */
2150
+ function determineOptimalTicketTtl(suspicionScore) {
2151
+ // Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
2152
+ const MIN_TTL = 300000;
2153
+ const MAX_TTL = 86400000;
2154
+
2155
+ const solverFunction = () => {
2156
+ const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
2157
+
2158
+ // Un "individu" est simplement une valeur de TTL en millisecondes.
2159
+ const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
2160
+ const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
2161
+ const mutate = (ttl) => {
2162
+ const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
2163
+ return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
2164
+ };
2165
+
2166
+ const paretoFront = Optimization.geneticAlgorithmMultiObjective(
2167
+ createIndividual,
2168
+ fitnessFunction,
2169
+ crossover,
2170
+ mutate,
2171
+ {
2172
+ generations: 40,
2173
+ populationSize: 30,
2174
+ }
2175
+ );
2176
+
2177
+ // Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
2178
+ // Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
2179
+ // en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
2180
+ if (!paretoFront || paretoFront.length === 0) {
2181
+ return { solution: null, fitness: Infinity };
2182
+ }
2183
+
2184
+ // Stratégie de sélection :
2185
+ // Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
2186
+ // Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
2187
+ let bestSolutionInFront;
2188
+ if (suspicionScore < 50) {
2189
+ bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
2190
+ } else {
2191
+ bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
2192
+ }
2193
+ return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
2194
+ };
2195
+
2196
+ // On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
2197
+ const { bestResult } = Optimization.runMultiple(solverFunction, 20);
2198
+
2199
+ if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
2200
+ // Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
2201
+ return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
2202
+ }
2203
+
2204
+ // runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
2205
+ // La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
2206
+ return Math.round(bestResult.solution);
2207
+ }
2208
+
2209
+ /**
2210
+ * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
2211
+ * @private
2212
+ * @param {string} str - La chaîne à vérifier.
2213
+ * @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
2214
+ * @returns {boolean} - True si un pattern malveillant est détecté.
2215
+ */
2216
+ function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
2217
+ if (typeof str !== 'string') return false;
2218
+
2219
+ for (const type of typesToDetect) {
2220
+ const regex = injectionPatterns[type];
2221
+ if (regex && regex.test(str)) {
2222
+ return true;
2223
+ }
2224
+ }
2225
+
2226
+ return false;
2227
+ }
2228
+
2229
+ // --- Middleware Proof-of-Work (Le péage) ---
2230
+ export { isMalicious };
2231
+
2232
+ /**
2233
+ * Returns a default list of security analyzers for honeypot detection.
2234
+ * This list can be used as a base and extended with custom rules.
2235
+ * Currently includes an XSS detection analyzer.
2236
+ * @returns {Array<Function>}
2237
+ */
2238
+ export const default_analyzers = () => [
2239
+ // Analyzer for Cross-Site Scripting (XSS) detection.
2240
+ // It uses the 'xss' library, which should be installed by the user (`npm install xss`).
2241
+ // If 'xss' is not available, this analyzer will be safely ignored.
2242
+ xss_analyzer
2243
+ ];
2244
+
2245
+ export const xss_analyzer = async (data) => {
2246
+ try {
2247
+ // Dynamically import the 'xss' library.
2248
+ // The module is loaded only once by Node's cache.
2249
+ const xss = (await import('xss')).default;
2250
+ const originalData = JSON.stringify(data);
2251
+ // If the sanitized string is different, it means malicious HTML/JS was found and removed.
2252
+ return xss(originalData) !== originalData;
2253
+ } catch (error) {
2254
+ // This catch block handles the case where the 'xss' module is not installed.
2255
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
2256
+ console.warn('[Fingerprint] Warning: The "xss" package is not installed. The default XSS analyzer is disabled. Run "npm install xss" to enable it.');
2257
+ // To avoid repeated warnings, we can replace this function with a no-op.
2258
+ this.isXssAnalyzerAvailable = false; // A flag to prevent future attempts.
2259
+ }
2260
+ return false; // In case of any error, we assume the data is not malicious.
2261
+ }
2262
+ }
2263
+ /**
2264
+ * Returns a powerful WAF (Web Application Firewall) analyzer based on ModSecurity.
2265
+ * This analyzer is highly effective against a wide range of attacks (SQLi, XSS, RCE, etc.)
2266
+ * by using the OWASP Core Rule Set.
2267
+ *
2268
+ * **Note:** This is an optional and advanced feature.
2269
+ * 1. The user must install the package: `npm install modsecurity-nodejs`
2270
+ * 2. ModSecurity rules (like the OWASP CRS) must be available on the server.
2271
+ *
2272
+ * If the package is not installed, the analyzer will be safely ignored.
2273
+ *
2274
+ * @param {string} rulesPath - The path to the ModSecurity rules configuration file (e.g., `crs-setup.conf`).
2275
+ * @returns {Function} An analyzer function to be used in the `honeypot.analyzers` array.
2276
+ */
2277
+ export const modsecurity_analyzer = (rulesPath) => {
2278
+ let wafInstance = null; // Singleton instance for the WAF
2279
+
2280
+ return async (data) => {
2281
+ if (!rulesPath) {
2282
+ console.warn('[Fingerprint] ModSecurity analyzer disabled: `rulesPath` is not provided.');
2283
+ return false;
2284
+ }
2285
+
2286
+ try {
2287
+ if (!wafInstance) {
2288
+ // Dynamically import the library only when needed.
2289
+ const { ModSecurity } = await import('modsecurity-nodejs');
2290
+ wafInstance = new ModSecurity();
2291
+ wafInstance.init();
2292
+ wafInstance.addRules(rulesPath);
2293
+ console.log('[Fingerprint] ModSecurity WAF analyzer initialized successfully.');
2294
+ }
2295
+
2296
+ // The `transaction` method checks the data against the loaded rules.
2297
+ // It returns `null` if no rules are matched, or an object with intervention details if a threat is found.
2298
+ const result = wafInstance.transaction(data);
2299
+ return result !== null; // A non-null result means a threat was detected.
2300
+ } catch (error) {
2301
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
2302
+ console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
2303
+ }
2304
+ return false; // Assume data is safe if any error occurs.
2305
+ }
2306
+ };
2307
+ };
2113
2308
  /**
2114
2309
  * Returns a default list of whitelisting rules for common and legitimate web crawlers.
2115
2310
  * This list can be used as a base and extended with custom rules.
@@ -2228,9 +2423,8 @@ export const powMiddleware = (securityConfig) => {
2228
2423
 
2229
2424
  // Provide a default for isApiRequest if not specified by the user.
2230
2425
  // This makes API challenge handling work more seamlessly out-of-the-box.
2231
- if (!securityConfig.thresholds?.isApiRequest) {
2232
- if (!securityConfig.thresholds) securityConfig.thresholds = {};
2233
- securityConfig.thresholds.isApiRequest = (req) =>
2426
+ if (!securityConfig?.isApiRequest) {
2427
+ securityConfig.isApiRequest = (req) =>
2234
2428
  req.headers?.accept?.includes('application/json');
2235
2429
  }
2236
2430
 
@@ -2242,9 +2436,10 @@ export const powMiddleware = (securityConfig) => {
2242
2436
  query: req.query,
2243
2437
  body: req.body,
2244
2438
  headers: req.headers,
2245
- isStatic: isStaticResource(req.path),
2439
+ isStatic: securityConfig?.isStaticResource?.(req.path) || isStaticResource(req.path),
2246
2440
  // Pass the original request object for the isApiRequest function
2247
2441
  rawReq: req,
2442
+ requestTimestamp: Date.now(), // Timestamp de début de requête
2248
2443
  // Add the newly required properties for full decoupling
2249
2444
  rawHeaders: req.rawHeaders,
2250
2445
  // Pass the raw request object for advanced inspection (e.g., JA3)
@@ -2306,6 +2501,7 @@ export const __internal = {
2306
2501
  getBehaviorScore, // Expose for testing
2307
2502
  getCrossLayerInconsistency, // Expose for testing
2308
2503
  // Expose page generators for security testing
2504
+ getTimeInconsistencyScore,
2309
2505
  generateCpuTargetChallengePage,
2310
2506
  generateCombinedPoWChallengePage,
2311
2507
  };