@anonympins/fingerprint 0.6.7 → 0.7.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -7
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/public/fp.wasm +0 -0
  7. package/src/js/build-client.js +9 -11
  8. package/src/js/dynamic-wasm.js +7 -7
  9. package/src/js/fingerprint.builder.js +37 -38
  10. package/src/js/fingerprint.client.js +236 -152
  11. package/src/js/fingerprint.client.obfuscated.js +1 -1
  12. package/src/js/fingerprint.js +517 -101
  13. package/src/js/fingerprint.utils.js +213 -213
  14. package/src/js/library.js +355 -384
  15. package/src/js/mongodb-store.js +1 -1
  16. package/src/js/optimization.worker.js +27 -27
  17. package/src/js/pow.solver.inline.js +206 -62
  18. package/src/js/pow.solver.js +235 -101
  19. package/src/js/pow.worker.js +48 -26
  20. package/src/js/redis-store.js +1 -1
  21. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  22. package/src/js/tests/fingerprint.test.js +101 -40
  23. package/src/js/tests/ip-reputation.test.js +1 -0
  24. package/src/js/upow-model-task.js +14 -15
  25. package/src/php/AutoTuner.php +34 -34
  26. package/src/php/Challenge/ChallengeUtils.php +204 -32
  27. package/src/php/Config/SecurityProfiles.php +21 -0
  28. package/src/php/DirectFingerprint.php +190 -145
  29. package/src/php/FingerprintBuilder.php +29 -30
  30. package/src/php/FingerprintClient.php +24 -25
  31. package/src/php/FingerprintEngine.php +1836 -1880
  32. package/src/php/Ja3AnomalyDetector.php +258 -227
  33. package/src/php/Optimization/FunctionRegistry.php +7 -7
  34. package/src/php/Optimization/Optimization.php +24 -24
  35. package/src/php/Optimization/OptimizationOperators.php +28 -27
  36. package/src/php/Optimization/ProblemInitializers.php +52 -52
  37. package/src/php/ProblemManager.php +31 -34
  38. package/src/php/RequestContext.php +8 -1
  39. package/src/php/Store/IStore.php +7 -8
  40. package/src/php/Store/InMemoryStore.php +66 -66
  41. package/src/php/Store/MongoDbStore.php +5 -5
  42. package/src/php/Store/RedisStore.php +5 -5
  43. package/src/php/Store/StoreManager.php +35 -35
  44. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  45. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  46. package/src/php/Tests/FingerprintEngineTest.php +662 -693
  47. package/src/php/Tests/IpReputationTest.php +10 -0
  48. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  49. package/src/php/Tests/MetricsTest.php +97 -97
  50. package/src/php/Tests/ProblemManagerTest.php +376 -376
  51. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  52. package/src/php/Tests/RequestUtilsTest.php +471 -471
  53. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  54. package/src/php/Tests/config/ed25519_key.json +3 -3
  55. package/src/php/Utils/Env.php +49 -49
  56. package/src/php/Utils/MaliciousPatterns.php +74 -74
  57. package/src/php/Utils/MetricsManager.php +209 -209
  58. package/src/php/Utils/RequestUtils.php +236 -19
  59. package/src/php/Utils/TLSClientHelloParser.php +369 -212
  60. package/src/php/WordPress/WpDbStore.php +155 -0
  61. package/src/php/WordPress/fingerprint-wordpress.php +217 -0
  62. package/src/php/WordPress/package.php +123 -0
  63. package/src/php/bin/auto-tune.php +118 -118
@@ -6,7 +6,7 @@ import {getProblemManager, problemManager} from "./problem-manager.js";
6
6
  import {Optimization} from "./library.js";
7
7
  import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
8
8
  import {DynamicWasmGenerator} from "./dynamic-wasm.js";
9
- import {writeFileSync, readFileSync, existsSync} from "node:fs";
9
+ import {writeFileSync, readFileSync, existsSync, promises as fsPromises} from "node:fs";
10
10
  import {fileURLToPath} from "node:url";
11
11
  import {dirname, join, resolve} from "node:path";
12
12
  import {GpuPowSolver} from "./gpu_pow.solver.js";
