@anonympins/fingerprint 0.5.2 → 0.5.5
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 +27 -0
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +280 -38
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +474 -163
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +24 -8
- package/src/js/mongodb-store.js +165 -79
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +509 -14
- package/src/js/pow.solver.js +350 -32
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.test.js +57 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +233 -57
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
package/src/js/fingerprint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import {BlockList, isIPv4, isIPv6} from "node:net";
|
|
3
3
|
import * as dns from "node:dns/promises";
|
|
4
|
+
import {Worker} from "node:worker_threads";
|
|
4
5
|
import {getProblemManager, problemManager} from "./problem-manager.js";
|
|
5
6
|
import {Optimization} from "./library.js";
|
|
6
7
|
import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
|
|
@@ -27,6 +28,57 @@ import {
|
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const __dirname = dirname(__filename);
|
|
29
30
|
|
|
31
|
+
let dnsCircuitBreaker = {
|
|
32
|
+
state: 'CLOSED', // 'CLOSED', 'OPEN', 'HALF-OPEN'
|
|
33
|
+
failureCount: 0,
|
|
34
|
+
lastStateChange: 0,
|
|
35
|
+
threshold: 5,
|
|
36
|
+
cooldownMs: 30000, // 30 seconds
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function recordDnsSuccess() {
|
|
40
|
+
dnsCircuitBreaker.failureCount = 0;
|
|
41
|
+
dnsCircuitBreaker.state = 'CLOSED';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function recordDnsFailure() {
|
|
45
|
+
dnsCircuitBreaker.failureCount++;
|
|
46
|
+
if (dnsCircuitBreaker.failureCount >= dnsCircuitBreaker.threshold) {
|
|
47
|
+
dnsCircuitBreaker.state = 'OPEN';
|
|
48
|
+
dnsCircuitBreaker.lastStateChange = Date.now();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canAttemptDns() {
|
|
53
|
+
if (dnsCircuitBreaker.state === 'CLOSED') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (dnsCircuitBreaker.state === 'OPEN') {
|
|
57
|
+
if (Date.now() - dnsCircuitBreaker.lastStateChange > dnsCircuitBreaker.cooldownMs) {
|
|
58
|
+
dnsCircuitBreaker.state = 'HALF-OPEN';
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true; // HALF-OPEN
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const withTimeout = (promise, ms) => {
|
|
67
|
+
let timeoutId;
|
|
68
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
69
|
+
timeoutId = setTimeout(() => {
|
|
70
|
+
reject(new Error('DNS_TIMEOUT'));
|
|
71
|
+
}, ms);
|
|
72
|
+
});
|
|
73
|
+
return Promise.race([
|
|
74
|
+
promise.then((res) => {
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
return res;
|
|
77
|
+
}),
|
|
78
|
+
timeoutPromise
|
|
79
|
+
]);
|
|
80
|
+
};
|
|
81
|
+
|
|
30
82
|
export { createRedisStore } from "./redis-store.js";
|
|
31
83
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
32
84
|
|
|
@@ -72,7 +124,7 @@ const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
|
72
124
|
]);
|
|
73
125
|
|
|
74
126
|
function generateSessionMapping() {
|
|
75
|
-
const randomStr = (len = 6) =>
|
|
127
|
+
const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
|
|
76
128
|
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
77
129
|
|
|
78
130
|
return {
|
|
@@ -133,18 +185,29 @@ async function compilePolymorphicJs(mapping) {
|
|
|
133
185
|
jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
|
|
134
186
|
}
|
|
135
187
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const worker = new Worker(new URL('./obfuscation.worker.js', import.meta.url));
|
|
190
|
+
worker.on('message', (message) => {
|
|
191
|
+
if (message.error) {
|
|
192
|
+
console.error('[Fingerprint] Obfuscation worker error:', message.error, message.stack);
|
|
193
|
+
reject(new Error('Obfuscation failed in worker.'));
|
|
194
|
+
} else {
|
|
195
|
+
resolve(message.obfuscatedCode);
|
|
196
|
+
}
|
|
197
|
+
worker.terminate();
|
|
198
|
+
});
|
|
199
|
+
worker.on('error', (error) => {
|
|
200
|
+
console.error('[Fingerprint] Obfuscation worker crashed:', error);
|
|
201
|
+
reject(error);
|
|
202
|
+
});
|
|
203
|
+
worker.on('exit', (code) => {
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
console.error(`[Fingerprint] Obfuscation worker stopped with exit code ${code}`);
|
|
206
|
+
reject(new Error(`Obfuscation worker stopped with exit code ${code}`));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
worker.postMessage({ jsCode, seed: mapping.wasmConstants.seed });
|
|
145
210
|
});
|
|
146
|
-
|
|
147
|
-
return obfuscationResult.getObfuscatedCode();
|
|
148
211
|
}
|
|
149
212
|
|
|
150
213
|
async function ensureLatestMapping() {
|
|
@@ -699,6 +762,54 @@ const cipherSuiteMap = {
|
|
|
699
762
|
'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
|
|
700
763
|
};
|
|
701
764
|
|
|
765
|
+
/**
|
|
766
|
+
* Valide cryptographiquement l'attestation/assertion WebAuthn émise par l'enclave sécurisée.
|
|
767
|
+
* @private
|
|
768
|
+
* @param {object} anchor Les données d'attestation WebAuthn reçues du client
|
|
769
|
+
* @param {object} deviceData Les données persistantes de l'appareil dans notre store
|
|
770
|
+
* @returns {boolean} True si la signature matérielle est valide
|
|
771
|
+
*/
|
|
772
|
+
function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
|
|
773
|
+
if (!anchor || !anchor.type) return false;
|
|
774
|
+
|
|
775
|
+
try {
|
|
776
|
+
const clientDataHash = crypto.createHash('sha256')
|
|
777
|
+
.update(Buffer.from(anchor.clientDataJSON, 'base64'))
|
|
778
|
+
.digest();
|
|
779
|
+
|
|
780
|
+
if (anchor.type === 'registration') {
|
|
781
|
+
if (!anchor.publicKey || !anchor.credentialId) return false;
|
|
782
|
+
|
|
783
|
+
// Enregistrement initial : on stocke la clé publique matérielle SPKI
|
|
784
|
+
deviceData.webauthnPublicKey = anchor.publicKey;
|
|
785
|
+
deviceData.webauthnCredentialId = anchor.credentialId;
|
|
786
|
+
return true;
|
|
787
|
+
} else if (anchor.type === 'assertion') {
|
|
788
|
+
const storedPublicKeyPem = deviceData.webauthnPublicKey;
|
|
789
|
+
if (!storedPublicKeyPem || deviceData.webauthnCredentialId !== anchor.credentialId) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Reconstitution du message signé (authenticatorData + clientDataHash)
|
|
794
|
+
const verifyBuffer = Buffer.concat([
|
|
795
|
+
Buffer.from(anchor.authenticatorData, 'base64'),
|
|
796
|
+
clientDataHash
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
const publicKey = crypto.createPublicKey(Buffer.from(storedPublicKeyPem, 'base64'));
|
|
800
|
+
return crypto.verify(
|
|
801
|
+
'sha256',
|
|
802
|
+
verifyBuffer,
|
|
803
|
+
publicKey,
|
|
804
|
+
Buffer.from(anchor.signature, 'base64')
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
} catch (e) {
|
|
808
|
+
console.error('[WebAuthn-Server] Verification failed:', e.message);
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
702
813
|
/**
|
|
703
814
|
* Extracts TLS fingerprints (JA3 and JA4) from request context.
|
|
704
815
|
* Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
|
|
@@ -1455,9 +1566,10 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1455
1566
|
}
|
|
1456
1567
|
|
|
1457
1568
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1458
|
-
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1569
|
+
const { nonce, sizeMb, queries, path, peerId, peerBlockIdx } = challengeDetails;
|
|
1459
1570
|
const solverCode = getPowSolverCode();
|
|
1460
1571
|
const safePath = sanitizeRedirectPath(path);
|
|
1572
|
+
const coopTimeout = securityConfig?.pospace?.coopTimeout ?? 15;
|
|
1461
1573
|
|
|
1462
1574
|
const challengeScript = `
|
|
1463
1575
|
async function solve() {
|
|
@@ -1466,16 +1578,49 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
|
|
|
1466
1578
|
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
1467
1579
|
const queries = ${JSON.stringify(queries)};
|
|
1468
1580
|
const sizeMb = ${sizeMb};
|
|
1581
|
+
const nodeId = nonce;
|
|
1582
|
+
const peerId = ${safeJsonStringify(peerId || '')};
|
|
1583
|
+
const peerBlockIdx = ${peerBlockIdx ?? -1};
|
|
1584
|
+
const coopTimeout = ${coopTimeout};
|
|
1469
1585
|
|
|
1470
1586
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1471
1587
|
await new Promise(r => setTimeout(r, 10));
|
|
1472
1588
|
|
|
1473
1589
|
try {
|
|
1474
1590
|
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
1591
|
+
|
|
1592
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
1593
|
+
// Enregistrement coopératif
|
|
1594
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1475
1597
|
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1476
|
-
|
|
1598
|
+
|
|
1599
|
+
let peerBlock = "";
|
|
1600
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
1601
|
+
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
1602
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
1603
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
|
|
1604
|
+
|
|
1605
|
+
let attempts = 0;
|
|
1606
|
+
while (attempts < coopTimeout) {
|
|
1607
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
|
|
1608
|
+
const data = await res.json();
|
|
1609
|
+
if (data.status === 'ready') {
|
|
1610
|
+
peerBlock = data.block_data;
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1614
|
+
attempts++;
|
|
1615
|
+
}
|
|
1616
|
+
if (!peerBlock) {
|
|
1617
|
+
document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
|
|
1477
1622
|
|
|
1478
|
-
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
1623
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
|
|
1479
1624
|
} catch(e) {
|
|
1480
1625
|
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
1481
1626
|
}
|
|
@@ -1879,10 +2024,29 @@ function getBehaviorScore(context) {
|
|
|
1879
2024
|
* @param {object} metrics - Les métriques comportementales parsées depuis le client.
|
|
1880
2025
|
* @returns {{timeInconsistencyScore: number}}
|
|
1881
2026
|
*/
|
|
1882
|
-
function getTimeInconsistencyScore(context, metrics) {
|
|
2027
|
+
function getTimeInconsistencyScore(context, metrics, deviceData = null) {
|
|
1883
2028
|
const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
|
|
1884
2029
|
let score = 0;
|
|
1885
2030
|
|
|
2031
|
+
if (deviceData && deviceData.sessionHmacKey && metrics.signature) {
|
|
2032
|
+
try {
|
|
2033
|
+
const copy = JSON.parse(JSON.stringify(metrics));
|
|
2034
|
+
const clientSig = copy.signature;
|
|
2035
|
+
delete copy.signature;
|
|
2036
|
+
const dataToSign = JSON.stringify(copy);
|
|
2037
|
+
const expectedSig = crypto.createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex'))
|
|
2038
|
+
.update(dataToSign)
|
|
2039
|
+
.digest('hex');
|
|
2040
|
+
if (!crypto.timingSafeEqual(Buffer.from(clientSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
|
|
2041
|
+
return { timeInconsistencyScore: 100 }; // Replay/tampering detected
|
|
2042
|
+
}
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
return { timeInconsistencyScore: 100 };
|
|
2045
|
+
}
|
|
2046
|
+
} else if (deviceData && deviceData.sessionHmacKey && !metrics.signature) {
|
|
2047
|
+
return { timeInconsistencyScore: 100 }; // Missing mandatory signature
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1886
2050
|
if (metrics.clientTimestamp && context.requestTimestamp) {
|
|
1887
2051
|
const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
|
|
1888
2052
|
|
|
@@ -2351,80 +2515,6 @@ function getClickVarianceScore(context) {
|
|
|
2351
2515
|
* @returns {string|null} The subnet CIDR or null if the IP is invalid.
|
|
2352
2516
|
*/
|
|
2353
2517
|
function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-line no-unused-vars
|
|
2354
|
-
/**
|
|
2355
|
-
* @private
|
|
2356
|
-
* Parses an IPv6 string, expanding '::' into a 16-byte Buffer.
|
|
2357
|
-
* @param {string} ipStr The IPv6 address string.
|
|
2358
|
-
* @returns {Buffer|null}
|
|
2359
|
-
*/
|
|
2360
|
-
const parseIPv6 = (ipStr) => {
|
|
2361
|
-
const parts = ipStr.split('::');
|
|
2362
|
-
if (parts.length > 2) return null;
|
|
2363
|
-
|
|
2364
|
-
let hextets = [];
|
|
2365
|
-
if (parts[0]) hextets.push(...parts[0].split(':'));
|
|
2366
|
-
|
|
2367
|
-
if (parts.length === 2) {
|
|
2368
|
-
const hextetsInPart2 = parts[1] ? parts[1].split(':').length : 0;
|
|
2369
|
-
const zerosToInsert = 8 - hextets.length - hextetsInPart2;
|
|
2370
|
-
for (let i = 0; i < zerosToInsert; i++) {
|
|
2371
|
-
hextets.push('0');
|
|
2372
|
-
}
|
|
2373
|
-
if (parts[1]) hextets.push(...parts[1].split(':'));
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
if (hextets.length !== 8) return null;
|
|
2377
|
-
|
|
2378
|
-
const buffer = Buffer.alloc(16);
|
|
2379
|
-
for (let i = 0; i < 8; i++) {
|
|
2380
|
-
const val = parseInt(hextets[i] || '0', 16);
|
|
2381
|
-
if (isNaN(val)) return null;
|
|
2382
|
-
buffer.writeUInt16BE(val, i * 2);
|
|
2383
|
-
}
|
|
2384
|
-
return buffer;
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
/**
|
|
2388
|
-
* @private
|
|
2389
|
-
* Formats a 16-byte IPv6 buffer into a compressed string representation.
|
|
2390
|
-
* @param {Buffer} buffer The 16-byte buffer.
|
|
2391
|
-
* @returns {string}
|
|
2392
|
-
*/
|
|
2393
|
-
const formatIPv6 = (buffer) => {
|
|
2394
|
-
const hextets = [];
|
|
2395
|
-
for (let i = 0; i < 16; i += 2) {
|
|
2396
|
-
hextets.push(buffer.readUInt16BE(i).toString(16));
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
let bestStart = -1, bestLength = 0, currentStart = -1, currentLength = 0;
|
|
2400
|
-
for (let i = 0; i < hextets.length; i++) {
|
|
2401
|
-
if (hextets[i] === '0') {
|
|
2402
|
-
if (currentStart === -1) currentStart = i;
|
|
2403
|
-
currentLength++;
|
|
2404
|
-
} else {
|
|
2405
|
-
if (currentLength > bestLength) {
|
|
2406
|
-
bestStart = currentStart;
|
|
2407
|
-
bestLength = currentLength;
|
|
2408
|
-
}
|
|
2409
|
-
currentStart = -1;
|
|
2410
|
-
currentLength = 0;
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
if (currentLength > bestLength) {
|
|
2414
|
-
bestStart = currentStart;
|
|
2415
|
-
bestLength = currentLength;
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
// For subnet calculations, an uncompressed view is often clearer.
|
|
2419
|
-
// We will avoid compression to match test expectations.
|
|
2420
|
-
// if (bestLength > 1) {
|
|
2421
|
-
// const part1 = hextets.slice(0, bestStart).join(':');
|
|
2422
|
-
// const part2 = hextets.slice(bestStart + bestLength).join(':');
|
|
2423
|
-
// return `${part1}::${part2}`;
|
|
2424
|
-
// }
|
|
2425
|
-
return hextets.join(':');
|
|
2426
|
-
};
|
|
2427
|
-
|
|
2428
2518
|
try {
|
|
2429
2519
|
if (isIPv4(ip)) {
|
|
2430
2520
|
const ipBuffer = Buffer.from(ip.split('.').map(Number));
|
|
@@ -2433,14 +2523,37 @@ function getIpSubnet(ip, ipv4Prefix = 24, ipv6Prefix = 48) { // eslint-disable-l
|
|
|
2433
2523
|
for (let i = 0; i < 4; i++) ipBuffer[i] &= mask[i];
|
|
2434
2524
|
return `${Array.from(ipBuffer).join('.')}/${ipv4Prefix}`;
|
|
2435
2525
|
} else if (isIPv6(ip)) {
|
|
2436
|
-
|
|
2437
|
-
if (
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2526
|
+
let normalized = ip.trim().toLowerCase();
|
|
2527
|
+
if (normalized.includes("::")) {
|
|
2528
|
+
const parts = normalized.split("::");
|
|
2529
|
+
if (parts.length > 2) return null;
|
|
2530
|
+
const left = parts[0] ? parts[0].split(":") : [];
|
|
2531
|
+
const right = parts[1] ? parts[1].split(":") : [];
|
|
2532
|
+
const missing = 8 - (left.length + right.length);
|
|
2533
|
+
const middle = Array(missing).fill("0000");
|
|
2534
|
+
normalized = [...left, ...middle, ...right].join(":");
|
|
2535
|
+
} else {
|
|
2536
|
+
const parts = normalized.split(":");
|
|
2537
|
+
if (parts.length !== 8) return null;
|
|
2538
|
+
}
|
|
2539
|
+
const groups = normalized.split(":").map(g => {
|
|
2540
|
+
const val = parseInt(g, 16);
|
|
2541
|
+
return isNaN(val) ? "0000" : val.toString(16).padStart(4, "0");
|
|
2542
|
+
});
|
|
2543
|
+
for (let i = 0; i < 8; i++) {
|
|
2544
|
+
const startBit = i * 16;
|
|
2545
|
+
if (ipv6Prefix >= (i + 1) * 16) {
|
|
2546
|
+
continue;
|
|
2547
|
+
} else if (ipv6Prefix <= startBit) {
|
|
2548
|
+
groups[i] = "0000";
|
|
2549
|
+
} else {
|
|
2550
|
+
const bitsToKeep = ipv6Prefix - startBit;
|
|
2551
|
+
const val = parseInt(groups[i], 16);
|
|
2552
|
+
const mask = (0xffff << (16 - bitsToKeep)) & 0xffff;
|
|
2553
|
+
groups[i] = (val & mask).toString(16).padStart(4, "0");
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return `${groups.join(":")}/${ipv6Prefix}`;
|
|
2444
2557
|
}
|
|
2445
2558
|
} catch (e) {
|
|
2446
2559
|
// Catch any unexpected errors during parsing or manipulation
|
|
@@ -2853,12 +2966,19 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
2853
2966
|
// Case 2: New user or lost/invalid cookie.
|
|
2854
2967
|
deviceId = crypto.randomUUID(); // Generate a new "passport".
|
|
2855
2968
|
|
|
2969
|
+
const isHttps = context.headers?.['x-forwarded-proto'] === 'https' ||
|
|
2970
|
+
context.rawReq?.secure ||
|
|
2971
|
+
context.rawReq?.protocol === 'https' ||
|
|
2972
|
+
context.rawReq?.connection?.encrypted;
|
|
2973
|
+
const secureOption = isHttps || process.env.NODE_ENV === "production";
|
|
2974
|
+
|
|
2856
2975
|
// Return the intention to set a cookie.
|
|
2857
2976
|
newCookie = {
|
|
2858
2977
|
name: "device_id",
|
|
2859
2978
|
value: deviceId,
|
|
2860
2979
|
options: {
|
|
2861
|
-
httpOnly: true, secure:
|
|
2980
|
+
httpOnly: true, secure: secureOption, sameSite: "strict",
|
|
2981
|
+
...(secureOption && { partitioned: true }),
|
|
2862
2982
|
// Le maxAge est maintenant configurable. Par défaut, c'est un cookie de session.
|
|
2863
2983
|
...(securityConfig.deviceIdCookieMaxAge && { maxAge: securityConfig.deviceIdCookieMaxAge }),
|
|
2864
2984
|
}
|
|
@@ -2931,6 +3051,23 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
2931
3051
|
deviceData.lastFpHash = currentFpHash;
|
|
2932
3052
|
deviceData.ips.add(clientIp); // Record the IP used by this device
|
|
2933
3053
|
|
|
3054
|
+
// --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
|
|
3055
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3056
|
+
let webauthnVerified = false;
|
|
3057
|
+
if (behaviorHeader) {
|
|
3058
|
+
try {
|
|
3059
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
3060
|
+
if (metrics && metrics.webauthnAnchor) {
|
|
3061
|
+
if (verifyWebAuthnHardwareAnchor(metrics.webauthnAnchor, deviceData)) {
|
|
3062
|
+
webauthnVerified = true;
|
|
3063
|
+
deviceData.webauthnVerified = true;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
} catch (e) {
|
|
3067
|
+
// Ignorer les erreurs de parsing
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2934
3071
|
// NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
|
|
2935
3072
|
// Très efficace contre la rotation de proxy.
|
|
2936
3073
|
const maxIpsForDevice = isSharedIp
|
|
@@ -2974,7 +3111,6 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2974
3111
|
context._newCookies = context._newCookies || [];
|
|
2975
3112
|
context._newCookies.push(newCookie);
|
|
2976
3113
|
}
|
|
2977
|
-
await store.set(`ip-device:${clientIp}`, deviceId, 600); // Link the IP to the device for 10 minutes
|
|
2978
3114
|
|
|
2979
3115
|
// Periodically clean up device data
|
|
2980
3116
|
if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
|
|
@@ -2983,48 +3119,56 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2983
3119
|
}
|
|
2984
3120
|
deviceData.lastUpdate = Date.now();
|
|
2985
3121
|
|
|
2986
|
-
const
|
|
2987
|
-
const
|
|
2988
|
-
// Calculate the inconsistency score here, separately.
|
|
2989
|
-
let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
2990
|
-
|
|
2991
|
-
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
2992
|
-
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
2993
|
-
inconsistencyScore = 100;
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
2997
|
-
|
|
2998
|
-
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
2999
|
-
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
3122
|
+
const stableFp = extractStablePart(currentDeviceHash);
|
|
3123
|
+
const stableFpHash = cyrb53(stableFp).toString();
|
|
3000
3124
|
|
|
3001
|
-
|
|
3125
|
+
// Execute non-interdependent asynchronous operations in parallel
|
|
3126
|
+
const [
|
|
3127
|
+
behavioral,
|
|
3128
|
+
{ tlsSpoofingScore },
|
|
3129
|
+
{ subnetScore },
|
|
3130
|
+
ipReputationScore,
|
|
3131
|
+
{ botnetClusterScore },
|
|
3132
|
+
_ // store.set result
|
|
3133
|
+
] = await Promise.all([
|
|
3134
|
+
getBehavioralIndicators(context, deviceData),
|
|
3135
|
+
getTlsSpoofingScore(context),
|
|
3136
|
+
getSubnetScore(context, deviceId),
|
|
3137
|
+
getIpReputationScore(clientIp),
|
|
3138
|
+
getBotnetClusterScore(context, stableFpHash),
|
|
3139
|
+
store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
|
|
3140
|
+
]);
|
|
3141
|
+
|
|
3142
|
+
// Synchronous calculations
|
|
3143
|
+
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
3144
|
+
let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
3145
|
+
|
|
3146
|
+
// NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
|
|
3147
|
+
if (consistencyScore < 0.7) { // Seuil de rupture
|
|
3148
|
+
inconsistencyScore = 100;
|
|
3149
|
+
}
|
|
3002
3150
|
|
|
3003
|
-
|
|
3151
|
+
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
3004
3152
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3153
|
+
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
3154
|
+
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
3007
3155
|
|
|
3008
|
-
|
|
3009
|
-
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
3156
|
+
const { botScore } = getBotScore(context);
|
|
3010
3157
|
|
|
3011
|
-
|
|
3012
|
-
|
|
3158
|
+
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
3159
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
|
|
3013
3160
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3161
|
+
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
3162
|
+
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
3016
3163
|
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
const { subnetScore } = await getSubnetScore(context, deviceId);
|
|
3164
|
+
// NOUVEAU: On calcule le score de variance des clics.
|
|
3165
|
+
const { clickVarianceScore } = getClickVarianceScore(context);
|
|
3020
3166
|
|
|
3021
|
-
|
|
3167
|
+
// NOUVEAU: On calcule le score d'incohérence des Client-Hints.
|
|
3168
|
+
const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
|
|
3022
3169
|
|
|
3023
|
-
|
|
3170
|
+
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
3024
3171
|
|
|
3025
|
-
const stableFp = extractStablePart(currentDeviceHash);
|
|
3026
|
-
const stableFpHash = cyrb53(stableFp).toString();
|
|
3027
|
-
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
3028
3172
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3029
3173
|
const { quicAnomalyScore } = getQuicAnomalyScore(context);
|
|
3030
3174
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
@@ -3163,7 +3307,40 @@ export function generateCpuTargetChallenge(
|
|
|
3163
3307
|
};
|
|
3164
3308
|
}
|
|
3165
3309
|
|
|
3166
|
-
|
|
3310
|
+
class SimpleLRUCache {
|
|
3311
|
+
constructor(maxSize = 100) {
|
|
3312
|
+
this.maxSize = maxSize;
|
|
3313
|
+
this.cache = new Map();
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
get(key) {
|
|
3317
|
+
if (!this.cache.has(key)) return undefined;
|
|
3318
|
+
const value = this.cache.get(key);
|
|
3319
|
+
this.cache.delete(key);
|
|
3320
|
+
this.cache.set(key, value);
|
|
3321
|
+
return value;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
set(key, value) {
|
|
3325
|
+
if (this.cache.has(key)) {
|
|
3326
|
+
this.cache.delete(key);
|
|
3327
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
3328
|
+
const lruKey = this.cache.keys().next().value;
|
|
3329
|
+
this.cache.delete(lruKey);
|
|
3330
|
+
}
|
|
3331
|
+
this.cache.set(key, value);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
has(key) {
|
|
3335
|
+
return this.cache.has(key);
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
clear() {
|
|
3339
|
+
this.cache.clear();
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
const htmlTemplateCache = new SimpleLRUCache(100);
|
|
3167
3344
|
|
|
3168
3345
|
/**
|
|
3169
3346
|
* Generates the HTML page for the CPU target challenge.
|
|
@@ -3250,7 +3427,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3250
3427
|
}
|
|
3251
3428
|
|
|
3252
3429
|
// Redirect with both solutions and the fingerprint used to solve.
|
|
3253
|
-
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
3430
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
|
|
3254
3431
|
window.location.href = finalUrl;
|
|
3255
3432
|
}
|
|
3256
3433
|
|
|
@@ -3705,6 +3882,10 @@ export class FingerprintEngine {
|
|
|
3705
3882
|
return false;
|
|
3706
3883
|
}
|
|
3707
3884
|
|
|
3885
|
+
if (!canAttemptDns()) {
|
|
3886
|
+
return false;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3708
3889
|
const cacheKey = `ip-whitelist:${clientIp}`;
|
|
3709
3890
|
const cachedStatus = await store.get(cacheKey);
|
|
3710
3891
|
|
|
@@ -3717,32 +3898,35 @@ export class FingerprintEngine {
|
|
|
3717
3898
|
|
|
3718
3899
|
try {
|
|
3719
3900
|
// 1. Reverse DNS lookup
|
|
3720
|
-
const hostnames = await dns.reverse(clientIp);
|
|
3901
|
+
const hostnames = await withTimeout(dns.reverse(clientIp), 500);
|
|
3721
3902
|
const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
|
|
3722
3903
|
|
|
3723
3904
|
if (!validHostname) {
|
|
3724
|
-
await store.set(cacheKey, 'failed',
|
|
3905
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3725
3906
|
return false;
|
|
3726
3907
|
}
|
|
3727
3908
|
|
|
3728
3909
|
// 2. Forward DNS lookup
|
|
3729
3910
|
let addresses = [];
|
|
3730
3911
|
try {
|
|
3731
|
-
|
|
3912
|
+
addresses = await withTimeout(dns.resolve(validHostname), 500);
|
|
3732
3913
|
} catch (e) {}
|
|
3733
3914
|
try {
|
|
3734
|
-
|
|
3915
|
+
const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
|
|
3735
3916
|
addresses = addresses.concat(ipv6);
|
|
3736
3917
|
} catch (e) {}
|
|
3737
3918
|
if (addresses.includes(clientIp)) {
|
|
3919
|
+
recordDnsSuccess();
|
|
3738
3920
|
await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
|
|
3739
3921
|
return true;
|
|
3740
3922
|
}
|
|
3741
3923
|
} catch (error) {
|
|
3742
|
-
|
|
3924
|
+
recordDnsFailure();
|
|
3925
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3926
|
+
return false;
|
|
3743
3927
|
}
|
|
3744
3928
|
|
|
3745
|
-
await store.set(cacheKey, 'failed',
|
|
3929
|
+
await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
|
|
3746
3930
|
return false;
|
|
3747
3931
|
}
|
|
3748
3932
|
|
|
@@ -3764,13 +3948,19 @@ export class FingerprintEngine {
|
|
|
3764
3948
|
|
|
3765
3949
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
3766
3950
|
|
|
3951
|
+
// Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
|
|
3952
|
+
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
3953
|
+
if (deviceData && deviceData.webauthnVerified) {
|
|
3954
|
+
this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
|
|
3955
|
+
return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3767
3958
|
if (isStatic) {
|
|
3768
3959
|
this._log('Static resource - skipping checks');
|
|
3769
3960
|
return { action: 'next', score: 0, vector: {} };
|
|
3770
3961
|
}
|
|
3771
3962
|
|
|
3772
3963
|
// Resolve identity and check for persisted "condemned" status early.
|
|
3773
|
-
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
3774
3964
|
const currentDeviceHash = getCompositeDeviceHash(requestContext);
|
|
3775
3965
|
const isNewDevice = !!newCookie;
|
|
3776
3966
|
const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
|
|
@@ -4040,6 +4230,11 @@ export class FingerprintEngine {
|
|
|
4040
4230
|
const finalQueryString = finalSearchParams.toString();
|
|
4041
4231
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
4042
4232
|
this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
|
|
4233
|
+
const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
|
|
4234
|
+
requestContext.rawReq?.secure ||
|
|
4235
|
+
requestContext.rawReq?.protocol === 'https' ||
|
|
4236
|
+
requestContext.rawReq?.connection?.encrypted;
|
|
4237
|
+
const secureOption = isHttps || this.isProduction;
|
|
4043
4238
|
return {
|
|
4044
4239
|
action: 'redirect',
|
|
4045
4240
|
path: finalRedirectPath,
|
|
@@ -4048,7 +4243,13 @@ export class FingerprintEngine {
|
|
|
4048
4243
|
cookie: {
|
|
4049
4244
|
name: 'pow_clearance',
|
|
4050
4245
|
value: ticket, // The ticket itself
|
|
4051
|
-
options: {
|
|
4246
|
+
options: {
|
|
4247
|
+
httpOnly: true,
|
|
4248
|
+
secure: secureOption,
|
|
4249
|
+
sameSite: 'strict',
|
|
4250
|
+
...(secureOption && { partitioned: true }),
|
|
4251
|
+
maxAge: finalTtl
|
|
4252
|
+
} // Options for setting the cookie
|
|
4052
4253
|
}
|
|
4053
4254
|
};
|
|
4054
4255
|
} else {
|
|
@@ -4123,7 +4324,7 @@ export class FingerprintEngine {
|
|
|
4123
4324
|
if (challengeContext) {
|
|
4124
4325
|
try {
|
|
4125
4326
|
const workResult = JSON.parse(pow_solution_work_result);
|
|
4126
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
4327
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
4127
4328
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
4128
4329
|
const manager = await getProblemManager({
|
|
4129
4330
|
configPath,
|
|
@@ -4178,17 +4379,22 @@ export class FingerprintEngine {
|
|
|
4178
4379
|
|
|
4179
4380
|
const isBlocked = finalScore >= blockThreshold;
|
|
4180
4381
|
|
|
4181
|
-
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
4382
|
+
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
|
|
4182
4383
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
4183
4384
|
const isSuspicious = finalScore >= thresholds.low;
|
|
4184
4385
|
const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
|
|
4185
4386
|
|
|
4186
4387
|
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
4187
|
-
const suspicionFactor = isSuspicious
|
|
4188
|
-
?
|
|
4189
|
-
|
|
4190
|
-
(
|
|
4191
|
-
|
|
4388
|
+
const suspicionFactor = isSuspicious // eslint-disable-line no-nested-ternary
|
|
4389
|
+
? (() => {
|
|
4390
|
+
const denominator = thresholds.high - thresholds.low;
|
|
4391
|
+
if (denominator === 0) {
|
|
4392
|
+
// If the range is zero, and finalScore is at or above low threshold,
|
|
4393
|
+
// return 0 to avoid NaN.
|
|
4394
|
+
return 0;
|
|
4395
|
+
}
|
|
4396
|
+
return Math.min(1.5, (finalScore - thresholds.low) / denominator);
|
|
4397
|
+
})()
|
|
4192
4398
|
: 0;
|
|
4193
4399
|
|
|
4194
4400
|
this._log('Suspicion levels evaluated', {
|
|
@@ -4319,7 +4525,7 @@ export class FingerprintEngine {
|
|
|
4319
4525
|
this._log('Issuing a useful work challenge', { finalScore });
|
|
4320
4526
|
|
|
4321
4527
|
try {
|
|
4322
|
-
const defaultPath = resolve(process.cwd(), 'problems.config.json');
|
|
4528
|
+
const defaultPath = resolve(process.cwd(), 'config', 'problems.config.json');
|
|
4323
4529
|
const configPath = this.securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
4324
4530
|
const manager = await getProblemManager({
|
|
4325
4531
|
configPath,
|
|
@@ -5278,14 +5484,14 @@ function getTlsSessionId(context) {
|
|
|
5278
5484
|
}
|
|
5279
5485
|
|
|
5280
5486
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
5281
|
-
const staticFileCache = new
|
|
5487
|
+
const staticFileCache = new SimpleLRUCache(50);
|
|
5282
5488
|
|
|
5283
5489
|
export const powMiddleware = (securityConfig) => {
|
|
5284
5490
|
const engine = new FingerprintEngine(securityConfig);
|
|
5285
5491
|
|
|
5286
5492
|
// Initialize the problem manager with the configured path, if provided.
|
|
5287
5493
|
if (securityConfig.enableUsefulWork) {
|
|
5288
|
-
const defaultPath = resolve(__dirname, '..', '..', 'problems.config.json');
|
|
5494
|
+
const defaultPath = resolve(__dirname, '..', '..', 'config', 'problems.config.json');
|
|
5289
5495
|
const configPath = securityConfig.usefulWorkConfigPath || (existsSync(defaultPath) ? defaultPath : undefined);
|
|
5290
5496
|
getProblemManager({
|
|
5291
5497
|
configPath,
|
|
@@ -5310,6 +5516,36 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5310
5516
|
}
|
|
5311
5517
|
|
|
5312
5518
|
return async (req, res, next) => {
|
|
5519
|
+
if (req.query && req.query.fp_handshake) {
|
|
5520
|
+
const clientKeyHex = req.headers['x-client-ephemeral-key'];
|
|
5521
|
+
if (clientKeyHex) {
|
|
5522
|
+
try {
|
|
5523
|
+
const serverECDH = crypto.createECDH('prime256v1');
|
|
5524
|
+
serverECDH.generateKeys();
|
|
5525
|
+
const serverPubKeyHex = serverECDH.getKeys('hex');
|
|
5526
|
+
const sharedSecret = serverECDH.computeSecret(clientKeyHex, 'hex');
|
|
5527
|
+
const hmacKey = crypto.createHash('sha256').update(sharedSecret).digest('hex');
|
|
5528
|
+
|
|
5529
|
+
const requestContext = {
|
|
5530
|
+
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
5531
|
+
headers: req.headers,
|
|
5532
|
+
cookies: req.cookies
|
|
5533
|
+
};
|
|
5534
|
+
const { deviceId, deviceData } = await resolveRequestIdentity(requestContext, securityConfig);
|
|
5535
|
+
if (deviceData) {
|
|
5536
|
+
deviceData.sessionHmacKey = hmacKey;
|
|
5537
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
res.setHeader('x-server-ephemeral-key', serverPubKeyHex);
|
|
5541
|
+
return res.status(200).json({ status: 'success' });
|
|
5542
|
+
} catch (e) {
|
|
5543
|
+
return res.status(400).json({ error: 'Handshake failed' });
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
return res.status(400).json({ error: 'Missing client key' });
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5313
5549
|
if (!req.headers_translated) {
|
|
5314
5550
|
req.headers_translated = true;
|
|
5315
5551
|
const matchedMapping = getActiveMappingForRequest(req.headers);
|
|
@@ -5484,6 +5720,7 @@ export const __internal = {
|
|
|
5484
5720
|
getTlsFingerprint, // NOUVEAU: Expose pour les tests
|
|
5485
5721
|
sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
|
|
5486
5722
|
getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
|
|
5723
|
+
verifyWebAuthnHardwareAnchor,
|
|
5487
5724
|
generateStatelessTicket,
|
|
5488
5725
|
parseStatelessTicket,
|
|
5489
5726
|
parseJa3,
|
|
@@ -5500,11 +5737,17 @@ export const __internal = {
|
|
|
5500
5737
|
setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
|
|
5501
5738
|
verifyZkpProof,
|
|
5502
5739
|
modPow,
|
|
5740
|
+
generateSessionMapping,
|
|
5741
|
+
compilePolymorphicJs,
|
|
5503
5742
|
parseTcpSyn, // Expose for testing
|
|
5504
5743
|
classifyTcpOs, // Expose for testing
|
|
5505
5744
|
getTcpAnomalyScore, // Expose for testing,
|
|
5506
5745
|
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5507
5746
|
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5747
|
+
dnsCircuitBreaker,
|
|
5748
|
+
recordDnsSuccess,
|
|
5749
|
+
recordDnsFailure,
|
|
5750
|
+
canAttemptDns,
|
|
5508
5751
|
registerCooperativeNode,
|
|
5509
5752
|
findPeerInSubnet,
|
|
5510
5753
|
handleCooperativeRequest
|
|
@@ -5526,48 +5769,79 @@ export function sanitizeTrafficData(trafficData) {
|
|
|
5526
5769
|
if (!trafficData || trafficData.length === 0) {
|
|
5527
5770
|
return [];
|
|
5528
5771
|
}
|
|
5529
|
-
const
|
|
5772
|
+
const suspiciousLogs = [];
|
|
5773
|
+
const passedLogs = [];
|
|
5530
5774
|
const deviceCounts = new Map();
|
|
5531
5775
|
const ipCounts = new Map();
|
|
5532
5776
|
const subnetCounts = new Map();
|
|
5777
|
+
const hardwareClusterCounts = new Map();
|
|
5778
|
+
const hwClusterCache = new Map();
|
|
5533
5779
|
|
|
5534
5780
|
// Calcul des quotas maximums pour éviter l'influence démesurée d'une entité
|
|
5535
5781
|
const maxLogsPerDevice = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% contribution per device
|
|
5536
5782
|
const maxLogsPerIp = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par adresse IP individuelle
|
|
5537
5783
|
const maxLogsPerSubnet = Math.max(5, Math.floor(trafficData.length * 0.05)); // Max 5% par bloc réseau (anti-proxy-rotation)
|
|
5784
|
+
const maxLogsPerHardwareCluster = Math.max(3, Math.floor(trafficData.length * 0.02)); // Max 2% par cluster matériel stable
|
|
5785
|
+
|
|
5786
|
+
const getHardwareCluster = (log) => {
|
|
5787
|
+
const fp = log.deviceHash || log.fingerprint || log.deviceFingerprint || '';
|
|
5788
|
+
if (!fp || typeof fp !== 'string') {
|
|
5789
|
+
return log.deviceId || 'anonymous-cluster';
|
|
5790
|
+
}
|
|
5791
|
+
if (hwClusterCache.has(fp)) {
|
|
5792
|
+
return hwClusterCache.get(fp);
|
|
5793
|
+
}
|
|
5794
|
+
const parts = fp.split('|');
|
|
5795
|
+
const hwComponents = [];
|
|
5796
|
+
for (const part of parts) {
|
|
5797
|
+
const pair = part.split(':');
|
|
5798
|
+
if (pair.length === 2 && (pair[0] === 'gpu' || pair[0] === 'cvs' || pair[0] === 'hw')) {
|
|
5799
|
+
hwComponents.push(part);
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
const result = hwComponents.length > 0 ? hwComponents.sort().join('|') : (log.deviceId || 'anonymous-cluster');
|
|
5803
|
+
hwClusterCache.set(fp, result);
|
|
5804
|
+
return result;
|
|
5805
|
+
};
|
|
5538
5806
|
|
|
5539
5807
|
for (const log of trafficData) {
|
|
5540
5808
|
const devId = log.deviceId || 'anonymous';
|
|
5541
5809
|
const ip = log.clientIp || log.ip || 'unknown';
|
|
5542
5810
|
const subnet = getIpSubnet(ip) || 'unknown-subnet';
|
|
5811
|
+
const hwCluster = getHardwareCluster(log);
|
|
5543
5812
|
|
|
5544
5813
|
const currentDeviceCount = deviceCounts.get(devId) || 0;
|
|
5545
5814
|
const currentIpCount = ipCounts.get(ip) || 0;
|
|
5546
5815
|
const currentSubnetCount = subnetCounts.get(subnet) || 0;
|
|
5816
|
+
const currentHwClusterCount = hardwareClusterCounts.get(hwCluster) || 0;
|
|
5547
5817
|
|
|
5548
|
-
// Filtrage anti-poisoning strict sur
|
|
5818
|
+
// Filtrage anti-poisoning strict sur 4 axes cumulatifs (incluant le clustering matériel stable)
|
|
5549
5819
|
if (
|
|
5550
5820
|
currentDeviceCount < maxLogsPerDevice &&
|
|
5551
5821
|
(ip === 'unknown' || currentIpCount < maxLogsPerIp) &&
|
|
5552
|
-
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet)
|
|
5822
|
+
(subnet === 'unknown-subnet' || currentSubnetCount < maxLogsPerSubnet) &&
|
|
5823
|
+
currentHwClusterCount < maxLogsPerHardwareCluster
|
|
5553
5824
|
) {
|
|
5554
5825
|
deviceCounts.set(devId, currentDeviceCount + 1);
|
|
5555
5826
|
if (ip !== 'unknown') ipCounts.set(ip, currentIpCount + 1);
|
|
5556
5827
|
if (subnet !== 'unknown-subnet') subnetCounts.set(subnet, currentSubnetCount + 1);
|
|
5828
|
+
hardwareClusterCounts.set(hwCluster, currentHwClusterCount + 1);
|
|
5557
5829
|
|
|
5558
|
-
|
|
5830
|
+
if (log.type === 'request_passed') {
|
|
5831
|
+
passedLogs.push(log);
|
|
5832
|
+
} else {
|
|
5833
|
+
suspiciousLogs.push(log);
|
|
5834
|
+
}
|
|
5559
5835
|
}
|
|
5560
5836
|
}
|
|
5561
5837
|
|
|
5562
|
-
const passedLogs = tempSanitized.filter(log => log.type === 'request_passed');
|
|
5563
|
-
const suspiciousLogs = tempSanitized.filter(log => log.type !== 'request_passed');
|
|
5564
|
-
|
|
5565
5838
|
const minDataPoints = 200; // Seuil par défaut
|
|
5566
5839
|
const maxPassedAllowed = Math.max(minDataPoints, suspiciousLogs.length * 9);
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5840
|
+
if (passedLogs.length > maxPassedAllowed) {
|
|
5841
|
+
const shuffledPassed = passedLogs.sort(() => 0.5 - Math.random());
|
|
5842
|
+
return [...suspiciousLogs, ...shuffledPassed.slice(0, maxPassedAllowed)];
|
|
5843
|
+
}
|
|
5844
|
+
return [...suspiciousLogs, ...passedLogs];
|
|
5571
5845
|
}
|
|
5572
5846
|
/**
|
|
5573
5847
|
* Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
|
|
@@ -5745,6 +6019,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
|
|
|
5745
6019
|
// Plus le ratio est équilibré et le volume important, plus nous faisons confiance au Front de Pareto.
|
|
5746
6020
|
const trafficConfidence = Math.min(1.5, Math.max(0.3, highConfidenceRatio * 4));
|
|
5747
6021
|
|
|
6022
|
+
// --- VALIDATION POST-CALCUL (Anti-empoisonnement & Validation Croisée) ---
|
|
6023
|
+
const tempConfig = {
|
|
6024
|
+
thresholds: { ...securityConfig.thresholds },
|
|
6025
|
+
weights: { ...securityConfig.weights },
|
|
6026
|
+
patterns: { ...securityConfig.patterns }
|
|
6027
|
+
};
|
|
6028
|
+
|
|
6029
|
+
applyInertialUpdate(tempConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
|
|
6030
|
+
applyInertialUpdate(tempConfig.weights, newConfig.weights, 'weights', trafficConfidence);
|
|
6031
|
+
applyInertialUpdate(tempConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
|
|
6032
|
+
|
|
6033
|
+
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: sanitizedData });
|
|
6034
|
+
const currentObjectives = fitnessFunction(securityConfig);
|
|
6035
|
+
const proposedObjectives = fitnessFunction(tempConfig);
|
|
6036
|
+
|
|
6037
|
+
const currentFPR = currentObjectives[0];
|
|
6038
|
+
const currentFNR = currentObjectives[1];
|
|
6039
|
+
const proposedFPR = proposedObjectives[0];
|
|
6040
|
+
const proposedFNR = proposedObjectives[1];
|
|
6041
|
+
|
|
6042
|
+
const validationTolerance = securityConfig?.autotuning?.validationTolerance ?? tuningOptions?.validationTolerance ?? 0.15;
|
|
6043
|
+
|
|
6044
|
+
if (proposedFPR > currentFPR + validationTolerance || proposedFNR > currentFNR + validationTolerance) {
|
|
6045
|
+
console.error(`[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: ${proposedFPR.toFixed(4)} (Current: ${currentFPR.toFixed(4)}), Proposed FNR: ${proposedFNR.toFixed(4)} (Current: ${currentFNR.toFixed(4)})`);
|
|
6046
|
+
if (securityConfig.logger && typeof securityConfig.logger === 'function') {
|
|
6047
|
+
securityConfig.logger({
|
|
6048
|
+
type: 'autotuning_instability_alert',
|
|
6049
|
+
proposedFPR,
|
|
6050
|
+
currentFPR,
|
|
6051
|
+
proposedFNR,
|
|
6052
|
+
currentFNR,
|
|
6053
|
+
timestamp: Date.now()
|
|
6054
|
+
});
|
|
6055
|
+
}
|
|
6056
|
+
return; // Rollback automatique : On arrête l'application
|
|
6057
|
+
}
|
|
6058
|
+
|
|
5748
6059
|
applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
|
|
5749
6060
|
applyInertialUpdate(securityConfig.weights, newConfig.weights, 'weights', trafficConfidence);
|
|
5750
6061
|
applyInertialUpdate(securityConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
|