@anonympins/fingerprint 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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...';
@@ -3019,6 +3155,25 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3019
3155
 
3020
3156
  await store.set(key, subnetData, 86400); // 24-hour TTL
3021
3157
  }
3158
+ /**
3159
+ * Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
3160
+ * @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
3161
+ * @param {number} [inflectionPoint=0.72] Point d'inflexion où la suspicion accélère.
3162
+ * @param {number} [steepness=12] Raideur de la transition sigmoïdale.
3163
+ * @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
3164
+ */
3165
+ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPoint = 0.72, steepness = 12) {
3166
+ const s = Math.max(0.0, Math.min(1.0, consistencyScore));
3167
+ if (s >= 0.98) return 0.0;
3168
+
3169
+ const asymptote = 99.9;
3170
+ const raw = 1.0 / (1.0 + Math.exp(steepness * (s - inflectionPoint)));
3171
+ const minVal = 1.0 / (1.0 + Math.exp(steepness * (1.0 - inflectionPoint)));
3172
+ const maxVal = 1.0 / (1.0 + Math.exp(steepness * (0.0 - inflectionPoint)));
3173
+ const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
3174
+
3175
+ return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
3176
+ }
3022
3177
 
3023
3178
  /**
3024
3179
  * Calculates a suspicion score based on the historical activity of the IP subnet.
@@ -3054,28 +3209,29 @@ async function getSubnetScore(context) {
3054
3209
  return { subnetScore: 0.0 };
3055
3210
  }
3056
3211
 
3057
- // Calculs analogues continus (sans sauts brusques)
3058
- const suspicionDensity = highScoreCount / deviceCount;
3059
- const ipDeviceRatio = ipCount / deviceCount;
3212
+ // 1. Estimation Bayésienne de densité (évite les sur-réactions sur 1 ou 2 appareils)
3213
+ // Prior: alpha=0.5, beta=2.0 (a priori réseau sain)
3214
+ const bayesianDensity = (highScoreCount + 0.5) / (deviceCount + 2.5);
3060
3215
 
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));
3216
+ // 2. Ratio IP / Terminal (distingue un proxy distribué d'un gros NAT / CGNAT)
3217
+ // Sur un proxy distribué, chaque terminal utilise une IP différente (ratio >= 1.0)
3218
+ // Sur un CGNAT, des dizaines de terminaux partagent peu d'IPs (ratio << 1.0)
3219
+ const ipDispersion = Math.min(2.0, ipCount / deviceCount);
3220
+ const ipMultiplier = 0.6 + 0.4 * Math.tanh(ipDispersion);
3064
3221
 
3065
- // Base score continu basé sur le volume de menaces
3066
- const baseScore = 100 * (1 - Math.exp(-0.15 * highScoreCount));
3222
+ // 3. Volatilité des User-Agents (rotation de navigateurs sur matériel identique)
3223
+ const uaDispersion = Math.min(3.0, Math.max(1, uaCount) / deviceCount);
3224
+ const uaMultiplier = 0.7 + 0.3 * Math.tanh(uaDispersion - 1.0);
3067
3225
 
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)
3226
+ // 4. Intensité brute continue de la menace (sans discontinuité)
3227
+ const rawThreatIntensity = highScoreCount * bayesianDensity * ipMultiplier * uaMultiplier;
3071
3228
 
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
3076
- }
3229
+ // 5. Saturation asymptotique continue (Asymptote à 99.9)
3230
+ // Même si rawThreatIntensity tend vers l'infini, tanh converge vers 1.0 sans jamais dépasser 85.0
3231
+ const ASYMPTOTE = 99.9;
3232
+ const scaleFactor = 4.0; // Sensibilité de la transition
3233
+ const finalScore = Math.round(ASYMPTOTE * Math.tanh(rawThreatIntensity / scaleFactor) * 10) / 10;
3077
3234
 
3078
- const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * uaMultiplier * dampening * 10) / 10);
3079
3235
  return { subnetScore: finalScore };
3080
3236
  }
3081
3237
 
@@ -3565,6 +3721,46 @@ async function getBehavioralIndicators(context, deviceData) {
3565
3721
  return { historyScore, rotationScore };
3566
3722
  }
3567
3723
 
3724
+ function getVirtualizationAnomalyScore(context) {
3725
+ const clientFp = context.headers?.['x-device-fingerprint'];
3726
+ if (!clientFp) return 0.0;
3727
+
3728
+ const fpMap = {};
3729
+ clientFp.split('|').forEach(part => {
3730
+ const pair = part.split(':');
3731
+ if (pair.length === 2) {
3732
+ fpMap[pair[0]] = pair[1];
3733
+ }
3734
+ });
3735
+
3736
+ let score = 0.0;
3737
+ const clientGpuHash = fpMap['gpu'];
3738
+ if (clientGpuHash) {
3739
+ const virtualGpus = [
3740
+ "Google SwiftShader", "SwiftShader",
3741
+ "Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
3742
+ "Microsoft Basic Render Driver", "HeadlessChrome",
3743
+ "Intel(R) HD Graphics"
3744
+ ];
3745
+ const virtualGpuHashes = new Set(virtualGpus.map(gpu => cyrb53(gpu).toString()));
3746
+ if (virtualGpuHashes.has(clientGpuHash)) {
3747
+ score += 75.0;
3748
+ }
3749
+ }
3750
+
3751
+ const clientScreenHash = fpMap['scr'];
3752
+ if (clientScreenHash) {
3753
+ const headlessResolutions = ["800x600_24", "1024x768_24"];
3754
+ const headlessHashes = new Set(headlessResolutions.map(res => cyrb53(res).toString()));
3755
+ if (headlessHashes.has(clientScreenHash)) {
3756
+ score += 25.0;
3757
+ }
3758
+ }
3759
+
3760
+ return Math.min(100.0, score);
3761
+ }
3762
+
3763
+
3568
3764
  /**
3569
3765
  * Returns a vector of raw (unweighted) suspicion scores.
3570
3766
  * @param {object} context - The request context object.
@@ -3611,7 +3807,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3611
3807
  _ // store.set result
3612
3808
  ] = await Promise.all([
3613
3809
  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é
3810
+ getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3615
3811
  getTlsSpoofingScore(context),
3616
3812
  getSubnetScore(context, deviceId),
3617
3813
  getIpReputationScore(clientIp),
@@ -3621,12 +3817,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
3621
3817
 
3622
3818
  // Synchronous calculations
3623
3819
  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
- }
3820
+ const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
3821
+ const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
3630
3822
 
3631
3823
  const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3632
3824
 
@@ -3652,6 +3844,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3652
3844
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3653
3845
  const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
3654
3846
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3847
+ const virtualizationScore = getVirtualizationAnomalyScore(context);
3655
3848
 
3656
3849
  // Save the updated device state to the store
3657
3850
  // 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 +3856,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3663
3856
  deviceData.ips = new Set(deviceData.ips);
3664
3857
  }
3665
3858
  // 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 };
3859
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
3667
3860
  };
3668
3861
 
3669
3862
  // A residential user can change networks (home, 4G, public wifi).
@@ -3879,6 +4072,8 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3879
4072
  const clientInitConfig = {
3880
4073
  mouse: true,
3881
4074
  keystrokes: true,
4075
+ wasmPath: securityConfig?.wasmPath || '/fp.wasm',
4076
+ workerPath: securityConfig?.workerPath || '/pow.worker.js',
3882
4077
  trapUrls: trapUrls // On passe directement le tableau d'URL
3883
4078
  };
3884
4079
 
@@ -4047,14 +4242,40 @@ function parseGraphQLQuery(body) {
4047
4242
  * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
4048
4243
  * @returns {Promise<{threatIntelScore: number}>}
4049
4244
  */