@@ -29,6 +29,8 @@ import {
29
29
  const __filename = fileURLToPath(import.meta.url);
30
30
  const __dirname = dirname(__filename);
31
31
 
32
+ const secureRandomFloat = () => crypto.randomInt(0, 4294967296) / 4294967296;
33
+
32
34
  let dnsCircuitBreaker = {
33
35
  state: 'CLOSED', // 'CLOSED', 'OPEN', 'HALF-OPEN'
34
36
  failureCount: 0,
@@ -277,9 +279,7 @@ async function ensureLatestMapping() {
277
279
  mapping.timestamp = now;
278
280
 
279
281
  activeMappings.unshift(mapping);
280
- if (activeMappings.length > 5) {
281
- activeMappings.pop();
282
- }
282
+ activeMappings = activeMappings.slice(0, 5);
283
283
  lastMappingTime = now;
284
284
 
285
285
  try {
@@ -402,16 +402,70 @@ function getProtocolAnomalyScore(context) {
402
402
  if (kv.length === 2) params[kv[0]] = kv[1];
403
403
  });
404
404
  const priorityOrder = parts[2] || '';
405
+ const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
406
+ const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
405
407
 
406
- if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
407
- const maxData = parseInt(params['1'] || '0', 10);
408
- const maxStreams = parseInt(params['4'] || '0', 10);
408
+ const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
409
+ const isFirefox = browser.startsWith('Firefox');
410
+ const isSafari = browser.startsWith('Safari');
411
+
412
+ const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
413
+ const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
414
+ const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
415
+ const bidiRemote = parseInt(params['6'] || params['0x06'] || '0', 10);
416
+
417
+ if (isChromium) {
418
+ // Contrôle de flux global et nombre de flux bidirectionnels
409
419
  if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
410
420
  if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
411
421
  if (priorityOrder && !priorityOrder.includes('u=')) quicAnomaly += 30.0;
412
- } else if (browser.startsWith('Firefox')) {
422
+
423
+ // Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
424
+ // curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
425
+ if (bidiLocal > 0 && (bidiLocal < 524288 || bidiLocal === 262144)) quicAnomaly += 40.0;
426
+ if (bidiRemote > 0 && (bidiRemote < 524288 || bidiRemote === 262144)) quicAnomaly += 30.0;
427
+
428
+ // Ordre des trames de contrôle QUIC (SETTINGS, MAX_STREAMS, PRIORITY)
429
+ if (frameOrder.length >= 2) {
430
+ const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
431
+ const mIdx = frameOrder.findIndex(f => f === 'm' || f === 'max_streams' || f === '18');
432
+ const pIdx = frameOrder.findIndex(f => f === 'p' || f === 'priority' || f === 'priority_update' || f === '15');
433
+
434
+ if (sIdx !== 0 && sIdx !== -1) {
435
+ quicAnomaly += 50.0; // SETTINGS doit obligatoirement être la première trame
436
+ }
437
+ if (mIdx !== -1 && sIdx !== -1 && mIdx < sIdx) {
438
+ quicAnomaly += 60.0; // MAX_STREAMS envoyé avant SETTINGS (défaut curl/quiche)
439
+ }
440
+ if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
441
+ quicAnomaly += 60.0;
442
+ }
443
+ }
444
+ } else if (isFirefox) {
413
445
  const maxData = parseInt(params['1'] || '0', 10);
414
446
  if (maxData > 0 && maxData > 5000000) quicAnomaly += 40.0;
447
+ // Necko (Firefox) n'utilise pas 100 flux bidi par défaut ni un buffer bidi de 6 Mo
448
+ if (maxStreams === 100) quicAnomaly += 50.0;
449
+ if (bidiLocal === 6291456) quicAnomaly += 50.0;
450
+
451
+ if (frameOrder.length >= 2) {
452
+ const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
453
+ if (sIdx !== 0 && sIdx !== -1) quicAnomaly += 50.0;
454
+ }
455
+ } else if (isSafari) {
456
+ // Safari Network.framework
457
+ if (maxStreams === 100 && maxData === 1572864 && priorityOrder.includes('u=2,i')) {
458
+ quicAnomaly += 60.0; // Usurpation par profil Cronet / curl-impersonate
459
+ }
460
+ if (bidiLocal === 6291456) quicAnomaly += 50.0;
461
+
462
+ if (frameOrder.length >= 2) {
463
+ const sIdx = frameOrder.findIndex(f => f === 's' || f === 'settings' || f === '4');
464
+ const mIdx = frameOrder.findIndex(f => f === 'm' || f === 'max_streams');
465
+ if (mIdx !== -1 && (mIdx === 0 || (sIdx !== -1 && mIdx < sIdx))) {
466
+ quicAnomaly += 50.0;
467
+ }
468
+ }
415
469
  }
416
470
  }
417
471
  }
@@ -422,7 +476,9 @@ function getProtocolAnomalyScore(context) {
422
476
  0.0,
423
477
  Math.min(100.0, http2Anomaly),
424
478
  Math.min(100.0, quicAnomaly)
425
- )
479
+ ),
480
+ http2AnomalyScore: Math.min(100.0, http2Anomaly),
481
+ quicAnomalyScore: Math.min(100.0, quicAnomaly)
426
482
  };
427
483
  }
428
484
  /**
@@ -612,6 +668,7 @@ const securityProfiles = {
612
668
  protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
613
669
  renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
614
670
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
671
+ virtualizationScore: 0.8,
615
672
  },
616
673
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
617
674
  patterns: {
@@ -840,17 +897,28 @@ const getPowSecret = () => {
840
897
  return secret || "fallback-dev-secret-32-chars-minimum";
841
898
  };
842
899
 
843
- let cachedPowSolverCode = null;
844
900
  /**
845
901
  * Loads the pow.solver.js content for inlining in HTML pages.
846
902
  * @returns {string} The solver JavaScript code.
847
903
  */
904
+ let cachedPowSolverCode = "";
905
+ async function preloadPowSolverCode() {
906
+ if (!cachedPowSolverCode) {
907
+ try {
908
+ const solverPath = join(__dirname, 'pow.solver.inline.js');
909
+ cachedPowSolverCode = await fsPromises.readFile(solverPath, 'utf-8');
910
+ } catch (e) {
911
+ console.error('[Fingerprint] Failed to pre-load inline solver:', e.message);
912
+ }
913
+ }
914
+ }
915
+
848
916
  const getPowSolverCode = () => {
849
- if (!cachedPowSolverCode) {
850
- const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
851
- cachedPowSolverCode = readFileSync(solverPath, 'utf-8');
852
- }
853
- return cachedPowSolverCode;
917
+ if (!cachedPowSolverCode) {
918
+ const solverPath = join(__dirname, 'pow.solver.inline.js');
919
+ cachedPowSolverCode = readFileSync(solverPath, 'utf-8');
920
+ }
921
+ return cachedPowSolverCode;
854
922
  };
