@anonympins/fingerprint 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -123,7 +124,7 @@ const yandexEntries = loadBotWhitelist('yandex.json', [
123
124
  ]);
124
125
 
125
126
  function generateSessionMapping() {
126
- const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
127
+ const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
127
128
  const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
128
129
 
129
130
  return {
@@ -184,18 +185,29 @@ async function compilePolymorphicJs(mapping) {
184
185
  jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
185
186
  }
186
187
 
187
- const obfuscationResult = JavaScriptObfuscator.obfuscate(jsCode, {
188
- compact: true,
189
- controlFlowFlattening: true,
190
- deadCodeInjection: true,
191
- stringArray: true,
192
- stringArrayRotate: true,
193
- stringArrayShuffle: true,
194
- seed: Math.abs(mapping.wasmConstants.seed),
195
- selfDefending: true,
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 });
196
210
  });
197
-
198
- return obfuscationResult.getObfuscatedCode();
199
211
  }
200
212
 
201
213
  async function ensureLatestMapping() {
@@ -750,6 +762,181 @@ const cipherSuiteMap = {
750
762
  'TLS_RSA_WITH_3DES_EDE_CBC_SHA': 10,
751
763
  };
752
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
+ const TRUSTED_HARDWARE_ROOTS = [
773
+ "-----BEGIN CERTIFICATE-----\n" +
774
+ "MIIDHzCCAfegAwIBAgIJANCvWjvF+2O6MA0GCSqGSIb3DQEBCwUAMC0xKzApBgNV\n" +
775
+ "BAMTIll1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMB4XDTE0MDgwNDAwMDAwMFox\n" +
776
+ "TSUxSDBGBgNVBAMMT1l1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMSowKAYDVQQK\n" +
777
+ "EyFZdWJpY28gQUIxDzANBgNVBAcTBVN0b2NraG9sbTELMAkGA1UEBhMCU0UwggEi\n" +
778
+ "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6XW0d87g+N6kGgSgC/H9UfA2p\n" +
779
+ "-----END CERTIFICATE-----",
780
+ "-----BEGIN CERTIFICATE-----\n" +
781
+ "MIIB1DCCAXWgAwIBAgIEUI70WjAKBggqhkjOPQQDAjArMSkwJwYDVQQDEyBGSURP\n" +
782
+ "IEFsbGlhbmNlIFJvb3QgQ0EgKFRlc3QpMB4XDTE0MDgxODA4MzA0NVoXDTM5MDgx\n" +
783
+ "ODA4MzA0NVowKzEpMCcGA1UEAxMgRklETyBBbGxpYW5jZSBSb290IENBIChUZXN0\n" +
784
+ "KTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFv81Jm9M7AehfOIdpCH567gP0yqS40m\n" +
785
+ "aN0j1a8n152G7n/nUf7J0j9F4pL9J2w1X8hN1N8f9Y3G9w8L29/m7/zX3O3n2e7/\n" +
786
+ "g==\n" +
787
+ "-----END CERTIFICATE-----"
788
+ ];
789
+
790
+ function decodeCBOR(buffer) {
791
+ let offset = 0;
792
+ function readByte() {
793
+ if (offset >= buffer.length) throw new Error("Unexpected end of CBOR data");
794
+ return buffer[offset++];
795
+ }
796
+ function readBytes(len) {
797
+ if (offset + len > buffer.length) throw new Error("Unexpected end of CBOR bytes");
798
+ const res = buffer.slice(offset, offset + len);
799
+ offset += len;
800
+ return res;
801
+ }
802
+ function readInt(val) {
803
+ if (val < 24) return val;
804
+ if (val === 24) return readByte();
805
+ if (val === 25) {
806
+ const b1 = readByte(); const b2 = readByte();
807
+ return (b1 << 8) | b2;
808
+ }
809
+ if (val === 26) {
810
+ const b1 = readByte(); const b2 = readByte();
811
+ const b3 = readByte(); const b4 = readByte();
812
+ return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
813
+ }
814
+ throw new Error("Unsupported integer size: " + val);
815
+ }
816
+ function decodeType() {
817
+ const initial = readByte();
818
+ const major = initial >> 5;
819
+ const val = initial & 0x1f;
820
+ if (major === 0) {
821
+ return readInt(val);
822
+ } else if (major === 1) {
823
+ return -1 - readInt(val);
824
+ } else if (major === 2) {
825
+ const len = readInt(val);
826
+ return readBytes(len);
827
+ } else if (major === 3) {
828
+ const len = readInt(val);
829
+ return readBytes(len).toString('utf8');
830
+ } else if (major === 4) {
831
+ const len = readInt(val);
832
+ const arr = [];
833
+ for (let i = 0; i < len; i++) arr.push(decodeType());
834
+ return arr;
835
+ } else if (major === 5) {
836
+ const len = readInt(val);
837
+ const obj = {};
838
+ for (let i = 0; i < len; i++) {
839
+ const k = decodeType();
840
+ const v = decodeType();
841
+ obj[k] = v;
842
+ }
843
+ return obj;
844
+ }
845
+ return null;
846
+ }
847
+ return decodeType();
848
+ }
849
+
850
+ function verifyCertificateChain(x5c) {
851
+ if (!x5c || x5c.length === 0) return false;
852
+ try {
853
+ const pems = x5c.map(der => {
854
+ const base64 = der.toString('base64');
855
+ return `-----BEGIN CERTIFICATE-----\n${base64.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`;
856
+ });
857
+
858
+ for (let i = 0; i < pems.length - 1; i++) {
859
+ const child = new crypto.X509Certificate(pems[i]);
860
+ const parent = new crypto.X509Certificate(pems[i + 1]);
861
+ if (!child.verify(parent.publicKey)) {
862
+ return false;
863
+ }
864
+ }
865
+
866
+ const rootCert = new crypto.X509Certificate(pems[pems.length - 1]);
867
+ let trusted = false;
868
+ for (const trustedRootPem of TRUSTED_HARDWARE_ROOTS) {
869
+ const trustedRoot = new crypto.X509Certificate(trustedRootPem);
870
+ if (rootCert.subject === trustedRoot.subject) {
871
+ trusted = true;
872
+ break;
873
+ }
874
+ if (rootCert.verify(trustedRoot.publicKey)) {
875
+ trusted = true;
876
+ break;
877
+ }
878
+ }
879
+ return trusted;
880
+ } catch (e) {
881
+ return false;
882
+ }
883
+ }
884
+
885
+ function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
886
+ if (!anchor || !anchor.type) return false;
887
+
888
+ try {
889
+ const clientDataHash = crypto.createHash('sha256')
890
+ .update(Buffer.from(anchor.clientDataJSON, 'base64'))
891
+ .digest();
892
+
893
+ if (anchor.type === 'registration') {
894
+ if (!anchor.publicKey || !anchor.credentialId || !anchor.attestationObject) return false;
895
+
896
+ const attestationBytes = Buffer.from(anchor.attestationObject, 'base64');
897
+ const decoded = decodeCBOR(attestationBytes);
898
+
899
+ if (!decoded || !decoded.fmt || !decoded.attStmt) return false;
900
+
901
+ if (decoded.fmt !== 'none') {
902
+ const attStmt = decoded.attStmt;
903
+ if (!attStmt.x5c || !Array.isArray(attStmt.x5c)) return false;
904
+
905
+ if (!verifyCertificateChain(attStmt.x5c)) {
906
+ return false;
907
+ }
908
+ }
909
+
910
+ // Enregistrement initial : on stocke la clé publique matérielle SPKI
911
+ deviceData.webauthnPublicKey = anchor.publicKey;
912
+ deviceData.webauthnCredentialId = anchor.credentialId;
913
+ return true;
914
+ } else if (anchor.type === 'assertion') {
915
+ const storedPublicKeyPem = deviceData.webauthnPublicKey;
916
+ if (!storedPublicKeyPem || deviceData.webauthnCredentialId !== anchor.credentialId) {
917
+ return false;
918
+ }
919
+
920
+ // Reconstitution du message signé (authenticatorData + clientDataHash)
921
+ const verifyBuffer = Buffer.concat([
922
+ Buffer.from(anchor.authenticatorData, 'base64'),
923
+ clientDataHash
924
+ ]);
925
+
926
+ const publicKey = crypto.createPublicKey(Buffer.from(storedPublicKeyPem, 'base64'));
927
+ return crypto.verify(
928
+ 'sha256',
929
+ verifyBuffer,
930
+ publicKey,
931
+ Buffer.from(anchor.signature, 'base64')
932
+ );
933
+ }
934
+ } catch (e) {
935
+ console.error('[WebAuthn-Server] Verification failed:', e.message);
936
+ }
937
+ return false;
938
+ }
939
+
753
940
  /**
754
941
  * Extracts TLS fingerprints (JA3 and JA4) from request context.
755
942
  * Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
@@ -1523,6 +1710,14 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1523
1710
  const peerBlockIdx = ${peerBlockIdx ?? -1};
1524
1711
  const coopTimeout = ${coopTimeout};
1525
1712
 
1713
+ async function signCoop(op, nid, extra = "") {
1714
+ const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
1715
+ const encoder = new TextEncoder();
1716
+ const data = encoder.encode(msg);
1717
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1718
+ return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
1719
+ }
1720
+
1526
1721
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1527
1722
  await new Promise(r => setTimeout(r, 10));
1528
1723
 
@@ -1531,20 +1726,40 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1531
1726
 
1532
1727
  if (peerId && peerBlockIdx !== -1) {
1533
1728
  // Enregistrement coopératif
1534
- await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
1729
+ const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
1730
+ await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
1535
1731
  }
1536
1732
 
1537
- document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1733
+ // Écoute des requêtes entrantes de nos pairs
1734
+ setInterval(async () => {
1735
+ try {
1736
+ const sig = await signCoop("poll_requests", nodeId);
1737
+ const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
1738
+ const data = await res.json();
1739
+ if (data.requests && data.requests.length > 0) {
1740
+ for (const req of data.requests) {
1741
+ document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
1742
+ const blockData = await window.readSpaceBlock(req.block_idx);
1743
+ const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
1744
+ await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData) + "&coop_sig=" + respSig);
1745
+ }
1746
+ }
1747
+ } catch (e) {
1748
+ console.error("Cooperative polling error", e);
1749
+ }
1750
+ }, 1000);
1538
1751
 
1539
1752
  let peerBlock = "";
1540
1753
  if (peerId && peerBlockIdx !== -1) {
1541
1754
  document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1542
1755
  const reqId = Math.random().toString(36).substring(2);
1543
- await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
1756
+ const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1757
+ 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);
1544
1758
 
1545
1759
  let attempts = 0;
1546
1760
  while (attempts < coopTimeout) {
1547
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
1761
+ const pollSig = await signCoop("poll_response", nodeId, reqId);
1762
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1548
1763
  const data = await res.json();
1549
1764
  if (data.status === 'ready') {
1550
1765
  peerBlock = data.block_data;
@@ -1558,6 +1773,7 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1558
1773
  }
1559
1774
  }
1560
1775
 
1776
+ document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1561
1777
  const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
1562
1778
 
1563
1779
  window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
@@ -1964,10 +2180,29 @@ function getBehaviorScore(context) {
1964
2180
  * @param {object} metrics - Les métriques comportementales parsées depuis le client.
1965
2181
  * @returns {{timeInconsistencyScore: number}}
1966
2182
  */