4050
- async function getThreatIntelScore(zkpY) {
4051
- if (!zkpY) return { threatIntelScore: 0 };
4245
+ async function getThreatIntelScore(context, zkpY) {
4246
+ let score = 0;
4247
+ if (zkpY) {
4248
+ const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4249
+ if (isBanned) {
4250
+ score = 100;
4251
+ }
4252
+ }
4253
+
4254
+ const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
4255
+ const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4052
4256
 
4053
- const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4054
- if (isBanned) {
4055
- return { threatIntelScore: 100 };
4257
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
4258
+ if (behaviorHeader) {
4259
+ try {
4260
+ const metrics = JSON.parse(behaviorHeader);
4261
+ if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
4262
+ const appLatency = context.requestTimestamp - metrics.clientTimestamp;
4263
+ if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
4264
+ const clientToProxyDelta = appLatency - tcpRtt;
4265
+ if (tcpRtt < 35 && clientToProxyDelta > 150) {
4266
+ score = Math.max(score, 85);
4267
+ }
4268
+ } else {
4269
+ if (appLatency > 350) {
4270
+ score = Math.max(score, 40);
4271
+ }
4272
+ }
4273
+ }
4274
+ } catch (e) {
4275
+ // Ignorer silencieusement
4276
+ }
4056
4277
  }
4057
- return { threatIntelScore: 0 };
4278
+ return { threatIntelScore: score };
4058
4279
  }