855
923
  /**
856
924
  * Extracts the "stable" part of a fingerprint string.
@@ -1807,7 +1875,7 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
1807
1875
  const queries = [];
1808
1876
  const maxBlocks = sizeMb * 1024;
1809
1877
  while (queries.length < numQueries) {
1810
- const idx = Math.floor(Math.random() * maxBlocks);
1878
+ const idx = crypto.randomInt(0, maxBlocks);
1811
1879
  if (!queries.includes(idx)) {
1812
1880
  queries.push(idx);
1813
1881
  }
@@ -1824,7 +1892,7 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
1824
1892
  const peer = await findPeerInSubnet(clientIp, nonce);
1825
1893
  if (peer) {
1826
1894
  challenge.peerId = peer.nodeId;
1827
- challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
1895
+ challenge.peerBlockIdx = crypto.randomInt(0, maxBlocks);
1828
1896
 
1829
1897
  await store.set(`coop-assoc:${nonce}`, {
1830
1898
  peerNodeId: peer.nodeId,
@@ -1861,7 +1929,12 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1861
1929
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1862
1930
  return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
1863
1931
  }
1864
-
1932
+
1933
+ async function sendWebRtcSignal(targetId, type, data) {
1934
+ const sig = await signCoop("webrtc_signal", nodeId, targetId + ":" + type + ":" + data);
1935
+ await fetch(window.location.pathname + "?coop_op=webrtc_signal&node_id=" + nodeId + "&target_peer_id=" + targetId + "&signal_type=" + type + "&signal_data=" + encodeURIComponent(data) + "&coop_sig=" + sig);
1936
+ }
1937
+
1865
1938
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1866
1939
  await new Promise(r => setTimeout(r, 10));
1867
1940
 
@@ -1874,47 +1947,110 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1874
1947
  await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
1875
1948
  }
1876
1949
 
1950
+ // Gestionnaire de connexions WebRTC P2P
1951
+ const peerConnections = {};
1952
+
1877
1953
  // Écoute des requêtes entrantes de nos pairs
1878
1954
  setInterval(async () => {
1879
- try {
1880
- const sig = await signCoop("poll_requests", nodeId);
1881
- const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
1882
- const data = await res.json();
1883
- if (data.requests && data.requests.length > 0) {
1884
- for (const req of data.requests) {
1885
- document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
1886
- const blockData = await window.readSpaceBlock(req.block_idx);
1887
- const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
1888
- await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData) + "&coop_sig=" + respSig);
1889
- }
1890
- }
1891
- } catch (e) {
1892
- console.error("Cooperative polling error", e);
1893
- }
1894
- }, 1000);
1955
+ try {
1956
+ // 1. Récupération des signaux WebRTC P2P entrants
1957
+ const sigWebrtc = await signCoop("poll_signals", nodeId);
1958
+ const resWebrtc = await fetch(window.location.pathname + "?coop_op=poll_signals&node_id=" + nodeId + "&coop_sig=" + sigWebrtc);
1959
+ const dataWebrtc = await resWebrtc.json();
1960
+ if (dataWebrtc.signals && dataWebrtc.signals.length > 0) {
1961
+ for (const sig of dataWebrtc.signals) {
1962
+ const fromId = sig.from_peer_id;
1963
+ if (sig.signal_type === 'offer') {
1964
+ const pc = new RTCPeerConnection({ iceServers: [] });
1965
+ peerConnections[fromId] = pc;
1966
+ pc.onicecandidate = (e) => {
1967
+ if (e.candidate) sendWebRtcSignal(fromId, 'candidate', JSON.stringify(e.candidate));
1968
+ };
1969
+ pc.ondatachannel = (e) => {
1970
+ const dc = e.channel;
1971
+ dc.onmessage = async (evt) => {
1972
+ try {
1973
+ const req = JSON.parse(evt.data);
1974
+ if (req.type === 'get_block') {
1975
+ document.getElementById('loader').innerText = '📤 Transfert direct P2P (WebRTC) du bloc vers le pair...';
1976
+ const blockData = await window.readSpaceBlock(req.block_idx);
1977
+ dc.send(JSON.stringify({ type: 'block_data', block_data: blockData }));
1978
+ }
1979
+ } catch (err) {}
1980
+ };
1981
+ };
1982
+ await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1983
+ const answer = await pc.createAnswer();
1984
+ await pc.setLocalDescription(answer);
1985
+ await sendWebRtcSignal(fromId, 'answer', JSON.stringify(answer));
1986
+ } else if (sig.signal_type === 'answer' && peerConnections[fromId]) {
1987
+ await peerConnections[fromId].setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1988
+ } else if (sig.signal_type === 'candidate' && peerConnections[fromId]) {
1989
+ await peerConnections[fromId].addIceCandidate(new RTCIceCandidate(JSON.parse(sig.signal_data)));
1990
+ }
1991
+ }
1992
+ }
1993
+ } catch (e) {}
1994
+ }, 800);
1895
1995
 
1896
1996
  let peerBlock = "";
1897
1997
  if (peerId && peerBlockIdx !== -1) {
1898
- document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1899
- const reqId = Math.random().toString(36).substring(2);
1900
- const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1901
- await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
1902
-
1903
- let attempts = 0;
1904
- while (attempts < coopTimeout) {
1905
- const pollSig = await signCoop("poll_response", nodeId, reqId);
1906
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1907
- const data = await res.json();
1908
- if (data.status === 'ready') {
1909
- peerBlock = data.block_data;
1910
- break;
1911
- }
1912
- await new Promise(r => setTimeout(r, 1000));
1913
- attempts++;
1914
- }
1915
- if (!peerBlock) {
1916
- document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
1917
- }
1998
+ document.getElementById('loader').innerText = '📥 Connexion WebRTC P2P directe au pair (' + peerId + ')...';
1999
+
2000
+ // 1. Tentative d'échange direct via WebRTC DataChannel (charge serveur = 0)
2001
+ const webrtcTransferPromise = new Promise(async (resolve) => {
2002
+ if (!window.RTCPeerConnection) return resolve(null);
2003
+ try {
2004
+ const pc = new RTCPeerConnection({ iceServers: [] });
2005
+ peerConnections[peerId] = pc;
2006
+ const dc = pc.createDataChannel("pospace-transfer");
2007
+ pc.onicecandidate = (e) => {
2008
+ if (e.candidate) sendWebRtcSignal(peerId, 'candidate', JSON.stringify(e.candidate));
2009
+ };
2010
+ dc.onopen = () => {
2011
+ dc.send(JSON.stringify({ type: 'get_block', block_idx: peerBlockIdx }));
2012
+ };
2013
+ dc.onmessage = (e) => {
2014
+ try {
2015
+ const msg = JSON.parse(e.data);
2016
+ if (msg.type === 'block_data' && msg.block_data) {
2017
+ resolve(msg.block_data);
2018
+ }
2019
+ } catch (err) {}
2020
+ };
2021
+ const offer = await pc.createOffer();
2022
+ await pc.setLocalDescription(offer);
2023
+ await sendWebRtcSignal(peerId, 'offer', JSON.stringify(offer));
2024
+ } catch (err) {
2025
+ resolve(null);
2026
+ }
2027
+ });
2028
+
2029
+ const webrtcTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000));
2030
+ peerBlock = await Promise.race([webrtcTransferPromise, webrtcTimeoutPromise]);
2031
+
2032
+ if (peerBlock) {
2033
+ document.getElementById('loader').innerText = '⚡ Bloc reçu en direct via WebRTC P2P sans transit serveur !';
2034
+ } else {
2035
+ // 2. Repli transparent vers la boîte aux lettres HTTP en cas de restriction réseau
2036
+ document.getElementById('loader').innerText = '⚠️ WebRTC P2P indisponible. Téléchargement via relais HTTP...';
2037
+ const reqId = Math.random().toString(36).substring(2);
2038
+ const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
2039
+ await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
2040
+
2041
+ let attempts = 0;
2042
+ while (attempts < coopTimeout) {
2043
+ const pollSig = await signCoop("poll_response", nodeId, reqId);
2044
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
2045
+ const data = await res.json();
2046
+ if (data.status === 'ready') {
2047
+ peerBlock = data.block_data;
2048
+ break;
2049
+ }
2050
+ await new Promise(r => setTimeout(r, 1000));
2051
+ attempts++;
2052
+ }
2053
+ }
1918
2054
  }
1919
2055
 
1920
2056
  document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
@@ -2211,7 +2347,9 @@ function getBehaviorScore(context) {
2211
2347
  if (metrics.honeypotInteraction) {
2212
2348
  return { behaviorScore: 100 };
2213
2349
  }
2214
-
2350
+ if (metrics.prototypeTampered) {
2351
+ score += 80; // Altération flagrante de l'environnement JS
2352
+ }
2215
2353
  // 2. Analyse des mouvements de la souris
2216
2354
  const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
2217
2355
  const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
@@ -2263,6 +2401,14 @@ function getBehaviorScore(context) {
2263
2401
  const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2264
2402
  if (benfordDev > 0.18) score += 35;
2265
2403
  }
2404
+
2405
+ // Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
2406
+ const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2407
+ if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
2408
+ if (metrics.motionVariance === 0) {
2409
+ score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2410
+ }
2411
+ }
2266
2412
  }
2267
2413
 
2268
2414
  // Plausibilité de la latence de frappe
@@ -2896,6 +3042,62 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
2896
3042
  return null;
2897
3043
  }
2898
3044
 
3045
+ /**
3046
+ * Calcule la longueur du préfixe commun (en bits) entre deux adresses IP (IPv4 ou IPv6).
3047
+ * @param {string} ip1
3048
+ * @param {string} ip2
3049
+ * @returns {number} Nombre de bits identiques en tête (0 à 32 pour IPv4, 0 à 128 pour IPv6).
3050
+ */
3051
+ function getIpCommonPrefixLength(ip1, ip2) {
3052
+ if (!ip1 || !ip2) return 0;
3053
+ if (isIPv4(ip1) && isIPv4(ip2)) {
3054
+ const b1 = ip1.split('.').map(Number);
3055
+ const b2 = ip2.split('.').map(Number);
3056
+ const int1 = ((b1[0] << 24) | (b1[1] << 16) | (b1[2] << 8) | b1[3]) >>> 0;
3057
+ const int2 = ((b2[0] << 24) | (b2[1] << 16) | (b2[2] << 8) | b2[3]) >>> 0;
3058
+ const xor = (int1 ^ int2) >>> 0;
3059
+ return xor === 0 ? 32 : Math.clz32(xor);
3060
+ }
3061
+ if (isIPv6(ip1) && isIPv6(ip2)) {
3062
+ const normalize = (ip) => {
3063
+ let normalized = ip.trim().toLowerCase();
3064
+ if (normalized.includes("::")) {
3065
+ const parts = normalized.split("::");
3066
+ const left = parts[0] ? parts[0].split(":") : [];
3067
+ const right = parts[1] ? parts[1].split(":") : [];
3068
+ const missing = 8 - (left.length + right.length);
3069
+ const middle = Array(missing).fill("0000");
3070
+ normalized = [...left, ...middle, ...right].join(":");
3071
+ } else {
3072
+ const parts = normalized.split(":");
3073
+ if (parts.length !== 8) return null;
3074
+ }
3075
+ return normalized.split(":").map(g => parseInt(g, 16) || 0);
3076
+ };
3077
+
3078
+ try {
3079
+ const g1 = normalize(ip1);
3080
+ const g2 = normalize(ip2);
3081
+ if (!g1 || !g2) return 0;
3082
+ let prefix = 0;
3083
+ for (let i = 0; i < 8; i++) {
3084
+ const xor = (g1[i] ^ g2[i]) & 0xffff;
3085
+ if (xor === 0) {
3086
+ prefix += 16;
3087
+ } else {
3088
+ // clz32 opère sur des entiers 32 bits, ajuster pour 16 bits
3089
+ prefix += (Math.clz32(xor) - 16);
3090
+ break;
3091
+ }
3092
+ }
3093
+ return prefix;
3094
+ } catch (e) {
3095
+ return 0;
3096
+ }
3097
+ }
3098
+ return 0;
3099
+ }
3100
+
2899
3101
  /**
2900
3102
  * Applies temporal decay (half-life of 30 minutes) to subnet metrics.
2901
3103
  * @private
@@ -2943,6 +3145,11 @@ function decaySubnetData(subnetData, now) {
2943
3145
  subnetData.uas = subnetData.uas.slice(0, newLen);
2944
3146
  }
2945
3147
 
3148
+ if (Array.isArray(subnetData.attackerIps)) {
3149
+ const maxAttackerAgeMs = 60 * 60 * 1000; // 1 heure max
3150
+ subnetData.attackerIps = subnetData.attackerIps.filter(a => (now - (a.lastSeen || now)) < maxAttackerAgeMs);
3151
+ }
3152
+
2946
3153
  subnetData.lastActivity = now - (inactivityMs % (30 * 60 * 1000));
2947
3154
  }
2948
3155
  return subnetData;
@@ -2965,7 +3172,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
2965
3172
  highScoreDevices: {},
2966
3173
  lastActivity: 0,
2967
3174
  ips: [],
2968
- uas: []
3175
+ uas: [],
3176
+ attackerIps: []
2969
3177
  };
2970
3178
 
2971
3179
  if (!subnetData.highScoreDevices) {
@@ -2977,6 +3185,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
2977
3185
  subnetData.ips = [];
2978
3186
  }
2979
3187
  if (!subnetData.uas) subnetData.uas = [];
3188
+ if (!Array.isArray(subnetData.attackerIps)) {
3189
+ subnetData.attackerIps = [];
3190
+ }
2980
3191
 
2981
3192
  const now = Date.now();
2982
3193
  decaySubnetData(subnetData, now);
@@ -3000,6 +3211,20 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3000
3211
  }
3001
3212
 
3002
3213
  const userAgent = context.headers?.['user-agent'] || '';
3214
+
3215
+ if (finalScore >= 70) {
3216
+ const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
3217
+ if (existingAttacker) {
3218
+ existingAttacker.lastSeen = now;
3219
+ existingAttacker.score = Math.max(existingAttacker.score || 0, finalScore);
3220
+ } else {
3221
+ subnetData.attackerIps.push({ ip: context.clientIp, lastSeen: now, score: finalScore });
3222
+ if (subnetData.attackerIps.length > 30) {
3223
+ subnetData.attackerIps.shift();
3224
+ }
3225
+ }
3226
+ }
3227
+
3003
3228
  if (userAgent && !subnetData.uas.includes(userAgent)) {
3004
3229
  subnetData.uas.push(userAgent);
3005
3230
  }
@@ -3019,13 +3244,37 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3019
3244
 
3020
3245
  await store.set(key, subnetData, 86400); // 24-hour TTL
3021
3246
  }
3247
+ /**
3248
+ * Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
3249
+ * @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
3250
+ * @param {number} [inflectionPoint=0.72] Point d'inflexion où la suspicion accélère.
3251
+ * @param {number} [steepness=12] Raideur de la transition sigmoïdale.
3252
+ * @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
3253
+ */
3254
+ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPoint = 0.72, steepness = 12) {
3255
+ const s = Math.max(0.0, Math.min(1.0, consistencyScore));
3256
+ if (s >= 0.98) return 0.0;
3257
+
3258
+ const asymptote = 99.9;
3259
+ const raw = 1.0 / (1.0 + Math.exp(steepness * (s - inflectionPoint)));
3260
+ const minVal = 1.0 / (1.0 + Math.exp(steepness * (1.0 - inflectionPoint)));
3261
+ const maxVal = 1.0 / (1.0 + Math.exp(steepness * (0.0 - inflectionPoint)));
3262
+ const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
3263
+
3264
+ return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
3265
+ }
3022
3266
 
