@anonympins/fingerprint 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,7 +68,8 @@ function canAttemptDns() {
68
68
  }
69
69
 
70
70
  /**
71
- * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante).
71
+ * Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
72
+ * avec protection par Anonymat Différentiel (Differential Privacy / Bruit de Laplace & Leurres).
72
73
  * @private
73
74
  * @param {string} zkpY - La clé publique ZKP du terminal banni.
74
75
  * @param {object} config - La configuration de sécurité.
@@ -77,46 +78,87 @@ async function broadcastBannedZkp(zkpY, config) {
77
78
  const peers = config.federatedPeers || [];
78
79
  if (peers.length === 0) return;
79
80
 
80
- const timestamp = Date.now();
81
- const msg = `${timestamp}:${zkpY}`;
82
-
83
- let signature = '';
84
- let isAsymmetric = false;
81
+ const dpConfig = config.differentialPrivacy || {};
82
+ const dpEnabled = dpConfig.enabled !== false;
83
+ const epsilon = typeof dpConfig.epsilon === 'number' ? dpConfig.epsilon : (config.dpEpsilon || 1.0);
85
84
 
86
- if (process.env.ED25519_PRIVATE_KEY) {
87
- try {
88
- const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
89
- const signBuffer = crypto.sign(null, Buffer.from(msg), {
90
- key: cleanKey,
91
- format: 'pem',
92
- type: 'pkcs8'
93
- });
94
- signature = signBuffer.toString('hex');
95
- isAsymmetric = true;
96
- } catch (e) {
97
- console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
85
+ const sendReport = (targetZkpY, ts) => {
86
+ const msg = `${ts}:${targetZkpY}`;
87
+ let signature = '';
88
+ let isAsymmetric = false;
89
+
90
+ if (process.env.ED25519_PRIVATE_KEY) {
91
+ try {
92
+ const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
93
+ const signBuffer = crypto.sign(null, Buffer.from(msg), {
94
+ key: cleanKey,
95
+ format: 'pem',
96
+ type: 'pkcs8'
97
+ });
98
+ signature = signBuffer.toString('hex');
99
+ isAsymmetric = true;
100
+ } catch (e) {
101
+ console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
102
+ }
98
103
  }
99
- }
100
104
 
101
- if (!isAsymmetric) {
102
- const secret = config.federationSecret || getPowSecret();
103
- signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
105
+ if (!isAsymmetric) {
106
+ const secret = config.federationSecret || getPowSecret();
107
+ signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
108
+ }
109
+
110
+ peers.forEach(peerUrl => {
111
+ fetch(peerUrl + '?coop_op=share_threat_intel', {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'X-Federation-Signature': isAsymmetric ? '' : signature,
116
+ 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
117
+ 'X-Federation-Timestamp': String(ts)
118
+ },
119
+ body: JSON.stringify({ zkpY: targetZkpY })
120
+ }).catch(() => {
121
+ // Échec de propagation silencieux pour ne pas perturber le thread principal
122
+ });
123
+ });
124
+ };
125
+
126
+ let reportTimestamp = Date.now();
127
+
128
+ // 1. Differential Privacy : Perturbation laplacienne de l'horodatage
129
+ if (dpEnabled) {
130
+ const deltaT = 5000; // Sensibilité de 5s
131
+ const b = deltaT / Math.max(0.1, epsilon);
132
+ const u = secureRandomFloat() - 0.5;
133
+ const safeU = Math.abs(u) < 1e-7 ? (u >= 0 ? 1e-7 : -1e-7) : u;
134
+ const laplaceNoise = -b * Math.sign(safeU) * Math.log(1 - 2 * Math.abs(safeU));
135
+ const clampedNoise = Math.max(-60000, Math.min(60000, Math.round(laplaceNoise)));
136
+ reportTimestamp = Date.now() + clampedNoise;
104
137
  }
105
138
 
106
- peers.forEach(peerUrl => {
107
- fetch(peerUrl + '?coop_op=share_threat_intel', {
108
- method: 'POST',
109
- headers: {
110
- 'Content-Type': 'application/json',
111
- 'X-Federation-Signature': isAsymmetric ? '' : signature,
112
- 'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
113
- 'X-Federation-Timestamp': String(timestamp)
114
- },
115
- body: JSON.stringify({ zkpY })
116
- }).catch(() => {
117
- // Échec de propagation silencieux pour ne pas perturber le thread principal
118
- });
119
- });
139
+ sendReport(zkpY, reportTimestamp);
140
+
141
+ // 2. Differential Privacy : Réponse randomisée par injection de leurres (Decoy ZKP)
142
+ if (dpEnabled) {
143
+ const decoyProbability = dpConfig.dummyRate !== undefined
144
+ ? dpConfig.dummyRate
145
+ : (1 / (1 + Math.exp(epsilon)));
146
+
147
+ if (secureRandomFloat() < decoyProbability) {
148
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
149
+ const decoyBytes = crypto.randomBytes(32);
150
+ const decoyInt = (BigInt('0x' + decoyBytes.toString('hex')) % (ZKP_P - 2n)) + 1n;
151
+ const decoyZkpY = decoyInt.toString(16);
152
+
153
+ const uDecoy = secureRandomFloat() - 0.5;
154
+ const safeUDecoy = Math.abs(uDecoy) < 1e-7 ? (uDecoy >= 0 ? 1e-7 : -1e-7) : uDecoy;
155
+ const b = 5000 / Math.max(0.1, epsilon);
156
+ const decoyNoise = Math.max(-60000, Math.min(60000, Math.round(-b * Math.sign(safeUDecoy) * Math.log(1 - 2 * Math.abs(safeUDecoy)))));
157
+ const decoyTimestamp = Date.now() + decoyNoise;
158
+
159
+ sendReport(decoyZkpY, decoyTimestamp);
160
+ }
161
+ }
120
162
  }
121
163
 
122
164
  const withTimeout = (promise, ms) => {
@@ -158,25 +200,15 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
158
200
  };
159
201
 
160
202
  const googlebotEntries = loadBotWhitelist('googlebot.json', [
161
- "2001:4860:4801:10::/64",
162
- "2001:4860:4801:11::/64",
163
- "2001:4860:4801:12::/64",
164
- // ... [Keep fallback inline values for safety]
165
- "66.249.79.64"
166
203
  ]);
167
204
 
168
205
  const bingbotEntries = loadBotWhitelist('bingbot.json', [
169
- "157.55.39.0/24",
170
- "207.46.13.0/24",
171
- // ... [Keep fallback inline values for safety]
172
- "40.77.178.0/23"
173
206
  ]);
174
207
 
175
208
  const yandexEntries = loadBotWhitelist('yandex.json', [
176
- "2a02:6b8::/29",
177
- "5.45.192.0/18",
178
- // ... [Keep fallback inline values for safety]
179
- "213.180.192.0/19"
209
+ ]);
210
+
211
+ const facebookEntries = loadBotWhitelist('facebook.json', [
180
212
  ]);
181
213
 
182
214
  function generateSessionMapping() {
@@ -361,7 +393,7 @@ export function generateStatelessTicket(payload) {
361
393
  * @param {object} context - Le contexte de la requête.
362
394
  * @returns {{protocolAnomalyScore: number}}
363
395
  */
