@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.
- package/CHANGELOG.md +21 -0
- package/README.md +7 -6
- package/package.json +1 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +19 -5
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +430 -74
- package/src/js/problem-manager.js +22 -0
- package/src/js/tests/cross_parity.test.js +17 -11
- package/src/js/tests/fingerprint.client.test.js +130 -128
- package/src/js/tests/fingerprint.test.js +17 -2
- package/src/js/ttl-optimization.worker.js +51 -0
- package/src/php/Challenge/ChallengeUtils.php +120 -8
- package/src/php/FingerprintBuilder.php +193 -185
- package/src/php/FingerprintEngine.php +245 -3
- package/src/php/ProblemManager.php +20 -0
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +446 -285
- package/src/php/Utils/RequestUtils.php +26 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
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
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
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 ---
|