3023
3267
  /**
3024
3268
  * Calculates a suspicion score based on the historical activity of the IP subnet.
3025
3269
  * @param {object} context The request context.
3270
+ * @param {string} [deviceId=''] The device ID.
3271
+ * @param {object} [securityConfig=null] Security configuration containing thresholds.
3026
3272
  * @returns {Promise<{subnetScore: number}>}
3027
3273
  */
3028
- async function getSubnetScore(context) {
3274
+ async function getSubnetScore(context, deviceId = '', securityConfig = null) {
3275
+ if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
3276
+ securityConfig = deviceId;
3277
+ }
3029
3278
  const subnet = getIpSubnet(context.clientIp);
3030
3279
  if (!subnet) return { subnetScore: 0 };
3031
3280
  const key = `subnet:${subnet}`;
@@ -3054,28 +3303,76 @@ async function getSubnetScore(context) {
3054
3303
  return { subnetScore: 0.0 };
3055
3304
  }
3056
3305
 
3057
- // Calculs analogues continus (sans sauts brusques)
3058
- const suspicionDensity = highScoreCount / deviceCount;
3059
- const ipDeviceRatio = ipCount / deviceCount;
3060
-
3061
- // Ratio User-Agent / Device : détecte la rotation/spoofing de navigateurs sur une même empreinte matérielle
3062
- const uaDeviceRatio = Math.max(1, uaCount) / deviceCount;
3063
- const uaMultiplier = 0.6 + (0.4 * Math.min(2.5, uaDeviceRatio));
3064
-
3065
- // Base score continu basé sur le volume de menaces
3066
- const baseScore = 100 * (1 - Math.exp(-0.15 * highScoreCount));
3067
-
3068
- // Multiplicateurs continus
3069
- const densityMultiplier = 0.4 + (1.6 * suspicionDensity); // Favorise les densités de suspicion élevées
3070
- const distributionMultiplier = 0.5 + (1.0 * ipDeviceRatio); // NAT (faible ratio IP/Device) vs Proxy distribué (fort ratio)
3306
+ // 1. Estimation Bayésienne de densité (évite les sur-réactions sur 1 ou 2 appareils)
3307
+ // Prior: alpha=0.5, beta=2.0 (a priori réseau sain)
3308
+ const bayesianDensity = (highScoreCount + 0.5) / (deviceCount + 2.5);
3309
+
3310
+ // 2. Ratio IP / Terminal (distingue un proxy distribué d'un gros NAT / CGNAT)
3311
+ // Sur un proxy distribué, chaque terminal utilise une IP différente (ratio >= 1.0)
3312
+ // Sur un CGNAT, des dizaines de terminaux partagent peu d'IPs (ratio << 1.0)
3313
+ const ipDispersion = Math.min(2.0, ipCount / deviceCount);
3314
+ const ipMultiplier = 0.6 + 0.4 * Math.tanh(ipDispersion);
3315
+
3316
+ // 3. Volatilité des User-Agents (rotation de navigateurs sur matériel identique)
3317
+ const uaDispersion = Math.min(3.0, Math.max(1, uaCount) / deviceCount);
3318
+ const uaMultiplier = 0.7 + 0.3 * Math.tanh(uaDispersion - 1.0);
3319
+
3320
+ // 4. Intensité brute continue de la menace
3321
+ const rawThreatIntensity = highScoreCount * bayesianDensity * ipMultiplier * uaMultiplier;
3322
+
3323
+ // 5. Composante 1 : Score ambiant plafonné en zone Medium (asymptote au seuil medium)
3324
+ const mediumThreshold = (securityConfig?.thresholds?.medium !== undefined)
3325
+ ? Number(securityConfig.thresholds.medium)
3326
+ : 45.0;
3327
+ const ambientAsymptote = mediumThreshold;
3328
+ const maxProximityBoost = Math.max(0.0, 100.0 - ambientAsymptote);
3329
+ const scaleFactor = 10.0;
3330
+ const ambientScore = ambientAsymptote * Math.tanh(rawThreatIntensity / scaleFactor);
3331
+
3332
+ // 6. Composante 2 : Boost de proximité micro-réseau avec des attaquants récents (le reste va jusqu'à 100)
3333
+ let proximityBoost = 0.0;
3334
+ const attackerIps = subnetData.attackerIps || [];
3335
+ const clientIp = context.clientIp;
3336
+
3337
+ if (attackerIps.length > 0 && clientIp) {
3338
+ let maxCommonPrefix = 0;
3339
+ const isClientV4 = isIPv4(clientIp);
3340
+ const isClientV6 = isIPv6(clientIp);
3341
+
3342
+ for (const att of attackerIps) {
3343
+ if (att.ip && (now - (att.lastSeen || now)) <= 30 * 60 * 1000) {
3344
+ const prefixLen = getIpCommonPrefixLength(clientIp, att.ip);
3345
+ if (prefixLen > maxCommonPrefix) {
3346
+ maxCommonPrefix = prefixLen;
3347
+ }
3348
+ }
3349
+ }
3071
3350
 
3072
- // Amortissement pour éviter les faux positifs sur les réseaux NAT résidentiels (petits nombres d'appareils suspects)
3073
- let dampening = 1.0;
3074
- if (highScoreCount < 3) {
3075
- dampening = highScoreCount / 3.0; // 0.33 pour 1 appareil, 0.66 pour 2 appareils
3351
+ if (isClientV4) {
3352
+ if (maxCommonPrefix >= 32) {
3353
+ proximityBoost = maxProximityBoost; // Même adresse IP exacte
3354
+ } else if (maxCommonPrefix >= 30) {
3355
+ proximityBoost = maxProximityBoost * (45.0 / 55.0); // Même /30 (écart <= 3 adresses)
3356
+ } else if (maxCommonPrefix >= 28) {
3357
+ proximityBoost = maxProximityBoost * (30.0 / 55.0); // Même /28 (bloc de 16 adresses)
3358
+ } else if (maxCommonPrefix >= 26) {
3359
+ proximityBoost = maxProximityBoost * (15.0 / 55.0); // Même /26 (bloc de 64 adresses)
3360
+ }
3361
+ } else if (isClientV6) {
3362
+ if (maxCommonPrefix >= 128) {
3363
+ proximityBoost = maxProximityBoost; // Même IPv6 exacte
3364
+ } else if (maxCommonPrefix >= 120) {
3365
+ proximityBoost = maxProximityBoost * (45.0 / 55.0); // Même /120
3366
+ } else if (maxCommonPrefix >= 112) {
3367
+ proximityBoost = maxProximityBoost * (30.0 / 55.0); // Même /112
3368
+ } else if (maxCommonPrefix >= 96) {
3369
+ proximityBoost = maxProximityBoost * (15.0 / 55.0); // Même /96
3370
+ }
3371
+ }
3076
3372
  }
3077
3373
 
3078
- const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * uaMultiplier * dampening * 10) / 10);
3374
+ const finalScore = Math.min(100.0, Math.round((ambientScore + proximityBoost) * 10) / 10);
3375
+
3079
3376
  return { subnetScore: finalScore };
