@anonympins/fingerprint 0.5.5 → 0.6.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.
@@ -63,6 +63,58 @@ function canAttemptDns() {
63
63
  return true; // HALF-OPEN
64
64
  }
65
65
 
66
+ /**
67
+ * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante).
68
+ * @private
69
+ * @param {string} zkpY - La clé publique ZKP du terminal banni.
70
+ * @param {object} config - La configuration de sécurité.
71
+ */
72
+ async function broadcastBannedZkp(zkpY, config) {
73
+ const peers = config.federatedPeers || [];
74
+ if (peers.length === 0) return;
75
+
76
+ const timestamp = Date.now();
77
+ const msg = `${timestamp}:${zkpY}`;
78
+
79
+ let signature = '';
80
+ let isAsymmetric = false;
81
+
82
+ if (process.env.ED25519_PRIVATE_KEY) {
83
+ try {
84
+ const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
85
+ const signBuffer = crypto.sign(null, Buffer.from(msg), {
86
+ key: cleanKey,
87
+ format: 'pem',
88
+ type: 'pkcs8'
89
+ });
90
+ signature = signBuffer.toString('hex');
91
+ isAsymmetric = true;
92
+ } catch (e) {
93
+ console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
94
+ }
95
+ }
96
+
97
+ if (!isAsymmetric) {
98
+ const secret = config.federationSecret || getPowSecret();
99
+ signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
100
+ }
101
+
102
+ peers.forEach(peerUrl => {
103
+ fetch(peerUrl + '?coop_op=share_threat_intel', {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'X-Federation-Signature': isAsymmetric ? '' : signature,
108
+ 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
109
+ 'X-Federation-Timestamp': String(timestamp)
110
+ },
111
+ body: JSON.stringify({ zkpY })
112
+ }).catch(() => {
113
+ // Échec de propagation silencieux pour ne pas perturber le thread principal
114
+ });
115
+ });
116
+ }
117
+
66
118
  const withTimeout = (promise, ms) => {
67
119
  let timeoutId;
68
120
  const timeoutPromise = new Promise((_, reject) => {
@@ -494,7 +546,8 @@ const securityProfiles = {
494
546
  botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
495
547
  tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
496
548
  quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
497
- renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
549
+ renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
550
+ threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
498
551
  },
499
552
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
500
553
  patterns: {
@@ -511,6 +564,7 @@ const securityProfiles = {
511
564
  },
512
565
  allowCrossNetworkRoaming: true, // Profil balancé : tolérant par défaut
513
566
  wasm: true,
567
+ useAsymmetricTickets: true,
514
568
  },
515
569
  /**
516
570
  * @summary **Strict Profile**
@@ -531,7 +585,8 @@ const securityProfiles = {
531
585
  subnetScore: 0.5,
532
586
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
533
587
  botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
534
- renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
588
+ renderingAnomalyScore: 1.0, // NOUVEAU: Poids pour l'anomalie de rendu
589
+ threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
535
590
  },
536
591
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
537
592
  patterns: {
@@ -549,6 +604,7 @@ const securityProfiles = {
549
604
  challengeNewDevices: true, // Challenge all new devices
550
605
  allowCrossNetworkRoaming: false, // Strict : interdiction de changer complètement de réseau sans re-challenge
551
606
  wasm: true,
607
+ useAsymmetricTickets: true,
552
608
  },
553
609
  /**
554
610
  * @summary **API Profile**
@@ -588,6 +644,7 @@ const securityProfiles = {
588
644
  isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
589
645
  allowCrossNetworkRoaming: false, // Les API ne doivent pas subir de roaming inter-IP suspect
590
646
  wasm: true,
647
+ useAsymmetricTickets: true,
591
648
  }
592
649
  ,
593
650
  /**
@@ -611,7 +668,8 @@ const securityProfiles = {
611
668
  botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
612
669
  tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
613
670
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
614
- renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
671
+ renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
672
+ threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
615
673
  },
616
674
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
617
675
  patterns: {
@@ -628,6 +686,7 @@ const securityProfiles = {
628
686
  },
629
687
  allowCrossNetworkRoaming: true,
630
688
  wasm: true,
689
+ useAsymmetricTickets: true,
631
690
  },
632
691
  /**
633
692
  * @summary **E-commerce Profile**
@@ -651,7 +710,8 @@ const securityProfiles = {
651
710
  botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
652
711
  tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
653
712
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
654
- renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
713
+ renderingAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie de rendu,threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
714
+ threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
655
715
  },
656
716
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
657
717
  patterns: {
@@ -670,6 +730,7 @@ const securityProfiles = {
670
730
  isApiRequest: (req) => req.path.startsWith('/api/cart') || req.path.startsWith('/api/stock') || req.path.startsWith('/api/checkout'),
671
731
  allowCrossNetworkRoaming: false, // E-commerce : interdiction de changer de réseau sans re-challenge
672
732
  wasm: true,
733
+ useAsymmetricTickets: true,
673
734
  }
674
735
  };
675
736
 
@@ -769,6 +830,119 @@ const cipherSuiteMap = {
769
830
  * @param {object} deviceData Les données persistantes de l'appareil dans notre store
770
831
  * @returns {boolean} True si la signature matérielle est valide
771
832
  */
833
+ const TRUSTED_HARDWARE_ROOTS = [
834
+ "-----BEGIN CERTIFICATE-----\n" +
835
+ "MIIDHzCCAfegAwIBAgIJANCvWjvF+2O6MA0GCSqGSIb3DQEBCwUAMC0xKzApBgNV\n" +
836
+ "BAMTIll1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMB4XDTE0MDgwNDAwMDAwMFox\n" +
837
+ "TSUxSDBGBgNVBAMMT1l1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMSowKAYDVQQK\n" +
838
+ "EyFZdWJpY28gQUIxDzANBgNVBAcTBVN0b2NraG9sbTELMAkGA1UEBhMCU0UwggEi\n" +
839
+ "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6XW0d87g+N6kGgSgC/H9UfA2p\n" +
840
+ "-----END CERTIFICATE-----",
841
+ "-----BEGIN CERTIFICATE-----\n" +
842
+ "MIIB1DCCAXWgAwIBAgIEUI70WjAKBggqhkjOPQQDAjArMSkwJwYDVQQDEyBGSURP\n" +
843
+ "IEFsbGlhbmNlIFJvb3QgQ0EgKFRlc3QpMB4XDTE0MDgxODA4MzA0NVoXDTM5MDgx\n" +
844
+ "ODA4MzA0NVowKzEpMCcGA1UEAxMgRklETyBBbGxpYW5jZSBSb290IENBIChUZXN0\n" +
845
+ "KTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFv81Jm9M7AehfOIdpCH567gP0yqS40m\n" +
846
+ "aN0j1a8n152G7n/nUf7J0j9F4pL9J2w1X8hN1N8f9Y3G9w8L29/m7/zX3O3n2e7/\n" +
847
+ "g==\n" +
848
+ "-----END CERTIFICATE-----"
849
+ ];
850
+
851
+ function decodeCBOR(buffer) {
852
+ let offset = 0;
853
+ function readByte() {
854
+ if (offset >= buffer.length) throw new Error("Unexpected end of CBOR data");
855
+ return buffer[offset++];
856
+ }
857
+ function readBytes(len) {
858
+ if (offset + len > buffer.length) throw new Error("Unexpected end of CBOR bytes");
859
+ const res = buffer.slice(offset, offset + len);
860
+ offset += len;
861
+ return res;
862
+ }
863
+ function readInt(val) {
864
+ if (val < 24) return val;
865
+ if (val === 24) return readByte();
866
+ if (val === 25) {
867
+ const b1 = readByte(); const b2 = readByte();
868
+ return (b1 << 8) | b2;
869
+ }
870
+ if (val === 26) {
871
+ const b1 = readByte(); const b2 = readByte();
872
+ const b3 = readByte(); const b4 = readByte();
873
+ return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
874
+ }
875
+ throw new Error("Unsupported integer size: " + val);
876
+ }
877
+ function decodeType() {
878
+ const initial = readByte();
879
+ const major = initial >> 5;
880
+ const val = initial & 0x1f;
881
+ if (major === 0) {
882
+ return readInt(val);
883
+ } else if (major === 1) {
884
+ return -1 - readInt(val);
885
+ } else if (major === 2) {
886
+ const len = readInt(val);
887
+ return readBytes(len);
888
+ } else if (major === 3) {
889
+ const len = readInt(val);
890
+ return readBytes(len).toString('utf8');
891
+ } else if (major === 4) {
892
+ const len = readInt(val);
893
+ const arr = [];
894
+ for (let i = 0; i < len; i++) arr.push(decodeType());
895
+ return arr;
896
+ } else if (major === 5) {
897
+ const len = readInt(val);
898
+ const obj = {};
899
+ for (let i = 0; i < len; i++) {
900
+ const k = decodeType();
901
+ const v = decodeType();
902
+ obj[k] = v;
903
+ }
904
+ return obj;
905
+ }
906
+ return null;
907
+ }
908
+ return decodeType();
909
+ }
910
+
911
+ function verifyCertificateChain(x5c) {
912
+ if (!x5c || x5c.length === 0) return false;
913
+ try {
914
+ const pems = x5c.map(der => {
915
+ const base64 = der.toString('base64');
916
+ return `-----BEGIN CERTIFICATE-----\n${base64.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`;
917
+ });
918
+
919
+ for (let i = 0; i < pems.length - 1; i++) {
920
+ const child = new crypto.X509Certificate(pems[i]);
921
+ const parent = new crypto.X509Certificate(pems[i + 1]);
922
+ if (!child.verify(parent.publicKey)) {
923
+ return false;
924
+ }
925
+ }
926
+
927
+ const rootCert = new crypto.X509Certificate(pems[pems.length - 1]);
928
+ let trusted = false;
929
+ for (const trustedRootPem of TRUSTED_HARDWARE_ROOTS) {
930
+ const trustedRoot = new crypto.X509Certificate(trustedRootPem);
931
+ if (rootCert.subject === trustedRoot.subject) {
932
+ trusted = true;
933
+ break;
934
+ }
935
+ if (rootCert.verify(trustedRoot.publicKey)) {
936
+ trusted = true;
937
+ break;
938
+ }
939
+ }
940
+ return trusted;
941
+ } catch (e) {
942
+ return false;
943
+ }
944
+ }
945
+
772
946
  function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
773
947
  if (!anchor || !anchor.type) return false;
774
948
 
@@ -778,8 +952,22 @@ function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
778
952
  .digest();
779
953
 
780
954
  if (anchor.type === 'registration') {
781
- if (!anchor.publicKey || !anchor.credentialId) return false;
782
-
955
+ if (!anchor.publicKey || !anchor.credentialId || !anchor.attestationObject) return false;
956
+
957
+ const attestationBytes = Buffer.from(anchor.attestationObject, 'base64');
958
+ const decoded = decodeCBOR(attestationBytes);
959
+
960
+ if (!decoded || !decoded.fmt || !decoded.attStmt) return false;
961
+
962
+ if (decoded.fmt !== 'none') {
963
+ const attStmt = decoded.attStmt;
964
+ if (!attStmt.x5c || !Array.isArray(attStmt.x5c)) return false;
965
+
966
+ if (!verifyCertificateChain(attStmt.x5c)) {
967
+ return false;
968
+ }
969
+ }
970
+
783
971
  // Enregistrement initial : on stocke la clé publique matérielle SPKI
784
972
  deviceData.webauthnPublicKey = anchor.publicKey;
785
973
  deviceData.webauthnCredentialId = anchor.credentialId;
@@ -1583,6 +1771,14 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1583
1771
  const peerBlockIdx = ${peerBlockIdx ?? -1};
1584
1772
  const coopTimeout = ${coopTimeout};
1585
1773
 
1774
+ async function signCoop(op, nid, extra = "") {
1775
+ const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
1776
+ const encoder = new TextEncoder();
1777
+ const data = encoder.encode(msg);
1778
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1779
+ return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
1780
+ }
1781
+
1586
1782
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1587
1783
  await new Promise(r => setTimeout(r, 10));
1588
1784
 
@@ -1591,20 +1787,40 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1591
1787
 
1592
1788
  if (peerId && peerBlockIdx !== -1) {
1593
1789
  // Enregistrement coopératif
1594
- await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
1790
+ const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
1791
+ await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
1595
1792
  }
1596
1793
 
1597
- document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1794
+ // Écoute des requêtes entrantes de nos pairs
1795
+ setInterval(async () => {
1796
+ try {
1797
+ const sig = await signCoop("poll_requests", nodeId);
1798
+ const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
1799
+ const data = await res.json();
1800
+ if (data.requests && data.requests.length > 0) {
1801
+ for (const req of data.requests) {
1802
+ document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
1803
+ const blockData = await window.readSpaceBlock(req.block_idx);
1804
+ const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
1805
+ 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);
1806
+ }
1807
+ }
1808
+ } catch (e) {
1809
+ console.error("Cooperative polling error", e);
1810
+ }
1811
+ }, 1000);
1598
1812
 
1599
1813
  let peerBlock = "";
1600
1814
  if (peerId && peerBlockIdx !== -1) {
1601
1815
  document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1602
1816
  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);
1817
+ const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1818
+ 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);
1604
1819
 
1605
1820
  let attempts = 0;
1606
1821
  while (attempts < coopTimeout) {
1607
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
1822
+ const pollSig = await signCoop("poll_response", nodeId, reqId);
1823
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1608
1824
  const data = await res.json();
1609
1825
  if (data.status === 'ready') {
1610
1826
  peerBlock = data.block_data;
@@ -1618,6 +1834,7 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1618
1834
  }
1619
1835
  }
1620
1836
 
1837
+ document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1621
1838
  const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
1622
1839
 
1623
1840
  window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
@@ -3051,6 +3268,20 @@ async function getBehavioralIndicators(context, deviceData) {
3051
3268
  deviceData.lastFpHash = currentFpHash;
3052
3269
  deviceData.ips.add(clientIp); // Record the IP used by this device
3053
3270
 
3271
+ // Nettoyage par fenêtre glissante pour éviter l'accumulation sur les sessions longues
3272
+ if (!deviceData.ipTimes) {
3273
+ deviceData.ipTimes = {};
3274
+ }
3275
+ deviceData.ipTimes[clientIp] = now;
3276
+
3277
+ const slidingWindow = 2 * 60 * 60 * 1000; // 2 heures
3278
+ const cutOff = now - slidingWindow;
3279
+ for (const [ip, lastSeen] of Object.entries(deviceData.ipTimes)) {
3280
+ if (lastSeen < cutOff) {
3281
+ deviceData.ips.delete(ip);
3282
+ delete deviceData.ipTimes[ip];
3283
+ }
3284
+ }
3054
3285
  // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3055
3286
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
3056
3287
  let webauthnVerified = false;
@@ -3116,6 +3347,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3116
3347
  if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
3117
3348
  deviceData.ips.clear();
3118
3349
  deviceData.rapidChangeCount = 0;
3350
+ deviceData.ipTimes = {};
3119
3351
  }
3120
3352
  deviceData.lastUpdate = Date.now();
3121
3353
 
@@ -3123,15 +3355,20 @@ export const getSuspicionVector = async (context, securityConfig) => {
3123
3355
  const stableFpHash = cyrb53(stableFp).toString();
3124
3356
 
3125
3357
  // Execute non-interdependent asynchronous operations in parallel
3126
- const [
3358
+ const zkpProof = (context.getHeader ? context.getHeader('x-zkp-proof') : null) || (context.headers ? context.headers['x-zkp-proof'] : null) || (context.query ? context.query['pow_zkp'] : null) || (context.queryParams ? context.queryParams['pow_zkp'] : null) || '';
3359
+ const zkpY = zkpProof ? zkpProof.split(":")[0] : null;
3360
+
3361
+ const [
3127
3362
  behavioral,
3363
+ { threatIntelScore },
3128
3364
  { tlsSpoofingScore },
3129
3365
  { subnetScore },
3130
3366
  ipReputationScore,
3131
3367
  { botnetClusterScore },
3132
3368
  _ // store.set result
3133
3369
  ] = await Promise.all([
3134
- getBehavioralIndicators(context, deviceData),
3370
+ getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
3371
+ getThreatIntelScore(zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
3135
3372
  getTlsSpoofingScore(context),
3136
3373
  getSubnetScore(context, deviceId),
3137
3374
  getIpReputationScore(clientIp),
@@ -3183,7 +3420,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3183
3420
  deviceData.ips = new Set(deviceData.ips);
3184
3421
  }
3185
3422
  // Le vecteur de suspicion est maintenant complet.
3186
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
3423
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore };
3187
3424
  };
3188
3425
 
3189
3426
  // A residential user can change networks (home, 4G, public wifi).
@@ -3558,6 +3795,22 @@ function parseGraphQLQuery(body) {
3558
3795
  }
3559
3796
  return null;
3560
3797
  }
3798
+
3799
+ /**
3800
+ * Calculates a score based on whether the client's ZKP public key (y) is found in a banned list.
3801
+ * @param {string} zkpY - The 'y' component of the ZKP proof (public key).
3802
+ * @returns {Promise<{threatIntelScore: number}>}
3803
+ */
3804
+ async function getThreatIntelScore(zkpY) {
3805
+ if (!zkpY) return { threatIntelScore: 0 };
3806
+
3807
+ const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
3808
+ if (isBanned) {
3809
+ return { threatIntelScore: 100 };
3810
+ }
3811
+ return { threatIntelScore: 0 };
3812
+ }
3813
+
3561
3814
  export class FingerprintEngine {
3562
3815
  constructor(securityConfig) {
3563
3816
  const isProduction = process.env.NODE_ENV === 'production';
@@ -3570,6 +3823,20 @@ export class FingerprintEngine {
3570
3823
  process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
3571
3824
  }
3572
3825
 
3826
+ // Auto-generate Ed25519 key pair on load if indicated and keys are not set
3827
+ if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
3828
+ try {
3829
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
3830
+ privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
3831
+ publicKeyEncoding: { format: 'pem', type: 'spki' }
3832
+ });
3833
+ process.env.ED25519_PRIVATE_KEY = privateKey;
3834
+ process.env.ED25519_PUBLIC_KEY = publicKey;
3835
+ } catch (e) {
3836
+ console.error('[Fingerprint] Native Ed25519 key generation failed:', e.message);
3837
+ }
3838
+ }
3839
+
3573
3840
  let finalConfig = securityConfig;
3574
3841
  if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
3575
3842
  const sPath = securityConfig.autotuning.savePath;
@@ -3621,7 +3888,8 @@ export class FingerprintEngine {
3621
3888
  'trustedProxies',
3622
3889
  'wasm',
3623
3890
  'similarityThreshold',
3624
- 'ed25519_private_key', 'ed25519_public_key'
3891
+ 'ed25519_private_key', 'ed25519_public_key',
3892
+ 'federatedPeers', 'federationSecret'
3625
3893
  ]);
3626
3894
 
3627
3895
  // 1. Check for essential keys
@@ -3668,10 +3936,13 @@ export class FingerprintEngine {
3668
3936
  (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
3669
3937
  (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
3670
3938
  (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
3939
+ (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
3671
3940
  (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
3672
3941
 
3673
3942
  return Math.min(100, score);
3674
3943
  }
3944
+
3945
+
3675
3946
  /**
3676
3947
  * Checks if an IP address is in the static allowlist (IPs or CIDR ranges).
3677
3948
  * This is the fastest check and should be performed first.
@@ -3936,7 +4207,13 @@ export class FingerprintEngine {
3936
4207
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3937
4208
 
3938
4209
  if (query.coop_op) {
3939
- const result = await handleCooperativeRequest(query, clientIp);
4210
+ const coopParams = {
4211
+ ...query,
4212
+ signature: requestContext.headers?.['x-federation-signature'] || query.signature,
4213
+ signature_ed25519: requestContext.headers?.['x-federation-signature-ed25519'] || query.signature_ed25519,
4214
+ timestamp: requestContext.headers?.['x-federation-timestamp'] || query.timestamp
4215
+ };
4216
+ const result = await handleCooperativeRequest(coopParams, clientIp, this.securityConfig);
3940
4217
  return {
3941
4218
  action: 'challenge',
3942
4219
  status: 200,
@@ -4379,6 +4656,22 @@ export class FingerprintEngine {
4379
4656
 
4380
4657
  const isBlocked = finalScore >= blockThreshold;
4381
4658
 
4659
+ // NOUVEAU: Si l'action est de bloquer, on enregistre le ZKP du client dans la liste des bannis.
4660
+ if (isBlocked) {
4661
+ const zkpProof = requestContext.headers['x-zkp-proof'] || requestContext.query.pow_zkp || '';
4662
+ const parts = zkpProof.split(':');
4663
+ if (parts.length === 3) {
4664
+ const [zkpY, zkpT, zkpS] = parts;
4665
+ // 1. Valider cryptographiquement la preuve avant de bannir/diffuser
4666
+ if (verifyZkpProof(zkpY, zkpT, zkpS)) {
4667
+ // 2. Dédoublonner : Ne diffuser que si la clé n'est pas déjà connue comme bannie
4668
+ if (!(await store.has(`banned-zkp-y:${zkpY}`))) {
4669
+ await store.set(`banned-zkp-y:${zkpY}`, true, 86400 * 30); // Banni pour 30 jours
4670
+ broadcastBannedZkp(zkpY, this.securityConfig).catch(() => {});
4671
+ }
4672
+ }
4673
+ }
4674
+ }
4382
4675
  const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked && finalScore > 0;
4383
4676
  const isSuspiciousMedium = finalScore >= thresholds.medium;
4384
4677
  const isSuspicious = finalScore >= thresholds.low;
@@ -4799,15 +5092,118 @@ export async function findPeerInSubnet(clientIp, excludeNodeId) {
4799
5092
  return activePeers[randomIndex];
4800
5093
  }
4801
5094
 
4802
- export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
5095
+ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', config = {}) {
4803
5096
  const op = params.coop_op;
4804
5097
  if (!op) return null;
4805
5098
 
5099
+ if (op === 'share_threat_intel') {
5100
+ const peers = config.federatedPeers || [];
5101
+ if (peers.length > 0) {
5102
+ const allowedIPsAndHosts = peers.map(url => {
5103
+ try { return new URL(url).hostname; } catch(e) { return url; }
5104
+ });
5105
+ if (!allowedIPsAndHosts.includes(clientIp)) {
5106
+ return { error: 'Unauthorized federation sender IP' };
5107
+ }
5108
+ }
5109
+ const zkpY = params.zkpY || '';
5110
+ const timestamp = Number(params.timestamp || 0);
5111
+
5112
+ if (!zkpY || !timestamp) {
5113
+ return { error: 'Missing threat intel parameters' };
5114
+ }
5115
+
5116
+ // Vérification de la fraîcheur du message pour éviter les attaques par rejeu
5117
+ if (Math.abs(Date.now() - timestamp) > 300000) { // 5 minutes max skew
5118
+ return { error: 'Message expired or clock skew too high' };
5119
+ }
5120
+
5121
+ const msg = `${timestamp}:${zkpY}`;
5122
+ const sigEd25519 = params.signature_ed25519 || '';
5123
+ const sigHmac = params.signature || '';
5124
+
5125
+ if (sigEd25519) {
5126
+ const publicKey = config.ed25519_public_key || process.env.ED25519_PUBLIC_KEY;
5127
+ if (!publicKey) {
5128
+ return { error: 'Missing public key for asymmetric verification' };
5129
+ }
5130
+ try {
5131
+ const cleanKey = publicKey.replace(/\\n/g, '\n');
5132
+ const isVerified = crypto.verify(
5133
+ null,
5134
+ Buffer.from(msg),
5135
+ { key: cleanKey, format: 'pem', type: 'spki' },
5136
+ Buffer.from(sigEd25519, 'hex')
5137
+ );
5138
+ if (!isVerified) {
5139
+ return { error: 'Invalid asymmetric federation signature' };
5140
+ }
5141
+ } catch (e) {
5142
+ return { error: 'Asymmetric signature verification failed' };
5143
+ }
5144
+ } else if (sigHmac) {
5145
+ const secret = config.federationSecret || getPowSecret();
5146
+ const expectedSig = crypto.createHmac('sha256', secret).update(msg).digest('hex');
5147
+ try {
5148
+ const isSigValid = crypto.timingSafeEqual(
5149
+ Buffer.from(sigHmac, 'hex'),
5150
+ Buffer.from(expectedSig, 'hex')
5151
+ );
5152
+ if (!isSigValid) {
5153
+ return { error: 'Invalid federation signature' };
5154
+ }
5155
+ } catch (e) {
5156
+ return { error: 'Invalid signature verification' };
5157
+ }
5158
+ } else {
5159
+ return { error: 'Missing signature' };
5160
+ }
5161
+
5162
+ await store.set(`banned-zkp-y:${zkpY}`, true, 86400 * 30); // 30 jours
5163
+ return { status: 'synchronized' };
5164
+ }
5165
+
4806
5166
  const nodeId = params.node_id || '';
4807
5167
  if (!nodeId) {
4808
5168
  return { error: 'Missing node_id' };
4809
5169
  }
4810
5170
 
5171
+ // --- VÉRIFICATION DE LA SIGNATURE COOPÉRATIVE ---
5172
+ const challengeContext = await store.get(`secret:${nodeId}`);
5173
+ if (!challengeContext || !challengeContext.clientSecret) {
5174
+ return { error: 'Invalid or expired node_id' };
5175
+ }
5176
+
5177
+ const clientSecret = challengeContext.clientSecret;
5178
+ const coopSig = params.coop_sig || '';
5179
+
5180
+ let expectedMsg = '';
5181
+ switch (op) {
5182
+ case 'register':
5183
+ expectedMsg = `${clientSecret}:register:${nodeId}:${params.seed || ''}`;
5184
+ break;
5185
+ case 'request_peer_block':
5186
+ expectedMsg = `${clientSecret}:request_peer_block:${nodeId}:${params.peer_id || ''}:${params.block_idx || '0'}:${params.req_id || ''}`;
5187
+ break;
5188
+ case 'poll_requests':
5189
+ expectedMsg = `${clientSecret}:poll_requests:${nodeId}`;
5190
+ break;
5191
+ case 'respond_block':
5192
+ expectedMsg = `${clientSecret}:respond_block:${nodeId}:${params.requester_id || ''}:${params.req_id || ''}:${params.block_data || ''}`;
5193
+ break;
5194
+ case 'poll_response':
5195
+ expectedMsg = `${clientSecret}:poll_response:${nodeId}:${params.req_id || ''}`;
5196
+ break;
5197
+ default:
5198
+ return { error: 'Invalid cooperative operation' };
5199
+ }
5200
+
5201
+ const expectedSig = crypto.createHash('sha256').update(expectedMsg).digest('hex');
5202
+ if (coopSig.length !== expectedSig.length || !crypto.timingSafeEqual(Buffer.from(coopSig, 'hex'), Buffer.from(expectedSig, 'hex'))) {
5203
+ return { error: 'Invalid cooperative signature' };
5204
+ }
5205
+ // --- FIN DE LA VÉRIFICATION ---
5206
+
4811
5207
  switch (op) {
4812
5208
  case 'register':
4813
5209
  const seed = params.seed || '';
@@ -4920,66 +5316,25 @@ function sanitizeProxyHeaders(context, securityConfig) {
4920
5316
 
4921
5317
  /**
4922
5318
  * Exécute l'optimisation des TTL en tâche de fond de manière asynchrone et non-bloquante.
4923
- * Utilise l'algorithme génétique multi-objectifs de Pareto pour trouver des solutions stables.
5319
+ * Déporté dans un worker thread dédié pour libérer l'Event Loop principale de Node.js.
4924
5320
  */
4925
5321
  export async function runBackgroundTtlOptimization() {
4926
- const MIN_TTL = 300000;
4927
- const MAX_TTL = 86400000;
4928
- const tempCache = new Map();
4929
- const keyScores = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
4930
-
4931
- for (const suspicionScore of keyScores) {
4932
- // Rend la main à la boucle d'événements Node.js à chaque itération pour ne pas bloquer les requêtes web actives
4933
- await new Promise(resolve => {
4934
- if (typeof setImmediate === 'function') {
4935
- setImmediate(resolve);
4936
- } else {
4937
- setTimeout(resolve, 0);
5322
+ return new Promise((resolve, reject) => {
5323
+ const worker = new Worker(new URL('./ttl-optimization.worker.js', import.meta.url));
5324
+ worker.on("message", (tempCache) => {
5325
+ const tempMap = new Map();
5326
+ for (const [key, value] of Object.entries(tempCache)) {
5327
+ tempMap.set(Number(key), value);
4938
5328
  }
5329
+ optimizedTtlCache = tempMap;
5330
+ resolve();
5331
+ worker.terminate();
4939
5332
  });
4940
-
4941
- const solverFunction = () => {
4942
- const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
4943
- const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
4944
- const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
4945
- const mutate = (ttl) => {
4946
- const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1;
4947
- return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
4948
- };
4949
-
4950
- const paretoFront = Optimization.geneticAlgorithmMultiObjective(
4951
- createIndividual,
4952
- fitnessFunction,
4953
- crossover,
4954
- mutate,
4955
- {
4956
- generations: 40,
4957
- populationSize: 30,
4958
- }
4959
- );
4960
-
4961
- if (!paretoFront || paretoFront.length === 0) {
4962
- return { solution: null, fitness: Infinity };
4963
- }
4964
-
4965
- let bestSolutionInFront;
4966
- if (suspicionScore < 50) {
4967
- bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
4968
- } else {
4969
- bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
4970
- }
4971
- return { solution: bestSolutionInFront, fitness: 0 };
4972
- };
4973
-
4974
- const { bestResult } = Optimization.runMultiple(solverFunction, 20);
4975
- if (bestResult && bestResult.solution && bestResult.solution !== Infinity) {
4976
- tempCache.set(suspicionScore, Math.round(bestResult.solution));
4977
- } else {
4978
- tempCache.set(suspicionScore, Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL));
4979
- }
4980
- }
4981
-
4982
- optimizedTtlCache = tempCache;
5333
+ worker.on("error", (err) => {
5334
+ console.error('[Fingerprint] TTL optimization worker error:', err);
5335
+ reject(err);
5336
+ });
5337
+ });
4983
5338
  }
4984
5339
 
4985
5340
  // Lancement de l'optimisation initiale immédiate en arrière-plan
@@ -5700,7 +6055,7 @@ export const powMiddleware = (securityConfig) => {
5700
6055
  * This is a common pattern to allow mocking of ES module functions.
5701
6056
  */
5702
6057
  export const __internal = {
5703
- store, // Export the store for testing
6058
+ get store() { return store; }, // Export the store for testing
5704
6059
  getDeviceHash,
5705
6060
  getCompositeDeviceHash,
5706
6061
  getSuspicionVector,
@@ -5750,7 +6105,8 @@ export const __internal = {
5750
6105
  canAttemptDns,
5751
6106
  registerCooperativeNode,
5752
6107
  findPeerInSubnet,
5753
- handleCooperativeRequest
6108
+ handleCooperativeRequest,
6109
+ broadcastBannedZkp
5754
6110
  };
5755
6111
 
5756
6112
  // --- THRESHOLD AUTO-TUNING SECTION ---