@anonympins/fingerprint 0.7.3 → 0.7.4

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.
@@ -158,25 +158,12 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
158
158
  };
159
159
 
160
160
  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
161
  ]);
167
162
 
168
163
  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
164
  ]);
174
165
 
175
166
  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"
180
167
  ]);
181
168
 
182
169
  function generateSessionMapping() {
@@ -361,7 +348,7 @@ export function generateStatelessTicket(payload) {
361
348
  * @param {object} context - Le contexte de la requête.
362
349
  * @returns {{protocolAnomalyScore: number}}
363
350
  */
364
- function getProtocolAnomalyScore(context) {
351
+ export function getProtocolAnomalyScore(context) {
365
352
  let http2Anomaly = 0.0;
366
353
  let quicAnomaly = 0.0;
367
354
 
@@ -374,9 +361,10 @@ function getProtocolAnomalyScore(context) {
374
361
  const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
375
362
  if (h2Fp && typeof h2Fp === 'string') {
376
363
  const parts = h2Fp.split('|');
377
- if (parts.length >= 4) {
364
+ if (parts.length >= 3) {
378
365
  const connWindow = parseInt(parts[1], 10);
379
- const headerOrder = parts[3];
366
+ const streamPriority = parts[2] || '';
367
+ const headerOrder = parts.length > 3 ? parts[3] : '';
380
368
  const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
381
369
  const isFirefox = browser.startsWith('Firefox');
382
370
  const isSafari = browser.startsWith('Safari');
@@ -384,11 +372,43 @@ function getProtocolAnomalyScore(context) {
384
372
  if (isChromium) {
385
373
  if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
386
374
  if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
375
+ if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
387
376
  } else if (isFirefox) {
388
377
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
389
378
  } else if (isSafari) {
390
379
  if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
391
380
  }
381
+
382
+ // Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
383
+ if (parts.length >= 5) {
384
+ const frameCountsStr = parts[4];
385
+ const frameCounts = {};
386
+ frameCountsStr.split(',').forEach(item => {
387
+ const kv = item.split(':');
388
+ if (kv.length === 2) {
389
+ frameCounts[kv[0]] = parseInt(kv[1], 10);
390
+ }
391
+ });
392
+
393
+ const priorityCount = frameCounts.p || 0;
394
+ const windowUpdateCount = frameCounts.w || 0;
395
+ const continuationCount = frameCounts.c || 0;
396
+
397
+ if (isChromium) {
398
+ // Chrome envoie des trames PRIORITY pour l'arbre de dépendances
399
+ if (priorityCount === 0) http2Anomaly += 25.0;
400
+ // Chrome est agressif avec les WINDOW_UPDATE
401
+ if (windowUpdateCount < 2) http2Anomaly += 20.0;
402
+ } else if (isFirefox) {
403
+ // Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
404
+ if (priorityCount > 1) http2Anomaly += 20.0;
405
+ }
406
+
407
+ // Les bots génériques n'envoient souvent pas de trames CONTINUATION pour les en-têtes longs
408
+ if (continuationCount === 0 && (headerOrder.match(/,/g) || []).length > 3) {
409
+ http2Anomaly += 30.0;
410
+ }
411
+ }
392
412
  }
393
413
  }
394
414
 
@@ -490,7 +510,7 @@ function getProtocolAnomalyScore(context) {
490
510
  */
491
511
  function getRenderingAnomalyScore(context) {
492
512
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
493
- if (!behaviorHeader) {
513
+ if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
494
514
  return { renderingAnomalyScore: 0.0 };
495
515
  }
496
516
  try {
@@ -580,33 +600,45 @@ export function parseStatelessTicket(ticket) {
580
600
  }
581
601
 
582
602
  /**
583
- * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
603
+ * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau par domaine.
584
604
  * @param {string} clientIp - L'adresse IP du client.
605
+ * @param {string} [domain='default'] - Le domaine/hôte ciblé (en-tête Host).
606
+ * @param {object} [rateLimitConfig] - Configuration optionnelle du limiteur.
585
607
  * @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
586
608
  */
587
- async function checkChallengeRateLimit(clientIp) {
588
- const subnet = getIpSubnet(clientIp);
609
+ async function checkChallengeRateLimit(clientIp, domain = 'default', rateLimitConfig = {}) {
610
+ if (rateLimitConfig && rateLimitConfig.enabled === false) {
611
+ return true;
612
+ }
613
+ if (!clientIp || isLoopbackIp(clientIp)) {
614
+ return true; // Bypass local/dev
615
+ }
616
+
617
+ const subnet = getIpSubnet(clientIp) || clientIp;
589
618
  if (!subnet) return false;
590
619
 
591
- const key = `rate-limit:${subnet}`;
592
- const rateLimitData = (await store.get(key)) || {
593
- tokens: 5.0,
594
- lastRefill: Date.now() / 1000
595
- };
620
+ const host = (domain || 'default').toLowerCase().split(':')[0];
621
+ const key = `rate-limit:${host}:${subnet}`;
596
622
 
597
- const capacity = 5.0;
598
- const refillRate = 0.1; // 1 token toutes les 10 secondes
623
+ const capacity = Number(rateLimitConfig?.capacity ?? 30.0);
624
+ const refillRate = Number(rateLimitConfig?.refillRate ?? 1.0); // 1 token par seconde (au lieu de 0.1)
599
625
  const now = Date.now() / 1000;
600
626
 
601
- const elapsed = now - rateLimitData.lastRefill;
627
+ const rateLimitData = (await store.get(key)) || {
628
+ tokens: capacity,
629
+ lastRefill: now
630
+ };
631
+
632
+ const elapsed = Math.max(0, now - rateLimitData.lastRefill);
602
633
  const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
634
+ const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
603
635
 
604
636
  if (tokens < 1.0) {
605
- await store.set(key, { tokens, lastRefill: now }, 60);
637
+ await store.set(key, { tokens, lastRefill: now }, ttl);
606
638
  return false;
607
639
  }
608
640
 
609
- await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, 60);
641
+ await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
610
642
  return true;
611
643
  }
612
644
 
@@ -656,18 +688,22 @@ const securityProfiles = {
656
688
  headerAnomalyScore: 0.1,
657
689
  requestPatternScore: 0.6,
658
690
  inconsistencyScore: 0.8,
659
- behaviorScore: 0.7, // Poids pour les métriques comportementales (souris, clavier)
691
+ behaviorScore: 0.7,
660
692
  honeypotScore: 1.0,
693
+ botScore: 1.0,
694
+ cookieDroppingScore: 0.9,
661
695
  crossLayerInconsistencyScore: 0.4,
662
696
  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
697
+ tlsSpoofingScore: 0.8,
698
+ clientHintsInconsistencyScore: 0.7,
699
+ clickVarianceScore: 0.6,
700
+ subnetScore: 0.4,
701
+ ipReputationScore: 0.5,
702
+ botnetClusterScore: 0.6,
703
+ tcpAnomalyScore: 0.8,
668
704
  quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
669
- protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
670
705
  renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
706
+ http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
671
707
  threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
672
708
  virtualizationScore: 0.8,
673
709
  },
@@ -703,15 +739,22 @@ const securityProfiles = {
703
739
  inconsistencyScore: 1.0,
704
740
  behaviorScore: 0.8,
705
741
  honeypotScore: 1.0,
742
+ botScore: 1.0,
743
+ cookieDroppingScore: 1.0,
706
744
  crossLayerInconsistencyScore: 0.6,
707
745
  timeInconsistencyScore: 1.0,
708
- tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
746
+ tlsSpoofingScore: 1.0,
747
+ clientHintsInconsistencyScore: 0.9,
748
+ clickVarianceScore: 0.7,
709
749
  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é
750
+ ipReputationScore: 0.6,
751
+ botnetClusterScore: 0.8,
752
+ tcpAnomalyScore: 1.0,
753
+ quicAnomalyScore: 1.0,
754
+ renderingAnomalyScore: 1.0,
755
+ http2AnomalyScore: 1.0,
756
+ threatIntelScore: 1.0,
757
+ virtualizationScore: 1.0,
715
758
  },
716
759
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
717
760
  patterns: {
@@ -746,15 +789,22 @@ const securityProfiles = {
746
789
  inconsistencyScore: 0.7,
747
790
  behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
748
791
  honeypotScore: 1.0,
792
+ botScore: 0.8,
793
+ cookieDroppingScore: 0.8,
749
794
  crossLayerInconsistencyScore: 0.5,
750
795
  timeInconsistencyScore: 0.8,
751
- tlsSpoofingScore: 0.7, // Important pour les API
796
+ tlsSpoofingScore: 0.7,
797
+ clientHintsInconsistencyScore: 0.6,
798
+ clickVarianceScore: 0.3,
752
799
  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
800
+ ipReputationScore: 0.5,
801
+ botnetClusterScore: 0.7,
802
+ tcpAnomalyScore: 0.8,
803
+ quicAnomalyScore: 0.8,
804
+ http2AnomalyScore: 0.8,
805
+ renderingAnomalyScore: 0.2,
806
+ threatIntelScore: 0.6,
807
+ virtualizationScore: 0.8,
758
808
  },
759
809
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
760
810
  patterns: {
@@ -801,9 +851,10 @@ const securityProfiles = {
801
851
  ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
802
852
  botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
803
853
  tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
804
- protocolAnomalyScore: 0.5,
805
854
  quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
855
+ http2AnomalyScore: 0.5,
806
856
  renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
857
+ virtualizationScore: 0.8,
807
858
  },
808
859
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
809
860
  patterns: {
@@ -832,7 +883,6 @@ const securityProfiles = {
832
883
  historyScore: 0.4,
833
884
  rotationScore: 0.6,
834
885
  headerAnomalyScore: 0.2,
835
- // Utilisation d'un score de pattern unifié avec un poids très élevé
836
886
  requestPatternScore: 0.9,
837
887
  inconsistencyScore: 1.0, // Crucial for preventing account takeover
838
888
  behaviorScore: 0.8, // Important for checkout/login forms
@@ -849,9 +899,10 @@ const securityProfiles = {
849
899
  ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
850
900
  botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
851
901
  tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
852
- protocolAnomalyScore: 0.9,
853
902
  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é
903
+ http2AnomalyScore: 0.9,
904
+ renderingAnomalyScore: 0.9,
905
+ virtualizationScore: 0.8,
855
906
  },
856
907
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
857
908
  patterns: {
@@ -1466,88 +1517,6 @@ export const verifyTspChallenge = (
1466
1517
  }
1467
1518
  };
1468
1519
 
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
1520
  /**
1552
1521
  * Verifies a memory PoW solution.
1553
1522
  *
@@ -2335,13 +2304,37 @@ function analyzeTouchMovements(history) {
2335
2304
  * @returns {{behaviorScore: number}}
2336
2305
  */
2337
2306
  function getBehaviorScore(context) {
2338
- const behaviorHeader = context.headers["x-behavior-metrics"];
2339
- if (!behaviorHeader) {
2307
+ const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
2308
+ if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
2340
2309
  return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
2341
2310
  }
2342
2311
 
2312
+ // Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
2313
+ if (behaviorHeader === 1 || behaviorHeader === '1') {
2314
+ return { behaviorScore: 0 };
2315
+ }
2316
+ if (behaviorHeader === 0 || behaviorHeader === '0') {
2317
+ return { behaviorScore: 100 };
2318
+ }
2319
+
2320
+ // Traitement direct si un score numérique est envoyé côté client
2321
+ if (typeof behaviorHeader === 'number') {
2322
+ return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
2323
+ }
2324
+ if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
2325
+ const trimmed = behaviorHeader.trim();
2326
+ if (trimmed === '1') return { behaviorScore: 0 };
2327
+ if (trimmed === '0') return { behaviorScore: 100 };
2328
+ return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
2329
+ }
2330
+
2343
2331
  try {
2344
2332
  const metrics = JSON.parse(behaviorHeader);
2333
+ if (metrics === 1) return { behaviorScore: 0 };
2334
+ if (metrics === 0) return { behaviorScore: 100 };
2335
+ if (typeof metrics === 'number') {
2336
+ return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
2337
+ }
2345
2338
  let score = 0;
2346
2339
 
2347
2340
  // 1. Pénalité maximale si un honeypot client a été déclenché.
@@ -2355,9 +2348,18 @@ function getBehaviorScore(context) {
2355
2348
  const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
2356
2349
  const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
2357
2350
 
2358
- // Pénalité pour absence totale d'interaction (pas de mouvements, pas de frappes).
2351
+ // Pénalité pour absence totale d'interaction. Un utilisateur légitime peut simplement lire la page.
2352
+ // On applique donc une pénalité de base faible, qui est amplifiée uniquement si d'autres
2353
+ // signaux passifs de bot (ex: rendu offscreen) sont présents.
2359
2354
  if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
2360
- score += 40;
2355
+ let noInteractionPenalty = 5; // Pénalité de base très faible.
2356
+
2357
+ // Amplification si d'autres signaux passifs de bot sont présents.
2358
+ if (metrics.rendering?.offscreenAnom) {
2359
+ noInteractionPenalty += 40;
2360
+ }
2361
+
2362
+ score += noInteractionPenalty;
2361
2363
  }
2362
2364
 
2363
2365
  // 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
@@ -2397,21 +2399,19 @@ function getBehaviorScore(context) {
2397
2399
  if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
2398
2400
  score += 30; // Spoofed pointer area size
2399
2401
  }
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)
2402
+ if (touch.segments.length > 10) {
2403
+ const benfordDev = Optimization.Operators.benfordTest(touch.segments);
2404
+ if (benfordDev > 0.18) score += 35;
2411
2405
  }
2412
2406
  }
2413
2407
  }
2414
2408
 
2409
+ // Détection de ferme mobile : un appareil mobile parfaitement immobile est suspect, indépendamment des interactions tactiles.
2410
+ const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
2411
+ if (isMobileDevice && typeof metrics.motionVariance === 'number' && metrics.motionVariance === 0) {
2412
+ score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
2413
+ }
2414
+
2415
2415
  // Plausibilité de la latence de frappe
2416
2416
  if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
2417
2417
  if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
@@ -2981,6 +2981,10 @@ function analyzeClickPositions(history) {
2981
2981
  * @returns {{clickVarianceScore: number}}
2982
2982
  */
2983
2983
  function getClickVarianceScore(context) {
2984
+ const rawMetrics = context.headers?.['x-behavior-metrics'];
2985
+ if (!rawMetrics || !isNaN(Number(rawMetrics))) {
2986
+ return { clickVarianceScore: 0 };
2987
+ }
2984
2988
  const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
2985
2989
  const score = analyzeClickPositions(metrics.clicksHistory);
2986
2990
  return { clickVarianceScore: score };
@@ -3832,7 +3836,7 @@ async function getBehavioralIndicators(context, deviceData) {
3832
3836
  // --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
3833
3837
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
3834
3838
  let webauthnVerified = false;
3835
- if (behaviorHeader) {
3839
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
3836
3840
  try {
3837
3841
  const metrics = JSON.parse(behaviorHeader);
3838
3842
  if (metrics && metrics.webauthnAnchor) {
@@ -3976,7 +3980,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
3976
3980
  const { botScore } = getBotScore(context);
3977
3981
 
3978
3982
  // NOUVEAU: On calcule le score d'incohérence temporelle.
3979
- const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'), deviceData);
3983
+ let parsedBehaviorMetrics = {};
3984
+ const rawBehaviorHeader = context.headers?.['x-behavior-metrics'];
3985
+ if (rawBehaviorHeader && typeof rawBehaviorHeader === 'string' && rawBehaviorHeader.startsWith('{')) {
3986
+ try { parsedBehaviorMetrics = JSON.parse(rawBehaviorHeader); } catch (e) {}
3987
+ }
3988
+ const { timeInconsistencyScore } = getTimeInconsistencyScore(context, parsedBehaviorMetrics, deviceData);
3980
3989
 
3981
3990
  // NOUVEAU: On calcule le score d'incohérence entre les couches.
3982
3991
  const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
@@ -3990,7 +3999,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
3990
3999
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
3991
4000
 
3992
4001
  const { tcpAnomalyScore } = getTcpAnomalyScore(context);
3993
- const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
4002
+ const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
3994
4003
  const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
3995
4004
  const virtualizationScore = getVirtualizationAnomalyScore(context);
3996
4005
 
@@ -4003,8 +4012,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
4003
4012
  if (Array.isArray(deviceData.ips)) {
4004
4013
  deviceData.ips = new Set(deviceData.ips);
4005
4014
  }
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 };
4015
+ // Le vecteur de suspicion est maintenant complet.
4016
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
4008
4017
  };
4009
4018
 
4010
4019
  // A residential user can change networks (home, 4G, public wifi).
@@ -4027,7 +4036,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
4027
4036
  // This function now acts as a lightweight wrapper around the engine's identifyRequest method.
4028
4037
  // It requires a default configuration to work.
4029
4038
  const config = securityConfig || {
4030
- weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8, honeypotScore: 1.0 },
4039
+ weights: {
4040
+ historyScore: 0.3,
4041
+ rotationScore: 0.5,
4042
+ headerAnomalyScore: 0.2,
4043
+ requestPatternScore: 0.6,
4044
+ inconsistencyScore: 0.8,
4045
+ behaviorScore: 0.7,
4046
+ honeypotScore: 1.0,
4047
+ botScore: 1.0,
4048
+ cookieDroppingScore: 0.9,
4049
+ crossLayerInconsistencyScore: 0.4,
4050
+ timeInconsistencyScore: 0.9,
4051
+ tlsSpoofingScore: 0.8,
4052
+ clientHintsInconsistencyScore: 0.7,
4053
+ clickVarianceScore: 0.6,
4054
+ subnetScore: 0.4,
4055
+ ipReputationScore: 0.5,
4056
+ botnetClusterScore: 0.7,
4057
+ tcpAnomalyScore: 0.8,
4058
+ quicAnomalyScore: 0.8,
4059
+ renderingAnomalyScore: 0.8,
4060
+ http2AnomalyScore: 0.8,
4061
+ threatIntelScore: 1.0,
4062
+ virtualizationScore: 0.8
4063
+ },
4031
4064
  thresholds: { low: 20, medium: 40, high: 75 },
4032
4065
  honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
4033
4066
  };
@@ -4415,7 +4448,7 @@ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
4415
4448
  const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
4416
4449
 
4417
4450
  const behaviorHeader = context.headers?.['x-behavior-metrics'];
4418
- if (behaviorHeader) {
4451
+ if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
4419
4452
  try {
4420
4453
  const metrics = JSON.parse(behaviorHeader);
4421
4454
  if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
@@ -4577,7 +4610,8 @@ export class FingerprintEngine {
4577
4610
  'wasm',
4578
4611
  'similarityThreshold', 'reset',
4579
4612
  'ed25519_private_key', 'ed25519_public_key', 'upowModel',
4580
- 'federatedPeers', 'federationSecret', 'filterWhitelist'
4613
+ 'federatedPeers', 'federationSecret', 'filterWhitelist',
4614
+ 'challengeRateLimit'
4581
4615
  ]);
4582
4616
 
4583
4617
  // 1. Check for essential keys
@@ -4637,8 +4671,7 @@ export class FingerprintEngine {
4637
4671
  (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
4638
4672
  (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
4639
4673
  (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) +
4674
+ (suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
4642
4675
  (suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
4643
4676
  (suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
4644
4677
  (suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
@@ -5589,10 +5622,11 @@ export class FingerprintEngine {
5589
5622
  this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
5590
5623
  }
5591
5624
 
5592
- // --- AJOUT : Limiteur de débit (Token Bucket) ---
5593
- const rateLimitPassed = await checkChallengeRateLimit(clientIp);
5625
+ // --- Limiteur de débit par domaine et sous-réseau (Token Bucket) ---
5626
+ const domain = requestContext.headers?.host || 'default';
5627
+ const rateLimitPassed = await checkChallengeRateLimit(clientIp, domain, this.securityConfig?.challengeRateLimit);
5594
5628
  if (!rateLimitPassed) {
5595
- this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
5629
+ this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
5596
5630
  const decision = {
5597
5631
  action: 'block',
5598
5632
  status: 429,
@@ -6962,6 +6996,7 @@ export const powMiddleware = (securityConfig) => {
6962
6996
  */
6963
6997
  export const __internal = {
6964
6998
  get store() { return store; }, // Export the store for testing
6999
+ checkChallengeRateLimit,
6965
7000
  getDeviceHash,
6966
7001
  getCompositeDeviceHash,
6967
7002
  getSuspicionVector,
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Service Worker for transparent network interception and credential enrichment.
3
+ * Leaves global window.fetch native and untouched to prevent tampering detection.
4
+ */
5
+
6
+ let clientCredentials = {
7
+ fp: '',
8
+ behavior: 1,
9
+ targetDomains: []
10
+ };
11
+
12
+ self.addEventListener('install', () => {
13
+ self.skipWaiting();
14
+ });
15
+
16
+ self.addEventListener('activate', (event) => {
17
+ event.waitUntil(self.clients.claim());
18
+ });
19
+
20
+ self.addEventListener('message', (event) => {
21
+ if (event.data && event.data.type === 'UPDATE_CREDENTIALS') {
22
+ if (typeof event.data.fp !== 'undefined') {
23
+ clientCredentials.fp = event.data.fp;
24
+ }
25
+ if (typeof event.data.behavior !== 'undefined') {
26
+ clientCredentials.behavior = event.data.behavior;
27
+ }
28
+ if (Array.isArray(event.data.targetDomains)) {
29
+ clientCredentials.targetDomains = event.data.targetDomains;
30
+ }
31
+ }
32
+ });
33
+
34
+ self.addEventListener('fetch', (event) => {
35
+ const url = new URL(event.request.url);
36
+
37
+ // Only process HTTP and HTTPS requests
38
+ if (!url.protocol.startsWith('http')) return;
39
+
40
+ // Scope filtering: same-origin or configured domains
41
+ const targets = clientCredentials.targetDomains;
42
+ const shouldProtect = (targets.length === 0 && url.origin === self.location.origin) ||
43
+ (targets.length > 0 && targets.includes(url.hostname));
44
+
45
+ if (!shouldProtect) return;
46
+
47
+ event.respondWith((async () => {
48
+ const newHeaders = new Headers(event.request.headers);
49
+
50
+ if (clientCredentials.fp) {
51
+ newHeaders.set('X-Device-Fingerprint', clientCredentials.fp);
52
+ }
53
+ if (typeof clientCredentials.behavior !== 'undefined') {
54
+ newHeaders.set('X-Behavior-Metrics', String(clientCredentials.behavior));
55
+ }
56
+
57
+ const requestInit = {
58
+ method: event.request.method,
59
+ headers: newHeaders,
60
+ credentials: event.request.credentials,
61
+ cache: event.request.cache,
62
+ redirect: event.request.redirect,
63
+ referrer: event.request.referrer,
64
+ integrity: event.request.integrity
65
+ };
66
+
67
+ if (event.request.mode !== 'navigate') {
68
+ requestInit.mode = event.request.mode;
69
+ }
70
+
71
+ if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
72
+ try {
73
+ const bodyBlob = await event.request.clone().blob();
74
+ if (bodyBlob.size > 0) requestInit.body = bodyBlob;
75
+ } catch (e) {}
76
+ }
77
+
78
+ const modifiedRequest = new Request(event.request.url, requestInit);
79
+ return fetch(modifiedRequest);
80
+ })());
81
+ });