3080
3377
  }
3081
3378
 
@@ -3565,6 +3862,46 @@ async function getBehavioralIndicators(context, deviceData) {
3565
3862
  return { historyScore, rotationScore };
3566
3863
  }
3567
3864
 
3865
+ function getVirtualizationAnomalyScore(context) {
3866
+ const clientFp = context.headers?.['x-device-fingerprint'];
3867
+ if (!clientFp) return 0.0;
3868
+
3869
+ const fpMap = {};
3870
+ clientFp.split('|').forEach(part => {
3871
+ const pair = part.split(':');
3872
+ if (pair.length === 2) {
3873
+ fpMap[pair[0]] = pair[1];
3874
+ }
3875
+ });
3876
+
3877
+ let score = 0.0;
3878
+ const clientGpuHash = fpMap['gpu'];
3879
+ if (clientGpuHash) {
3880
+ const virtualGpus = [
3881
+ "Google SwiftShader", "SwiftShader",
3882
+ "Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
3883
+ "Microsoft Basic Render Driver", "HeadlessChrome",
3884
+ "Intel(R) HD Graphics"
3885
+ ];
3886
+ const virtualGpuHashes = new Set(virtualGpus.map(gpu => cyrb53(gpu).toString()));
3887
+ if (virtualGpuHashes.has(clientGpuHash)) {
3888
+ score += 75.0;
3889
+ }
3890
+ }
3891
+
3892
+ const clientScreenHash = fpMap['scr'];
3893
+ if (clientScreenHash) {
3894
+ const headlessResolutions = ["800x600_24", "1024x768_24"];
3895
+ const headlessHashes = new Set(headlessResolutions.map(res => cyrb53(res).toString()));
3896
+ if (headlessHashes.has(clientScreenHash)) {
3897
+ score += 25.0;
3898
+ }
3899
+ }
3900
+
3901
+ return Math.min(100.0, score);
3902
+ }
3903
+
3904
+
3568
3905
  /**
3569
3906
  * Returns a vector of raw (unweighted) suspicion scores.
3570
3907
  * @param {object} context - The request context object.
@@ -3611,9 +3948,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
3611
3948
  _ // store.set result
3612
3949
  ] = await Promise.all([
3613
3950
  getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
3614
- getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3951
+ getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3615
3952
  getTlsSpoofingScore(context),
3616
- getSubnetScore(context, deviceId),
3953
+ getSubnetScore(context, deviceId, securityConfig),
3617
3954
  getIpReputationScore(clientIp),
3618
3955
  getBotnetClusterScore(context, stableFpHash),
3619
3956
  store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
@@ -3621,12 +3958,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
3621
3958
 
3622
3959
  // Synchronous calculations
3623
3960
  const { headerAnomalyScore } = getHeaderAnomalies(context);
3624
- let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
3625
-
3626
- // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
3627
- if (consistencyScore < 0.7) { // Seuil de rupture
3628
- inconsistencyScore = 100;
3629
- }
3961
+ const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
3962
+ const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
3630
3963
 
3631
3964
  const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3632
3965
 
@@ -3652,6 +3985,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3652
3985
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3653
3986
  const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
3654
3987
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3988
+ const virtualizationScore = getVirtualizationAnomalyScore(context);
3655
3989
 
3656
3990
  // Save the updated device state to the store
3657
3991
  // 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.
@@ -3663,7 +3997,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3663
3997
  deviceData.ips = new Set(deviceData.ips);
3664
3998
  }
3665
3999
  // Le vecteur de suspicion est maintenant complet.
3666
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore };
4000
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
3667
4001
  };
3668
4002
 
3669
4003
  // A residential user can change networks (home, 4G, public wifi).
@@ -3879,6 +4213,8 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3879
4213
  const clientInitConfig = {
3880
4214
  mouse: true,
3881
4215
  keystrokes: true,
4216
+ wasmPath: securityConfig?.wasmPath || '/fp.wasm',
4217
+ workerPath: securityConfig?.workerPath || '/pow.worker.js',
3882
4218
  trapUrls: trapUrls // On passe directement le tableau d'URL
3883
4219
  };
3884
4220
 
@@ -4047,14 +4383,40 @@ function parseGraphQLQuery(body) {
4047
4383
  * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
4048
4384
  * @returns {Promise<{threatIntelScore: number}>}
4049
4385
  */
