@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.
- package/CHANGELOG.md +32 -0
- package/README.md +11 -2
- package/package.json +1 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.client.js +1463 -1447
- package/src/js/fingerprint.js +386 -94
- package/src/js/library.js +15 -8
- package/src/js/pow.solver.inline.js +56 -10
- package/src/js/pow.solver.js +56 -10
- package/src/js/pow.worker.js +35 -13
- package/src/js/problem-manager.js +10 -3
- package/src/js/tests/fingerprint.engine.test.js +25 -0
- package/src/js/tests/fingerprint.test.js +69 -35
- package/src/php/Challenge/ChallengeUtils.php +149 -23
- package/src/php/Config/SecurityProfiles.php +1 -0
- package/src/php/FingerprintClient.php +148 -148
- package/src/php/FingerprintEngine.php +39 -84
- package/src/php/Ja3AnomalyDetector.php +257 -227
- package/src/php/Optimization/OptimizationOperators.php +2 -2
- package/src/php/ProblemManager.php +83 -0
- package/src/php/Tests/FingerprintEngineTest.php +2 -33
- package/src/php/Utils/RequestUtils.php +114 -11
- package/src/php/Utils/TLSClientHelloParser.php +158 -1
- package/src/js/fingerprint.client.obfuscated.js +0 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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(
|
|
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
|
-
//
|
|
3057
|
-
|
|
3058
|
-
const
|
|
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
|
-
//
|
|
3061
|
-
const
|
|
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
|
-
//
|
|
3064
|
-
const
|
|
3065
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4043
|
-
if (
|
|
4044
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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({
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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 -
|
|
6923
|
+
const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
|
|
6632
6924
|
return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
|
|
6633
6925
|
}
|
|
6634
6926
|
return [...suspiciousLogs, ...passedLogs];
|