@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.
@@ -172,7 +172,9 @@ const ClientLibrary = {
172
172
  try {
173
173
  const canvas = document.createElement("canvas");
174
174
  const gl =
175
- canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
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 la latence.
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 (keystrokeTimestamps.length > 0) return;
505
+ if (this._keystrokeTrackerAttached) return;
506
+ this._keystrokeTrackerAttached = true;
458
507
 
459
- document.addEventListener('keydown', () => {
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
- // On ignore les latences irréalistes (trop longues ou trop courtes)
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(); // Garder la taille de l'historique
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 trapContainer = document.createElement('div');
676
- trapContainer.setAttribute('aria-hidden', 'true');
677
- trapContainer.style.position = 'absolute';
678
- trapContainer.style.left = '-9999px';
679
- trapContainer.style.top = '-9999px';
680
- trapContainer.style.transform = 'scale(0)';
681
- trapContainer.style.pointerEvents = 'none';
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; // Make it unfocusable
688
- link.innerHTML = `<span>&gt; ${i+1}</span>`; // SEO-insignificant content
689
- trapContainer.appendChild(link);
870
+ link.tabIndex = -1;
871
+ link.innerHTML = `<span>&gt; ${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
- document.body.appendChild(trapContainer);
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);