4050
- async function getThreatIntelScore(zkpY) {
4051
- if (!zkpY) return { threatIntelScore: 0 };
4386
+ async function getThreatIntelScore(context, zkpY) {
4387
+ let score = 0;
4388
+ if (zkpY) {
4389
+ const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4390
+ if (isBanned) {
4391
+ score = 100;
4392
+ }
4393
+ }
4394
+
4395
+ const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
4396
+ const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4052
4397
 
4053
- const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4054
- if (isBanned) {
4055
- return { threatIntelScore: 100 };
4398
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
4399
+ if (behaviorHeader) {
4400
+ try {
4401
+ const metrics = JSON.parse(behaviorHeader);
4402
+ if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
4403
+ const appLatency = context.requestTimestamp - metrics.clientTimestamp;
4404
+ if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
4405
+ const clientToProxyDelta = appLatency - tcpRtt;
4406
+ if (tcpRtt < 35 && clientToProxyDelta > 150) {
4407
+ score = Math.max(score, 85);
4408
+ }
4409
+ } else {
4410
+ if (appLatency > 350) {
4411
+ score = Math.max(score, 40);
4412
+ }
4413
+ }
4414
+ }
4415
+ } catch (e) {
4416
+ // Ignorer silencieusement
4417
+ }
4056
4418
  }
4057
- return { threatIntelScore: 0 };
4419
+ return { threatIntelScore: score };
4058
4420
  }
