@anonympins/fingerprint 0.5.0 → 0.5.2
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 +31 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +224 -24
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +382 -63
- package/src/js/fingerprint.utils.js +213 -183
- package/src/js/gpu_pow.solver.js +312 -0
- package/src/js/library.js +16 -13
- package/src/js/pow.solver.inline.js +138 -27
- package/src/js/pow.solver.js +141 -28
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +150 -3
- package/src/js/tests/gpu_pow.test.js +199 -0
- package/src/js/tests/pow.solver.test.js +4 -4
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +158 -27
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +124 -2
- 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
|
@@ -172,7 +172,9 @@ const ClientLibrary = {
|
|
|
172
172
|
try {
|
|
173
173
|
const canvas = document.createElement("canvas");
|
|
174
174
|
const gl =
|
|
175
|
-
canvas.getContext("
|
|
175
|
+
canvas.getContext("webgl2") ||
|
|
176
|
+
canvas.getContext("webgl") ||
|
|
177
|
+
canvas.getContext("experimental-webgl");
|
|
176
178
|
if (gl) {
|
|
177
179
|
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
178
180
|
if (debugInfo) {
|
|
@@ -340,6 +342,52 @@ const ClientLibrary = {
|
|
|
340
342
|
document.addEventListener('touchend', handleTouch, { passive: true });
|
|
341
343
|
},
|
|
342
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Démarre le suivi de la régularité d'affichage (V-Sync/rAF) pour détecter les framebuffers logiciels sans V-Sync.
|
|
347
|
+
*/
|
|
348
|
+
startRenderingTracker() {
|
|
349
|
+
if (this._renderingTrackerAttached) return;
|
|
350
|
+
this._renderingTrackerAttached = true;
|
|
351
|
+
|
|
352
|
+
if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
|
|
353
|
+
|
|
354
|
+
const rAfTimestamps = [];
|
|
355
|
+
let lastTime = performance.now();
|
|
356
|
+
const maxSamples = 15;
|
|
357
|
+
|
|
358
|
+
const checkOffscreenAnom = () => {
|
|
359
|
+
try {
|
|
360
|
+
if ('OffscreenCanvas' in window && HTMLCanvasElement.prototype.transferControlToOffscreen) {
|
|
361
|
+
const nativeToString = Function.prototype.toString.call(HTMLCanvasElement.prototype.transferControlToOffscreen);
|
|
362
|
+
return !nativeToString.includes('[native code]');
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {}
|
|
365
|
+
return false;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const loop = (time) => {
|
|
369
|
+
const delta = time - lastTime;
|
|
370
|
+
lastTime = time;
|
|
371
|
+
if (rAfTimestamps.length < maxSamples) {
|
|
372
|
+
if (rAfTimestamps.length > 0) { // Skip first delta
|
|
373
|
+
rAfTimestamps.push(delta);
|
|
374
|
+
}
|
|
375
|
+
window.requestAnimationFrame(loop);
|
|
376
|
+
} else {
|
|
377
|
+
const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
|
|
378
|
+
const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
|
|
379
|
+
const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
|
|
380
|
+
|
|
381
|
+
metrics.rendering = {
|
|
382
|
+
fps: Math.round((1000 / avg) * 100) / 100,
|
|
383
|
+
jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
|
|
384
|
+
offscreenAnom: checkOffscreenAnom()
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
window.requestAnimationFrame(loop);
|
|
389
|
+
},
|
|
390
|
+
|
|
343
391
|
/**
|
|
344
392
|
* Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
|
|
345
393
|
*/
|
|
@@ -449,27 +497,77 @@ const ClientLibrary = {
|
|
|
449
497
|
},
|
|
450
498
|
|
|
451
499
|
/**
|
|
452
|
-
* Démarre le suivi de la dynamique de frappe pour calculer
|
|
500
|
+
* Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
|
|
453
501
|
* À appeler une fois sur la page.
|
|
454
502
|
*/
|
|
455
503
|
startKeystrokeDynamicsTracker() {
|
|
456
504
|
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
457
|
-
if (
|
|
505
|
+
if (this._keystrokeTrackerAttached) return;
|
|
506
|
+
this._keystrokeTrackerAttached = true;
|
|
458
507
|
|
|
459
|
-
|
|
508
|
+
const activeKeys = new Map();
|
|
509
|
+
let lastKeyDownTime = 0;
|
|
510
|
+
let lastKeyName = '';
|
|
511
|
+
|
|
512
|
+
document.addEventListener('keydown', (e) => {
|
|
460
513
|
const now = performance.now();
|
|
514
|
+
const key = e.key;
|
|
515
|
+
const code = e.code;
|
|
516
|
+
if (!key && !code) return;
|
|
517
|
+
|
|
518
|
+
const keyIdentifier = code || key;
|
|
519
|
+
|
|
520
|
+
// Prevent key repeat triggering multiple events
|
|
521
|
+
if (activeKeys.has(keyIdentifier)) return;
|
|
522
|
+
activeKeys.set(keyIdentifier, now);
|
|
523
|
+
|
|
461
524
|
if (keystrokeTimestamps.length > 0) {
|
|
462
525
|
const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
|
|
463
526
|
const latency = now - lastTimestamp;
|
|
464
|
-
|
|
465
|
-
if (latency > 10 && latency < 2000) { // Augmenté à 2s
|
|
527
|
+
if (latency > 10 && latency < 2000) {
|
|
466
528
|
if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
|
|
467
|
-
keystrokeLatencies.shift();
|
|
529
|
+
keystrokeLatencies.shift();
|
|
468
530
|
}
|
|
469
531
|
keystrokeLatencies.push(latency);
|
|
470
532
|
}
|
|
471
533
|
}
|
|
472
534
|
keystrokeTimestamps.push(now);
|
|
535
|
+
|
|
536
|
+
// Flight Time (KeyDown to KeyDown)
|
|
537
|
+
if (lastKeyDownTime > 0) {
|
|
538
|
+
const flightTime = now - lastKeyDownTime;
|
|
539
|
+
if (flightTime > 10 && flightTime < 2000) {
|
|
540
|
+
if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
541
|
+
keystrokeFlightTimes.shift();
|
|
542
|
+
}
|
|
543
|
+
const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
|
|
544
|
+
keystrokeFlightTimes.push({ digraph, time: flightTime });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
lastKeyDownTime = now;
|
|
548
|
+
lastKeyName = keyIdentifier;
|
|
549
|
+
}, {passive: true});
|
|
550
|
+
|
|
551
|
+
document.addEventListener('keyup', (e) => {
|
|
552
|
+
const now = performance.now();
|
|
553
|
+
const key = e.key;
|
|
554
|
+
const code = e.code;
|
|
555
|
+
if (!key && !code) return;
|
|
556
|
+
|
|
557
|
+
const keyIdentifier = code || key;
|
|
558
|
+
|
|
559
|
+
if (activeKeys.has(keyIdentifier)) {
|
|
560
|
+
const pressTime = activeKeys.get(keyIdentifier);
|
|
561
|
+
const dwellTime = now - pressTime;
|
|
562
|
+
activeKeys.delete(keyIdentifier);
|
|
563
|
+
|
|
564
|
+
if (dwellTime > 5 && dwellTime < 1000) {
|
|
565
|
+
if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
566
|
+
keystrokeDwellTimes.shift();
|
|
567
|
+
}
|
|
568
|
+
keystrokeDwellTimes.push(dwellTime);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
473
571
|
}, {passive: true});
|
|
474
572
|
},
|
|
475
573
|
|
|
@@ -509,22 +607,75 @@ const ClientLibrary = {
|
|
|
509
607
|
});
|
|
510
608
|
activeHoneypotListeners.clear();
|
|
511
609
|
|
|
512
|
-
// 2. Ajouter les nouveaux écouteurs
|
|
610
|
+
// 2. Ajouter les nouveaux écouteurs sur le DOM classique
|
|
513
611
|
honeypotFieldNames.forEach(fieldName => {
|
|
514
612
|
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
515
613
|
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
614
|
const listener = () => {
|
|
520
615
|
this.onHoneypotTrigger();
|
|
521
|
-
// Se supprime lui-même après exécution, comme { once: true }
|
|
522
616
|
field.removeEventListener('input', listener);
|
|
523
617
|
};
|
|
524
618
|
field.addEventListener('input', listener);
|
|
525
619
|
activeHoneypotListeners.set(field, listener); // On stocke la référence
|
|
526
620
|
}
|
|
527
621
|
});
|
|
622
|
+
|
|
623
|
+
// 3. Générer des champs d'input pièges masqués dans un Shadow DOM fermé
|
|
624
|
+
if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
|
|
625
|
+
const host = document.createElement('div');
|
|
626
|
+
host.setAttribute('aria-hidden', 'true');
|
|
627
|
+
host.style.position = 'absolute';
|
|
628
|
+
host.style.width = '0';
|
|
629
|
+
host.style.height = '0';
|
|
630
|
+
host.style.overflow = 'hidden';
|
|
631
|
+
|
|
632
|
+
const shadow = host.attachShadow({ mode: 'closed' });
|
|
633
|
+
|
|
634
|
+
const style = document.createElement('style');
|
|
635
|
+
style.textContent = `
|
|
636
|
+
:host {
|
|
637
|
+
--trap-pos-state: absolute;
|
|
638
|
+
--trap-off-val: -9999px;
|
|
639
|
+
--trap-vis-state: hidden;
|
|
640
|
+
--trap-scale-val: 0;
|
|
641
|
+
}
|
|
642
|
+
.shadow-form-wrapper {
|
|
643
|
+
position: var(--trap-pos-state);
|
|
644
|
+
left: var(--trap-off-val);
|
|
645
|
+
top: var(--trap-off-val);
|
|
646
|
+
visibility: var(--trap-vis-state);
|
|
647
|
+
transform: scale(var(--trap-scale-val));
|
|
648
|
+
}
|
|
649
|
+
`;
|
|
650
|
+
shadow.appendChild(style);
|
|
651
|
+
|
|
652
|
+
const wrapper = document.createElement('div');
|
|
653
|
+
wrapper.className = 'shadow-form-wrapper';
|
|
654
|
+
|
|
655
|
+
honeypotFieldNames.forEach(fieldName => {
|
|
656
|
+
const label = document.createElement('label');
|
|
657
|
+
label.textContent = fieldName;
|
|
658
|
+
const input = document.createElement('input');
|
|
659
|
+
input.type = 'text';
|
|
660
|
+
input.name = fieldName;
|
|
661
|
+
input.tabIndex = -1;
|
|
662
|
+
input.autocomplete = 'off';
|
|
663
|
+
|
|
664
|
+
const trigger = () => {
|
|
665
|
+
this.onHoneypotTrigger();
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
input.addEventListener('input', trigger, { passive: true });
|
|
669
|
+
input.addEventListener('change', trigger, { passive: true });
|
|
670
|
+
input.addEventListener('focus', trigger, { passive: true });
|
|
671
|
+
|
|
672
|
+
wrapper.appendChild(label);
|
|
673
|
+
wrapper.appendChild(input);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
shadow.appendChild(wrapper);
|
|
677
|
+
document.body.appendChild(host);
|
|
678
|
+
}
|
|
528
679
|
},
|
|
529
680
|
|
|
530
681
|
/**
|
|
@@ -544,6 +695,10 @@ const ClientLibrary = {
|
|
|
544
695
|
// NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
|
|
545
696
|
metrics.mouseMovementsHistory = mouseMovementsHistory;
|
|
546
697
|
|
|
698
|
+
// NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
|
|
699
|
+
metrics.keystrokeDwellTimes = keystrokeDwellTimes;
|
|
700
|
+
metrics.keystrokeFlightTimes = keystrokeFlightTimes;
|
|
701
|
+
|
|
547
702
|
// Calcule la latence moyenne des frappes
|
|
548
703
|
if (keystrokeLatencies.length > 0) {
|
|
549
704
|
const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
|
|
@@ -672,24 +827,61 @@ const ClientLibrary = {
|
|
|
672
827
|
return;
|
|
673
828
|
}
|
|
674
829
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
830
|
+
const host = document.createElement('div');
|
|
831
|
+
host.setAttribute('aria-hidden', 'true');
|
|
832
|
+
host.style.position = 'absolute';
|
|
833
|
+
host.style.width = '0';
|
|
834
|
+
host.style.height = '0';
|
|
835
|
+
host.style.overflow = 'hidden';
|
|
836
|
+
|
|
837
|
+
const shadow = host.attachShadow({ mode: 'closed' });
|
|
838
|
+
|
|
839
|
+
const style = document.createElement('style');
|
|
840
|
+
style.textContent = `
|
|
841
|
+
:host {
|
|
842
|
+
--trap-layout-pos: absolute;
|
|
843
|
+
--trap-offset-val: -9999px;
|
|
844
|
+
--trap-visibility-state: hidden;
|
|
845
|
+
--trap-scale-factor: 0;
|
|
846
|
+
--trap-ptr-events: none;
|
|
847
|
+
}
|
|
848
|
+
.shadow-trap-wrapper {
|
|
849
|
+
position: var(--trap-layout-pos);
|
|
850
|
+
left: var(--trap-offset-val);
|
|
851
|
+
top: var(--trap-offset-val);
|
|
852
|
+
visibility: var(--trap-visibility-state);
|
|
853
|
+
transform: scale(var(--trap-scale-factor));
|
|
854
|
+
pointer-events: var(--trap-ptr-events);
|
|
855
|
+
}
|
|
856
|
+
a {
|
|
857
|
+
color: transparent;
|
|
858
|
+
text-decoration: none;
|
|
859
|
+
}
|
|
860
|
+
`;
|
|
861
|
+
shadow.appendChild(style);
|
|
862
|
+
|
|
863
|
+
const wrapper = document.createElement('div');
|
|
864
|
+
wrapper.className = 'shadow-trap-wrapper';
|
|
682
865
|
|
|
683
|
-
urls.forEach((url,i) => {
|
|
866
|
+
urls.forEach((url, i) => {
|
|
684
867
|
const link = document.createElement('a');
|
|
685
868
|
link.href = url;
|
|
686
869
|
link.rel = 'nofollow';
|
|
687
|
-
link.tabIndex = -1;
|
|
688
|
-
link.innerHTML = `<span>> ${i+1}</span>`;
|
|
689
|
-
|
|
870
|
+
link.tabIndex = -1;
|
|
871
|
+
link.innerHTML = `<span>> ${i + 1}</span>`;
|
|
872
|
+
|
|
873
|
+
const trigger = () => {
|
|
874
|
+
this.onHoneypotTrigger();
|
|
875
|
+
};
|
|
876
|
+
link.addEventListener('click', trigger, { passive: true });
|
|
877
|
+
link.addEventListener('focus', trigger, { passive: true });
|
|
878
|
+
link.addEventListener('mouseover', trigger, { passive: true });
|
|
879
|
+
|
|
880
|
+
wrapper.appendChild(link);
|
|
690
881
|
});
|
|
691
882
|
|
|
692
|
-
|
|
883
|
+
shadow.appendChild(wrapper);
|
|
884
|
+
document.body.appendChild(host);
|
|
693
885
|
},
|
|
694
886
|
/**
|
|
695
887
|
* Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
|
|
@@ -749,6 +941,7 @@ const ClientLibrary = {
|
|
|
749
941
|
keystrokes = true,
|
|
750
942
|
clicks = true, // Add new option
|
|
751
943
|
touches = true, // Nouveau paramètre tactiles
|
|
944
|
+
rendering = true,
|
|
752
945
|
phantomTraps = true, // NOUVEAU
|
|
753
946
|
honeypots = [],
|
|
754
947
|
trapUrls = [], // Nouveau paramètre pour les URL pièges
|
|
@@ -773,6 +966,9 @@ const ClientLibrary = {
|
|
|
773
966
|
if (touches) {
|
|
774
967
|
this.startTouchEventTracker();
|
|
775
968
|
}
|
|
969
|
+
if (rendering) {
|
|
970
|
+
this.startRenderingTracker();
|
|
971
|
+
}
|
|
776
972
|
if (phantomTraps) {
|
|
777
973
|
this.injectPhantomTraps();
|
|
778
974
|
}
|
|
@@ -941,6 +1137,7 @@ const metrics = {
|
|
|
941
1137
|
honeypotInteraction: false,
|
|
942
1138
|
historyLength: 0,
|
|
943
1139
|
clientTimestamp: 0,
|
|
1140
|
+
rendering: { fps: 0, jitter: 0, offscreenAnom: false },
|
|
944
1141
|
};
|
|
945
1142
|
|
|
946
1143
|
let lastMousePos = { x: 0, y: 0 };
|
|
@@ -953,6 +1150,8 @@ const CLICKS_HISTORY_MAX = 50;
|
|
|
953
1150
|
let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
|
|
954
1151
|
let keystrokeTimestamps = [];
|
|
955
1152
|
let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
|
|
1153
|
+
let keystrokeDwellTimes = [];
|
|
1154
|
+
let keystrokeFlightTimes = [];
|
|
956
1155
|
const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
|
|
957
1156
|
|
|
958
1157
|
|
|
@@ -966,6 +1165,7 @@ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.b
|
|
|
966
1165
|
export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
|
|
967
1166
|
export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
|
|
968
1167
|
export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
|
|
1168
|
+
export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
|
|
969
1169
|
export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
|
|
970
1170
|
export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
|
|
971
1171
|
export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
|