@anonympins/fingerprint 0.5.5 → 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.
@@ -769,6 +769,119 @@ const cipherSuiteMap = {
769
769
  * @param {object} deviceData Les données persistantes de l'appareil dans notre store
770
770
  * @returns {boolean} True si la signature matérielle est valide
771
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
+
772
885
  function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
773
886
  if (!anchor || !anchor.type) return false;
774
887
 
@@ -778,8 +891,22 @@ function verifyWebAuthnHardwareAnchor(anchor, deviceData) {
778
891
  .digest();
779
892
 
780
893
  if (anchor.type === 'registration') {
781
- if (!anchor.publicKey || !anchor.credentialId) return false;
782
-
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
+
783
910
  // Enregistrement initial : on stocke la clé publique matérielle SPKI
784
911
  deviceData.webauthnPublicKey = anchor.publicKey;
785
912
  deviceData.webauthnCredentialId = anchor.credentialId;
@@ -1583,6 +1710,14 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1583
1710
  const peerBlockIdx = ${peerBlockIdx ?? -1};
1584
1711
  const coopTimeout = ${coopTimeout};
1585
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
+
1586
1721
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1587
1722
  await new Promise(r => setTimeout(r, 10));
1588
1723
 
@@ -1591,20 +1726,40 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1591
1726
 
1592
1727
  if (peerId && peerBlockIdx !== -1) {
1593
1728
  // Enregistrement coopératif
1594
- 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);
1595
1731
  }
1596
1732
 
1597
- 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);
1598
1751
 
1599
1752
  let peerBlock = "";
1600
1753
  if (peerId && peerBlockIdx !== -1) {
1601
1754
  document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1602
1755
  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);
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);
1604
1758
 
1605
1759
  let attempts = 0;
1606
1760
  while (attempts < coopTimeout) {
1607
- 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);
1608
1763
  const data = await res.json();
1609
1764
  if (data.status === 'ready') {
1610
1765
  peerBlock = data.block_data;
@@ -1618,6 +1773,7 @@ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConf
1618
1773
  }
1619
1774
  }
1620
1775
 
1776
+ document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1621
1777
  const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
1622
1778
 
1623
1779
  window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
@@ -3051,6 +3207,20 @@ async function getBehavioralIndicators(context, deviceData) {
3051
3207
  deviceData.lastFpHash = currentFpHash;
3052
3208
  deviceData.ips.add(clientIp); // Record the IP used by this device
3053
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
+ }
3054
3224
  // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3055
3225
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
3056
3226
  let webauthnVerified = false;
@@ -3116,6 +3286,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3116
3286
  if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
3117
3287
  deviceData.ips.clear();
3118
3288
  deviceData.rapidChangeCount = 0;
3289
+ deviceData.ipTimes = {};
3119
3290
  }
3120
3291
  deviceData.lastUpdate = Date.now();
3121
3292
 
@@ -4808,6 +4979,42 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
4808
4979
  return { error: 'Missing node_id' };
4809
4980
  }
4810
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
+
4811
5018
  switch (op) {
4812
5019
  case 'register':
4813
5020
  const seed = params.seed || '';
@@ -4920,66 +5127,25 @@ function sanitizeProxyHeaders(context, securityConfig) {
4920
5127
 
4921
5128
  /**
4922
5129
  * 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.
5130
+ * Déporté dans un worker thread dédié pour libérer l'Event Loop principale de Node.js.
4924
5131
  */
4925
5132
  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);
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);
4938
5139
  }
5140
+ optimizedTtlCache = tempMap;
5141
+ resolve();
5142
+ worker.terminate();
4939
5143
  });
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;
5144
+ worker.on("error", (err) => {
5145
+ console.error('[Fingerprint] TTL optimization worker error:', err);
5146
+ reject(err);
5147
+ });
5148
+ });
4983
5149
  }
4984
5150
 
4985
5151
  // Lancement de l'optimisation initiale immédiate en arrière-plan
@@ -5700,7 +5866,7 @@ export const powMiddleware = (securityConfig) => {
5700
5866
  * This is a common pattern to allow mocking of ES module functions.
5701
5867
  */
5702
5868
  export const __internal = {
5703
- store, // Export the store for testing
5869
+ get store() { return store; }, // Export the store for testing
5704
5870
  getDeviceHash,
5705
5871
  getCompositeDeviceHash,
5706
5872
  getSuspicionVector,
@@ -341,6 +341,18 @@ class ProblemManager {
341
341
  console.error(`[ProblemManager] Propriété 'solution' manquante dans solutionData pour ${problemId}.`);
342
342
  return;
343
343
  }
344
+ // DoS mitigation: validate solution size and structure
345
+ if (Array.isArray(solutionData.solution) && solutionData.solution.length > 500) {
346
+ console.error(`[ProblemManager] Solution array too large for ${problemId} verification.`);
347
+ return;
348
+ }
349
+ try {
350
+ const serializedSolution = JSON.stringify(solutionData.solution);
351
+ if (serializedSolution && serializedSolution.length > 65536) {
352
+ console.error(`[ProblemManager] Solution payload size exceeds safe limit for ${problemId}.`);
353
+ return;
354
+ }
355
+ } catch (e) {}
344
356
  // 1. Ne JAMAIS faire confiance au score du client. Recalculer systématiquement.
345
357
  const scoreFunction = problem.workUnit.scoreFunction;
346
358
  if (!scoreFunction) {
@@ -368,6 +380,16 @@ class ProblemManager {
368
380
  return; // Ne rien faire si la vérification est impossible.
369
381
  }
370
382
 
383
+ if (solutionData.population.length > 150) {
384
+ console.error(`[ProblemManager] Population size exceeds safe limit for ${problemId}.`);
385
+ return;
386
+ }
387
+ for (const individual of solutionData.population) {
388
+ if (individual && individual.chromosome && Array.isArray(individual.chromosome) && individual.chromosome.length > 100) {
389
+ console.error(`[ProblemManager] Chromosome size too large for ${problemId}.`);
390
+ return;
391
+ }
392
+ }
371
393
  // 1. On choisit un petit échantillon aléatoire de la population soumise.
372
394
  const sampleSize = Math.min(5, solutionData.population.length);
373
395
  const sampleIndices = new Set();