@anonympins/fingerprint 0.6.6 → 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.
@@ -3028,8 +3183,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
3028
3183
  async function getSubnetScore(context) {
3029
3184
  const subnet = getIpSubnet(context.clientIp);
3030
3185
  if (!subnet) return { subnetScore: 0 };
3186
+ const key = `subnet:${subnet}`;
3031
3187
 
3032
- const subnetData = await store.get(`subnet:${subnet}`);
3188
+ const subnetData = await store.get(key);
3033
3189
  if (!subnetData) return { subnetScore: 0 };
3034
3190
 
3035
3191
  const now = Date.now();
@@ -3053,18 +3209,29 @@ async function getSubnetScore(context) {
3053
3209
  return { subnetScore: 0.0 };
3054
3210
  }
3055
3211
 
3056
- // Calculs analogues continus (sans sauts brusques)
3057
- const suspicionDensity = highScoreCount / deviceCount;
3058
- 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);
3215
+
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);
3059
3221
 
3060
- // Base score continu basé sur le volume de menaces
3061
- 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);
3062
3225
 
3063
- // Multiplicateurs continus
3064
- const densityMultiplier = 0.4 + (1.6 * suspicionDensity); // Favorise les densités de suspicion élevées
3065
- 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;
3228
+
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;
3066
3234
 
3067
- const finalScore = Math.min(100, Math.round(baseScore * densityMultiplier * distributionMultiplier * 10) / 10);
3068
3235
  return { subnetScore: finalScore };
3069
3236
  }
3070
3237
 
@@ -3554,6 +3721,46 @@ async function getBehavioralIndicators(context, deviceData) {
3554
3721
  return { historyScore, rotationScore };
3555
3722
  }
3556
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
+
3557
3764
  /**
3558
3765
  * Returns a vector of raw (unweighted) suspicion scores.
3559
3766
  * @param {object} context - The request context object.
@@ -3600,7 +3807,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3600
3807
  _ // store.set result
3601
3808
  ] = await Promise.all([
3602
3809
  getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
3603
- getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3810
+ getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3604
3811
  getTlsSpoofingScore(context),
3605
3812
  getSubnetScore(context, deviceId),
3606
3813
  getIpReputationScore(clientIp),
@@ -3610,12 +3817,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
3610
3817
 
3611
3818
  // Synchronous calculations
3612
3819
  const { headerAnomalyScore } = getHeaderAnomalies(context);
3613
- let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
3614
-
3615
- // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
3616
- if (consistencyScore < 0.7) { // Seuil de rupture
3617
- inconsistencyScore = 100;
3618
- }
3820
+ const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
3821
+ const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
3619
3822
 
3620
3823
  const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3621
3824
 
@@ -3641,6 +3844,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3641
3844
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3642
3845
  const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
3643
3846
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3847
+ const virtualizationScore = getVirtualizationAnomalyScore(context);
3644
3848
 
3645
3849
  // Save the updated device state to the store
3646
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.
@@ -3652,7 +3856,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3652
3856
  deviceData.ips = new Set(deviceData.ips);
3653
3857
  }
3654
3858
  // Le vecteur de suspicion est maintenant complet.
3655
- 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 };
3656
3860
  };
3657
3861
 
3658
3862
  // A residential user can change networks (home, 4G, public wifi).
@@ -3868,6 +4072,8 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3868
4072
  const clientInitConfig = {
3869
4073
  mouse: true,
3870
4074
  keystrokes: true,
4075
+ wasmPath: securityConfig?.wasmPath || '/fp.wasm',
4076
+ workerPath: securityConfig?.workerPath || '/pow.worker.js',
3871
4077
  trapUrls: trapUrls // On passe directement le tableau d'URL
3872
4078
  };
3873
4079
 
@@ -3891,7 +4097,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3891
4097
  await new Promise(r => setTimeout(r, 10)); // Yield to update UI
3892
4098
  let memSolution = 0;
3893
4099
  try {
3894
- const memSeed = nonce + ":" + clientSecret;
4100
+ const memSeed = ":" + nonce + ":" + clientSecret;
3895
4101
  memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
3896
4102
  } catch(e) {
3897
4103
  document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
@@ -4036,14 +4242,40 @@ function parseGraphQLQuery(body) {
4036
4242
  * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
4037
4243
  * @returns {Promise<{threatIntelScore: number}>}
4038
4244
  */
4039
- async function getThreatIntelScore(zkpY) {
4040
- 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;
4041
4256
 
4042
- const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
4043
- if (isBanned) {
4044
- 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
+ }
4045
4277
  }
4046
- return { threatIntelScore: 0 };
4278
+ return { threatIntelScore: score };
4047
4279
  }
4048
4280
 