4059
4421
 
4060
4422
  export class FingerprintEngine {
@@ -4116,6 +4478,10 @@ export class FingerprintEngine {
4116
4478
  this._validateConfig(finalConfig); // Validate the configuration
4117
4479
  this.verbose = finalConfig.verbose || false;
4118
4480
  this.dryRun = finalConfig.dryRun || false;
4481
+
4482
+ // Preload solver file asynchronously to liberate event loop during run
4483
+ preloadPowSolverCode().catch(() => {});
4484
+
4119
4485
  if (finalConfig.reset) {
4120
4486
  this.resetStore().catch(err => {
4121
4487
  console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
@@ -5136,9 +5502,12 @@ export class FingerprintEngine {
5136
5502
  // 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
5137
5503
  const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
5138
5504
  const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
5139
- const mustReChallenge = isSuspiciousHigh && !isBlocked && hasValidTicket;
5505
+ // Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
5506
+ // on fait confiance au ticket valide tant qu'il n'a pas expiré.
5507
+ const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
5508
+ const mustReChallenge = (suspicionVector.honeypotScore >= (thresholds.medium ?? 45)) || (maxIndicatorsCount >= 1);
5140
5509
 
5141
- if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
5510
+ if ((isSuspicious && !hasValidTicket) || mustReChallenge) {
5142
5511
  if (mustReChallenge) {
5143
5512
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5144
5513
  }
@@ -5185,7 +5554,7 @@ export class FingerprintEngine {
5185
5554
  let usefulWorkDispatched = false;
5186
5555
  let challengePayload = null;
5187
5556
 
5188
- if (isSuspicious && shouldUseUsefulWork) {
5557
+ if ((isSuspicious || mustReChallenge) && shouldUseUsefulWork) {
5189
5558
  this._log('Issuing a useful work challenge', { finalScore });
5190
5559
 
5191
5560
  try {
@@ -5217,7 +5586,7 @@ export class FingerprintEngine {
5217
5586
  }
5218
5587
  }
5219
5588
 
5220
- if (isSuspicious && usefulWorkDispatched) {
5589
+ if ((isSuspicious || mustReChallenge) && usefulWorkDispatched) {
5221
5590
  if (isApi) {
5222
5591
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
5223
5592
  } else {
@@ -5233,7 +5602,7 @@ export class FingerprintEngine {
5233
5602
  </script></body></html>`;
5234
5603
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: html };
5235
5604
  }
5236
- } else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
5605
+ } else if (isSuspicious || mustReChallenge) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
5237
5606
  const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
5238
5607
  if (this.dryRun) {
5239
5608
  this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
@@ -5489,7 +5858,7 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
5489
5858
  if (activePeers.length === 0) return null;
5490
5859
 
5491
5860
  // Select a random peer
5492
- const randomIndex = Math.floor(Math.random() * activePeers.length);
5861
+ const randomIndex = crypto.randomInt(0, activePeers.length);
5493
5862
  return activePeers[randomIndex];
5494
5863
  }
5495
5864
 
@@ -5597,6 +5966,15 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5597
5966
  case 'register':
5598
5967
  expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
5599
5968
  break;
5969
+ case 'find_peer':
5970
+ expectedMsg = `${clientSecret}:find_peer:${nodeId}`;
5971
+ break;
5972
+ case 'webrtc_signal':
5973
+ expectedMsg = `${clientSecret}:webrtc_signal:${nodeId}:${params.target_peer_id || ''}:${params.signal_type || ''}:${params.signal_data || ''}`;
5974
+ break;
5975
+ case 'poll_signals':
5976
+ expectedMsg = `${clientSecret}:poll_signals:${nodeId}`;
5977
+ break;
5600
5978
  case 'request_peer_block':
5601
5979
  expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
5602
5980
  break;
@@ -5625,14 +6003,49 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5625
6003
  await registerCooperativeNode(clientIp, nodeId, seed);
5626
6004
  return { status: 'registered' };
5627
6005
 
6006
+ case 'find_peer': {
6007
+ const peer = await findPeerInSubnet(clientIp, nodeId);
6008
+ if (!peer) {
6009
+ return { status: 'no_peers' };
6010
+ }
6011
+ return { status: 'peer_found', peer_id: peer.nodeId };
6012
+ }
6013
+
6014
+ case 'webrtc_signal': {
6015
+ const targetPeerId = params.target_peer_id || '';
6016
+ const signalType = params.signal_type || '';
6017
+ const signalData = params.signal_data || '';
6018
+ if (!targetPeerId || !signalType) {
6019
+ return { error: 'Invalid parameters' };
6020
+ }
6021
+ const signalQueueKey = `coop-webrtc:signals:${targetPeerId}`;
6022
+ const signals = (await store.get(signalQueueKey)) || [];
6023
+ signals.push({
6024
+ from_peer_id: nodeId,
6025
+ signal_type: signalType,
6026
+ signal_data: signalData
6027
+ });
6028
+ await store.set(signalQueueKey, signals, 30);
6029
+ return { status: 'signal_queued' };
6030
+ }
6031
+
6032
+ case 'poll_signals': {
6033
+ const pollSignalKey = `coop-webrtc:signals:${nodeId}`;
6034
+ const signals = (await store.get(pollSignalKey)) || [];
6035
+ if (signals.length > 0) {
6036
+ await store.delete(pollSignalKey);
6037
+ }
6038
+ return { status: 'ok', signals };
6039
+ }
6040
+
5628
6041
  case 'request_peer_block':
6042
+ {
5629
6043
  const peerId = params.peer_id || '';
5630
6044
  const blockIdx = parseInt(params.block_idx || '0', 10);
5631
6045
  const requestId = params.req_id || '';
5632
6046
  if (!peerId || !requestId) {
5633
- return { error: 'Invalid parameters' };
6047
+ return {error: 'Invalid parameters'};
5634
6048
  }
5635
-
5636
6049
  const queueKey = `coop-mailbox:queue:${peerId}`;
5637
6050
  const requests = (await store.get(queueKey)) || [];
5638
6051
  requests.push({
@@ -5641,7 +6054,8 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5641
6054
  block_idx: blockIdx
5642
6055
  });
5643
6056
  await store.set(queueKey, requests, 30);
5644
- return { status: 'queued' };
6057
+ return {status: 'queued'};
6058
+ }
5645
6059
 
5646
6060
  case 'poll_requests':
5647
6061
  const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
@@ -6481,6 +6895,7 @@ export const __internal = {
6481
6895
  calculateTarget,
6482
6896
  determineOptimalTicketTtl,
6483
6897
  runBackgroundTtlOptimization,
6898
+ calculateAnalogInconsistencyScore,
6484
6899
  getRequestPatternScore, // Expose for testing
6485
6900
  getBehaviorScore, // Expose for testing
6486
6901
  getCrossLayerInconsistency, // Expose for testing
@@ -6500,6 +6915,7 @@ export const __internal = {
6500
6915
  generateCombinedPoWChallengePage,
6501
6916
  problemManager, // Re-export the problemManager promise
6502
6917
  getIpSubnet, // Expose for testing
6918
+ getIpCommonPrefixLength, // Expose for testing
6503
6919
  updateSubnetMetrics, // Expose for testing
6504
6920
  getSubnetScore, // Expose for testing
6505
6921
  getIpReputationScore, // Expose for testing
@@ -6646,7 +7062,7 @@ export function sanitizeTrafficData(trafficData) {
6646
7062
  const minDataPoints = 200; // Seuil par défaut
6647
7063
  const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
6648
7064
  if (passedLogs.length > maxPassedAllowed) {
6649
- const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
7065
+ const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
6650
7066
  return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
6651
7067
  }
6652
7068
  return [...suspiciousLogs, ...passedLogs];