1967
- function getTimeInconsistencyScore(context, metrics) {
2183
+ function getTimeInconsistencyScore(context, metrics, deviceData = null) {
1968
2184
  const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
1969
2185
  let score = 0;
1970
2186
 
2187
+ if (deviceData && deviceData.sessionHmacKey && metrics.signature) {
2188
+ try {
2189
+ const copy = JSON.parse(JSON.stringify(metrics));
2190
+ const clientSig = copy.signature;
2191
+ delete copy.signature;
2192
+ const dataToSign = JSON.stringify(copy);
2193
+ const expectedSig = crypto.createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex'))
2194
+ .update(dataToSign)
2195
+ .digest('hex');
2196
+ if (!crypto.timingSafeEqual(Buffer.from(clientSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
2197
+ return { timeInconsistencyScore: 100 }; // Replay/tampering detected
2198
+ }
2199
+ } catch (e) {
2200
+ return { timeInconsistencyScore: 100 };
2201
+ }
2202
+ } else if (deviceData && deviceData.sessionHmacKey && !metrics.signature) {
2203
+ return { timeInconsistencyScore: 100 }; // Missing mandatory signature
2204
+ }
2205
+
1971
2206
  if (metrics.clientTimestamp && context.requestTimestamp) {
1972
2207
  const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
1973
2208
 
@@ -2972,6 +3207,37 @@ async function getBehavioralIndicators(context, deviceData) {
2972
3207
  deviceData.lastFpHash = currentFpHash;
2973
3208
  deviceData.ips.add(clientIp); // Record the IP used by this device
2974
3209
 
3210
+ // Nettoyage par fenêtre glissante pour éviter l'accumulation sur les sessions longues
3211
+ if (!deviceData.ipTimes) {
3212
+ deviceData.ipTimes = {};
3213
+ }
3214
+ deviceData.ipTimes[clientIp] = now;
3215
+
3216
+ const slidingWindow = 2 * 60 * 60 * 1000; // 2 heures
3217
+ const cutOff = now - slidingWindow;
3218
+ for (const [ip, lastSeen] of Object.entries(deviceData.ipTimes)) {
3219
+ if (lastSeen < cutOff) {
3220
+ deviceData.ips.delete(ip);
3221
+ delete deviceData.ipTimes[ip];
3222
+ }
3223
+ }
3224
+ // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3225
+ const behaviorHeader = context.headers?.['x-behavior-metrics'];
3226
+ let webauthnVerified = false;
3227
+ if (behaviorHeader) {
3228
+ try {
3229
+ const metrics = JSON.parse(behaviorHeader);
3230
+ if (metrics && metrics.webauthnAnchor) {
3231
+ if (verifyWebAuthnHardwareAnchor(metrics.webauthnAnchor, deviceData)) {
3232
+ webauthnVerified = true;
3233
+ deviceData.webauthnVerified = true;
3234
+ }
3235
+ }
3236
+ } catch (e) {
3237
+ // Ignorer les erreurs de parsing
3238
+ }
3239
+ }
3240
+
2975
3241
  // NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
2976
3242
  // Très efficace contre la rotation de proxy.
2977
3243
  const maxIpsForDevice = isSharedIp
@@ -3015,57 +3281,65 @@ export const getSuspicionVector = async (context, securityConfig) => {
3015
3281
  context._newCookies = context._newCookies || [];
3016
3282
  context._newCookies.push(newCookie);
3017
3283
  }
3018
- await store.set(`ip-device:${clientIp}`, deviceId, 600); // Link the IP to the device for 10 minutes
3019
3284
 
3020
3285
  // Periodically clean up device data
3021
3286
  if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
3022
3287
  deviceData.ips.clear();
3023
3288
  deviceData.rapidChangeCount = 0;
3289
+ deviceData.ipTimes = {};
3024
3290
  }
3025
3291
  deviceData.lastUpdate = Date.now();
3026
3292
 
3027
- const behavioral = await getBehavioralIndicators(context, deviceData);
3028
- const { headerAnomalyScore } = getHeaderAnomalies(context);
3029
- // Calculate the inconsistency score here, separately.
3030
- let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
3031
-
3032
- // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
3033
- if (consistencyScore < 0.7) { // Seuil de rupture
3034
- inconsistencyScore = 100;
3035
- }
3036
-
3037
- const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3038
-
3039
- // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
3040
- const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
3293
+ const stableFp = extractStablePart(currentDeviceHash);
3294
+ const stableFpHash = cyrb53(stableFp).toString();
3041
3295
 
3042
- const { tlsSpoofingScore } = await getTlsSpoofingScore(context);
3296
+ // Execute non-interdependent asynchronous operations in parallel
3297
+ const [
3298
+ behavioral,
3299
+ { tlsSpoofingScore },
3300
+ { subnetScore },
3301
+ ipReputationScore,
3302
+ { botnetClusterScore },
3303
+ _ // store.set result
3304
+ ] = await Promise.all([
3305
+ getBehavioralIndicators(context, deviceData),
3306
+ getTlsSpoofingScore(context),
3307
+ getSubnetScore(context, deviceId),
3308
+ getIpReputationScore(clientIp),
3309
+ getBotnetClusterScore(context, stableFpHash),
3310
+ store.set(`ip-device:${clientIp}`, deviceId, 600) // Link the IP to the device for 10 minutes
3311
+ ]);
3312
+
3313
+ // Synchronous calculations
3314
+ const { headerAnomalyScore } = getHeaderAnomalies(context);
3315
+ let inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
3316
+
3317
+ // NOUVEAU: Si l'incohérence est très forte (cookie probablement volé), on applique une pénalité maximale.
3318
+ if (consistencyScore < 0.7) { // Seuil de rupture
3319
+ inconsistencyScore = 100;
3320
+ }
3043
3321
 
3044
- const { botScore } = getBotScore(context);
3322
+ const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
3045
3323
 
3046
- // NOUVEAU: On calcule le score d'incohérence temporelle.
3047
- const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
3324
+ // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
3325
+ const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
3048
3326
 
3049
- // NOUVEAU: On calcule le score d'incohérence entre les couches.
3050
- const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
3327
+ const { botScore } = getBotScore(context);
3051
3328
 
3052
- // NOUVEAU: On calcule le score de variance des clics.
3053
- const { clickVarianceScore } = getClickVarianceScore(context);
3329
+ // NOUVEAU: On calcule le score d'incohérence temporelle.
3330
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
3054
3331
 
3055
- // NOUVEAU: On calcule le score d'incohérence des Client-Hints.
3056
- const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
3332
+ // NOUVEAU: On calcule le score d'incohérence entre les couches.
3333
+ const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
3057
3334
 
3058
- // NOUVEAU: On calcule le score de réputation du sous-réseau.
3059
- // FIX: Pass the deviceId to getSubnetScore
3060
- const { subnetScore } = await getSubnetScore(context, deviceId);
3335
+ // NOUVEAU: On calcule le score de variance des clics.
3336
+ const { clickVarianceScore } = getClickVarianceScore(context);
3061
3337
 
3062
- const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3338
+ // NOUVEAU: On calcule le score d'incohérence des Client-Hints.
3339
+ const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
3063
3340
 
3064
- const ipReputationScore = await getIpReputationScore(clientIp);
3341
+ const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3065
3342
 
3066
- const stableFp = extractStablePart(currentDeviceHash);
3067
- const stableFpHash = cyrb53(stableFp).toString();
3068
- const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
3069
3343
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3070
3344
  const { quicAnomalyScore } = getQuicAnomalyScore(context);
3071
3345
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
@@ -3204,7 +3478,40 @@ export function generateCpuTargetChallenge(
3204
3478
  };
3205
3479
  }
3206
3480
 
3207
- const htmlTemplateCache = new Map();
3481
+ class SimpleLRUCache {
3482
+ constructor(maxSize = 100) {
3483
+ this.maxSize = maxSize;
3484
+ this.cache = new Map();
3485
+ }
3486
+
3487
+ get(key) {
3488
+ if (!this.cache.has(key)) return undefined;
3489
+ const value = this.cache.get(key);
3490
+ this.cache.delete(key);
3491
+ this.cache.set(key, value);
3492
+ return value;
3493
+ }
3494
+
3495
+ set(key, value) {
3496
+ if (this.cache.has(key)) {
3497
+ this.cache.delete(key);
3498
+ } else if (this.cache.size >= this.maxSize) {
3499
+ const lruKey = this.cache.keys().next().value;
3500
+ this.cache.delete(lruKey);
3501
+ }
3502
+ this.cache.set(key, value);
3503
+ }
3504
+
3505
+ has(key) {
3506
+ return this.cache.has(key);
3507
+ }
3508
+
3509
+ clear() {
3510
+ this.cache.clear();
3511
+ }
3512
+ }
3513
+
3514
+ const htmlTemplateCache = new SimpleLRUCache(100);
3208
3515
 
3209
3516
  /**
3210
3517
  * Generates the HTML page for the CPU target challenge.
@@ -3812,13 +4119,19 @@ export class FingerprintEngine {
3812
4119
 
3813
4120
  this._log('Processing request', { clientIp, path, isStatic });
3814
4121
 
4122
+ // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
4123
+ const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
4124
+ if (deviceData && deviceData.webauthnVerified) {
4125
+ this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
4126
+ return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
4127
+ }
4128
+
3815
4129
  if (isStatic) {
3816
4130
  this._log('Static resource - skipping checks');
3817
4131
  return { action: 'next', score: 0, vector: {} };
3818
4132
  }
3819
4133
 
3820
4134
  // Resolve identity and check for persisted "condemned" status early.
3821
- const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
3822
4135
  const currentDeviceHash = getCompositeDeviceHash(requestContext);
3823
4136
  const isNewDevice = !!newCookie;
3824
4137
  const allowRoaming = this.securityConfig?.allowCrossNetworkRoaming ?? false;
@@ -4237,17 +4550,22 @@ export class FingerprintEngine {
4237
4550
 
4238
4551
  const isBlocked = finalScore >= blockThreshold;
4239
4552
 
4240
- const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
4553
+ const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
4241
4554
  const isSuspiciousMedium = finalScore >= thresholds.medium;
4242
4555
  const isSuspicious = finalScore >= thresholds.low;
4243
4556
  const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
4244
4557
 
4245
4558
  // Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
4246
- const suspicionFactor = isSuspicious
4247
- ? Math.min(
4248
- 1.5, // On autorise un dépassement pour rendre les challenges très difficiles si le score est très élevé
4249
- (finalScore - thresholds.low) / (thresholds.high - thresholds.low),
4250
- )
4559
+ const suspicionFactor = isSuspicious // eslint-disable-line no-nested-ternary
4560
+ ? (() => {
4561
+ const denominator = thresholds.high - thresholds.low;
4562
+ if (denominator === 0) {
4563
+ // If the range is zero, and finalScore is at or above low threshold,
4564
+ // return 0 to avoid NaN.
4565
+ return 0;
4566
+ }
4567
+ return Math.min(1.5, (finalScore - thresholds.low) / denominator);
4568
+ })()
4251
4569
  : 0;
4252
4570
 
4253
4571
  this._log('Suspicion levels evaluated', {
@@ -4661,6 +4979,42 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
4661
4979
  return { error: 'Missing node_id' };
4662
4980
  }
4663
4981
 
4982
+ // --- VÉRIFICATION DE LA SIGNATURE COOPÉRATIVE ---
4983
+ const challengeContext = await store.get(`secret:${nodeId}`);
4984
+ if (!challengeContext || !challengeContext.clientSecret) {
4985
+ return { error: 'Invalid or expired node_id' };
4986
+ }
4987
+
4988
+ const clientSecret = challengeContext.clientSecret;
4989
+ const coopSig = params.coop_sig || '';
4990
+
4991
+ let expectedMsg = '';
4992
+ switch (op) {
4993
+ case 'register':
4994
+ expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
4995
+ break;
4996
+ case 'request_peer_block':
4997
+ expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
4998
+ break;
4999
+ case 'poll_requests':
5000
+ expectedMsg = `${clientSecret}:poll_requests:${nodeId}`;
5001
+ break;
5002
+ case 'respond_block':
5003
+ expectedMsg = `${clientSecret}:respond_block:${nodeId}:${params.requester_id || ''}:${params.req_id || ''}:${params.block_data || ''}`;
5004
+ break;
5005
+ case 'poll_response':
5006
+ expectedMsg = `${clientSecret}:poll_response:${nodeId}:${params.req_id || ''}`;
5007
+ break;
5008
+ default:
5009
+ return { error: 'Invalid cooperative operation' };
5010
+ }
5011
+
5012
+ const expectedSig = crypto.createHash('sha256').update(expectedMsg).digest('hex');
5013
+ if (coopSig.length !== expectedSig.length || !crypto.timingSafeEqual(Buffer.from(coopSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
5014
+ return { error: 'Invalid cooperative signature' };
5015
+ }
5016
+ // --- FIN DE LA VÉRIFICATION ---
5017
+
4664
5018
  switch (op) {
4665
5019
  case 'register':
4666
5020
  const seed = params.seed || '';
@@ -4773,66 +5127,25 @@ function sanitizeProxyHeaders(context, securityConfig) {
4773
5127
 
4774
5128
  /**
4775
5129
  * Exécute l'optimisation des TTL en tâche de fond de manière asynchrone et non-bloquante.
4776
- * Utilise l'algorithme génétique multi-objectifs de Pareto pour trouver des solutions stables.
5130
+ * Déporté dans un worker thread dédié pour libérer l'Event Loop principale de Node.js.
4777
5131
  */
4778
5132
  export async function runBackgroundTtlOptimization() {
4779
- const MIN_TTL = 300000;
4780
- const MAX_TTL = 86400000;
4781
- const tempCache = new Map();
4782
- const keyScores = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
4783
-
4784
- for (const suspicionScore of keyScores) {
4785
- // Rend la main à la boucle d'événements Node.js à chaque itération pour ne pas bloquer les requêtes web actives
4786
- await new Promise(resolve => {
4787
- if (typeof setImmediate === 'function') {
4788
- setImmediate(resolve);
4789
- } else {
4790
- setTimeout(resolve, 0);
5133
+ return new Promise((resolve, reject) => {
5134
+ const worker = new Worker(new URL('./ttl-optimization.worker.js', import.meta.url));
5135
+ worker.on("message", (tempCache) => {
5136
+ const tempMap = new Map();
5137
+ for (const [key, value] of Object.entries(tempCache)) {
5138
+ tempMap.set(Number(key), value);
4791
5139
  }
5140
+ optimizedTtlCache = tempMap;
5141
+ resolve();
5142
+ worker.terminate();
4792
5143
  });
4793
-
4794
- const solverFunction = () => {
4795
- const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
4796
- const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
4797
- const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
4798
- const mutate = (ttl) => {
4799
- const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1;
4800
- return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
4801
- };
4802
-
4803
- const paretoFront = Optimization.geneticAlgorithmMultiObjective(
4804
- createIndividual,
4805
- fitnessFunction,
4806
- crossover,
4807
- mutate,
4808
- {
4809
- generations: 40,
4810
- populationSize: 30,
4811
- }
4812
- );
4813
-
4814
- if (!paretoFront || paretoFront.length === 0) {
4815
- return { solution: null, fitness: Infinity };
4816
- }
4817
-
4818
- let bestSolutionInFront;
4819
- if (suspicionScore < 50) {
4820
- bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
4821
- } else {
4822
- bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
4823
- }
4824
- return { solution: bestSolutionInFront, fitness: 0 };
4825
- };
4826
-
4827
- const { bestResult } = Optimization.runMultiple(solverFunction, 20);
4828
- if (bestResult && bestResult.solution && bestResult.solution !== Infinity) {
4829
- tempCache.set(suspicionScore, Math.round(bestResult.solution));
4830
- } else {
4831
- tempCache.set(suspicionScore, Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL));
4832
- }
4833
- }
4834
-
4835
- optimizedTtlCache = tempCache;
5144
+ worker.on("error", (err) => {
5145
+ console.error('[Fingerprint] TTL optimization worker error:', err);
5146
+ reject(err);
5147
+ });
5148
+ });
4836
5149
  }
4837
5150
 
4838
5151
  // Lancement de l'optimisation initiale immédiate en arrière-plan
@@ -5337,7 +5650,7 @@ function getTlsSessionId(context) {
5337
5650
  }
5338
5651
 
5339
5652
  // --- Proof-of-Work Middleware (The Tollbooth) ---
5340
- const staticFileCache = new Map();
5653
+ const staticFileCache = new SimpleLRUCache(50);
5341
5654
 
5342
5655
  export const powMiddleware = (securityConfig) => {
5343
5656
  const engine = new FingerprintEngine(securityConfig);
@@ -5369,6 +5682,36 @@ export const powMiddleware = (securityConfig) => {
5369
5682
  }
5370
5683
 
5371
5684
  return async (req, res, next) => {
5685
+ if (req.query && req.query.fp_handshake) {
5686
+ const clientKeyHex = req.headers['x-client-ephemeral-key'];
5687
+ if (clientKeyHex) {
5688
+ try {
5689
+ const serverECDH = crypto.createECDH('prime256v1');
5690
+ serverECDH.generateKeys();
5691
+ const serverPubKeyHex = serverECDH.getKeys('hex');
5692
+ const sharedSecret = serverECDH.computeSecret(clientKeyHex, 'hex');
5693
+ const hmacKey = crypto.createHash('sha256').update(sharedSecret).digest('hex');
5694
+
5695
+ const requestContext = {
5696
+ clientIp: req.ip || req.socket?.remoteAddress || "unknown",
5697
+ headers: req.headers,
5698
+ cookies: req.cookies
5699
+ };
5700
+ const { deviceId, deviceData } = await resolveRequestIdentity(requestContext, securityConfig);
5701
+ if (deviceData) {
5702
+ deviceData.sessionHmacKey = hmacKey;
5703
+ await store.set(`device:${deviceId}`, deviceData);
5704
+ }
5705
+
5706
+ res.setHeader('x-server-ephemeral-key', serverPubKeyHex);
5707
+ return res.status(200).json({ status: 'success' });
5708
+ } catch (e) {
5709
+ return res.status(400).json({ error: 'Handshake failed' });
5710
+ }
5711
+ }
5712
+ return res.status(400).json({ error: 'Missing client key' });
5713
+ }
5714
+
5372
5715
  if (!req.headers_translated) {
5373
5716
  req.headers_translated = true;
5374
5717
  const matchedMapping = getActiveMappingForRequest(req.headers);
@@ -5523,7 +5866,7 @@ export const powMiddleware = (securityConfig) => {
5523
5866
  * This is a common pattern to allow mocking of ES module functions.
5524
5867
  */
5525
5868
  export const __internal = {
5526
- store, // Export the store for testing
5869
+ get store() { return store; }, // Export the store for testing
5527
5870
  getDeviceHash,
5528
5871
  getCompositeDeviceHash,
5529
5872
  getSuspicionVector,
@@ -5543,6 +5886,7 @@ export const __internal = {
5543
5886
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
5544
5887
  sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
5545
5888
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
5889
+ verifyWebAuthnHardwareAnchor,
5546
5890
  generateStatelessTicket,
5547
5891
  parseStatelessTicket,
5548
5892
  parseJa3,
@@ -5559,6 +5903,8 @@ export const __internal = {
5559
5903
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5560
5904
  verifyZkpProof,
5561
5905
  modPow,
5906
+ generateSessionMapping,
5907
+ compilePolymorphicJs,
5562
5908
  parseTcpSyn, // Expose for testing
5563
5909
  classifyTcpOs, // Expose for testing
5564
5910
  getTcpAnomalyScore, // Expose for testing,
@@ -5839,6 +6185,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5839
6185
  // Plus le ratio est équilibré et le volume important, plus nous faisons confiance au Front de Pareto.
5840
6186
  const trafficConfidence = Math.min(1.5, Math.max(0.3, highConfidenceRatio * 4));
5841
6187
 
6188
+ // --- VALIDATION POST-CALCUL (Anti-empoisonnement & Validation Croisée) ---
6189
+ const tempConfig = {
6190
+ thresholds: { ...securityConfig.thresholds },
6191
+ weights: { ...securityConfig.weights },
6192
+ patterns: { ...securityConfig.patterns }
6193
+ };
6194
+
6195
+ applyInertialUpdate(tempConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
6196
+ applyInertialUpdate(tempConfig.weights, newConfig.weights, 'weights', trafficConfidence);
6197
+ applyInertialUpdate(tempConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);
6198
+
6199
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: sanitizedData });
6200
+ const currentObjectives = fitnessFunction(securityConfig);
6201
+ const proposedObjectives = fitnessFunction(tempConfig);
6202
+
6203
+ const currentFPR = currentObjectives[0];
6204
+ const currentFNR = currentObjectives[1];
6205
+ const proposedFPR = proposedObjectives[0];
6206
+ const proposedFNR = proposedObjectives[1];
6207
+
6208
+ const validationTolerance = securityConfig?.autotuning?.validationTolerance ?? tuningOptions?.validationTolerance ?? 0.15;
6209
+
6210
+ if (proposedFPR > currentFPR + validationTolerance || proposedFNR > currentFNR + validationTolerance) {
6211
+ 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)})`);
6212
+ if (securityConfig.logger && typeof securityConfig.logger === 'function') {
6213
+ securityConfig.logger({
6214
+ type: 'autotuning_instability_alert',
6215
+ proposedFPR,
6216
+ currentFPR,
6217
+ proposedFNR,
6218
+ currentFNR,
6219
+ timestamp: Date.now()
6220
+ });
6221
+ }
6222
+ return; // Rollback automatique : On arrête l'application
6223
+ }
6224
+
5842
6225
  applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds, 'thresholds', trafficConfidence);
5843
6226
  applyInertialUpdate(securityConfig.weights, newConfig.weights, 'weights', trafficConfidence);
5844
6227
  applyInertialUpdate(securityConfig.patterns, newConfig.patterns, 'patterns', trafficConfidence);