@anonympins/fingerprint 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +221 -23
- package/src/js/fingerprint.js +148 -9
- package/src/js/gpu_pow.solver.js +217 -0
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.test.js +63 -0
- package/src/js/tests/gpu_pow.test.js +118 -0
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +42 -0
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +21 -0
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +77 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/RequestUtils.php +120 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## Version 0.5.1
|
|
2
|
+
|
|
3
|
+
### ✨ New Features
|
|
4
|
+
|
|
5
|
+
- **GPU Proof-of-Work (PoW) Challenge**: Introduced a highly parallelized chaotic logistic map float computation challenge utilizing WebGPU (with a fallback to WebGL2). This challenge is specifically designed to exhaust CPU-based headless emulators (such as SwiftShader). It includes sample-based server-side verification to prevent DoS vectors.
|
|
6
|
+
- **Biometric Keystroke Dynamics (Dwell & Flight Times)**: Introduced advanced behavioral biometric tracking by measuring key press duration (*dwell time*) and key-to-key transition intervals (*flight time*) to build a unique digraph/trigraph motor profile for the user.
|
|
7
|
+
- *Why it's a Killer Feature*: Automated text-injecting bots often simulate simple randomized delays between characters, but they fail to replicate natural human muscle memory patterns (such as ultra-fast cognitive transitions between adjacent keys on physical or virtual layouts). Server-side statistical checks (utilizing standard deviation, variance thresholds, and Benford's Law) immediately flag these robotic, uniform input patterns.
|
|
8
|
+
- **Stealthy Honeypot Traps (Shadow DOM)**: Implemented a new honeypot mechanism that conceals trap links and form fields within a closed Shadow DOM, with dynamic rendering styles calculated by nested CSS variables. This makes them invisible to legitimate users and standard browser automation tools, but highly detectable by bots that inject specific JS or use complex selectors, significantly increasing their behavioral signature.
|
|
9
|
+
- **Hot-Reloadable Security Configuration**: Enabled dynamic, in-memory updates of security configurations (weights, thresholds, patterns) without requiring a server restart. This ensures continuous adaptability to evolving threats (e.g., DDoS L7, scraping campaigns) without service interruption or latency.
|
|
10
|
+
|
|
11
|
+
### 🚀 Improvements
|
|
12
|
+
|
|
13
|
+
- **Display & Protocol Anomaly Scoring**: Fully integrated `renderingAnomalyScore` and `quicAnomalyScore` across all backend engines (Node.js, PHP, Python). This enables real-time detection of virtual software framebuffers (like `Xvfb`) lacking physical V-Sync through jitter analysis, as well as HTTP/3 stream setting inconsistencies.
|
|
14
|
+
- **Security Profile Tuning**: Integrated display and QUIC anomaly detectors into the default security profiles (`balanced`, `strict`, `blog`, `ecommerce`) with custom weights.
|
|
15
|
+
- **Layout-Agnostic Client Tracking**: Enhanced the keystroke dynamics tracker to prioritize physical key locations (`KeyboardEvent.code`) over localized characters (`KeyboardEvent.key`), ensuring robust detection across different keyboard layouts (QWERTY, AZERTY) and virtual mobile keyboards.
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
- **Node.js 24 Test Suite Compatibility**: Fixed unit test suite execution and environment configuration issues specifically encountered on Node.js 24.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
1
23
|
## Version 0.5.0
|
|
2
24
|
|
|
3
25
|
### ✨ New Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -340,6 +340,52 @@ const ClientLibrary = {
|
|
|
340
340
|
document.addEventListener('touchend', handleTouch, { passive: true });
|
|
341
341
|
},
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Démarre le suivi de la régularité d'affichage (V-Sync/rAF) pour détecter les framebuffers logiciels sans V-Sync.
|
|
345
|
+
*/
|
|
346
|
+
startRenderingTracker() {
|
|
347
|
+
if (this._renderingTrackerAttached) return;
|
|
348
|
+
this._renderingTrackerAttached = true;
|
|
349
|
+
|
|
350
|
+
if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
|
|
351
|
+
|
|
352
|
+
const rAfTimestamps = [];
|
|
353
|
+
let lastTime = performance.now();
|
|
354
|
+
const maxSamples = 15;
|
|
355
|
+
|
|
356
|
+
const checkOffscreenAnom = () => {
|
|
357
|
+
try {
|
|
358
|
+
if ('OffscreenCanvas' in window && HTMLCanvasElement.prototype.transferControlToOffscreen) {
|
|
359
|
+
const nativeToString = Function.prototype.toString.call(HTMLCanvasElement.prototype.transferControlToOffscreen);
|
|
360
|
+
return !nativeToString.includes('[native code]');
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {}
|
|
363
|
+
return false;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const loop = (time) => {
|
|
367
|
+
const delta = time - lastTime;
|
|
368
|
+
lastTime = time;
|
|
369
|
+
if (rAfTimestamps.length < maxSamples) {
|
|
370
|
+
if (rAfTimestamps.length > 0) { // Skip first delta
|
|
371
|
+
rAfTimestamps.push(delta);
|
|
372
|
+
}
|
|
373
|
+
window.requestAnimationFrame(loop);
|
|
374
|
+
} else {
|
|
375
|
+
const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
|
|
376
|
+
const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
|
|
377
|
+
const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
|
|
378
|
+
|
|
379
|
+
metrics.rendering = {
|
|
380
|
+
fps: Math.round((1000 / avg) * 100) / 100,
|
|
381
|
+
jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
|
|
382
|
+
offscreenAnom: checkOffscreenAnom()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
window.requestAnimationFrame(loop);
|
|
387
|
+
},
|
|
388
|
+
|
|
343
389
|
/**
|
|
344
390
|
* Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
|
|
345
391
|
*/
|
|
@@ -449,27 +495,77 @@ const ClientLibrary = {
|
|
|
449
495
|
},
|
|
450
496
|
|
|
451
497
|
/**
|
|
452
|
-
* Démarre le suivi de la dynamique de frappe pour calculer
|
|
498
|
+
* Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
|
|
453
499
|
* À appeler une fois sur la page.
|
|
454
500
|
*/
|
|
455
501
|
startKeystrokeDynamicsTracker() {
|
|
456
502
|
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
457
|
-
if (
|
|
503
|
+
if (this._keystrokeTrackerAttached) return;
|
|
504
|
+
this._keystrokeTrackerAttached = true;
|
|
458
505
|
|
|
459
|
-
|
|
506
|
+
const activeKeys = new Map();
|
|
507
|
+
let lastKeyDownTime = 0;
|
|
508
|
+
let lastKeyName = '';
|
|
509
|
+
|
|
510
|
+
document.addEventListener('keydown', (e) => {
|
|
460
511
|
const now = performance.now();
|
|
512
|
+
const key = e.key;
|
|
513
|
+
const code = e.code;
|
|
514
|
+
if (!key && !code) return;
|
|
515
|
+
|
|
516
|
+
const keyIdentifier = code || key;
|
|
517
|
+
|
|
518
|
+
// Prevent key repeat triggering multiple events
|
|
519
|
+
if (activeKeys.has(keyIdentifier)) return;
|
|
520
|
+
activeKeys.set(keyIdentifier, now);
|
|
521
|
+
|
|
461
522
|
if (keystrokeTimestamps.length > 0) {
|
|
462
523
|
const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
|
|
463
524
|
const latency = now - lastTimestamp;
|
|
464
|
-
|
|
465
|
-
if (latency > 10 && latency < 2000) { // Augmenté à 2s
|
|
525
|
+
if (latency > 10 && latency < 2000) {
|
|
466
526
|
if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
|
|
467
|
-
keystrokeLatencies.shift();
|
|
527
|
+
keystrokeLatencies.shift();
|
|
468
528
|
}
|
|
469
529
|
keystrokeLatencies.push(latency);
|
|
470
530
|
}
|
|
471
531
|
}
|
|
472
532
|
keystrokeTimestamps.push(now);
|
|
533
|
+
|
|
534
|
+
// Flight Time (KeyDown to KeyDown)
|
|
535
|
+
if (lastKeyDownTime > 0) {
|
|
536
|
+
const flightTime = now - lastKeyDownTime;
|
|
537
|
+
if (flightTime > 10 && flightTime < 2000) {
|
|
538
|
+
if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
539
|
+
keystrokeFlightTimes.shift();
|
|
540
|
+
}
|
|
541
|
+
const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
|
|
542
|
+
keystrokeFlightTimes.push({ digraph, time: flightTime });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
lastKeyDownTime = now;
|
|
546
|
+
lastKeyName = keyIdentifier;
|
|
547
|
+
}, {passive: true});
|
|
548
|
+
|
|
549
|
+
document.addEventListener('keyup', (e) => {
|
|
550
|
+
const now = performance.now();
|
|
551
|
+
const key = e.key;
|
|
552
|
+
const code = e.code;
|
|
553
|
+
if (!key && !code) return;
|
|
554
|
+
|
|
555
|
+
const keyIdentifier = code || key;
|
|
556
|
+
|
|
557
|
+
if (activeKeys.has(keyIdentifier)) {
|
|
558
|
+
const pressTime = activeKeys.get(keyIdentifier);
|
|
559
|
+
const dwellTime = now - pressTime;
|
|
560
|
+
activeKeys.delete(keyIdentifier);
|
|
561
|
+
|
|
562
|
+
if (dwellTime > 5 && dwellTime < 1000) {
|
|
563
|
+
if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
564
|
+
keystrokeDwellTimes.shift();
|
|
565
|
+
}
|
|
566
|
+
keystrokeDwellTimes.push(dwellTime);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
473
569
|
}, {passive: true});
|
|
474
570
|
},
|
|
475
571
|
|
|
@@ -509,22 +605,75 @@ const ClientLibrary = {
|
|
|
509
605
|
});
|
|
510
606
|
activeHoneypotListeners.clear();
|
|
511
607
|
|
|
512
|
-
// 2. Ajouter les nouveaux écouteurs
|
|
608
|
+
// 2. Ajouter les nouveaux écouteurs sur le DOM classique
|
|
513
609
|
honeypotFieldNames.forEach(fieldName => {
|
|
514
610
|
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
515
611
|
if (field) {
|
|
516
|
-
// On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
|
|
517
|
-
// L'option { once: true } est excellente, mais pour une réinitialisation complète,
|
|
518
|
-
// il est plus propre de gérer le nettoyage nous-mêmes.
|
|
519
612
|
const listener = () => {
|
|
520
613
|
this.onHoneypotTrigger();
|
|
521
|
-
// Se supprime lui-même après exécution, comme { once: true }
|
|
522
614
|
field.removeEventListener('input', listener);
|
|
523
615
|
};
|
|
524
616
|
field.addEventListener('input', listener);
|
|
525
617
|
activeHoneypotListeners.set(field, listener); // On stocke la référence
|
|
526
618
|
}
|
|
527
619
|
});
|
|
620
|
+
|
|
621
|
+
// 3. Générer des champs d'input pièges masqués dans un Shadow DOM fermé
|
|
622
|
+
if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
|
|
623
|
+
const host = document.createElement('div');
|
|
624
|
+
host.setAttribute('aria-hidden', 'true');
|
|
625
|
+
host.style.position = 'absolute';
|
|
626
|
+
host.style.width = '0';
|
|
627
|
+
host.style.height = '0';
|
|
628
|
+
host.style.overflow = 'hidden';
|
|
629
|
+
|
|
630
|
+
const shadow = host.attachShadow({ mode: 'closed' });
|
|
631
|
+
|
|
632
|
+
const style = document.createElement('style');
|
|
633
|
+
style.textContent = `
|
|
634
|
+
:host {
|
|
635
|
+
--trap-pos-state: absolute;
|
|
636
|
+
--trap-off-val: -9999px;
|
|
637
|
+
--trap-vis-state: hidden;
|
|
638
|
+
--trap-scale-val: 0;
|
|
639
|
+
}
|
|
640
|
+
.shadow-form-wrapper {
|
|
641
|
+
position: var(--trap-pos-state);
|
|
642
|
+
left: var(--trap-off-val);
|
|
643
|
+
top: var(--trap-off-val);
|
|
644
|
+
visibility: var(--trap-vis-state);
|
|
645
|
+
transform: scale(var(--trap-scale-val));
|
|
646
|
+
}
|
|
647
|
+
`;
|
|
648
|
+
shadow.appendChild(style);
|
|
649
|
+
|
|
650
|
+
const wrapper = document.createElement('div');
|
|
651
|
+
wrapper.className = 'shadow-form-wrapper';
|
|
652
|
+
|
|
653
|
+
honeypotFieldNames.forEach(fieldName => {
|
|
654
|
+
const label = document.createElement('label');
|
|
655
|
+
label.textContent = fieldName;
|
|
656
|
+
const input = document.createElement('input');
|
|
657
|
+
input.type = 'text';
|
|
658
|
+
input.name = fieldName;
|
|
659
|
+
input.tabIndex = -1;
|
|
660
|
+
input.autocomplete = 'off';
|
|
661
|
+
|
|
662
|
+
const trigger = () => {
|
|
663
|
+
this.onHoneypotTrigger();
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
input.addEventListener('input', trigger, { passive: true });
|
|
667
|
+
input.addEventListener('change', trigger, { passive: true });
|
|
668
|
+
input.addEventListener('focus', trigger, { passive: true });
|
|
669
|
+
|
|
670
|
+
wrapper.appendChild(label);
|
|
671
|
+
wrapper.appendChild(input);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
shadow.appendChild(wrapper);
|
|
675
|
+
document.body.appendChild(host);
|
|
676
|
+
}
|
|
528
677
|
},
|
|
529
678
|
|
|
530
679
|
/**
|
|
@@ -544,6 +693,10 @@ const ClientLibrary = {
|
|
|
544
693
|
// NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
|
|
545
694
|
metrics.mouseMovementsHistory = mouseMovementsHistory;
|
|
546
695
|
|
|
696
|
+
// NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
|
|
697
|
+
metrics.keystrokeDwellTimes = keystrokeDwellTimes;
|
|
698
|
+
metrics.keystrokeFlightTimes = keystrokeFlightTimes;
|
|
699
|
+
|
|
547
700
|
// Calcule la latence moyenne des frappes
|
|
548
701
|
if (keystrokeLatencies.length > 0) {
|
|
549
702
|
const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
|
|
@@ -672,24 +825,61 @@ const ClientLibrary = {
|
|
|
672
825
|
return;
|
|
673
826
|
}
|
|
674
827
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
828
|
+
const host = document.createElement('div');
|
|
829
|
+
host.setAttribute('aria-hidden', 'true');
|
|
830
|
+
host.style.position = 'absolute';
|
|
831
|
+
host.style.width = '0';
|
|
832
|
+
host.style.height = '0';
|
|
833
|
+
host.style.overflow = 'hidden';
|
|
834
|
+
|
|
835
|
+
const shadow = host.attachShadow({ mode: 'closed' });
|
|
836
|
+
|
|
837
|
+
const style = document.createElement('style');
|
|
838
|
+
style.textContent = `
|
|
839
|
+
:host {
|
|
840
|
+
--trap-layout-pos: absolute;
|
|
841
|
+
--trap-offset-val: -9999px;
|
|
842
|
+
--trap-visibility-state: hidden;
|
|
843
|
+
--trap-scale-factor: 0;
|
|
844
|
+
--trap-ptr-events: none;
|
|
845
|
+
}
|
|
846
|
+
.shadow-trap-wrapper {
|
|
847
|
+
position: var(--trap-layout-pos);
|
|
848
|
+
left: var(--trap-offset-val);
|
|
849
|
+
top: var(--trap-offset-val);
|
|
850
|
+
visibility: var(--trap-visibility-state);
|
|
851
|
+
transform: scale(var(--trap-scale-factor));
|
|
852
|
+
pointer-events: var(--trap-ptr-events);
|
|
853
|
+
}
|
|
854
|
+
a {
|
|
855
|
+
color: transparent;
|
|
856
|
+
text-decoration: none;
|
|
857
|
+
}
|
|
858
|
+
`;
|
|
859
|
+
shadow.appendChild(style);
|
|
860
|
+
|
|
861
|
+
const wrapper = document.createElement('div');
|
|
862
|
+
wrapper.className = 'shadow-trap-wrapper';
|
|
682
863
|
|
|
683
|
-
urls.forEach((url,i) => {
|
|
864
|
+
urls.forEach((url, i) => {
|
|
684
865
|
const link = document.createElement('a');
|
|
685
866
|
link.href = url;
|
|
686
867
|
link.rel = 'nofollow';
|
|
687
|
-
link.tabIndex = -1;
|
|
688
|
-
link.innerHTML = `<span>> ${i+1}</span>`;
|
|
689
|
-
|
|
868
|
+
link.tabIndex = -1;
|
|
869
|
+
link.innerHTML = `<span>> ${i + 1}</span>`;
|
|
870
|
+
|
|
871
|
+
const trigger = () => {
|
|
872
|
+
this.onHoneypotTrigger();
|
|
873
|
+
};
|
|
874
|
+
link.addEventListener('click', trigger, { passive: true });
|
|
875
|
+
link.addEventListener('focus', trigger, { passive: true });
|
|
876
|
+
link.addEventListener('mouseover', trigger, { passive: true });
|
|
877
|
+
|
|
878
|
+
wrapper.appendChild(link);
|
|
690
879
|
});
|
|
691
880
|
|
|
692
|
-
|
|
881
|
+
shadow.appendChild(wrapper);
|
|
882
|
+
document.body.appendChild(host);
|
|
693
883
|
},
|
|
694
884
|
/**
|
|
695
885
|
* Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
|
|
@@ -749,6 +939,7 @@ const ClientLibrary = {
|
|
|
749
939
|
keystrokes = true,
|
|
750
940
|
clicks = true, // Add new option
|
|
751
941
|
touches = true, // Nouveau paramètre tactiles
|
|
942
|
+
rendering = true,
|
|
752
943
|
phantomTraps = true, // NOUVEAU
|
|
753
944
|
honeypots = [],
|
|
754
945
|
trapUrls = [], // Nouveau paramètre pour les URL pièges
|
|
@@ -773,6 +964,9 @@ const ClientLibrary = {
|
|
|
773
964
|
if (touches) {
|
|
774
965
|
this.startTouchEventTracker();
|
|
775
966
|
}
|
|
967
|
+
if (rendering) {
|
|
968
|
+
this.startRenderingTracker();
|
|
969
|
+
}
|
|
776
970
|
if (phantomTraps) {
|
|
777
971
|
this.injectPhantomTraps();
|
|
778
972
|
}
|
|
@@ -941,6 +1135,7 @@ const metrics = {
|
|
|
941
1135
|
honeypotInteraction: false,
|
|
942
1136
|
historyLength: 0,
|
|
943
1137
|
clientTimestamp: 0,
|
|
1138
|
+
rendering: { fps: 0, jitter: 0, offscreenAnom: false },
|
|
944
1139
|
};
|
|
945
1140
|
|
|
946
1141
|
let lastMousePos = { x: 0, y: 0 };
|
|
@@ -953,6 +1148,8 @@ const CLICKS_HISTORY_MAX = 50;
|
|
|
953
1148
|
let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
|
|
954
1149
|
let keystrokeTimestamps = [];
|
|
955
1150
|
let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
|
|
1151
|
+
let keystrokeDwellTimes = [];
|
|
1152
|
+
let keystrokeFlightTimes = [];
|
|
956
1153
|
const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
|
|
957
1154
|
|
|
958
1155
|
|
|
@@ -966,6 +1163,7 @@ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.b
|
|
|
966
1163
|
export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
|
|
967
1164
|
export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
|
|
968
1165
|
export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
|
|
1166
|
+
export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
|
|
969
1167
|
export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
|
|
970
1168
|
export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
|
|
971
1169
|
export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
|
package/src/js/fingerprint.js
CHANGED
|
@@ -189,7 +189,82 @@ export function generateStatelessTicket(payload) {
|
|
|
189
189
|
const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
|
|
190
190
|
return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
|
|
194
|
+
* @private
|
|
195
|
+
* @param {object} context - Le contexte de la requête.
|
|
196
|
+
* @returns {{quicAnomalyScore: number}}
|
|
197
|
+
*/
|
|
198
|
+
function getQuicAnomalyScore(context) {
|
|
199
|
+
const quicFp = context.headers?.['x-quic-fp'] || context.quicFingerprint || null;
|
|
200
|
+
if (!quicFp || typeof quicFp !== 'string') {
|
|
201
|
+
return { quicAnomalyScore: 0.0 };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const parts = quicFp.split(';');
|
|
205
|
+
if (parts.length < 2) return { quicAnomalyScore: 0.0 };
|
|
206
|
+
|
|
207
|
+
const params = {};
|
|
208
|
+
parts[1].split(',').forEach(p => {
|
|
209
|
+
const kv = p.split('=');
|
|
210
|
+
if (kv.length === 2) params[kv[0]] = kv[1];
|
|
211
|
+
});
|
|
212
|
+
const priorityOrder = parts[2] || '';
|
|
213
|
+
|
|
214
|
+
const ua = context.headers?.['user-agent'] || '';
|
|
215
|
+
const uaParts = parseUserAgent(ua);
|
|
216
|
+
const browser = uaParts.browser;
|
|
217
|
+
|
|
218
|
+
if (!browser) return { quicAnomalyScore: 0.0 };
|
|
192
219
|
|
|
220
|
+
let anomaly = 0.0;
|
|
221
|
+
if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
|
|
222
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
223
|
+
const maxStreams = parseInt(params['4'] || '0', 10);
|
|
224
|
+
if (maxData > 0 && maxData < 1048576) anomaly += 40.0;
|
|
225
|
+
if (maxStreams > 0 && maxStreams !== 100) anomaly += 30.0;
|
|
226
|
+
if (priorityOrder && !priorityOrder.includes('u=')) anomaly += 30.0;
|
|
227
|
+
} else if (browser.startsWith('Firefox')) {
|
|
228
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
229
|
+
if (maxData > 0 && maxData > 5000000) anomaly += 40.0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { quicAnomalyScore: Math.max(0.0, Math.min(100.0, anomaly)) };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Détecte les anomalies de rendu (V-Sync, FPS, gigue) à partir des métriques d'affichage.
|
|
236
|
+
* @private
|
|
237
|
+
* @param {object} context - Le contexte de la requête.
|
|
238
|
+
* @returns {{renderingAnomalyScore: number}}
|
|
239
|
+
*/
|
|
240
|
+
function getRenderingAnomalyScore(context) {
|
|
241
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
242
|
+
if (!behaviorHeader) {
|
|
243
|
+
return { renderingAnomalyScore: 0.0 };
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
247
|
+
if (!metrics || !metrics.rendering) {
|
|
248
|
+
return { renderingAnomalyScore: 0.0 };
|
|
249
|
+
}
|
|
250
|
+
const rendering = metrics.rendering;
|
|
251
|
+
let score = 0.0;
|
|
252
|
+
if (rendering.offscreenAnom) {
|
|
253
|
+
score += 100.0;
|
|
254
|
+
}
|
|
255
|
+
const fps = parseFloat(rendering.fps || 0.0);
|
|
256
|
+
const jitter = parseFloat(rendering.jitter || 0.0);
|
|
257
|
+
if (fps > 250.0 || (fps > 0.0 && fps < 15.0)) {
|
|
258
|
+
score += 50.0;
|
|
259
|
+
}
|
|
260
|
+
if (jitter > 6.0) {
|
|
261
|
+
score += Math.min(80.0, (jitter - 6.0) * 10.0);
|
|
262
|
+
}
|
|
263
|
+
return { renderingAnomalyScore: Math.min(100.0, score) };
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return { renderingAnomalyScore: 0.0 };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
193
268
|
export function parseStatelessTicket(ticket) {
|
|
194
269
|
try {
|
|
195
270
|
if (ticket.startsWith('ed25519.')) {
|
|
@@ -304,7 +379,10 @@ const securityProfiles = {
|
|
|
304
379
|
tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
|
|
305
380
|
subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
|
|
306
381
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
307
|
-
botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
|
|
382
|
+
botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
383
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
384
|
+
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
385
|
+
renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
308
386
|
},
|
|
309
387
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
310
388
|
patterns: {
|
|
@@ -340,7 +418,8 @@ const securityProfiles = {
|
|
|
340
418
|
tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
|
|
341
419
|
subnetScore: 0.5,
|
|
342
420
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
343
|
-
botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
|
|
421
|
+
botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
422
|
+
renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
344
423
|
},
|
|
345
424
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
346
425
|
patterns: {
|
|
@@ -377,7 +456,9 @@ const securityProfiles = {
|
|
|
377
456
|
tlsSpoofingScore: 0.7, // Important pour les API
|
|
378
457
|
subnetScore: 0.4,
|
|
379
458
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
380
|
-
botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
|
|
459
|
+
botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
460
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
461
|
+
quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
|
|
381
462
|
},
|
|
382
463
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
383
464
|
patterns: {
|
|
@@ -415,7 +496,10 @@ const securityProfiles = {
|
|
|
415
496
|
tlsSpoofingScore: 0.6, // Moins critique pour les blogs
|
|
416
497
|
subnetScore: 0.2,
|
|
417
498
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
418
|
-
botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
|
|
499
|
+
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
500
|
+
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
501
|
+
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
502
|
+
renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
419
503
|
},
|
|
420
504
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
421
505
|
patterns: {
|
|
@@ -452,7 +536,10 @@ const securityProfiles = {
|
|
|
452
536
|
tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
|
|
453
537
|
subnetScore: 0.5,
|
|
454
538
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
455
|
-
botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
|
|
539
|
+
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
540
|
+
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
541
|
+
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
542
|
+
renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
456
543
|
},
|
|
457
544
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
458
545
|
patterns: {
|
|
@@ -1600,6 +1687,41 @@ function getBehaviorScore(context) {
|
|
|
1600
1687
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
1601
1688
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
1602
1689
|
|
|
1690
|
+
// NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
|
|
1691
|
+
const dwellTimes = metrics.keystrokeDwellTimes || [];
|
|
1692
|
+
const flightTimes = metrics.keystrokeFlightTimes || [];
|
|
1693
|
+
|
|
1694
|
+
if (dwellTimes.length >= 5) {
|
|
1695
|
+
const meanDwell = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
|
|
1696
|
+
const varDwell = dwellTimes.reduce((a, b) => a + Math.pow(b - meanDwell, 2), 0) / dwellTimes.length;
|
|
1697
|
+
const stdDevDwell = Math.sqrt(varDwell);
|
|
1698
|
+
|
|
1699
|
+
if (stdDevDwell < 2.0) {
|
|
1700
|
+
score += 35; // Suspicion d'automatisation (pas de variation humaine de pression)
|
|
1701
|
+
}
|
|
1702
|
+
if (meanDwell < 15.0) {
|
|
1703
|
+
score += 25; // Dwell time irréaliste
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (flightTimes.length >= 5) {
|
|
1708
|
+
const times = flightTimes.map(f => f.time);
|
|
1709
|
+
const meanFlight = times.reduce((a, b) => a + b, 0) / times.length;
|
|
1710
|
+
const varFlight = times.reduce((a, b) => a + Math.pow(b - meanFlight, 2), 0) / times.length;
|
|
1711
|
+
const stdDevFlight = Math.sqrt(varFlight);
|
|
1712
|
+
|
|
1713
|
+
if (stdDevFlight < 3.0) {
|
|
1714
|
+
score += 35; // Pas de variation de transition (flight time robotique)
|
|
1715
|
+
}
|
|
1716
|
+
if (meanFlight < 25.0) {
|
|
1717
|
+
score += 25; // Transitions trop rapides
|
|
1718
|
+
}
|
|
1719
|
+
const benfordDev = Optimization.Operators.benfordTest(times);
|
|
1720
|
+
if (benfordDev > 0.18) {
|
|
1721
|
+
score += 30; // Les intervalles ne suivent pas la loi de Benford
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1603
1725
|
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
1604
1726
|
if (segments.length > 10) {
|
|
1605
1727
|
const benfordDeviation = Optimization.Operators.benfordTest(segments);
|
|
@@ -2766,8 +2888,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2766
2888
|
const stableFp = extractStablePart(currentDeviceHash);
|
|
2767
2889
|
const stableFpHash = cyrb53(stableFp).toString();
|
|
2768
2890
|
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
2769
|
-
|
|
2770
|
-
|
|
2891
|
+
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
2892
|
+
const { quicAnomalyScore } = getQuicAnomalyScore(context);
|
|
2893
|
+
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
2771
2894
|
|
|
2772
2895
|
// Save the updated device state to the store
|
|
2773
2896
|
// 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.
|
|
@@ -2779,7 +2902,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2779
2902
|
deviceData.ips = new Set(deviceData.ips);
|
|
2780
2903
|
}
|
|
2781
2904
|
// Le vecteur de suspicion est maintenant complet.
|
|
2782
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
|
|
2905
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
|
|
2783
2906
|
};
|
|
2784
2907
|
|
|
2785
2908
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -3144,6 +3267,18 @@ export class FingerprintEngine {
|
|
|
3144
3267
|
this.dryRun = finalConfig.dryRun || false;
|
|
3145
3268
|
}
|
|
3146
3269
|
|
|
3270
|
+
/**
|
|
3271
|
+
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
3272
|
+
* sans nécessiter de redémarrage.
|
|
3273
|
+
* @param {object} newConfig - La nouvelle configuration partielle ou complète.
|
|
3274
|
+
*/
|
|
3275
|
+
updateConfig(newConfig) {
|
|
3276
|
+
this._validateConfig(newConfig);
|
|
3277
|
+
this.securityConfig = deepMerge(this.securityConfig, newConfig);
|
|
3278
|
+
this.dryRun = this.securityConfig.dryRun || false;
|
|
3279
|
+
this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3147
3282
|
/**
|
|
3148
3283
|
* Validates the security configuration object to detect potential typos or missing essential keys.
|
|
3149
3284
|
* @private
|
|
@@ -3208,7 +3343,9 @@ export class FingerprintEngine {
|
|
|
3208
3343
|
(suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
|
|
3209
3344
|
(suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
|
|
3210
3345
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
3211
|
-
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0)
|
|
3346
|
+
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
3347
|
+
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
3348
|
+
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
3212
3349
|
|
|
3213
3350
|
return Math.min(100, score);
|
|
3214
3351
|
}
|
|
@@ -5186,6 +5323,8 @@ export const __internal = {
|
|
|
5186
5323
|
parseTcpSyn, // Expose for testing
|
|
5187
5324
|
classifyTcpOs, // Expose for testing
|
|
5188
5325
|
getTcpAnomalyScore, // Expose for testing,
|
|
5326
|
+
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5327
|
+
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5189
5328
|
registerCooperativeNode,
|
|
5190
5329
|
findPeerInSubnet,
|
|
5191
5330
|
handleCooperativeRequest
|