@anonympins/fingerprint 0.6.7 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +3 -7
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +9 -11
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +236 -152
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +517 -101
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +355 -384
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +206 -62
- package/src/js/pow.solver.js +235 -101
- package/src/js/pow.worker.js +48 -26
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +101 -40
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +204 -32
- package/src/php/Config/SecurityProfiles.php +21 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +24 -25
- package/src/php/FingerprintEngine.php +1836 -1880
- package/src/php/Ja3AnomalyDetector.php +258 -227
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +28 -27
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -693
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +236 -19
- package/src/php/Utils/TLSClientHelloParser.php +369 -212
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
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...';
|
|
@@ -2211,7 +2347,9 @@ function getBehaviorScore(context) {
|
|
|
2211
2347
|
if (metrics.honeypotInteraction) {
|
|
2212
2348
|
return { behaviorScore: 100 };
|
|
2213
2349
|
}
|
|
2214
|
-
|
|
2350
|
+
if (metrics.prototypeTampered) {
|
|
2351
|
+
score += 80; // Altération flagrante de l'environnement JS
|
|
2352
|
+
}
|
|
2215
2353
|
// 2. Analyse des mouvements de la souris
|
|
2216
2354
|
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
2217
2355
|
const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
|
|
@@ -2263,6 +2401,14 @@ function getBehaviorScore(context) {
|
|
|
2263
2401
|
const benfordDev = Optimization.Operators.benfordTest(touch.segments);
|
|
2264
2402
|
if (benfordDev > 0.18) score += 35;
|
|
2265
2403
|
}
|
|
2404
|
+
|
|
2405
|
+
// Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
|
|
2406
|
+
const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
|
|
2407
|
+
if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
|
|
2408
|
+
if (metrics.motionVariance === 0) {
|
|
2409
|
+
score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2266
2412
|
}
|
|
2267
2413
|
|
|
2268
2414
|
// Plausibilité de la latence de frappe
|
|
@@ -2896,6 +3042,62 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
|
|
|
2896
3042
|
return null;
|
|
2897
3043
|
}
|
|
2898
3044
|
|
|
3045
|
+
/**
|
|
3046
|
+
* Calcule la longueur du préfixe commun (en bits) entre deux adresses IP (IPv4 ou IPv6).
|
|
3047
|
+
* @param {string} ip1
|
|
3048
|
+
* @param {string} ip2
|
|
3049
|
+
* @returns {number} Nombre de bits identiques en tête (0 à 32 pour IPv4, 0 à 128 pour IPv6).
|
|
3050
|
+
*/
|
|
3051
|
+
function getIpCommonPrefixLength(ip1, ip2) {
|
|
3052
|
+
if (!ip1 || !ip2) return 0;
|
|
3053
|
+
if (isIPv4(ip1) && isIPv4(ip2)) {
|
|
3054
|
+
const b1 = ip1.split('.').map(Number);
|
|
3055
|
+
const b2 = ip2.split('.').map(Number);
|
|
3056
|
+
const int1 = ((b1[0] << 24) | (b1[1] << 16) | (b1[2] << 8) | b1[3]) >>> 0;
|
|
3057
|
+
const int2 = ((b2[0] << 24) | (b2[1] << 16) | (b2[2] << 8) | b2[3]) >>> 0;
|
|
3058
|
+
const xor = (int1 ^ int2) >>> 0;
|
|
3059
|
+
return xor === 0 ? 32 : Math.clz32(xor);
|
|
3060
|
+
}
|
|
3061
|
+
if (isIPv6(ip1) && isIPv6(ip2)) {
|
|
3062
|
+
const normalize = (ip) => {
|
|
3063
|
+
let normalized = ip.trim().toLowerCase();
|
|
3064
|
+
if (normalized.includes("::")) {
|
|
3065
|
+
const parts = normalized.split("::");
|
|
3066
|
+
const left = parts[0] ? parts[0].split(":") : [];
|
|
3067
|
+
const right = parts[1] ? parts[1].split(":") : [];
|
|
3068
|
+
const missing = 8 - (left.length + right.length);
|
|
3069
|
+
const middle = Array(missing).fill("0000");
|
|
3070
|
+
normalized = [...left, ...middle, ...right].join(":");
|
|
3071
|
+
} else {
|
|
3072
|
+
const parts = normalized.split(":");
|
|
3073
|
+
if (parts.length !== 8) return null;
|
|
3074
|
+
}
|
|
3075
|
+
return normalized.split(":").map(g => parseInt(g, 16) || 0);
|
|
3076
|
+
};
|
|
3077
|
+
|
|
3078
|
+
try {
|
|
3079
|
+
const g1 = normalize(ip1);
|
|
3080
|
+
const g2 = normalize(ip2);
|
|
3081
|
+
if (!g1 || !g2) return 0;
|
|
3082
|
+
let prefix = 0;
|
|
3083
|
+
for (let i = 0; i < 8; i++) {
|
|
3084
|
+
const xor = (g1[i] ^ g2[i]) & 0xffff;
|
|
3085
|
+
if (xor === 0) {
|
|
3086
|
+
prefix += 16;
|
|
3087
|
+
} else {
|
|
3088
|
+
// clz32 opère sur des entiers 32 bits, ajuster pour 16 bits
|
|
3089
|
+
prefix += (Math.clz32(xor) - 16);
|
|
3090
|
+
break;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return prefix;
|
|
3094
|
+
} catch (e) {
|
|
3095
|
+
return 0;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
return 0;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
2899
3101
|
/**
|
|
2900
3102
|
* Applies temporal decay (half-life of 30 minutes) to subnet metrics.
|
|
2901
3103
|
* @private
|
|
@@ -2943,6 +3145,11 @@ function decaySubnetData(subnetData, now) {
|
|
|
2943
3145
|
subnetData.uas = subnetData.uas.slice(0, newLen);
|
|
2944
3146
|
}
|
|
2945
3147
|
|
|
3148
|
+
if (Array.isArray(subnetData.attackerIps)) {
|
|
3149
|
+
const maxAttackerAgeMs = 60 * 60 * 1000; // 1 heure max
|
|
3150
|
+
subnetData.attackerIps = subnetData.attackerIps.filter(a => (now - (a.lastSeen || now)) < maxAttackerAgeMs);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
2946
3153
|
subnetData.lastActivity = now - (inactivityMs % (30 * 60 * 1000));
|
|
2947
3154
|
}
|
|
2948
3155
|
return subnetData;
|
|
@@ -2965,7 +3172,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
2965
3172
|
highScoreDevices: {},
|
|
2966
3173
|
lastActivity: 0,
|
|
2967
3174
|
ips: [],
|
|
2968
|
-
uas: []
|
|
3175
|
+
uas: [],
|
|
3176
|
+
attackerIps: []
|
|
2969
3177
|
};
|
|
2970
3178
|
|
|
2971
3179
|
if (!subnetData.highScoreDevices) {
|
|
@@ -2977,6 +3185,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
2977
3185
|
subnetData.ips = [];
|
|
2978
3186
|
}
|
|
2979
3187
|
if (!subnetData.uas) subnetData.uas = [];
|
|
3188
|
+
if (!Array.isArray(subnetData.attackerIps)) {
|
|
3189
|
+
subnetData.attackerIps = [];
|
|
3190
|
+
}
|
|
2980
3191
|
|
|
2981
3192
|
const now = Date.now();
|
|
2982
3193
|
decaySubnetData(subnetData, now);
|
|
@@ -3000,6 +3211,20 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3000
3211
|
}
|
|
3001
3212
|
|
|
3002
3213
|
const userAgent = context.headers?.['user-agent'] || '';
|
|
3214
|
+
|
|
3215
|
+
if (finalScore >= 70) {
|
|
3216
|
+
const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
|
|
3217
|
+
if (existingAttacker) {
|
|
3218
|
+
existingAttacker.lastSeen = now;
|
|
3219
|
+
existingAttacker.score = Math.max(existingAttacker.score || 0, finalScore);
|
|
3220
|
+
} else {
|
|
3221
|
+
subnetData.attackerIps.push({ ip: context.clientIp, lastSeen: now, score: finalScore });
|
|
3222
|
+
if (subnetData.attackerIps.length > 30) {
|
|
3223
|
+
subnetData.attackerIps.shift();
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3003
3228
|
if (userAgent && !subnetData.uas.includes(userAgent)) {
|
|
3004
3229
|
subnetData.uas.push(userAgent);
|
|
3005
3230
|
}
|
|
@@ -3019,13 +3244,37 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3019
3244
|
|
|
3020
3245
|
await store.set(key, subnetData, 86400); // 24-hour TTL
|
|
3021
3246
|
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
|
|
3249
|
+
* @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
|
|
3250
|
+
* @param {number} [inflectionPoint=0.72] Point d'inflexion où la suspicion accélère.
|
|
3251
|
+
* @param {number} [steepness=12] Raideur de la transition sigmoïdale.
|
|
3252
|
+
* @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
|
|
3253
|
+
*/
|
|
3254
|
+
export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPoint = 0.72, steepness = 12) {
|
|
3255
|
+
const s = Math.max(0.0, Math.min(1.0, consistencyScore));
|
|
3256
|
+
if (s >= 0.98) return 0.0;
|
|
3257
|
+
|
|
3258
|
+
const asymptote = 99.9;
|
|
3259
|
+
const raw = 1.0 / (1.0 + Math.exp(steepness * (s - inflectionPoint)));
|
|
3260
|
+
const minVal = 1.0 / (1.0 + Math.exp(steepness * (1.0 - inflectionPoint)));
|
|
3261
|
+
const maxVal = 1.0 / (1.0 + Math.exp(steepness * (0.0 - inflectionPoint)));
|
|
3262
|
+
const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
|
|
3263
|
+
|
|
3264
|
+
return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
|
|
3265
|
+
}
|
|
3022
3266
|
|
|
3023
3267
|
/**
|
|
3024
3268
|
* Calculates a suspicion score based on the historical activity of the IP subnet.
|
|
3025
3269
|
* @param {object} context The request context.
|
|
3270
|
+
* @param {string} [deviceId=''] The device ID.
|
|
3271
|
+
* @param {object} [securityConfig=null] Security configuration containing thresholds.
|
|
3026
3272
|
* @returns {Promise<{subnetScore: number}>}
|
|
3027
3273
|
*/
|
|
3028
|
-
async function getSubnetScore(context) {
|
|
3274
|
+
async function getSubnetScore(context, deviceId = '', securityConfig = null) {
|
|
3275
|
+
if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
|
|
3276
|
+
securityConfig = deviceId;
|
|
3277
|
+
}
|
|
3029
3278
|
const subnet = getIpSubnet(context.clientIp);
|
|
3030
3279
|
if (!subnet) return { subnetScore: 0 };
|
|
3031
3280
|
const key = `subnet:${subnet}`;
|
|
@@ -3054,28 +3303,76 @@ async function getSubnetScore(context) {
|
|
|
3054
3303
|
return { subnetScore: 0.0 };
|
|
3055
3304
|
}
|
|
3056
3305
|
|
|
3057
|
-
//
|
|
3058
|
-
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3061
|
-
// Ratio
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
const
|
|
3070
|
-
|
|
3306
|
+
// 1. Estimation Bayésienne de densité (évite les sur-réactions sur 1 ou 2 appareils)
|
|
3307
|
+
// Prior: alpha=0.5, beta=2.0 (a priori réseau sain)
|
|
3308
|
+
const bayesianDensity = (highScoreCount + 0.5) / (deviceCount + 2.5);
|
|
3309
|
+
|
|
3310
|
+
// 2. Ratio IP / Terminal (distingue un proxy distribué d'un gros NAT / CGNAT)
|
|
3311
|
+
// Sur un proxy distribué, chaque terminal utilise une IP différente (ratio >= 1.0)
|
|
3312
|
+
// Sur un CGNAT, des dizaines de terminaux partagent peu d'IPs (ratio << 1.0)
|
|
3313
|
+
const ipDispersion = Math.min(2.0, ipCount / deviceCount);
|
|
3314
|
+
const ipMultiplier = 0.6 + 0.4 * Math.tanh(ipDispersion);
|
|
3315
|
+
|
|
3316
|
+
// 3. Volatilité des User-Agents (rotation de navigateurs sur matériel identique)
|
|
3317
|
+
const uaDispersion = Math.min(3.0, Math.max(1, uaCount) / deviceCount);
|
|
3318
|
+
const uaMultiplier = 0.7 + 0.3 * Math.tanh(uaDispersion - 1.0);
|
|
3319
|
+
|
|
3320
|
+
// 4. Intensité brute continue de la menace
|
|
3321
|
+
const rawThreatIntensity = highScoreCount * bayesianDensity * ipMultiplier * uaMultiplier;
|
|
3322
|
+
|
|
3323
|
+
// 5. Composante 1 : Score ambiant plafonné en zone Medium (asymptote au seuil medium)
|
|
3324
|
+
const mediumThreshold = (securityConfig?.thresholds?.medium !== undefined)
|
|
3325
|
+
? Number(securityConfig.thresholds.medium)
|
|
3326
|
+
: 45.0;
|
|
3327
|
+
const ambientAsymptote = mediumThreshold;
|
|
3328
|
+
const maxProximityBoost = Math.max(0.0, 100.0 - ambientAsymptote);
|
|
3329
|
+
const scaleFactor = 10.0;
|
|
3330
|
+
const ambientScore = ambientAsymptote * Math.tanh(rawThreatIntensity / scaleFactor);
|
|
3331
|
+
|
|
3332
|
+
// 6. Composante 2 : Boost de proximité micro-réseau avec des attaquants récents (le reste va jusqu'à 100)
|
|
3333
|
+
let proximityBoost = 0.0;
|
|
3334
|
+
const attackerIps = subnetData.attackerIps || [];
|
|
3335
|
+
const clientIp = context.clientIp;
|
|
3336
|
+
|
|
3337
|
+
if (attackerIps.length > 0 && clientIp) {
|
|
3338
|
+
let maxCommonPrefix = 0;
|
|
3339
|
+
const isClientV4 = isIPv4(clientIp);
|
|
3340
|
+
const isClientV6 = isIPv6(clientIp);
|
|
3341
|
+
|
|
3342
|
+
for (const att of attackerIps) {
|
|
3343
|
+
if (att.ip && (now - (att.lastSeen || now)) <= 30 * 60 * 1000) {
|
|
3344
|
+
const prefixLen = getIpCommonPrefixLength(clientIp, att.ip);
|
|
3345
|
+
if (prefixLen > maxCommonPrefix) {
|
|
3346
|
+
maxCommonPrefix = prefixLen;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3071
3350
|
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3351
|
+
if (isClientV4) {
|
|
3352
|
+
if (maxCommonPrefix >= 32) {
|
|
3353
|
+
proximityBoost = maxProximityBoost; // Même adresse IP exacte
|
|
3354
|
+
} else if (maxCommonPrefix >= 30) {
|
|
3355
|
+
proximityBoost = maxProximityBoost * (45.0 / 55.0); // Même /30 (écart <= 3 adresses)
|
|
3356
|
+
} else if (maxCommonPrefix >= 28) {
|
|
3357
|
+
proximityBoost = maxProximityBoost * (30.0 / 55.0); // Même /28 (bloc de 16 adresses)
|
|
3358
|
+
} else if (maxCommonPrefix >= 26) {
|
|
3359
|
+
proximityBoost = maxProximityBoost * (15.0 / 55.0); // Même /26 (bloc de 64 adresses)
|
|
3360
|
+
}
|
|
3361
|
+
} else if (isClientV6) {
|
|
3362
|
+
if (maxCommonPrefix >= 128) {
|
|
3363
|
+
proximityBoost = maxProximityBoost; // Même IPv6 exacte
|
|
3364
|
+
} else if (maxCommonPrefix >= 120) {
|
|
3365
|
+
proximityBoost = maxProximityBoost * (45.0 / 55.0); // Même /120
|
|
3366
|
+
} else if (maxCommonPrefix >= 112) {
|
|
3367
|
+
proximityBoost = maxProximityBoost * (30.0 / 55.0); // Même /112
|
|
3368
|
+
} else if (maxCommonPrefix >= 96) {
|
|
3369
|
+
proximityBoost = maxProximityBoost * (15.0 / 55.0); // Même /96
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3076
3372
|
}
|
|
3077
3373
|
|
|
3078
|
-
const finalScore = Math.min(100, Math.round(
|
|
3374
|
+
const finalScore = Math.min(100.0, Math.round((ambientScore + proximityBoost) * 10) / 10);
|
|
3375
|
+
|
|
3079
3376
|
return { subnetScore: finalScore };
|
|
3080
3377
|
}
|
|
3081
3378
|
|
|
@@ -3565,6 +3862,46 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3565
3862
|
return { historyScore, rotationScore };
|
|
3566
3863
|
}
|
|
3567
3864
|
|
|
3865
|
+
function getVirtualizationAnomalyScore(context) {
|
|
3866
|
+
const clientFp = context.headers?.['x-device-fingerprint'];
|
|
3867
|
+
if (!clientFp) return 0.0;
|
|
3868
|
+
|
|
3869
|
+
const fpMap = {};
|
|
3870
|
+
clientFp.split('|').forEach(part => {
|
|
3871
|
+
const pair = part.split(':');
|
|
3872
|
+
if (pair.length === 2) {
|
|
3873
|
+
fpMap[pair[0]] = pair[1];
|
|
3874
|
+
}
|
|
3875
|
+
});
|
|
3876
|
+
|
|
3877
|
+
let score = 0.0;
|
|
3878
|
+
const clientGpuHash = fpMap['gpu'];
|
|
3879
|
+
if (clientGpuHash) {
|
|
3880
|
+
const virtualGpus = [
|
|
3881
|
+
"Google SwiftShader", "SwiftShader",
|
|
3882
|
+
"Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
|
|
3883
|
+
"Microsoft Basic Render Driver", "HeadlessChrome",
|
|
3884
|
+
"Intel(R) HD Graphics"
|
|
3885
|
+
];
|
|
3886
|
+
const virtualGpuHashes = new Set(virtualGpus.map(gpu => cyrb53(gpu).toString()));
|
|
3887
|
+
if (virtualGpuHashes.has(clientGpuHash)) {
|
|
3888
|
+
score += 75.0;
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
const clientScreenHash = fpMap['scr'];
|
|
3893
|
+
if (clientScreenHash) {
|
|
3894
|
+
const headlessResolutions = ["800x600_24", "1024x768_24"];
|
|
3895
|
+
const headlessHashes = new Set(headlessResolutions.map(res => cyrb53(res).toString()));
|
|
3896
|
+
if (headlessHashes.has(clientScreenHash)) {
|
|
3897
|
+
score += 25.0;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
return Math.min(100.0, score);
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
|
|
3568
3905
|
/**
|
|
3569
3906
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
3570
3907
|
* @param {object} context - The request context object.
|
|
@@ -3611,9 +3948,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3611
3948
|
_ // store.set result
|
|
3612
3949
|
] = await Promise.all([
|
|
3613
3950
|
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
3614
|
-
getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3951
|
+
getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3615
3952
|
getTlsSpoofingScore(context),
|
|
3616
|
-
getSubnetScore(context, deviceId),
|
|
3953
|
+
getSubnetScore(context, deviceId, securityConfig),
|
|
3617
3954
|
getIpReputationScore(clientIp),
|
|
3618
3955
|
getBotnetClusterScore(context, stableFpHash),
|
|
3619
3956
|
store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
|
|
@@ -3621,12 +3958,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3621
3958
|
|
|
3622
3959
|
// Synchronous calculations
|
|
3623
3960
|
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
3627
|
-
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
3628
|
-
inconsistencyScore = 100;
|
|
3629
|
-
}
|
|
3961
|
+
const similarityThreshold = securityConfig?.similarityThreshold ?? 0.72;
|
|
3962
|
+
const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
|
|
3630
3963
|
|
|
3631
3964
|
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
3632
3965
|
|
|
@@ -3652,6 +3985,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3652
3985
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3653
3986
|
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
3654
3987
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
3988
|
+
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
3655
3989
|
|
|
3656
3990
|
// Save the updated device state to the store
|
|
3657
3991
|
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
@@ -3663,7 +3997,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3663
3997
|
deviceData.ips = new Set(deviceData.ips);
|
|
3664
3998
|
}
|
|
3665
3999
|
// Le vecteur de suspicion est maintenant complet.
|
|
3666
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore };
|
|
4000
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
|
|
3667
4001
|
};
|
|
3668
4002
|
|
|
3669
4003
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -3879,6 +4213,8 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3879
4213
|
const clientInitConfig = {
|
|
3880
4214
|
mouse: true,
|
|
3881
4215
|
keystrokes: true,
|
|
4216
|
+
wasmPath: securityConfig?.wasmPath || '/fp.wasm',
|
|
4217
|
+
workerPath: securityConfig?.workerPath || '/pow.worker.js',
|
|
3882
4218
|
trapUrls: trapUrls // On passe directement le tableau d'URL
|
|
3883
4219
|
};
|
|
3884
4220
|
|
|
@@ -4047,14 +4383,40 @@ function parseGraphQLQuery(body) {
|
|
|
4047
4383
|
* @param {string} zkpY - The 'y' component of the ZKP proof (public key).
|
|
4048
4384
|
* @returns {Promise<{threatIntelScore: number}>}
|
|
4049
4385
|
*/
|
|
4050
|
-
async function getThreatIntelScore(zkpY) {
|
|
4051
|
-
|
|
4386
|
+
async function getThreatIntelScore(context, zkpY) {
|
|
4387
|
+
let score = 0;
|
|
4388
|
+
if (zkpY) {
|
|
4389
|
+
const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
|
|
4390
|
+
if (isBanned) {
|
|
4391
|
+
score = 100;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
|
|
4396
|
+
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4052
4397
|
|
|
4053
|
-
const
|
|
4054
|
-
if (
|
|
4055
|
-
|
|
4398
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4399
|
+
if (behaviorHeader) {
|
|
4400
|
+
try {
|
|
4401
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
4402
|
+
if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
|
|
4403
|
+
const appLatency = context.requestTimestamp - metrics.clientTimestamp;
|
|
4404
|
+
if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0) {
|
|
4405
|
+
const clientToProxyDelta = appLatency - tcpRtt;
|
|
4406
|
+
if (tcpRtt < 35 && clientToProxyDelta > 150) {
|
|
4407
|
+
score = Math.max(score, 85);
|
|
4408
|
+
}
|
|
4409
|
+
} else {
|
|
4410
|
+
if (appLatency > 350) {
|
|
4411
|
+
score = Math.max(score, 40);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
} catch (e) {
|
|
4416
|
+
// Ignorer silencieusement
|
|
4417
|
+
}
|
|
4056
4418
|
}
|
|
4057
|
-
return { threatIntelScore:
|
|
4419
|
+
return { threatIntelScore: score };
|
|
4058
4420
|
}
|
|
4059
4421
|
|
|
4060
4422
|
export class FingerprintEngine {
|
|
@@ -4116,6 +4478,10 @@ export class FingerprintEngine {
|
|
|
4116
4478
|
this._validateConfig(finalConfig); // Validate the configuration
|
|
4117
4479
|
this.verbose = finalConfig.verbose || false;
|
|
4118
4480
|
this.dryRun = finalConfig.dryRun || false;
|
|
4481
|
+
|
|
4482
|
+
// Preload solver file asynchronously to liberate event loop during run
|
|
4483
|
+
preloadPowSolverCode().catch(() => {});
|
|
4484
|
+
|
|
4119
4485
|
if (finalConfig.reset) {
|
|
4120
4486
|
this.resetStore().catch(err => {
|
|
4121
4487
|
console.error('[FingerprintEngine] Failed to reset store on startup:', err.message);
|
|
@@ -5136,9 +5502,12 @@ export class FingerprintEngine {
|
|
|
5136
5502
|
// 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
5137
5503
|
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
5138
5504
|
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
5139
|
-
|
|
5505
|
+
// Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
|
|
5506
|
+
// on fait confiance au ticket valide tant qu'il n'a pas expiré.
|
|
5507
|
+
const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
|
|
5508
|
+
const mustReChallenge = (suspicionVector.honeypotScore >= (thresholds.medium ?? 45)) || (maxIndicatorsCount >= 1);
|
|
5140
5509
|
|
|
5141
|
-
if (isSuspicious &&
|
|
5510
|
+
if ((isSuspicious && !hasValidTicket) || mustReChallenge) {
|
|
5142
5511
|
if (mustReChallenge) {
|
|
5143
5512
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
5144
5513
|
}
|
|
@@ -5185,7 +5554,7 @@ export class FingerprintEngine {
|
|
|
5185
5554
|
let usefulWorkDispatched = false;
|
|
5186
5555
|
let challengePayload = null;
|
|
5187
5556
|
|
|
5188
|
-
if (isSuspicious && shouldUseUsefulWork) {
|
|
5557
|
+
if ((isSuspicious || mustReChallenge) && shouldUseUsefulWork) {
|
|
5189
5558
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
5190
5559
|
|
|
5191
5560
|
try {
|
|
@@ -5217,7 +5586,7 @@ export class FingerprintEngine {
|
|
|
5217
5586
|
}
|
|
5218
5587
|
}
|
|
5219
5588
|
|
|
5220
|
-
if (isSuspicious && usefulWorkDispatched) {
|
|
5589
|
+
if ((isSuspicious || mustReChallenge) && usefulWorkDispatched) {
|
|
5221
5590
|
if (isApi) {
|
|
5222
5591
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
5223
5592
|
} else {
|
|
@@ -5233,7 +5602,7 @@ export class FingerprintEngine {
|
|
|
5233
5602
|
</script></body></html>`;
|
|
5234
5603
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: html };
|
|
5235
5604
|
}
|
|
5236
|
-
} else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
|
|
5605
|
+
} else if (isSuspicious || mustReChallenge) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi / a échoué
|
|
5237
5606
|
const decision = { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404 };
|
|
5238
5607
|
if (this.dryRun) {
|
|
5239
5608
|
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
@@ -5489,7 +5858,7 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
|
|
|
5489
5858
|
if (activePeers.length === 0) return null;
|
|
5490
5859
|
|
|
5491
5860
|
// Select a random peer
|
|
5492
|
-
const randomIndex =
|
|
5861
|
+
const randomIndex = crypto.randomInt(0, activePeers.length);
|
|
5493
5862
|
return activePeers[randomIndex];
|
|
5494
5863
|
}
|
|
5495
5864
|
|
|
@@ -5597,6 +5966,15 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5597
5966
|
case 'register':
|
|
5598
5967
|
expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
|
|
5599
5968
|
break;
|
|
5969
|
+
case 'find_peer':
|
|
5970
|
+
expectedMsg = `${clientSecret}:find_peer:${nodeId}`;
|
|
5971
|
+
break;
|
|
5972
|
+
case 'webrtc_signal':
|
|
5973
|
+
expectedMsg = `${clientSecret}:webrtc_signal:${nodeId}:${params.target_peer_id || ''}:${params.signal_type || ''}:${params.signal_data || ''}`;
|
|
5974
|
+
break;
|
|
5975
|
+
case 'poll_signals':
|
|
5976
|
+
expectedMsg = `${clientSecret}:poll_signals:${nodeId}`;
|
|
5977
|
+
break;
|
|
5600
5978
|
case 'request_peer_block':
|
|
5601
5979
|
expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
|
|
5602
5980
|
break;
|
|
@@ -5625,14 +6003,49 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5625
6003
|
await registerCooperativeNode(clientIp, nodeId, seed);
|
|
5626
6004
|
return { status: 'registered' };
|
|
5627
6005
|
|
|
6006
|
+
case 'find_peer': {
|
|
6007
|
+
const peer = await findPeerInSubnet(clientIp, nodeId);
|
|
6008
|
+
if (!peer) {
|
|
6009
|
+
return { status: 'no_peers' };
|
|
6010
|
+
}
|
|
6011
|
+
return { status: 'peer_found', peer_id: peer.nodeId };
|
|
6012
|
+
}
|
|
6013
|
+
|
|
6014
|
+
case 'webrtc_signal': {
|
|
6015
|
+
const targetPeerId = params.target_peer_id || '';
|
|
6016
|
+
const signalType = params.signal_type || '';
|
|
6017
|
+
const signalData = params.signal_data || '';
|
|
6018
|
+
if (!targetPeerId || !signalType) {
|
|
6019
|
+
return { error: 'Invalid parameters' };
|
|
6020
|
+
}
|
|
6021
|
+
const signalQueueKey = `coop-webrtc:signals:${targetPeerId}`;
|
|
6022
|
+
const signals = (await store.get(signalQueueKey)) || [];
|
|
6023
|
+
signals.push({
|
|
6024
|
+
from_peer_id: nodeId,
|
|
6025
|
+
signal_type: signalType,
|
|
6026
|
+
signal_data: signalData
|
|
6027
|
+
});
|
|
6028
|
+
await store.set(signalQueueKey, signals, 30);
|
|
6029
|
+
return { status: 'signal_queued' };
|
|
6030
|
+
}
|
|
6031
|
+
|
|
6032
|
+
case 'poll_signals': {
|
|
6033
|
+
const pollSignalKey = `coop-webrtc:signals:${nodeId}`;
|
|
6034
|
+
const signals = (await store.get(pollSignalKey)) || [];
|
|
6035
|
+
if (signals.length > 0) {
|
|
6036
|
+
await store.delete(pollSignalKey);
|
|
6037
|
+
}
|
|
6038
|
+
return { status: 'ok', signals };
|
|
6039
|
+
}
|
|
6040
|
+
|
|
5628
6041
|
case 'request_peer_block':
|
|
6042
|
+
{
|
|
5629
6043
|
const peerId = params.peer_id || '';
|
|
5630
6044
|
const blockIdx = parseInt(params.block_idx || '0', 10);
|
|
5631
6045
|
const requestId = params.req_id || '';
|
|
5632
6046
|
if (!peerId || !requestId) {
|
|
5633
|
-
return {
|
|
6047
|
+
return {error: 'Invalid parameters'};
|
|
5634
6048
|
}
|
|
5635
|
-
|
|
5636
6049
|
const queueKey = `coop-mailbox:queue:${peerId}`;
|
|
5637
6050
|
const requests = (await store.get(queueKey)) || [];
|
|
5638
6051
|
requests.push({
|
|
@@ -5641,7 +6054,8 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
5641
6054
|
block_idx: blockIdx
|
|
5642
6055
|
});
|
|
5643
6056
|
await store.set(queueKey, requests, 30);
|
|
5644
|
-
return {
|
|
6057
|
+
return {status: 'queued'};
|
|
6058
|
+
}
|
|
5645
6059
|
|
|
5646
6060
|
case 'poll_requests':
|
|
5647
6061
|
const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
|
|
@@ -6481,6 +6895,7 @@ export const __internal = {
|
|
|
6481
6895
|
calculateTarget,
|
|
6482
6896
|
determineOptimalTicketTtl,
|
|
6483
6897
|
runBackgroundTtlOptimization,
|
|
6898
|
+
calculateAnalogInconsistencyScore,
|
|
6484
6899
|
getRequestPatternScore, // Expose for testing
|
|
6485
6900
|
getBehaviorScore, // Expose for testing
|
|
6486
6901
|
getCrossLayerInconsistency, // Expose for testing
|
|
@@ -6500,6 +6915,7 @@ export const __internal = {
|
|
|
6500
6915
|
generateCombinedPoWChallengePage,
|
|
6501
6916
|
problemManager, // Re-export the problemManager promise
|
|
6502
6917
|
getIpSubnet, // Expose for testing
|
|
6918
|
+
getIpCommonPrefixLength, // Expose for testing
|
|
6503
6919
|
updateSubnetMetrics, // Expose for testing
|
|
6504
6920
|
getSubnetScore, // Expose for testing
|
|
6505
6921
|
getIpReputationScore, // Expose for testing
|
|
@@ -6646,7 +7062,7 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
6646
7062
|
const minDataPoints = 200; // Seuil par défaut
|
|
6647
7063
|
const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
|
|
6648
7064
|
if (passedLogs.length > maxPassedAllowed) {
|
|
6649
|
-
const shuffledPassed = passedLogs.sort(() => 0.5 -
|
|
7065
|
+
const shuffledPassed = passedLogs.sort(() => 0.5 - secureRandomFloat());
|
|
6650
7066
|
return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
|
|
6651
7067
|
}
|
|
6652
7068
|
return [...suspiciousLogs, ...passedLogs];
|