364
- function getProtocolAnomalyScore(context) {
396
+ export function getProtocolAnomalyScore(context) {
365
397
  let http2Anomaly = 0.0;
366
398
  let quicAnomaly = 0.0;
367
399
 
@@ -374,9 +406,10 @@ function getProtocolAnomalyScore(context) {
374
406
  const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
375
407
  if (h2Fp && typeof h2Fp === 'string') {
376
408
  const parts = h2Fp.split('|');
377
- if (parts.length >= 4) {
409
+ if (parts.length >= 3) {
378
410
  const connWindow = parseInt(parts[1], 10);
379
- const headerOrder = parts[3];
411
+ const streamPriority = parts[2] || '';
412
+ const headerOrder = parts.length > 3 ? parts[3] : '';
380
413
  const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
381
414
  const isFirefox = browser.startsWith('Firefox');
382
415
  const isSafari = browser.startsWith('Safari');
@@ -384,11 +417,43 @@ function getProtocolAnomalyScore(context) {
384
417
  if (isChromium) {
385
418
  if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
386
419
  if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
420
+ if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
387
421
  } else if (isFirefox) {
388
422
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
389
423
  } else if (isSafari) {
390
424
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
391
425
  }
426
+
427
+ // Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
428
+ if (parts.length >= 5) {
429
+ const frameCountsStr = parts[4];
430
+ const frameCounts = {};
431
+ frameCountsStr.split(',').forEach(item => {
432
+ const kv = item.split(':');
433
+ if (kv.length === 2) {
434
+ frameCounts[kv[0]] = parseInt(kv[1], 10);
435
+ }
436
+ });
437
+
438
+ const priorityCount = frameCounts.p || 0;
439
+ const windowUpdateCount = frameCounts.w || 0;
440
+ const continuationCount = frameCounts.c || 0;
441
+
442
+ if (isChromium) {
443
+ // Chrome envoie des trames PRIORITY pour l'arbre de dépendances
444
+ if (priorityCount === 0) http2Anomaly += 25.0;
445
+ // Chrome est agressif avec les WINDOW_UPDATE
446
+ if (windowUpdateCount < 2) http2Anomaly += 20.0;
447
+ } else if (isFirefox) {
448
+ // Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
449
+ if (priorityCount > 1) http2Anomaly += 20.0;
450
+ }
451
+
452
+ // Les bots génériques n'envoient souvent pas de trames CONTINUATION pour les en-têtes longs
453
+ if (continuationCount === 0 && (headerOrder.match(/,/g) || []).length > 3) {
454
+ http2Anomaly += 30.0;
455
+ }
456
+ }
392
457
  }
393
458
  }
394
459
 
@@ -490,7 +555,7 @@ function getProtocolAnomalyScore(context) {
490
555
  */
491
556
  function getRenderingAnomalyScore(context) {
492
557
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
493
- if (!behaviorHeader) {
558
+ if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
494
559
  return { renderingAnomalyScore: 0.0 };
495
560
  }
496
561
  try {
@@ -580,33 +645,45 @@ export function parseStatelessTicket(ticket) {
580
645
  }
581
646
 
582
647
  /**
583
- * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
648
+ * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau par domaine.
584
649
  * @param {string} clientIp - L'adresse IP du client.
650
+ * @param {string} [domain='default'] - Le domaine/hôte ciblé (en-tête Host).
651
+ * @param {object} [rateLimitConfig] - Configuration optionnelle du limiteur.
585
652
  * @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
586
653
  */
587
- async function checkChallengeRateLimit(clientIp) {
588
- const subnet = getIpSubnet(clientIp);
654
+ async function checkChallengeRateLimit(clientIp, domain = 'default', rateLimitConfig = {}) {
655
+ if (rateLimitConfig && rateLimitConfig.enabled === false) {
656
+ return true;
657
+ }
658
+ if (!clientIp || isLoopbackIp(clientIp)) {
659
+ return true; // Bypass local/dev
660
+ }
661
+
662
+ const subnet = getIpSubnet(clientIp) || clientIp;
589
663
  if (!subnet) return false;
590
664
 
591
- const key = `rate-limit:${subnet}`;
592
- const rateLimitData = (await store.get(key)) || {
593
- tokens: 5.0,
594
- lastRefill: Date.now() / 1000
595
- };
665
+ const host = (domain || 'default').toLowerCase().split(':')[0];
666
+ const key = `rate-limit:${host}:${subnet}`;
596
667
 
597
- const capacity = 5.0;
598
- const refillRate = 0.1; // 1 token toutes les 10 secondes
668
+ const capacity = Number(rateLimitConfig?.capacity ?? 30.0);
669
+ const refillRate = Number(rateLimitConfig?.refillRate ?? 1.0); // 1 token par seconde (au lieu de 0.1)
599
670
  const now = Date.now() / 1000;
600
671
 
601
- const elapsed = now - rateLimitData.lastRefill;
672
+ const rateLimitData = (await store.get(key)) || {
673
+ tokens: capacity,
674
+ lastRefill: now
675
+ };
676
+
677
+ const elapsed = Math.max(0, now - rateLimitData.lastRefill);
602
678
  const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
679
+ const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
603
680
 
604
681
  if (tokens < 1.0) {
605
- await store.set(key, { tokens, lastRefill: now }, 60);
682
+ await store.set(key, { tokens, lastRefill: now }, ttl);
606
683
  return false;
607
684
  }
608
685
 
609
- await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, 60);
686
+ await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
610
687
  return true;
611
688
  }
612
689
 
@@ -656,20 +733,25 @@ const securityProfiles = {
656
733
  headerAnomalyScore: 0.1,
657
734
  requestPatternScore: 0.6,
658
735
  inconsistencyScore: 0.8,
659
- behaviorScore: 0.7, // Poids pour les métriques comportementales (souris, clavier)
736
+ behaviorScore: 0.7,
660
737
  honeypotScore: 1.0,
738
+ botScore: 1.0,
739
+ cookieDroppingScore: 0.9,
661
740
  crossLayerInconsistencyScore: 0.4,
662
741
  timeInconsistencyScore: 0.9,
663
- tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
664
- subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
665
- ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
666
- botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
667
- tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
742
+ tlsSpoofingScore: 0.8,
743
+ clientHintsInconsistencyScore: 0.7,
744
+ clickVarianceScore: 0.6,
745
+ subnetScore: 0.4,
746
+ ipReputationScore: 0.5,
747
+ botnetClusterScore: 0.6,
748
+ tcpAnomalyScore: 0.8,
668
749
  quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
669
- protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
670
750
  renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
751
+ http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
671
752
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
672
753
  virtualizationScore: 0.8,
754
+ mtuAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
673
755
  },
674
756
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
675
757
  patterns: {
@@ -703,15 +785,23 @@ const securityProfiles = {
703
785
  inconsistencyScore: 1.0,
704
786
  behaviorScore: 0.8,
705
787
  honeypotScore: 1.0,
788
+ botScore: 1.0,
789
+ cookieDroppingScore: 1.0,
706
790
  crossLayerInconsistencyScore: 0.6,
707
791
  timeInconsistencyScore: 1.0,
708
- tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
792
+ tlsSpoofingScore: 1.0,
793
+ clientHintsInconsistencyScore: 0.9,
794
+ clickVarianceScore: 0.7,
709
795
  subnetScore: 0.5,
710
- ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
711
- botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
712
- renderingAnomalyScore: 1.0, // NOUVEAU: Poids pour l'anomalie de rendu
713
- protocolAnomalyScore: 1.0, // NOUVEAU
714
- threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
796
+ ipReputationScore: 0.6,
797
+ botnetClusterScore: 0.8,
798
+ tcpAnomalyScore: 1.0,
799
+ quicAnomalyScore: 1.0,
800
+ renderingAnomalyScore: 1.0,
801
+ http2AnomalyScore: 1.0,
802
+ threatIntelScore: 1.0,
803
+ mtuAnomalyScore: 0.9,
804
+ virtualizationScore: 1.0,
715
805
  },
716
806
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
717
807
  patterns: {
@@ -746,15 +836,23 @@ const securityProfiles = {
746
836
  inconsistencyScore: 0.7,
747
837
  behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
748
838
  honeypotScore: 1.0,
839
+ botScore: 0.8,
840
+ cookieDroppingScore: 0.8,
749
841
  crossLayerInconsistencyScore: 0.5,
750
842
  timeInconsistencyScore: 0.8,
751
- tlsSpoofingScore: 0.7, // Important pour les API
843
+ tlsSpoofingScore: 0.7,
844
+ clientHintsInconsistencyScore: 0.6,
845
+ clickVarianceScore: 0.3,
752
846
  subnetScore: 0.4,
753
- ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
754
- botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
755
- tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
756
- protocolAnomalyScore: 0.8,
757
- quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
847
+ ipReputationScore: 0.5,
848
+ botnetClusterScore: 0.7,
849
+ tcpAnomalyScore: 0.8,
850
+ quicAnomalyScore: 0.8,
851
+ http2AnomalyScore: 0.8,
852
+ renderingAnomalyScore: 0.2,
853
+ threatIntelScore: 0.6,
854
+ virtualizationScore: 0.8,
855
+ mtuAnomalyScore: 0.9,
758
856
  },
759
857
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
760
858
  patterns: {
@@ -801,9 +899,11 @@ const securityProfiles = {
801
899
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
802
900
  botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
803
901
  tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
804
- protocolAnomalyScore: 0.5,
805
902
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
903
+ http2AnomalyScore: 0.5,
806
904
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
905
+ mtuAnomalyScore: 0.9,
906
+ virtualizationScore: 0.8,
807
907
  },
808
908
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
809
909
  patterns: {
@@ -832,7 +932,6 @@ const securityProfiles = {
832
932
  historyScore: 0.4,
833
933
  rotationScore: 0.6,
834
934
  headerAnomalyScore: 0.2,
835
- // Utilisation d'un score de pattern unifié avec un poids très élevé
836
935
  requestPatternScore: 0.9,
837
936
  inconsistencyScore: 1.0, // Crucial for preventing account takeover
838
937
  behaviorScore: 0.8, // Important for checkout/login forms
@@ -849,9 +948,11 @@ const securityProfiles = {
849
948
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
850
949
  botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
851
950
  tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
852
- protocolAnomalyScore: 0.9,
853
951
  quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
854
- 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é
952
+ http2AnomalyScore: 0.9,
953
+ renderingAnomalyScore: 0.9,
954
+ mtuAnomalyScore: 0.9,
955
+ virtualizationScore: 0.8,
855
956
  },
856
957
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
857
958
  patterns: {
@@ -1466,88 +1567,6 @@ export const verifyTspChallenge = (
1466
1567
  }
1467
1568
  };
1468
1569
 
1469
- /**
1470
- * Generates the HTML content for a memory-intensive PoW challenge.
1471
- */
1472
- const generateMemoryPoWChallenge = (
1473
- clientIp,
1474
- nonce,
1475
- difficulty = 16,
1476
- path = "",
1477
- ) => {
1478
- const safePath = sanitizeRedirectPath(path);
1479
- // difficulty here is the buffer size in MB.
1480
- return `
1481
- <html>
1482
- <head><title>Advanced Security Check</title></head>
1483
- <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1484
- <h1>Enhanced Verification... (Level 2)</h1>
1485
- <p>Your activity requires an additional security check.</p>
1486
- <div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
1487
- <script>
1488
- async function solve() {
1489
- const nonce = ${safeJsonStringify(nonce)};
1490
- const size = ${difficulty} * 1024 * 1024; // en octets
1491
- const iterations = size / 16;
1492
-
1493
- try {
1494
- const buffer = new Uint32Array(size / 4);
1495
- let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
1496
- for (let i = 0; i < buffer.length; i++) {
1497
- buffer[i] = (h = Math.imul(h ^ i, 1597334677));
1498
- }
1499
-
1500
- let finalHash = 0;
1501
- for(let i = 0; i < iterations; i++) {
1502
- const addr = buffer[i % buffer.length] % buffer.length;
1503
- finalHash ^= buffer[addr];
1504
- }
1505
- window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1506
- } catch(e) {
1507
- window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1508
- }
1509
- }
1510
- solve();
1511
- </script>
1512
- </body>
1513
- </html>`;
1514
- };
1515
-
1516
- /**
1517
- * Verifies if a PoW solution is valid and generates a clearance ticket.
1518
- */
1519
- export const verifyPoWAndGenerateTicket = async (
1520
- ip,
1521
- nonce,
1522
- solution,
1523
- difficulty = 4,
1524
- deviceId = '',
1525
- deviceHash = ''
1526
- ) => {
1527
- // 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
1528
- const hash = crypto
1529
- .createHash("sha256")
1530
- .update(`${ip}:${nonce}:${solution}`)
1531
- .digest("hex");
1532
-
1533
- if (!hash.startsWith("0".repeat(difficulty))) {
1534
- return null;
1535
- }
1536
-
1537
- // 2. Generate a signed and encrypted stateless ticket
1538
- const expiry = Date.now() + 3600000; // 1 hour
1539
- const payload = {
1540
- expiry,
1541
- originalIp: ip,
1542
- deviceId,
1543
- deviceHash
1544
- };
1545
-
1546
- return generateStatelessTicket(payload);
1547
- };
1548
-
1549
-
1550
-
1551
1570
  /**
1552
1571
  * Verifies a memory PoW solution.
1553
1572
  *
@@ -1725,6 +1744,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1725
1744
  if (!expiry || Date.now() > expiry) {
1726
1745
  return false;
1727
1746
  }
1747
+ if (statelessData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1748
+ if (!deviceId || deviceId === storedDeviceId) {
1749
+ return true;
1750
+ }
1751
+ }
1728
1752
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1729
1753
  const expectedY = storedDeviceHash.split(':')[1];
1730
1754
  if (zkpProof) {
@@ -1752,6 +1776,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1752
1776
  await store.delete(`ticket:${ticket}`);
1753
1777
  return false;
1754
1778
  }
1779
+ if (ticketData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
1780
+ if (!deviceId || deviceId === storedDeviceId) {
1781
+ return true;
1782
+ }
1783
+ }
1755
1784
  if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1756
1785
  const expectedY = storedDeviceHash.split(':')[1];
1757
1786
  if (zkpProof) {
@@ -2335,13 +2364,37 @@ function analyzeTouchMovements(history) {
2335
2364
  * @returns {{behaviorScore: number}}
2336
2365
  */
2337
2366
  function getBehaviorScore(context) {
2338
- const behaviorHeader = context.headers["x-behavior-metrics"];
2339
- if (!behaviorHeader) {
2367
+ const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
2368
+ if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
2340
2369
  return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
2341
2370
  }
2342
2371
 
2372
+ // Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
2373
+ if (behaviorHeader === 1 || behaviorHeader === '1') {
2374
+ return { behaviorScore: 0 };
2375
+ }
2376
+ if (behaviorHeader === 0 || behaviorHeader === '0') {
2377
+ return { behaviorScore: 100 };
2378
+ }
2379
+
2380
+ // Traitement direct si un score numérique est envoyé côté client
2381
+ if (typeof behaviorHeader === 'number') {
2382
+ return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
2383
+ }
2384
+ if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
2385
+ const trimmed = behaviorHeader.trim();
2386
+ if (trimmed === '1') return { behaviorScore: 0 };
2387
+ if (trimmed === '0') return { behaviorScore: 100 };
2388
+ return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
2389
+ }
2390
+
2343
2391
  try {
2344
2392
  const metrics = JSON.parse(behaviorHeader);
2393
+ if (metrics === 1) return { behaviorScore: 0 };
2394
+ if (metrics === 0) return { behaviorScore: 100 };
2395
+ if (typeof metrics === 'number') {
2396
+ return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
2397
+ }
2345
2398
  let score = 0;
2346
2399
 
2347
2400
  // 1. Pénalité maximale si un honeypot client a été déclenché.
@@ -2355,9 +2408,18 @@ function getBehaviorScore(context) {
2355
2408
  const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
2356
2409
  const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
2357
2410
 
2358
- // Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
2411
+ // Pénalité pour absence totale d'interaction. Un utilisateur légitime peut simplement lire la page.
2412
+ // On applique donc une pénalité de base faible, qui est amplifiée uniquement si d'autres
2413
+ // signaux passifs de bot (ex: rendu offscreen) sont présents.
2359
2414
  if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
2360
- score += 40;
2415
+ let noInteractionPenalty = 5; // Pénalité de base très faible.
2416
+
2417
+ // Amplification si d'autres signaux passifs de bot sont présents.
2418
+ if (metrics.rendering?.offscreenAnom) {
2419
+ noInteractionPenalty += 40;
2420
+ }
2421
+
2422
+ score += noInteractionPenalty;
2361
2423
  }
2362
2424
 
2363
2425
  // 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
@@ -2397,21 +2459,19 @@ function getBehaviorScore(context) {
2397
2459
  if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
2398
2460
  score += 30; // Spoofed pointer area size
2399
2461
  }
2400
- }
2401
- if (touch.segments.length > 10) {
2402
- const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2403
- if (benfordDev > 0.18) score += 35;
2404
- }
2405
-
2406
- // Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
2407
- const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2408
- if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
2409
- if (metrics.motionVariance === 0) {
2410
- score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2462
+ if (touch.segments.length > 10) {
2463
+ const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2464
+ if (benfordDev > 0.18) score += 35;
2411
2465
  }
2412
2466
  }
2413
2467
  }
2414
2468
 
2469
+ // Détection de ferme mobile : un appareil mobile parfaitement immobile est suspect, indépendamment des interactions tactiles.
2470
+ const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2471
+ if (isMobileDevice && typeof metrics.motionVariance === 'number' && metrics.motionVariance === 0) {
2472
+ score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2473
+ }
2474
+
2415
2475
  // Plausibilité de la latence de frappe
2416
2476
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
2417
2477
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
@@ -2981,6 +3041,10 @@ function analyzeClickPositions(history) {
2981
3041
  * @returns {{clickVarianceScore: number}}
2982
3042
  */
2983
3043
  function getClickVarianceScore(context) {
3044
+ const rawMetrics = context.headers?.['x-behavior-metrics'];
3045
+ if (!rawMetrics || !isNaN(Number(rawMetrics))) {
3046
+ return { clickVarianceScore: 0 };
3047
+ }
2984
3048
  const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
2985
3049
  const score = analyzeClickPositions(metrics.clicksHistory);
2986
3050
  return { clickVarianceScore: score };
@@ -3832,7 +3896,7 @@ async function getBehavioralIndicators(context, deviceData) {
3832
3896
  // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3833
3897
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
3834
3898
  let webauthnVerified = false;
3835
- if (behaviorHeader) {
3899
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
3836
3900
  try {
3837
3901
  const metrics = JSON.parse(behaviorHeader);
3838
3902
  if (metrics && metrics.webauthnAnchor) {
@@ -3908,6 +3972,52 @@ function getVirtualizationAnomalyScore(context) {
3908
3972
  return Math.min(100.0, score);
3909
3973
  }
3910
3974
 
3975
+ /**
3976
+ * @private
3977
+ * Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
3978
+ * @param {object} context - The request context.
3979
+ * @returns {{mtuAnomalyScore: number}}
3980
+ */
3981
+ function getMtuAnomalyScore(context) {
3982
+ const mtuHeader = context.headers?.['x-tcp-mtu-info'];
3983
+ if (!mtuHeader || typeof mtuHeader !== 'string') {
3984
+ return { mtuAnomalyScore: 0.0 };
3985
+ }
3986
+
3987
+ const parts = mtuHeader.split(':');
3988
+ if (parts.length < 2) {
3989
+ return { mtuAnomalyScore: 0.0 };
3990
+ }
3991
+
3992
+ const mtu = parseInt(parts[0], 10);
3993
+ const df = parseInt(parts[1], 10); // Don't Fragment bit (1 or 0)
3994
+
3995
+ if (isNaN(mtu) || isNaN(df)) {
3996
+ return { mtuAnomalyScore: 0.0 };
3997
+ }
3998
+
3999
+ let score = 0.0;
4000
+
4001
+ // Pénalité modérée pour les MTU typiques des VPNs/tunnels.
4002
+ if (mtu > 1200 && mtu <= 1420) {
4003
+ score += 35.0;
4004
+ } else if (mtu > 1420 && mtu < 1492) {
4005
+ score += 20.0;
4006
+ }
4007
+
4008
+ const ua = context.headers?.['user-agent'] || '';
4009
+ const uaParts = parseUserAgent(ua);
4010
+ const os = uaParts.os;
4011
+
4012
+ // Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
4013
+ if (os) {
4014
+ if (os.startsWith('Windows') && mtu < 1492) score += 20.0;
4015
+ if ((os.startsWith('Android') || os.startsWith('iOS')) && mtu < 1480) score += 15.0;
4016
+ if (df === 0 && (os.startsWith('Windows') || os.startsWith('Mac') || os.startsWith('Linux'))) score += 40.0;
4017
+ }
4018
+
4019
+ return { mtuAnomalyScore: Math.min(100.0, score) };
4020
+ }
3911
4021
 
3912
4022
  /**
3913
4023
  * Returns a vector of raw (unweighted) suspicion scores.
@@ -3976,7 +4086,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
3976
4086
  const { botScore } = getBotScore(context);
3977
4087
 
3978
4088
  // NOUVEAU: On calcule le score d'incohérence temporelle.
3979
- const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
4089
+ let parsedBehaviorMetrics = {};
4090
+ const rawBehaviorHeader = context.headers?.['x-behavior-metrics'];
4091
+ if (rawBehaviorHeader && typeof rawBehaviorHeader === 'string' && rawBehaviorHeader.startsWith('{')) {
4092
+ try { parsedBehaviorMetrics = JSON.parse(rawBehaviorHeader); } catch (e) {}
4093
+ }
4094
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, parsedBehaviorMetrics, deviceData);
3980
4095
 
3981
4096
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
3982
4097
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
@@ -3990,9 +4105,10 @@ export const getSuspicionVector = async (context, securityConfig) => {
3990
4105
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3991
4106
 
3992
4107
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3993
- const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
4108
+ const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
3994
4109
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3995
4110
  const virtualizationScore = getVirtualizationAnomalyScore(context);
4111
+ const { mtuAnomalyScore } = getMtuAnomalyScore(context);
3996
4112
 
3997
4113
  // Save the updated device state to the store
3998
4114
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
@@ -4003,8 +4119,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
4003
4119
  if (Array.isArray(deviceData.ips)) {
4004
4120
  deviceData.ips = new Set(deviceData.ips);
4005
4121
  }
4006
- // Le vecteur de suspicion est maintenant complet.
4007
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4122
+ // Le vecteur de suspicion est maintenant complet.
4123
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore, mtuAnomalyScore };
4008
4124
  };
4009
4125
 
4010
4126
  // A residential user can change networks (home, 4G, public wifi).
@@ -4027,7 +4143,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
4027
4143
  // This function now acts as a lightweight wrapper around the engine's identifyRequest method.
4028
4144
  // It requires a default configuration to work.
4029
4145
  const config = securityConfig || {
4030
- weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8, honeypotScore: 1.0 },
4146
+ weights: {
4147
+ historyScore: 0.3,
4148
+ rotationScore: 0.5,
4149
+ headerAnomalyScore: 0.2,
4150
+ requestPatternScore: 0.6,
4151
+ inconsistencyScore: 0.8,
4152
+ behaviorScore: 0.7,
4153
+ honeypotScore: 1.0,
4154
+ botScore: 1.0,
4155
+ cookieDroppingScore: 0.9,
4156
+ crossLayerInconsistencyScore: 0.4,
4157
+ timeInconsistencyScore: 0.9,
4158
+ tlsSpoofingScore: 0.8,
4159
+ clientHintsInconsistencyScore: 0.7,
4160
+ clickVarianceScore: 0.6,
4161
+ subnetScore: 0.4,
4162
+ ipReputationScore: 0.5,
4163
+ botnetClusterScore: 0.7,
4164
+ tcpAnomalyScore: 0.8,
4165
+ quicAnomalyScore: 0.8,
4166
+ renderingAnomalyScore: 0.8,
4167
+ http2AnomalyScore: 0.8,
4168
+ threatIntelScore: 1.0,
4169
+ virtualizationScore: 0.8
4170
+ },
4031
4171
  thresholds: { low: 20, medium: 40, high: 75 },
4032
4172
  honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
4033
4173
  };
@@ -4199,6 +4339,24 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
4199
4339
  </body></html>`;
4200
4340
  }
4201
4341
 
4342
+ /**
4343
+ * Generates closed-shadow DOM honeypot markup for server-rendered challenge pages.
4344
+ * Utilizes Declarative Shadow DOM with mode="closed" to prevent screen reader false positives
4345
+ * while capturing bot web scrapers and crawlers.
4346
+ * @param {string[]} trapUrls
4347
+ * @returns {string}
4348
+ */
4349
+ function generateClosedShadowTraps(trapUrls = []) {
4350
+ const list = Array.isArray(trapUrls) ? trapUrls : [];
4351
+ if (list.length === 0) return '';
4352
+
4353
+ const trapLinksHtml = list.map((url, idx) => {
4354
+ return `<a href="${url}" rel="nofollow" tabindex="-1"><span>&gt; ${idx + 1}</span></a>`;
4355
+ }).join(' ');
4356
+
4357
+ return `<div style="position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;" aria-hidden="true"><template shadowrootmode="closed" shadowroot="closed"><style>:host { position: absolute; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; } a { color: transparent; text-decoration: none; }</style>${trapLinksHtml}</template></div>`;
4358
+ }
4359
+
4202
4360
  /**
4203
4361
  * Generates the HTML content for a combined CPU + Memory PoW challenge.
4204
4362
  * @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
@@ -4215,6 +4373,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4215
4373
  const fingerprint = originalFingerprint;
4216
4374
  const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
4217
4375
  const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
4376
+ const trapContainerHtml = generateClosedShadowTraps(trapUrls);
4218
4377
 
4219
4378
  // Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
4220
4379
  const clientInitConfig = {
@@ -4282,12 +4441,24 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
4282
4441
  }
4283
4442
 
4284
4443
  if (!htmlTemplate) {
4285
- htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script></body></html>`; // eslint-disable-line max-len
4444
+ htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`; // eslint-disable-line max-len
4286
4445
  }
4287
4446
 
4288
- return htmlTemplate
4447
+ let renderedHtml = htmlTemplate
4289
4448
  .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
4290
4449
  .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
4450
+
4451
+ if (renderedHtml.includes('<!-- FINGERPRINT_TRAPS -->')) {
4452
+ renderedHtml = renderedHtml.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
4453
+ } else if (trapContainerHtml) {
4454
+ if (renderedHtml.includes('</body>')) {
4455
+ renderedHtml = renderedHtml.replace('</body>', `${trapContainerHtml}</body>`);
4456
+ } else {
4457
+ renderedHtml += trapContainerHtml;
4458
+ }
4459
+ }
4460
+
4461
+ return renderedHtml;
4291
4462
  }
4292
4463
 
4293
4464
  /**
@@ -4415,7 +4586,7 @@ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
4415
4586
  const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4416
4587
 
4417
4588
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
4418
- if (behaviorHeader) {
4589
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
4419
4590
  try {
4420
4591
  const metrics = JSON.parse(behaviorHeader);
4421
4592
  if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
@@ -4577,7 +4748,9 @@ export class FingerprintEngine {
4577
4748
  'wasm',
4578
4749
  'similarityThreshold', 'reset',
4579
4750
  'ed25519_private_key', 'ed25519_public_key', 'upowModel',
4580
- 'federatedPeers', 'federationSecret', 'filterWhitelist'
4751
+ 'federatedPeers', 'federationSecret', 'filterWhitelist',
4752
+ 'challengeRateLimit',
4753
+ 'differentialPrivacy', 'dpEpsilon'
4581
4754
  ]);
4582
4755
 
4583
4756
  // 1. Check for essential keys
@@ -4615,36 +4788,31 @@ export class FingerprintEngine {
4615
4788
  }
4616
4789
  }
4617
4790
  calculateFinalScore(suspicionVector) {
4618
- const { weights } = this.securityConfig;
4619
- if (!weights) return 0;
4620
-
4621
- const score =
4622
- (suspicionVector.historyScore || 0) * (weights.historyScore || 0) +
4623
- (suspicionVector.rotationScore || 0) * (weights.rotationScore || 0) +
4624
- (suspicionVector.headerAnomalyScore || 0) * (weights.headerAnomalyScore || 0) +
4625
- (suspicionVector.requestPatternScore || 0) * (weights.requestPatternScore || 0) +
4626
- (suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
4627
- (suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
4628
- (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
4629
- (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
4630
- (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
4631
- (suspicionVector.botnetClusterScore || 0) * (weights.botnetClusterScore || 0) +
4632
- (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
4633
- (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0) +
4634
- (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
4635
- (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
4636
- (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
4637
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
4638
- (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4639
- (suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
4640
- (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) + // NOUVEAU: HTTP/2 Anomaly
4641
- (suspicionVector.protocolAnomalyScore || 0) * (weights.protocolAnomalyScore || 0) +
4642
- (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4643
- (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4644
- (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
4645
- (suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
4646
-
4647
- return Math.min(100, score);
4791
+ const baseWeights = this.securityConfig.weights;
4792
+ if (!baseWeights) return 0;
4793
+
4794
+ const dynamicWeights = { ...baseWeights };
4795
+ if ((suspicionVector.mtuAnomalyScore || 0) > 50.0) {
4796
+ this._log('Tunnel detected, amplifying suspicion weights.', { mtuScore: suspicionVector.mtuAnomalyScore });
4797
+
4798
+ // Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
4799
+ dynamicWeights.tlsSpoofingScore = (baseWeights.tlsSpoofingScore || 0.8) * 1.25;
4800
+ dynamicWeights.crossLayerInconsistencyScore = (baseWeights.crossLayerInconsistencyScore || 0.4) * 1.4;
4801
+ dynamicWeights.clientHintsInconsistencyScore = (baseWeights.clientHintsInconsistencyScore || 0.7) * 1.2;
4802
+
4803
+ // Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
4804
+ dynamicWeights.behaviorScore = (baseWeights.behaviorScore || 0.7) * 1.15;
4805
+ dynamicWeights.requestPatternScore = (baseWeights.requestPatternScore || 0.6) * 1.2;
4806
+ }
4807
+
4808
+ let score = 0;
4809
+ for (const key in dynamicWeights) {
4810
+ const metricScore = suspicionVector[key] || 0.0;
4811
+ if (metricScore > 0) {
4812
+ score += metricScore * dynamicWeights[key];
4813
+ }
4814
+ }
4815
+ return Math.min(100.0, score);
4648
4816
  }
4649
4817
 
4650
4818
 
@@ -4965,9 +5133,84 @@ export class FingerprintEngine {
4965
5133
 
4966
5134
  // Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
4967
5135
  const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
5136
+
5137
+ // Règle Anti-Ferme : Même avec du matériel sécurisé, un appareil condamné ou menant une attaque certaine est révoqué et bloqué
5138
+ if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
5139
+ if (deviceData) {
5140
+ deviceData.condemned = true;
5141
+ deviceData.webauthnVerified = false;
5142
+ await store.set(`device:${deviceId}`, deviceData);
5143
+ }
5144
+ this._log('Condemned device or attack detected - revoking hardware trust and blocking', { deviceId });
5145
+ const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
5146
+ if (this.dryRun) {
5147
+ this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
5148
+ decision.intendedAction = decision.action;
5149
+ decision.action = 'next';
5150
+ delete decision.status;
5151
+ delete decision.body;
5152
+ if (requestContext._newCookies) {
5153
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5154
+ if (deviceCookie) decision.newCookieForResponse = deviceCookie;
5155
+ }
5156
+ }
5157
+ return decision;
5158
+ }
5159
+
5160
+ // Validation immédiate de l'attestation matérielle WebAuthn si présente dans la requête
5161
+ const behaviorHeader = requestContext.headers?.['x-behavior-metrics'];
5162
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
5163
+ try {
5164
+ const parsedMetrics = JSON.parse(behaviorHeader);
5165
+ if (parsedMetrics && parsedMetrics.webauthnAnchor && deviceData) {
5166
+ if (verifyWebAuthnHardwareAnchor(parsedMetrics.webauthnAnchor, deviceData)) {
5167
+ deviceData.webauthnVerified = true;
5168
+ await store.set(`device:${deviceId}`, deviceData);
5169
+ }
5170
+ }
5171
+ } catch (e) {}
5172
+ }
5173
+
4968
5174
  if (deviceData && deviceData.webauthnVerified) {
4969
5175
  this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
4970
- return { action: 'next', score: 0, vector: { webauthn_verified: 100 } };
5176
+ const greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
5177
+ const greenlistTicket = generateStatelessTicket({
5178
+ expiry: Date.now() + greenlistTtl,
5179
+ originalIp: clientIp,
5180
+ deviceId,
5181
+ deviceHash: 'webauthn:greenlist:' + (deviceData.webauthnCredentialId || deviceId),
5182
+ greenlist: true
5183
+ });
5184
+ const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
5185
+ requestContext.rawReq?.secure ||
5186
+ requestContext.rawReq?.protocol === 'https' ||
5187
+ requestContext.rawReq?.connection?.encrypted;
5188
+ const secureOption = isHttps || this.isProduction;
5189
+
5190
+ const decision = {
5191
+ action: 'next',
5192
+ score: 0,
5193
+ vector: { webauthn_verified: 100 },
5194
+ cookie: {
5195
+ name: 'pow_clearance',
5196
+ value: greenlistTicket,
5197
+ options: {
5198
+ httpOnly: true,
5199
+ secure: secureOption,
5200
+ sameSite: 'strict',
5201
+ ...(secureOption && { partitioned: true }),
5202
+ maxAge: greenlistTtl,
5203
+ path: '/'
5204
+ }
5205
+ }
5206
+ };
5207
+ if (requestContext._newCookies) {
5208
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
5209
+ if (deviceCookie) {
5210
+ decision.newCookieForResponse = deviceCookie;
5211
+ }
5212
+ }
5213
+ return decision;
4971
5214
  }
4972
5215
 
4973
5216
  if (isStatic) {
@@ -5010,7 +5253,7 @@ export class FingerprintEngine {
5010
5253
  if (filterWhitelist === true) {
5011
5254
  bypassWhitelist = this._hasCertainAttack(requestContext);
5012
5255
  } else if (typeof filterWhitelist === 'number') {
5013
- const res = await getScoreAndVector();
5256
+ const res = await getScoreAndVector();
5014
5257
  if (res.score > filterWhitelist) {
5015
5258
  bypassWhitelist = true;
5016
5259
  }
@@ -5589,10 +5832,11 @@ export class FingerprintEngine {
5589
5832
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5590
5833
  }
5591
5834
 
5592
- // --- AJOUT : Limiteur de débit (Token Bucket) ---
5593
- const rateLimitPassed = await checkChallengeRateLimit(clientIp);
5835
+ // --- Limiteur de débit par domaine et sous-réseau (Token Bucket) ---
5836
+ const domain = requestContext.headers?.host || 'default';
5837
+ const rateLimitPassed = await checkChallengeRateLimit(clientIp, domain, this.securityConfig?.challengeRateLimit);
5594
5838
  if (!rateLimitPassed) {
5595
- this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
5839
+ this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
5596
5840
  const decision = {
5597
5841
  action: 'block',
5598
5842
  status: 429,
@@ -6599,6 +6843,7 @@ export const default_whitelist = () => [
6599
6843
  googlebot_whitelist(),
6600
6844
  bingbot_whitelist(),
6601
6845
  yandex_whitelist(),
6846
+ facebook_whitelist(),
6602
6847
  // === Moteurs de recherche majeurs ===
6603
6848
  { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
6604
6849
  { userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
@@ -6716,6 +6961,11 @@ export const bingbot_whitelist = () => ({
6716
6961
  entries: bingbotEntries
6717
6962
  });
6718
6963
 
6964
+ export const facebook_whitelist = () => ({
6965
+ type: 'allowlist',
6966
+ entries: facebookEntries
6967
+ });
6968
+
6719
6969
 
6720
6970
  /**
6721
6971
  * Extracts the TLS Session ID or ticket hash from the request context.
@@ -6949,6 +7199,9 @@ export const powMiddleware = (securityConfig) => {
6949
7199
 
6950
7200
  case 'next':
6951
7201
  default:
7202
+ if (decision.cookie && res && typeof res.cookie === 'function') {
7203
+ res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
7204
+ }
6952
7205
  return next();
6953
7206
  }
6954
7207
  };
@@ -6962,8 +7215,10 @@ export const powMiddleware = (securityConfig) => {
6962
7215
  */
6963
7216
  export const __internal = {
6964
7217
  get store() { return store; }, // Export the store for testing
7218
+ checkChallengeRateLimit,
6965
7219
  getDeviceHash,
6966
7220
  getCompositeDeviceHash,
7221
+ generateClosedShadowTraps,
6967
7222
  getSuspicionVector,
6968
7223
  getTlsSessionId,
6969
7224
  pruneTrafficData,