4049
4281
  export class FingerprintEngine {
@@ -4105,6 +4337,10 @@ export class FingerprintEngine {
4105
4337
  this._validateConfig(finalConfig); // Validate the configuration
4106
4338
  this.verbose = finalConfig.verbose || false;
4107
4339
  this.dryRun = finalConfig.dryRun || false;
4340
+
4341
+ // Preload solver file asynchronously to liberate event loop during run
4342
+ preloadPowSolverCode().catch(() => {});
4343
+
4108
4344
  if (finalConfig.reset) {
4109
4345
  this.resetStore().catch(err => {
4110
4346
  console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
@@ -5033,9 +5269,9 @@ export class FingerprintEngine {
5033
5269
  }
5034
5270
  }
5035
5271
  const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
5036
- const isSuspiciousMedium = finalScore >= thresholds.medium;
5037
- const isSuspicious = finalScore >= thresholds.low;
5038
- const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
5272
+ const isSuspiciousMedium = finalScore >= thresholds.medium && finalScore > 0;
5273
+ const isSuspicious = finalScore >= thresholds.low && finalScore > 0;
5274
+ const isVerySuspicious = finalScore >= thresholds.medium && finalScore > 0; // Seuil pour le challenge d'optimisation
5039
5275
 
5040
5276
  // Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
5041
5277
  const suspicionFactor = isSuspicious // eslint-disable-line no-nested-ternary
@@ -5122,12 +5358,15 @@ export class FingerprintEngine {
5122
5358
  // Un challenge est nécessaire si :
5123
5359
  // 1. La requête est suspecte ET il n'y a pas de ticket valide.
5124
5360
  // OU
5125
- // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
5361
+ // 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
5126
5362
  const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
5127
5363
  const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
5128
- const mustReChallenge = isSuspiciousHigh && 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);
5129
5368
 
5130
- if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
5369
+ if ((isSuspicious && !hasValidTicket) || mustReChallenge) {
5131
5370
  if (mustReChallenge) {
5132
5371
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5133
5372
  }
@@ -5174,7 +5413,7 @@ export class FingerprintEngine {
5174
5413
  let usefulWorkDispatched = false;
5175
5414
  let challengePayload = null;
5176
5415
 
5177
- if (isSuspicious && shouldUseUsefulWork) {
5416
+ if ((isSuspicious || mustReChallenge) && shouldUseUsefulWork) {
5178
5417
  this._log('Issuing a useful work challenge', { finalScore });
5179
5418
 
5180
5419
  try {
@@ -5206,16 +5445,23 @@ export class FingerprintEngine {
5206
5445
  }
5207
5446
  }
5208
5447
 
5209
- if (isSuspicious && usefulWorkDispatched) {
5448
+ if ((isSuspicious || mustReChallenge) && usefulWorkDispatched) {
5210
5449
  if (isApi) {
5211
5450
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
5212
5451
  } else {
5452
+ const taskType = challengePayload.challenge.usefulWorkTask.task.type;
5453
+ let dummyResult = { solution: [], energy: 0 };
5454
+ if (taskType === 'multi_objective_genetic_algorithm') {
5455
+ dummyResult = { paretoFront: [] };
5456
+ } else if (taskType === 'genetic_algorithm_generations') {
5457
+ dummyResult = { population: [] };
5458
+ }
5213
5459
  const html = `<html><body><script>
5214
- window.location.href = "${path}?pow_type=useful_work_task&pow_nonce=${nonce}&pow_problem_id=${challengePayload.challenge.usefulWorkTask.problemId}&pow_solution_work_result=" + encodeURIComponent(JSON.stringify({ solution: [], energy: 0 }));
5460
+ window.location.href = "${path}?pow_type=useful_work_task&pow_nonce=${nonce}&pow_problem_id=${challengePayload.challenge.usefulWorkTask.problemId}&pow_solution_work_result=" + encodeURIComponent(JSON.stringify(${JSON.stringify(dummyResult)}));
5215
5461
  </script></body></html>`;
5216
5462
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: html };
5217
5463
  }
5218
- } 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é
5219
5465
  const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
5220
5466
  if (this.dryRun) {
5221
5467
  this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
@@ -5471,7 +5717,7 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
5471
5717
  if (activePeers.length === 0) return null;
5472
5718
 
5473
5719
  // Select a random peer
5474
- const randomIndex = Math.floor(Math.random() * activePeers.length);
5720
+ const randomIndex = crypto.randomInt(0, activePeers.length);
5475
5721
  return activePeers[randomIndex];
5476
5722
  }
5477
5723
 
@@ -5579,6 +5825,15 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5579
5825
  case 'register':
5580
5826
  expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
5581
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;
5582
5837
  case 'request_peer_block':
5583
5838
  expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
5584
5839
  break;
@@ -5607,14 +5862,49 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5607
5862
  await registerCooperativeNode(clientIp, nodeId, seed);
5608
5863
  return { status: 'registered' };
5609
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
+
5610
5900
  case 'request_peer_block':
5901
+ {
5611
5902
  const peerId = params.peer_id || '';
5612
5903
  const blockIdx = parseInt(params.block_idx || '0', 10);
5613
5904
  const requestId = params.req_id || '';
5614
5905
  if (!peerId || !requestId) {
5615
- return { error: 'Invalid parameters' };
5906
+ return {error: 'Invalid parameters'};
5616
5907
  }
5617
-
5618
5908
  const queueKey = `coop-mailbox:queue:${peerId}`;
5619
5909
  const requests = (await store.get(queueKey)) || [];
5620
5910
  requests.push({
@@ -5623,7 +5913,8 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
5623
5913
  block_idx: blockIdx
5624
5914
  });
5625
5915
  await store.set(queueKey, requests, 30);
5626
- return { status: 'queued' };
5916
+ return {status: 'queued'};
5917
+ }
5627
5918
 
5628
5919
  case 'poll_requests':
5629
5920
  const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
@@ -6463,6 +6754,7 @@ export const __internal = {
6463
6754
  calculateTarget,
6464
6755
  determineOptimalTicketTtl,
6465
6756
  runBackgroundTtlOptimization,
6757
+ calculateAnalogInconsistencyScore,
6466
6758
  getRequestPatternScore, // Expose for testing
6467
6759
  getBehaviorScore, // Expose for testing
6468
6760
  getCrossLayerInconsistency, // Expose for testing
@@ -6628,7 +6920,7 @@ export function sanitizeTrafficData(trafficData) {
6628
6920
  const minDataPoints = 200; // Seuil par défaut
6629
6921
  const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
6630
6922
  if (passedLogs.length > maxPassedAllowed) {
6631
- const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
6923
+ const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
6632
6924
  return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
6633
6925
  }
6634
6926
  return [...suspiciousLogs, ...passedLogs];