4059
4280
 
4060
4281
  export class FingerprintEngine {
@@ -4116,6 +4337,10 @@ export class FingerprintEngine {
4116
4337
  this._validateConfig(finalConfig); // Validate the configuration
4117
4338
  this.verbose = finalConfig.verbose || false;
4118
4339
  this.dryRun = finalConfig.dryRun || false;
4340
+
4341
+ // Preload solver file asynchronously to liberate event loop during run
4342
+ preloadPowSolverCode().catch(() => {});
4343
+
4119
4344
  if (finalConfig.reset) {
4120
4345
  this.resetStore().catch(err => {
4121
4346
  console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
@@ -5136,9 +5361,12 @@ export class FingerprintEngine {
5136
5361
  // 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
5137
5362
  const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
5138
5363
  const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
5139
- const mustReChallenge = isSuspiciousHigh && !isBlocked && hasValidTicket;
5364
+ // Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
5365
+ // on fait confiance au ticket valide tant qu'il n'a pas expiré.
5366
+ const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
5367
+ const mustReChallenge = (suspicionVector.honeypotScore >= (thresholds.medium ?? 45)) || (maxIndicatorsCount >= 1);
5140
5368
 
5141
- if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
5369
+ if ((isSuspicious && !hasValidTicket) || mustReChallenge) {
5142
5370
  if (mustReChallenge) {
5143
5371
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5144
5372
  }
@@ -5185,7 +5413,7 @@ export class FingerprintEngine {
5185
5413
  let usefulWorkDispatched = false;
5186
5414
  let challengePayload = null;
5187
5415
 
5188
- if (isSuspicious && shouldUseUsefulWork) {
5416
+ if ((isSuspicious || mustReChallenge) && shouldUseUsefulWork) {
5189
5417
  this._log('Issuing a useful work challenge', { finalScore });
5190
5418
 
5191
5419
  try {
@@ -5217,7 +5445,7 @@ export class FingerprintEngine {
5217
5445
  }
5218
5446
  }
5219
5447
 
5220
- if (isSuspicious && usefulWorkDispatched) {
5448
+ if ((isSuspicious || mustReChallenge) && usefulWorkDispatched) {
5221
5449
  if (isApi) {
5222
5450
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
5223
5451
  } else {
@@ -5233,7 +5461,7 @@ export class FingerprintEngine {
5233
5461
  </script></body></html>`;
5234
5462
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: html };
5235
5463
  }
5236
- } else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
5464
+ } else if (isSuspicious || mustReChallenge) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
5237
5465
  const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
5238
5466
  if (this.dryRun) {
5239
5467
  this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
@@ -5489,7 +5717,7 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
5489
5717
  if (activePeers.length === 0) return null;
5490
5718
 
5491
5719
  // Select a random peer
5492
- const randomIndex = Math.floor(Math.random() * activePeers.length);
5720
+ const randomIndex = crypto.randomInt(0, activePeers.length);
5493
5721
  return activePeers[randomIndex];
5494
5722
  }
5495
5723
 
@@ -5597,6 +5825,15 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5597
5825
  case 'register':
5598
5826
  expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
5599
5827
  break;
5828
+ case 'find_peer':
5829
+ expectedMsg = `${clientSecret}:find_peer:${nodeId}`;
5830
+ break;
5831
+ case 'webrtc_signal':
5832
+ expectedMsg = `${clientSecret}:webrtc_signal:${nodeId}:${params.target_peer_id || ''}:${params.signal_type || ''}:${params.signal_data || ''}`;
5833
+ break;
5834
+ case 'poll_signals':
5835
+ expectedMsg = `${clientSecret}:poll_signals:${nodeId}`;
5836
+ break;
5600
5837
  case 'request_peer_block':
5601
5838
  expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
5602
5839
  break;
@@ -5625,14 +5862,49 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5625
5862
  await registerCooperativeNode(clientIp, nodeId, seed);
5626
5863
  return { status: 'registered' };
5627
5864
 
5865
+ case 'find_peer': {
5866
+ const peer = await findPeerInSubnet(clientIp, nodeId);
5867
+ if (!peer) {
5868
+ return { status: 'no_peers' };
5869
+ }
5870
+ return { status: 'peer_found', peer_id: peer.nodeId };
5871
+ }
5872
+
5873
+ case 'webrtc_signal': {
5874
+ const targetPeerId = params.target_peer_id || '';
5875
+ const signalType = params.signal_type || '';
5876
+ const signalData = params.signal_data || '';
5877
+ if (!targetPeerId || !signalType) {
5878
+ return { error: 'Invalid parameters' };
5879
+ }
5880
+ const signalQueueKey = `coop-webrtc:signals:${targetPeerId}`;
5881
+ const signals = (await store.get(signalQueueKey)) || [];
5882
+ signals.push({
5883
+ from_peer_id: nodeId,
5884
+ signal_type: signalType,
5885
+ signal_data: signalData
5886
+ });
5887
+ await store.set(signalQueueKey, signals, 30);
5888
+ return { status: 'signal_queued' };
5889
+ }
5890
+
5891
+ case 'poll_signals': {
5892
+ const pollSignalKey = `coop-webrtc:signals:${nodeId}`;
5893
+ const signals = (await store.get(pollSignalKey)) || [];
5894
+ if (signals.length > 0) {
5895
+ await store.delete(pollSignalKey);
5896
+ }
5897
+ return { status: 'ok', signals };
5898
+ }
5899
+
5628
5900
  case 'request_peer_block':
5901
+ {
5629
5902
  const peerId = params.peer_id || '';
5630
5903
  const blockIdx = parseInt(params.block_idx || '0', 10);
5631
5904
  const requestId = params.req_id || '';
5632
5905
  if (!peerId || !requestId) {
5633
- return { error: 'Invalid parameters' };
5906
+ return {error: 'Invalid parameters'};
5634
5907
  }
5635
-
5636
5908
  const queueKey = `coop-mailbox:queue:${peerId}`;
5637
5909
  const requests = (await store.get(queueKey)) || [];
5638
5910
  requests.push({
@@ -5641,7 +5913,8 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5641
5913
  block_idx: blockIdx
5642
5914
  });
5643
5915
  await store.set(queueKey, requests, 30);
5644
- return { status: 'queued' };
5916
+ return {status: 'queued'};
5917
+ }
5645
5918
 
5646
5919
  case 'poll_requests':
5647
5920
  const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
@@ -6481,6 +6754,7 @@ export const __internal = {
6481
6754
  calculateTarget,
6482
6755
  determineOptimalTicketTtl,
6483
6756
  runBackgroundTtlOptimization,
6757
+ calculateAnalogInconsistencyScore,
6484
6758
  getRequestPatternScore, // Expose for testing
6485
6759
  getBehaviorScore, // Expose for testing
6486
6760
  getCrossLayerInconsistency, // Expose for testing
@@ -6646,7 +6920,7 @@ export function sanitizeTrafficData(trafficData) {
6646
6920
  const minDataPoints = 200; // Seuil par défaut
6647
6921
  const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
6648
6922
  if (passedLogs.length > maxPassedAllowed) {
6649
- const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
6923
+ const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
6650
6924
  return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
6651
6925
  }
6652
6926
  return [...suspiciousLogs, ...passedLogs];