@anonympins/fingerprint 0.5.2 → 0.5.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +1 -1
  3. package/src/js/fingerprint.client.js +1271 -1191
  4. package/src/js/fingerprint.js +202 -108
  5. package/src/js/fingerprint.utils.js +213 -213
  6. package/src/js/library.js +24 -8
  7. package/src/js/mongodb-store.js +165 -79
  8. package/src/js/pow.solver.inline.js +500 -14
  9. package/src/js/pow.solver.js +323 -14
  10. package/src/js/problem-manager.js +1 -1
  11. package/src/js/redis-store.js +114 -42
  12. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  13. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  14. package/src/js/tests/fingerprint.client.test.js +128 -128
  15. package/src/js/tests/fingerprint.test.js +32 -2
  16. package/src/js/tests/ip-reputation.test.js +1 -1
  17. package/src/js/tests/pow.solver.test.js +5 -2
  18. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  19. package/src/php/Challenge/ChallengeUtils.php +21 -8
  20. package/src/php/FingerprintClient.php +22 -5
  21. package/src/php/FingerprintEngine.php +232 -56
  22. package/src/php/Optimization/Optimization.php +23 -16
  23. package/src/php/Optimization/OptimizationOperators.php +27 -0
  24. package/src/php/ProblemManager.php +1 -1
  25. package/src/php/Store/MongoDbStore.php +184 -104
  26. package/src/php/Store/RedisStore.php +91 -11
  27. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  28. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  29. package/src/php/Tests/IpReputationTest.php +1 -1
  30. package/src/php/Tests/PowTest.php +1 -1
  31. package/src/php/Tests/RequestUtilsTest.php +29 -0
  32. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  33. package/src/php/Utils/BigInt.php +202 -202
  34. package/src/php/Utils/RequestUtils.php +74 -21
  35. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -1,1192 +1,1272 @@
1
- import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
2
- import {solveChallenge} from './pow.solver.js';
3
-
4
- // Variable pour stocker la fonction de hachage active.
5
- // Par défaut, c'est l'implémentation JavaScript.
6
- let activeCyrb53 = jsCyrb53;
7
-
8
- const DB_NAME = 'wasm-cache-db';
9
- const DB_VERSION = 1;
10
- const STORE_NAME = 'wasm-modules';
11
-
12
- function getCachedWasm(url) {
13
- return new Promise((resolve) => {
14
- if (typeof indexedDB === 'undefined') return resolve(null);
15
- const request = indexedDB.open(DB_NAME, DB_VERSION);
16
- request.onupgradeneeded = (e) => {
17
- const db = e.target.result;
18
- if (!db.objectStoreNames.contains(STORE_NAME)) {
19
- db.createObjectStore(STORE_NAME);
20
- }
21
- };
22
- request.onsuccess = (e) => {
23
- const db = e.target.result;
24
- try {
25
- const transaction = db.transaction(STORE_NAME, 'readonly');
26
- const store = transaction.objectStore(STORE_NAME);
27
- const getReq = store.get(url);
28
- getReq.onsuccess = () => resolve(getReq.result);
29
- getReq.onerror = () => resolve(null);
30
- } catch (err) {
31
- resolve(null);
32
- }
33
- };
34
- request.onerror = () => resolve(null);
35
- });
36
- }
37
-
38
- function cacheWasm(url, data) {
39
- return new Promise((resolve) => {
40
- if (typeof indexedDB === 'undefined') return resolve(false);
41
- const request = indexedDB.open(DB_NAME, DB_VERSION);
42
- request.onupgradeneeded = (e) => {
43
- const db = e.target.result;
44
- if (!db.objectStoreNames.contains(STORE_NAME)) {
45
- db.createObjectStore(STORE_NAME);
46
- }
47
- };
48
- request.onsuccess = (e) => {
49
- const db = e.target.result;
50
- try {
51
- const transaction = db.transaction(STORE_NAME, 'readwrite');
52
- const store = transaction.objectStore(STORE_NAME);
53
- store.put(data, url);
54
- transaction.oncomplete = () => resolve(true);
55
- transaction.onerror = () => resolve(false);
56
- } catch (err) {
57
- resolve(false);
58
- }
59
- };
60
- request.onerror = () => resolve(false);
61
- });
62
- }
63
-
64
- const ClientLibrary = {
65
- // Cache pour éviter de recalculer les constantes (Hardware, etc.)
66
- _cachedBuilder: null,
67
- /**
68
- * @private
69
- * Dispatches a custom event from the window object.
70
- * @param {string} eventName - The name of the event.
71
- * @param {object} [detail={}] - The data to include in the event's detail property.
72
- */
73
- _dispatchEvent(eventName, detail = {}) {
74
- if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return;
75
- const event = new CustomEvent(`fingerprint:${eventName}`, { detail });
76
- window.dispatchEvent(event);
77
- },
78
-
79
- /**
80
- * Wrapper interne pour la fonction de hachage.
81
- * @private
82
- */
83
- _hasher: (str, seed) => activeCyrb53(str, seed),
84
-
85
- /**
86
- * Génère une preuve de connaissance à divulgation nulle (ZKP) de Schnorr pour l'empreinte de l'appareil.
87
- * Rend le tout stable et compatible avec les vérifications JS, PHP et Python.
88
- * @param {string} fingerprint - L'empreinte de l'appareil.
89
- * @returns {Promise<string>} La preuve sous format "y:t:s" en hexadécimal.
90
- */
91
- async generateZkpProof(fingerprint) {
92
- const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
93
- const ZKP_G = 2n;
94
- const cryptoObj = window.crypto || window.msCrypto;
95
-
96
- const sha256Hex = async (str) => {
97
- const encoder = new TextEncoder();
98
- const data = encoder.encode(str);
99
- const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
100
- const hashArray = Array.from(new Uint8Array(hashBuffer));
101
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
102
- };
103
-
104
- const modPow = (base, exponent, modulus) => {
105
- if (modulus === 1n) return 0n;
106
- let result = 1n;
107
- base = base % modulus;
108
- while (exponent > 0n) {
109
- if (exponent % 2n === 1n) {
110
- result = (result * base) % modulus;
111
- }
112
- exponent = exponent >> 1n;
113
- base = (base * base) % modulus;
114
- }
115
- return result;
116
- };
117
-
118
- const xHex = await sha256Hex(fingerprint);
119
- const x = BigInt('0x' + xHex) % ZKP_P;
120
- const y = modPow(ZKP_G, x, ZKP_P);
121
- const randomBytes = new Uint8Array(32);
122
- cryptoObj.getRandomValues(randomBytes);
123
- let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
124
- let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
125
- if (v === 0n) v = 1n;
126
- const t = modPow(ZKP_G, v, ZKP_P);
127
- const cStr = ZKP_G.toString() + y.toString() + t.toString();
128
- const cHex = await sha256Hex(cStr);
129
- const c = BigInt('0x' + cHex) % ZKP_P;
130
- const s = (v + c * x) % (ZKP_P - 1n);
131
-
132
- return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
133
- },
134
-
135
- /**
136
- * Génère l'empreinte de l'appareil actuel.
137
- */
138
- getDeviceFingerprint() {
139
- if (typeof window === "undefined") {
140
- console.error("getDeviceFingerprint can only be called on the client-side.");
141
- return "";
142
- }
143
-
144
- if (!this._cachedBuilder) {
145
- const nav = window.navigator;
146
- const screen = window.screen;
147
-
148
- this._cachedBuilder = new FingerprintBuilder();
149
-
150
- // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
151
- this._cachedBuilder.add(
152
- "hw", // Utilise maintenant le hasher actif
153
- `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
154
- );
155
-
156
- // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
157
- this._cachedBuilder.add(
158
- "geo",
159
- `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
160
- );
161
-
162
- // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
163
- this._cachedBuilder.add(
164
- "scr",
165
- `${screen.width}x${screen.height}_${screen.colorDepth}`,
166
- );
167
-
168
- // 4. Platform (Stable) : OS, Engine
169
- this._cachedBuilder.add("os", nav.platform);
170
-
171
- // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
172
- try {
173
- const canvas = document.createElement("canvas");
174
- const gl =
175
- canvas.getContext("webgl2") ||
176
- canvas.getContext("webgl") ||
177
- canvas.getContext("experimental-webgl");
178
- if (gl) {
179
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
180
- if (debugInfo) {
181
- const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
182
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
183
- this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
184
- }
185
- }
186
- } catch (e) {
187
- }
188
-
189
- // 6. Canvas Fingerprinting (Rendering quirks)
190
- try {
191
- const canvas = document.createElement("canvas");
192
- const ctx = canvas.getContext("2d");
193
- if (ctx) {
194
- canvas.width = 200;
195
- canvas.height = 50;
196
- ctx.textBaseline = "alphabetic";
197
- ctx.font = "14px 'Arial'";
198
- ctx.fillStyle = "#f60";
199
- ctx.fillRect(125, 1, 62, 20);
200
- ctx.fillStyle = "#069";
201
- ctx.fillText("fingerprint", 2, 15);
202
- ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
203
- ctx.fillText("fingerprint", 4, 17);
204
- this._cachedBuilder.add("cvs", canvas.toDataURL());
205
- }
206
- } catch (e) {
207
- }
208
-
209
- // 7. Détection des artefacts du Chrome DevTools Protocol (CDP)
210
- // Ces variables sont souvent injectées par les outils d'automatisation.
211
- const cdpFootprints = [
212
- 'cdc_adoQpoasnfa76pfcZLmcfl_Array',
213
- 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
214
- 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
215
- '$cdc_asdjflasutopfhvcZLmcfl_',
216
- '_selenium',
217
- '_driver'
218
- ];
219
- if (cdpFootprints.some(fp => window[fp])) {
220
- this._cachedBuilder.add("cdp", "true");
221
- }
222
-
223
- // 7. Bot Detection (Indication cachée)
224
- if (nav.webdriver) this._cachedBuilder.add("bot", "true");
225
- }
226
-
227
- return this._cachedBuilder.toString();
228
- },
229
-
230
- /**
231
- * Génère une signature de requête incluant le contexte.
232
- * @param {object} payload
233
- */
234
- /**
235
- * Génère une signature de requête incluant le contexte.
236
- * @param {object} payload
237
- */
238
- generateRequestSignature(payload = {}) {
239
- const deviceFp = this.getDeviceFingerprint();
240
- const sortedPayload = Object.keys(payload)
241
- .sort()
242
- .map((k) => `${k}=${payload[k]}`)
243
- .join("&");
244
- const payloadHash = this._hasher(sortedPayload);
245
- return `${deviceFp}|req:${payloadHash}`;
246
- },
247
-
248
- /**
249
- * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
250
- * @param {object} payload - Les données à signer.
251
- * @param {string} secret - La clé secrète partagée.
252
- * @returns {Promise<string>} La signature hexadécimale.
253
- */
254
- async generateClientSideSignature(payload, secret) {
255
- const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
256
- const encoder = new TextEncoder();
257
- const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
258
- name: "HMAC",
259
- hash: "SHA-256"
260
- }, false, ["sign"]);
261
- const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
262
- const hashArray = Array.from(new Uint8Array(signatureBuffer));
263
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
264
- },
265
-
266
- /**
267
- * @internal
268
- * Resets the cached fingerprint builder. Used for testing purposes.
269
- */
270
- _resetCache() {
271
- // Réinitialise le hasher à l'implémentation JS par défaut.
272
- activeCyrb53 = jsCyrb53;
273
- this._cachedBuilder = null;
274
- },
275
-
276
- /**
277
- * Injecte des éléments interactifs fantômes invisibles pour piéger les bots (focus/hover).
278
- */
279
- injectPhantomTraps() {
280
- if (typeof document === 'undefined') return;
281
-
282
- // Création d'un élément interactif fantôme
283
- const phantom = document.createElement('a');
284
- phantom.href = '#';
285
- // Nom trompeur pour attirer les analyseurs automatiques de liens / formulaires
286
- phantom.id = 'sys-session-recovery';
287
- phantom.tabIndex = 0; // Dans le flux naturel de tabulation
288
- phantom.setAttribute('aria-hidden', 'true'); // Masqué pour les screen readers légitimes
289
-
290
- // Style invisible mais interactif (1px x 1px, presque transparent)
291
- phantom.style.position = 'fixed';
292
- phantom.style.top = '1px';
293
- phantom.style.left = '1px';
294
- phantom.style.width = '1px';
295
- phantom.style.height = '1px';
296
- phantom.style.opacity = '0.001';
297
- phantom.style.zIndex = '99999';
298
- phantom.style.overflow = 'hidden';
299
- phantom.style.pointerEvents = 'auto';
300
-
301
- const triggerTrap = () => {
302
- this.onHoneypotTrigger();
303
- };
304
-
305
- phantom.addEventListener('focus', triggerTrap, { passive: true });
306
- phantom.addEventListener('mouseover', triggerTrap, { passive: true });
307
-
308
- document.body.appendChild(phantom);
309
- },
310
-
311
- /**
312
- * Démarre le suivi des événements tactiles sur mobile/tablette.
313
- */
314
- startTouchEventTracker() {
315
- if (this._touchTrackerAttached) return;
316
- this._touchTrackerAttached = true;
317
-
318
- const handleTouch = (e) => {
319
- if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
320
- touchMovementsHistory.shift();
321
- }
322
- const touch = e.touches[0] || e.changedTouches[0];
323
- if (!touch) return;
324
-
325
- const radiusX = touch.radiusX || 0;
326
- const radiusY = touch.radiusY || 0;
327
- const radius = (radiusX + radiusY) / 2;
328
- const force = touch.force || touch.webkitForce || 0;
329
-
330
- touchMovementsHistory.push({
331
- x: touch.clientX,
332
- y: touch.clientY,
333
- t: performance.now(),
334
- p: force,
335
- r: radius,
336
- num: e.touches.length
337
- });
338
- };
339
-
340
- document.addEventListener('touchstart', handleTouch, { passive: true });
341
- document.addEventListener('touchmove', handleTouch, { passive: true });
342
- document.addEventListener('touchend', handleTouch, { passive: true });
343
- },
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
-
391
- /**
392
- * Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
393
- */
394
- async initializeSpace(seed, sizeMb) {
395
- return new Promise((resolve, reject) => {
396
- const request = indexedDB.open('pospace-db', 1);
397
- request.onupgradeneeded = (e) => {
398
- const db = e.target.result;
399
- if (!db.objectStoreNames.contains('blocks')) {
400
- db.createObjectStore('blocks');
401
- }
402
- };
403
- request.onsuccess = async (e) => {
404
- const db = e.target.result;
405
- const tx = db.transaction('blocks', 'readwrite');
406
- const store = tx.objectStore('blocks');
407
-
408
- const maxBlocks = sizeMb * 1024;
409
- const countReq = store.count();
410
- countReq.onsuccess = async () => {
411
- if (countReq.result < maxBlocks) {
412
- for (let i = 0; i < maxBlocks; i++) {
413
- const block = new Uint8Array(1024);
414
- let h = 5381;
415
- for (let j = 0; j < seed.length; j++) {
416
- h = (h << 5) + h + seed.charCodeAt(j);
417
- }
418
- h = (h << 5) + h + i;
419
- for (let k = 0; k < 1024; k++) {
420
- h = Math.imul(h ^ k, 1597334677);
421
- block[k] = h & 0xff;
422
- }
423
- store.put(block, i);
424
- }
425
- }
426
- resolve();
427
- };
428
- };
429
- request.onerror = () => reject(new Error("Failed to open pospace database"));
430
- });
431
- },
432
-
433
- /**
434
- * Lit un bloc spécifique de l'IndexedDB locale sous format hexadécimal.
435
- */
436
- async readSpaceBlock(blockIdx) {
437
- return new Promise((resolve, reject) => {
438
- const request = indexedDB.open('pospace-db', 1);
439
- request.onsuccess = (e) => {
440
- const db = e.target.result;
441
- const tx = db.transaction('blocks', 'readonly');
442
- const store = tx.objectStore('blocks');
443
- const getReq = store.get(blockIdx);
444
- getReq.onsuccess = () => {
445
- const block = getReq.result;
446
- if (block) {
447
- const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
448
- resolve(hex);
449
- } else {
450
- reject(new Error("Block not found"));
451
- }
452
- };
453
- getReq.onerror = () => reject(getReq.error);
454
- };
455
- request.onerror = () => reject(new Error("Failed to open pospace database"));
456
- });
457
- },
458
-
459
- /**
460
- * Résout le Proof-of-Space en combinant optionnellement le bloc du pair.
461
- */
462
- async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
463
- const blocks = [];
464
- for (const idx of queries) {
465
- const blockHex = await this.readSpaceBlock(idx);
466
- blocks.push(blockHex);
467
- }
468
- let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
469
- const encoder = new TextEncoder();
470
- const data = encoder.encode(finalPayload);
471
- const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
472
- const hashArray = Array.from(new Uint8Array(hashBuffer));
473
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
474
- },
475
-
476
- /**
477
- * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
478
- * À appeler une fois sur la page.
479
- */
480
- startMouseEntropyTracker() {
481
- // Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
482
- if (this._mouseTrackerAttached) return;
483
- this._mouseTrackerAttached = true;
484
-
485
- document.addEventListener('mousemove', (e) => {
486
- // NOUVEAU: Capturer une série de points {x, y, t}
487
- if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
488
- // Garder la taille de l'historique constante pour éviter une consommation mémoire excessive.
489
- mouseMovementsHistory.shift();
490
- }
491
- mouseMovementsHistory.push({
492
- x: e.clientX,
493
- y: e.clientY,
494
- t: performance.now()
495
- });
496
- }, {passive: true});
497
- },
498
-
499
- /**
500
- * Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
501
- * À appeler une fois sur la page.
502
- */
503
- startKeystrokeDynamicsTracker() {
504
- // S'assurer de ne pas attacher l'écouteur plusieurs fois
505
- if (this._keystrokeTrackerAttached) return;
506
- this._keystrokeTrackerAttached = true;
507
-
508
- const activeKeys = new Map();
509
- let lastKeyDownTime = 0;
510
- let lastKeyName = '';
511
-
512
- document.addEventListener('keydown', (e) => {
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
-
524
- if (keystrokeTimestamps.length > 0) {
525
- const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
526
- const latency = now - lastTimestamp;
527
- if (latency > 10 && latency < 2000) {
528
- if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
529
- keystrokeLatencies.shift();
530
- }
531
- keystrokeLatencies.push(latency);
532
- }
533
- }
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
- }
571
- }, {passive: true});
572
- },
573
-
574
- /**
575
- * Starts tracking click events to analyze position variance.
576
- * @private
577
- */
578
- startClickTracker() {
579
- if (this._clickTrackerAttached) return;
580
- this._clickTrackerAttached = true;
581
-
582
- document.addEventListener('click', (e) => {
583
- if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
584
- clicksHistory.shift();
585
- }
586
- // Generate a simple identifier for the target element
587
- const target = e.target;
588
- const targetId = target.id || target.name || target.tagName;
589
-
590
- clicksHistory.push({
591
- x: e.clientX,
592
- y: e.clientY,
593
- t: performance.now(),
594
- targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
595
- });
596
- }, { passive: true });
597
- },
598
- /**
599
- * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
600
- * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
601
- * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
602
- */
603
- initializeHoneypots(honeypotFieldNames) {
604
- // 1. Nettoyer les anciens écouteurs
605
- activeHoneypotListeners.forEach((listener, field) => {
606
- field.removeEventListener('input', listener);
607
- });
608
- activeHoneypotListeners.clear();
609
-
610
- // 2. Ajouter les nouveaux écouteurs sur le DOM classique
611
- honeypotFieldNames.forEach(fieldName => {
612
- const field = document.querySelector(`[name="${fieldName}"]`);
613
- if (field) {
614
- const listener = () => {
615
- this.onHoneypotTrigger();
616
- field.removeEventListener('input', listener);
617
- };
618
- field.addEventListener('input', listener);
619
- activeHoneypotListeners.set(field, listener); // On stocke la référence
620
- }
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
- }
679
- },
680
-
681
- /**
682
- * Récupère les métriques comportementales collectées.
683
- * À appeler avant d'envoyer une requête sensible.
684
- * @returns {ClientBehaviorMetrics}
685
- */
686
- getClientBehaviorMetrics() {
687
- // Add history length as a behavioral signal.
688
- metrics.historyLength = window.history.length;
689
-
690
- // Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
691
- metrics.clicksHistory = clicksHistory;
692
- metrics.clientTimestamp = Date.now();
693
-
694
- metrics.touchMovementsHistory = touchMovementsHistory;
695
- // NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
696
- metrics.mouseMovementsHistory = mouseMovementsHistory;
697
-
698
- // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
699
- metrics.keystrokeDwellTimes = keystrokeDwellTimes;
700
- metrics.keystrokeFlightTimes = keystrokeFlightTimes;
701
-
702
- // Calcule la latence moyenne des frappes
703
- if (keystrokeLatencies.length > 0) {
704
- const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
705
- metrics.keystrokeLatency = sum / keystrokeLatencies.length;
706
- } else {
707
- metrics.keystrokeLatency = 0;
708
- }
709
- return metrics;
710
- },
711
-
712
- /**
713
- * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
714
- * @param {RequestInfo} resource
715
- * @param {RequestInit} [options]
716
- * @returns {Promise<Response>}
717
- */
718
- async protectedFetch(resource, options = {}) {
719
- const fp = this.getDeviceFingerprint();
720
- const behavior = this.getClientBehaviorMetrics();
721
-
722
- const headers = new Headers(options.headers || {});
723
- headers.set('X-Device-Fingerprint', fp);
724
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
725
-
726
- options.headers = headers;
727
- return fetch(resource, options);
728
- },
729
-
730
- // --- Système d'interception de Fetch robuste et anti-conflit ---
731
-
732
- _isFetchPatched: false,
733
- _interceptorChain: [],
734
- // On stocke la fonction fetch originale et on la lie à son contexte (window)
735
- // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
736
- _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
737
-
738
- /**
739
- * Adds an interceptor function to the `fetch` chain.
740
- * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
741
- * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
742
- * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
743
- */
744
- addFetchInterceptor(interceptor) {
745
- if (!this._isFetchPatched) {
746
- this.patchGlobalFetch();
747
- }
748
- this._interceptorChain.push(interceptor);
749
- },
750
-
751
- patchGlobalFetch() {
752
- if (this._isFetchPatched || !this._originalFetch) return;
753
-
754
- this._isFetchPatched = true;
755
- window.fetch = (resource, options) => {
756
- // Le "dispatcher" qui exécute la chaîne.
757
- const dispatch = (index, res, opts) => {
758
- if (index >= this._interceptorChain.length) {
759
- // Fin de la chaîne, on appelle le fetch original.
760
- return this._originalFetch(res, opts);
761
- }
762
- const nextInterceptor = this._interceptorChain[index];
763
- // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
764
- return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
765
- };
766
- return dispatch(0, resource, options || {});
767
- };
768
- },
769
-
770
- /**
771
- * La fonction qui est appelée lorsqu'un honeypot est déclenché.
772
- * @private
773
- */
774
- onHoneypotTrigger() {
775
- metrics.honeypotInteraction = true;
776
- // Émettre un événement pour que l'application puisse réagir.
777
- this._dispatchEvent('honeypotTriggered');
778
- },
779
-
780
- /**
781
- * Initialise l'intercepteur de fingerprinting.
782
- * Il s'ajoute à la chaîne d'interception sans écraser les autres.
783
- * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
784
- * Si non fourni, protège les requêtes de même origine.
785
- */
786
- initializeFetch(targetDomains = []) {
787
- const fingerprintInterceptor = (resource, options, next) => {
788
- const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
789
- let shouldProtect = false;
790
-
791
- try {
792
- const url = new URL(requestUrl, window.location.origin);
793
- // Protéger si la liste de domaines est vide ET que la requête est de même origine,
794
- // OU si le domaine de la requête est dans la liste fournie.
795
- shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
796
- (targetDomains.length > 0 && targetDomains.includes(url.hostname));
797
- } catch (e) {
798
- // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
799
- // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
800
- // On protège par défaut si aucune liste de domaines n'est spécifiée.
801
- shouldProtect = targetDomains.length === 0;
802
- }
803
-
804
- if (shouldProtect) {
805
- const fp = this.getDeviceFingerprint();
806
- const behavior = this.getClientBehaviorMetrics();
807
- const headers = new Headers(options.headers || {});
808
- headers.set('X-Device-Fingerprint', fp);
809
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
810
- options.headers = headers;
811
- }
812
-
813
- // Passe la main à l'intercepteur suivant dans la chaîne.
814
- return next(resource, options);
815
- };
816
-
817
- this.addFetchInterceptor(fingerprintInterceptor);
818
- }, // <-- VIRGULE AJOUTÉE ICI
819
-
820
- /**
821
- * Injects visually hidden "honeypot" links into the DOM to trap bots.
822
- * @param {string[]} urls - An array of trap URLs to inject.
823
- * @private
824
- */
825
- injectTrapLinks(urls) {
826
- if (!urls || urls.length === 0 || typeof document === 'undefined') {
827
- return;
828
- }
829
-
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';
865
-
866
- urls.forEach((url, i) => {
867
- const link = document.createElement('a');
868
- link.href = url;
869
- link.rel = 'nofollow';
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);
881
- });
882
-
883
- shadow.appendChild(wrapper);
884
- document.body.appendChild(host);
885
- },
886
- /**
887
- * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
888
- * @param {Response} response - La réponse initiale (potentiellement 429).
889
- * @param {RequestInfo} resource - La ressource de la requête originale.
890
- * @param {RequestInit} options - Les options de la requête originale.
891
- * @returns {Promise<Response>} - La réponse de la requête réessayée.
892
- * @private
893
- */
894
- async solveChallengeAndRetry(response, resource, options) {
895
- if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
896
- return response;
897
- }
898
-
899
- try {
900
- const challengeData = await response.json();
901
- if (!challengeData.challenge || !challengeData.challenge.type) {
902
- return response; // Pas un challenge JSON valide
903
- }
904
-
905
- console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
906
- this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
907
-
908
- // L'empreinte de l'appareil qui résout le challenge est cruciale.
909
- const solverFp = this.getDeviceFingerprint();
910
- const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
911
- console.log('[Fingerprint] Challenge solved. Retrying original request.');
912
-
913
- this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
914
- // Ajouter la solution aux paramètres de la requête pour le nouvel essai
915
- const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
916
- // La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
917
- solutionWrapper.applyToUrl(url);
918
-
919
- // On ajoute l'empreinte du solveur à la requête de réessai.
920
- url.searchParams.set('pow_fp', solverFp);
921
-
922
- // On utilise la chaîne d'intercepteurs pour la requête réessayée,
923
- // ce qui garantit que le fetch original est appelé avec le bon contexte.
924
- // Cela évite de réintroduire l'erreur "Illegal invocation".
925
- return window.fetch(url.toString(), options);
926
- } catch (e) {
927
- console.error('[Fingerprint] Failed to solve or retry challenge:', e);
928
- return response; // Retourne la réponse 429 originale en cas d'échec
929
- }
930
- }, // <-- VIRGULE AJOUTÉE ICI
931
-
932
- /**
933
- * Initialise toutes les protections côté client en une seule fois.
934
- * Tente également de charger le module WASM si `wasmPath` est fourni.
935
- * C'est la méthode d'initialisation recommandée.
936
- * @param {ClientConfig} [config={}] - L'objet de configuration.
937
- */
938
- initializeClient(config = {}) {
939
- const {
940
- mouse = true,
941
- keystrokes = true,
942
- clicks = true, // Add new option
943
- touches = true, // Nouveau paramètre tactiles
944
- rendering = true,
945
- phantomTraps = true, // NOUVEAU
946
- honeypots = [],
947
- trapUrls = [], // Nouveau paramètre pour les URL pièges
948
- wasmPath, // Nouveau paramètre
949
- fetch: fetchConfig = {}
950
- } = config;
951
-
952
- // Tentative de chargement du WASM si le chemin est fourni
953
- if (wasmPath) {
954
- this.initializeWasm(wasmPath);
955
- }
956
-
957
- if (mouse) {
958
- this.startMouseEntropyTracker();
959
- }
960
- if (keystrokes) {
961
- this.startKeystrokeDynamicsTracker();
962
- }
963
- if (clicks) {
964
- this.startClickTracker();
965
- }
966
- if (touches) {
967
- this.startTouchEventTracker();
968
- }
969
- if (rendering) {
970
- this.startRenderingTracker();
971
- }
972
- if (phantomTraps) {
973
- this.injectPhantomTraps();
974
- }
975
- if (honeypots.length > 0) {
976
- this.initializeHoneypots(honeypots);
977
- }
978
-
979
- // Injection dynamique des liens pièges au démarrage
980
- if (trapUrls.length > 0) {
981
- this.injectTrapLinks(trapUrls);
982
- }
983
- // On active l'interception si `fetch` est configuré, même avec un objet vide.
984
- if (config.fetch) {
985
- this.initializeFetch(fetchConfig.targetDomains);
986
-
987
- // Ajoute l'intercepteur pour la résolution de challenge
988
- if (fetchConfig.handleChallenges !== false) {
989
- this.addFetchInterceptor(async (resource, options, next) => {
990
- const originalResponse = await next(resource, options);
991
- // On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
992
- return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
993
- });
994
- }
995
- }
996
- },
997
-
998
- /**
999
- * Tente de charger et d'initialiser le module WebAssembly pour un hachage plus rapide.
1000
- * Si le chargement échoue, il se rabat silencieusement sur l'implémentation JS.
1001
- * @param {string} wasmPath - Le chemin vers le script de chargement du module WASM (ex: '/fp.js').
1002
- */
1003
- async initializeWasm(wasmPath) {
1004
- try {
1005
- // Direct standalone polymorphic WebAssembly loading
1006
- if (wasmPath.endsWith('.wasm')) {
1007
- const response = await fetch(wasmPath);
1008
- const arrayBuffer = await response.arrayBuffer();
1009
- const module = await WebAssembly.compile(arrayBuffer);
1010
- const instance = await WebAssembly.instantiate(module, {});
1011
- const exports = instance.exports;
1012
- const memory = exports.memory;
1013
- activeCyrb53 = (str) => {
1014
- const encoder = new TextEncoder();
1015
- const bytes = encoder.encode(str);
1016
- const view = new Uint8Array(memory.buffer, 0, bytes.length);
1017
- view.set(bytes);
1018
- return exports.hash(0, bytes.length);
1019
- };
1020
- console.log('[Fingerprint] Standalone polymorphic WASM loaded successfully. Using fast dynamic hashing.');
1021
- if (this._cachedBuilder) {
1022
- this._cachedBuilder.addRaw('wasm', 'true');
1023
- }
1024
- return;
1025
- }
1026
- // 1. Injecter le script qui charge le module WASM
1027
- const script = document.createElement('script');
1028
- script.src = wasmPath;
1029
- await new Promise((resolve, reject) => {
1030
- script.onload = resolve;
1031
- script.onerror = reject;
1032
- document.head.appendChild(script);
1033
- });
1034
-
1035
- // 2. Attendre que la fonction globale `createFingerprintModule` soit disponible
1036
- if (typeof window.createFingerprintModule !== 'function') {
1037
- throw new Error('WASM loader script did not expose createFingerprintModule.');
1038
- }
1039
-
1040
- // 3. Initialiser le module
1041
- const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
1042
- const wasmModule = await window.createFingerprintModule({
1043
- instantiateWasm: (imports, successCallback) => {
1044
- (async () => {
1045
- try {
1046
- const cached = await getCachedWasm(wasmUrl);
1047
- if (cached) {
1048
- let instance;
1049
- if (cached instanceof WebAssembly.Module) {
1050
- instance = await WebAssembly.instantiate(cached, imports);
1051
- } else {
1052
- const result = await WebAssembly.instantiate(cached, imports);
1053
- instance = result.instance;
1054
- }
1055
- successCallback(instance, cached);
1056
- return;
1057
- }
1058
-
1059
- const response = await fetch(wasmUrl);
1060
- const arrayBuffer = await response.arrayBuffer();
1061
-
1062
- let cachedData = arrayBuffer;
1063
- let isModuleCached = false;
1064
- try {
1065
- const compiledModule = await WebAssembly.compile(arrayBuffer);
1066
- const success = await cacheWasm(wasmUrl, compiledModule);
1067
- if (success) {
1068
- cachedData = compiledModule;
1069
- isModuleCached = true;
1070
- }
1071
- } catch (e) {
1072
- // Fallback if browser doesn't allow structured cloning of Compiled Modules
1073
- }
1074
-
1075
- if (!isModuleCached) {
1076
- await cacheWasm(wasmUrl, arrayBuffer);
1077
- }
1078
-
1079
- let instance;
1080
- if (cachedData instanceof WebAssembly.Module) {
1081
- instance = await WebAssembly.instantiate(cachedData, imports);
1082
- } else {
1083
- const result = await WebAssembly.instantiate(arrayBuffer, imports);
1084
- instance = result.instance;
1085
- }
1086
- successCallback(instance, cachedData);
1087
- } catch (err) {
1088
- console.warn('[Fingerprint] Custom WASM instantiation failed, falling back to default Emscripten loader:', err);
1089
- successCallback(null);
1090
- }
1091
- })();
1092
- return {}; // Async instantiation indicator for Emscripten
1093
- }
1094
- });
1095
- if (typeof wasmModule._hash_string !== 'function') {
1096
- throw new Error('WASM module did not export _hash_string.');
1097
- }
1098
- window.wasmModule = wasmModule;
1099
- ClientLibrary.wasmModule = wasmModule;
1100
-
1101
- // 4. Remplacer la fonction de hachage par la version WASM
1102
- activeCyrb53 = (str) => {
1103
- // La fonction C++ attend un pointeur, Emscripten gère la conversion
1104
- return wasmModule._hash_string(str);
1105
- };
1106
-
1107
- console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
1108
- // NOUVEAU: Ajoute un indicateur à l'empreinte pour que le serveur sache que le WASM est actif.
1109
- if (this._cachedBuilder) {
1110
- this._cachedBuilder.addRaw('wasm', 'true');
1111
- }
1112
- } catch (error) {
1113
- console.warn('[Fingerprint] WASM module failed to load. Falling back to JS implementation. Error:', error);
1114
- }
1115
- }
1116
- };
1117
-
1118
- /**
1119
- * @typedef {object} ClientBehaviorMetrics
1120
- * @property {number} mouseEntropy - Entropie des mouvements de la souris.
1121
- * @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Historique des points de la souris.
1122
- * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
1123
- * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
1124
- * @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Historique des clics.
1125
- * @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Historique des glissements tactiles.
1126
- * @property {number} historyLength - La longueur de l'historique de session du navigateur (`window.history.length`).
1127
- * @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
1128
- * @property {string[]} [trapUrls] - URLs pièges à injecter dynamiquement.
1129
- */
1130
- /** @type {ClientBehaviorMetrics} */
1131
- const metrics = {
1132
- mouseEntropy: 0, // Conservé pour la compatibilité, mais l'analyse se fait maintenant sur l'historique
1133
- mouseMovementsHistory: [],
1134
- touchMovementsHistory: [],
1135
- clicksHistory: [],
1136
- keystrokeLatency: 0,
1137
- honeypotInteraction: false,
1138
- historyLength: 0,
1139
- clientTimestamp: 0,
1140
- rendering: { fps: 0, jitter: 0, offscreenAnom: false },
1141
- };
1142
-
1143
- let lastMousePos = { x: 0, y: 0 };
1144
- let mouseMovementsHistory = []; // NOUVEAU: Historique des points de la souris
1145
- let touchMovementsHistory = []; // NOUVEAU: Historique des gestes tactiles
1146
- const TOUCH_HISTORY_MAX = 100;
1147
- const MOUSE_HISTORY_MAX = 100; // Limite le nombre de points stockés
1148
- let clicksHistory = [];
1149
- const CLICKS_HISTORY_MAX = 50;
1150
- let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
1151
- let keystrokeTimestamps = [];
1152
- let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
1153
- let keystrokeDwellTimes = [];
1154
- let keystrokeFlightTimes = [];
1155
- const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
1156
-
1157
-
1158
-
1159
- // Exporter les fonctions individuellement pour la compatibilité ascendante
1160
- export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
1161
- export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
1162
- export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
1163
- export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
1164
- export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
1165
- export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
1166
- export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
1167
- export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
1168
- export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
1169
- export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
1170
- export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
1171
- export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
1172
- export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
1173
- export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
1174
- export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
1175
- export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
1176
- export const initializeWasm = ClientLibrary.initializeWasm.bind(ClientLibrary);
1177
- export const injectTrapLinks = ClientLibrary.injectTrapLinks.bind(ClientLibrary);
1178
- export const injectPhantomTraps = ClientLibrary.injectPhantomTraps.bind(ClientLibrary);
1179
- export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
1180
- export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrary);
1181
- export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
1182
- export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
1183
- export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
1184
-
1185
- // Export the internal object for testing purposes
1186
- export default ClientLibrary;
1187
-
1188
- // --- Global Export for Browser ---
1189
- // Attach the library to the window object to make it accessible from inline scripts.
1190
- if (typeof window !== 'undefined') {
1191
- window.ClientLibrary = ClientLibrary;
1
+ import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
2
+ import {solveChallenge} from './pow.solver.js';
3
+
4
+ // Variable pour stocker la fonction de hachage active.
5
+ // Par défaut, c'est l'implémentation JavaScript.
6
+ let activeCyrb53 = jsCyrb53;
7
+
8
+ const DB_NAME = 'wasm-cache-db';
9
+ const DB_VERSION = 1;
10
+ const STORE_NAME = 'wasm-modules';
11
+
12
+ const genRandStr = (len = 8) => {
13
+ return Array.from({ length: len }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join('');
14
+ };
15
+
16
+ function getCachedWasm(url) {
17
+ return new Promise((resolve) => {
18
+ if (typeof indexedDB === 'undefined') return resolve(null);
19
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
20
+ request.onupgradeneeded = (e) => {
21
+ const db = e.target.result;
22
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
23
+ db.createObjectStore(STORE_NAME);
24
+ }
25
+ };
26
+ request.onsuccess = (e) => {
27
+ const db = e.target.result;
28
+ try {
29
+ const transaction = db.transaction(STORE_NAME, 'readonly');
30
+ const store = transaction.objectStore(STORE_NAME);
31
+ const getReq = store.get(url);
32
+ getReq.onsuccess = () => resolve(getReq.result);
33
+ getReq.onerror = () => resolve(null);
34
+ } catch (err) {
35
+ resolve(null);
36
+ }
37
+ };
38
+ request.onerror = () => resolve(null);
39
+ });
40
+ }
41
+
42
+ function cacheWasm(url, data) {
43
+ return new Promise((resolve) => {
44
+ if (typeof indexedDB === 'undefined') return resolve(false);
45
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
46
+ request.onupgradeneeded = (e) => {
47
+ const db = e.target.result;
48
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
49
+ db.createObjectStore(STORE_NAME);
50
+ }
51
+ };
52
+ request.onsuccess = (e) => {
53
+ const db = e.target.result;
54
+ try {
55
+ const transaction = db.transaction(STORE_NAME, 'readwrite');
56
+ const store = transaction.objectStore(STORE_NAME);
57
+ store.put(data, url);
58
+ transaction.oncomplete = () => resolve(true);
59
+ transaction.onerror = () => resolve(false);
60
+ } catch (err) {
61
+ resolve(false);
62
+ }
63
+ };
64
+ request.onerror = () => resolve(false);
65
+ });
66
+ }
67
+
68
+ const ClientLibrary = {
69
+ // Cache pour éviter de recalculer les constantes (Hardware, etc.)
70
+ _cachedBuilder: null,
71
+ /**
72
+ * @private
73
+ * Dispatches a custom event from the window object.
74
+ * @param {string} eventName - The name of the event.
75
+ * @param {object} [detail={}] - The data to include in the event's detail property.
76
+ */
77
+ _dispatchEvent(eventName, detail = {}) {
78
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return;
79
+ const event = new CustomEvent(`fingerprint:${eventName}`, { detail });
80
+ window.dispatchEvent(event);
81
+ },
82
+
83
+ /**
84
+ * Wrapper interne pour la fonction de hachage.
85
+ * @private
86
+ */
87
+ _hasher: (str, seed) => activeCyrb53(str, seed),
88
+
89
+ /**
90
+ * Génère une preuve de connaissance à divulgation nulle (ZKP) de Schnorr pour l'empreinte de l'appareil.
91
+ * Rend le tout stable et compatible avec les vérifications JS, PHP et Python.
92
+ * @param {string} fingerprint - L'empreinte de l'appareil.
93
+ * @returns {Promise<string>} La preuve sous format "y:t:s" en hexadécimal.
94
+ */
95
+ async generateZkpProof(fingerprint) {
96
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
97
+ const ZKP_G = 2n;
98
+ const cryptoObj = window.crypto || window.msCrypto;
99
+
100
+ const sha256Hex = async (str) => {
101
+ const encoder = new TextEncoder();
102
+ const data = encoder.encode(str);
103
+ const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
104
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
105
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
106
+ };
107
+
108
+ const modPow = (base, exponent, modulus) => {
109
+ if (modulus === 1n) return 0n;
110
+ let result = 1n;
111
+ base = base % modulus;
112
+ while (exponent > 0n) {
113
+ if (exponent % 2n === 1n) {
114
+ result = (result * base) % modulus;
115
+ }
116
+ exponent = exponent >> 1n;
117
+ base = (base * base) % modulus;
118
+ }
119
+ return result;
120
+ };
121
+
122
+ const xHex = await sha256Hex(fingerprint);
123
+ const x = BigInt('0x' + xHex) % ZKP_P;
124
+ const y = modPow(ZKP_G, x, ZKP_P);
125
+ const randomBytes = new Uint8Array(32);
126
+ cryptoObj.getRandomValues(randomBytes);
127
+ let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
128
+ let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
129
+ if (v === 0n) v = 1n;
130
+ const t = modPow(ZKP_G, v, ZKP_P);
131
+ const cStr = ZKP_G.toString() + y.toString() + t.toString();
132
+ const cHex = await sha256Hex(cStr);
133
+ const c = BigInt('0x' + cHex) % ZKP_P;
134
+ const s = (v + c * x) % (ZKP_P - 1n);
135
+
136
+ return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
137
+ },
138
+
139
+ /**
140
+ * Génère l'empreinte de l'appareil actuel.
141
+ */
142
+ getDeviceFingerprint() {
143
+ if (typeof window === "undefined") {
144
+ console.error("getDeviceFingerprint can only be called on the client-side.");
145
+ return "";
146
+ }
147
+
148
+ if (!this._cachedBuilder) {
149
+ const nav = window.navigator;
150
+ const screen = window.screen;
151
+
152
+ this._cachedBuilder = new FingerprintBuilder();
153
+
154
+ // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
155
+ this._cachedBuilder.add(
156
+ "hw", // Utilise maintenant le hasher actif
157
+ `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
158
+ );
159
+
160
+ // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
161
+ this._cachedBuilder.add(
162
+ "geo",
163
+ `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
164
+ );
165
+
166
+ // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
167
+ this._cachedBuilder.add(
168
+ "scr",
169
+ `${screen.width}x${screen.height}_${screen.colorDepth}`,
170
+ );
171
+
172
+ // 4. Platform (Stable) : OS, Engine
173
+ this._cachedBuilder.add("os", nav.platform);
174
+
175
+ // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
176
+ try {
177
+ const canvas = document.createElement("canvas");
178
+ const gl =
179
+ canvas.getContext("webgl2") ||
180
+ canvas.getContext("webgl") ||
181
+ canvas.getContext("experimental-webgl");
182
+ if (gl) {
183
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
184
+ if (debugInfo) {
185
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
186
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
187
+ this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
188
+ }
189
+ }
190
+ } catch (e) {
191
+ }
192
+
193
+ // 6. Canvas Fingerprinting (Rendering quirks)
194
+ try {
195
+ const canvas = document.createElement("canvas");
196
+ const ctx = canvas.getContext("2d");
197
+ if (ctx) {
198
+ canvas.width = 200;
199
+ canvas.height = 50;
200
+ ctx.textBaseline = "alphabetic";
201
+ ctx.font = "14px 'Arial'";
202
+ ctx.fillStyle = "#f60";
203
+ ctx.fillRect(125, 1, 62, 20);
204
+ ctx.fillStyle = "#069";
205
+ ctx.fillText("fingerprint", 2, 15);
206
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
207
+ ctx.fillText("fingerprint", 4, 17);
208
+ this._cachedBuilder.add("cvs", canvas.toDataURL());
209
+ }
210
+ } catch (e) {
211
+ }
212
+
213
+ // 7. Détection des artefacts du Chrome DevTools Protocol (CDP)
214
+ // Ces variables sont souvent injectées par les outils d'automatisation.
215
+ const cdpFootprints = [
216
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Array',
217
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
218
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
219
+ '$cdc_asdjflasutopfhvcZLmcfl_',
220
+ '_selenium',
221
+ '_driver'
222
+ ];
223
+ if (cdpFootprints.some(fp => window[fp])) {
224
+ this._cachedBuilder.add("cdp", "true");
225
+ }
226
+
227
+ // 7. Bot Detection (Indication cachée)
228
+ if (nav.webdriver) this._cachedBuilder.add("bot", "true");
229
+ }
230
+
231
+ return this._cachedBuilder.toString();
232
+ },
233
+
234
+ /**
235
+ * Génère une signature de requête incluant le contexte.
236
+ * @param {object} payload
237
+ */
238
+ /**
239
+ * Génère une signature de requête incluant le contexte.
240
+ * @param {object} payload
241
+ */
242
+ generateRequestSignature(payload = {}) {
243
+ const deviceFp = this.getDeviceFingerprint();
244
+ const sortedPayload = Object.keys(payload)
245
+ .sort()
246
+ .map((k) => `${k}=${payload[k]}`)
247
+ .join("&");
248
+ const payloadHash = this._hasher(sortedPayload);
249
+ return `${deviceFp}|req:${payloadHash}`;
250
+ },
251
+
252
+ /**
253
+ * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
254
+ * @param {object} payload - Les données à signer.
255
+ * @param {string} secret - La clé secrète partagée.
256
+ * @returns {Promise<string>} La signature hexadécimale.
257
+ */
258
+ async generateClientSideSignature(payload, secret) {
259
+ const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
260
+ const encoder = new TextEncoder();
261
+ const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
262
+ name: "HMAC",
263
+ hash: "SHA-256"
264
+ }, false, ["sign"]);
265
+ const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
266
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
267
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
268
+ },
269
+
270
+ /**
271
+ * @internal
272
+ * Resets the cached fingerprint builder. Used for testing purposes.
273
+ */
274
+ _resetCache() {
275
+ // Réinitialise le hasher à l'implémentation JS par défaut.
276
+ activeCyrb53 = jsCyrb53;
277
+ this._cachedBuilder = null;
278
+ },
279
+
280
+ /**
281
+ * Injecte des éléments interactifs fantômes invisibles pour piéger les bots (focus/hover).
282
+ */
283
+ injectPhantomTraps() {
284
+ if (typeof document === 'undefined') return;
285
+
286
+ // Création d'un élément interactif fantôme
287
+ const phantom = document.createElement('a');
288
+ phantom.href = '#';
289
+ // Nom trompeur aléatoire pour attirer les analyseurs automatiques de liens / formulaires
290
+ const phantomNames = ['sys-session-recovery', 'auth-token-refresh', 'debug-console-login', 'admin-portal-access', 'security-bypass-bypass', 'recovery-key-session', 'api-key-test', 'client-secrets-access'];
291
+ phantom.id = phantomNames[Math.floor(Math.random() * phantomNames.length)] + '-' + genRandStr(6);
292
+ phantom.className = genRandStr(8);
293
+ phantom.tabIndex = 0; // Dans le flux naturel de tabulation
294
+ phantom.setAttribute('aria-hidden', 'true'); // Masqué pour les screen readers légitimes
295
+
296
+ // Style invisible mais interactif (1px x 1px, presque transparent)
297
+ phantom.style.position = 'fixed';
298
+ phantom.style.top = '1px';
299
+ phantom.style.left = '1px';
300
+ phantom.style.width = '1px';
301
+ phantom.style.height = '1px';
302
+ phantom.style.opacity = '0.001';
303
+ phantom.style.zIndex = '99999';
304
+ phantom.style.overflow = 'hidden';
305
+ phantom.style.pointerEvents = 'auto';
306
+
307
+ const triggerTrap = () => {
308
+ this.onHoneypotTrigger();
309
+ };
310
+
311
+ phantom.addEventListener('focus', triggerTrap, { passive: true });
312
+ phantom.addEventListener('mouseover', triggerTrap, { passive: true });
313
+
314
+ // Imbrication polymorphique du piège fantôme dans le corps du document
315
+ const nestingOptions = [
316
+ () => document.body.appendChild(phantom),
317
+ () => {
318
+ const wrapper = document.createElement(Math.random() > 0.5 ? 'span' : 'div');
319
+ wrapper.className = genRandStr(8);
320
+ wrapper.style.position = 'absolute';
321
+ wrapper.style.width = '0';
322
+ wrapper.style.height = '0';
323
+ wrapper.style.overflow = 'hidden';
324
+ wrapper.appendChild(phantom);
325
+ document.body.appendChild(wrapper);
326
+ }
327
+ ];
328
+ nestingOptions[Math.floor(Math.random() * nestingOptions.length)]();
329
+ },
330
+
331
+ /**
332
+ * Démarre le suivi des événements tactiles sur mobile/tablette.
333
+ */
334
+ startTouchEventTracker() {
335
+ if (this._touchTrackerAttached) return;
336
+ this._touchTrackerAttached = true;
337
+
338
+ const handleTouch = (e) => {
339
+ if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
340
+ touchMovementsHistory.shift();
341
+ }
342
+ const touch = e.touches[0] || e.changedTouches[0];
343
+ if (!touch) return;
344
+
345
+ const radiusX = touch.radiusX || 0;
346
+ const radiusY = touch.radiusY || 0;
347
+ const radius = (radiusX + radiusY) / 2;
348
+ const force = touch.force || touch.webkitForce || 0;
349
+
350
+ touchMovementsHistory.push({
351
+ x: touch.clientX,
352
+ y: touch.clientY,
353
+ t: performance.now(),
354
+ p: force,
355
+ r: radius,
356
+ num: e.touches.length
357
+ });
358
+ };
359
+
360
+ document.addEventListener('touchstart', handleTouch, { passive: true });
361
+ document.addEventListener('touchmove', handleTouch, { passive: true });
362
+ document.addEventListener('touchend', handleTouch, { passive: true });
363
+ },
364
+
365
+ /**
366
+ * Démarre le suivi de la régularité d'affichage (V-Sync/rAF) pour détecter les framebuffers logiciels sans V-Sync.
367
+ */
368
+ startRenderingTracker() {
369
+ if (this._renderingTrackerAttached) return;
370
+ this._renderingTrackerAttached = true;
371
+
372
+ if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
373
+
374
+ const rAfTimestamps = [];
375
+ let lastTime = performance.now();
376
+ const maxSamples = 15;
377
+
378
+ const checkOffscreenAnom = () => {
379
+ try {
380
+ if ('OffscreenCanvas' in window && HTMLCanvasElement.prototype.transferControlToOffscreen) {
381
+ const nativeToString = Function.prototype.toString.call(HTMLCanvasElement.prototype.transferControlToOffscreen);
382
+ return !nativeToString.includes('[native code]');
383
+ }
384
+ } catch (e) {}
385
+ return false;
386
+ };
387
+
388
+ const loop = (time) => {
389
+ const delta = time - lastTime;
390
+ lastTime = time;
391
+ if (rAfTimestamps.length < maxSamples) {
392
+ if (rAfTimestamps.length > 0) { // Skip first delta
393
+ rAfTimestamps.push(delta);
394
+ }
395
+ window.requestAnimationFrame(loop);
396
+ } else {
397
+ const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
398
+ const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
399
+ const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
400
+
401
+ metrics.rendering = {
402
+ fps: Math.round((1000 / avg) * 100) / 100,
403
+ jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
404
+ offscreenAnom: checkOffscreenAnom()
405
+ };
406
+ }
407
+ };
408
+ window.requestAnimationFrame(loop);
409
+ },
410
+
411
+ /**
412
+ * Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
413
+ */
414
+ async initializeSpace(seed, sizeMb) {
415
+ return new Promise((resolve, reject) => {
416
+ const request = indexedDB.open('pospace-db', 1);
417
+ request.onupgradeneeded = (e) => {
418
+ const db = e.target.result;
419
+ if (!db.objectStoreNames.contains('blocks')) {
420
+ db.createObjectStore('blocks');
421
+ }
422
+ };
423
+ request.onsuccess = async (e) => {
424
+ const db = e.target.result;
425
+ const tx = db.transaction('blocks', 'readwrite');
426
+ const store = tx.objectStore('blocks');
427
+
428
+ const maxBlocks = sizeMb * 1024;
429
+ const countReq = store.count();
430
+ countReq.onsuccess = async () => {
431
+ if (countReq.result < maxBlocks) {
432
+ for (let i = 0; i < maxBlocks; i++) {
433
+ const block = new Uint8Array(1024);
434
+ let h = 5381;
435
+ for (let j = 0; j < seed.length; j++) {
436
+ h = (h << 5) + h + seed.charCodeAt(j);
437
+ }
438
+ h = (h << 5) + h + i;
439
+ for (let k = 0; k < 1024; k++) {
440
+ h = Math.imul(h ^ k, 1597334677);
441
+ block[k] = h & 0xff;
442
+ }
443
+ store.put(block, i);
444
+ }
445
+ }
446
+ resolve();
447
+ };
448
+ };
449
+ request.onerror = () => reject(new Error("Failed to open pospace database"));
450
+ });
451
+ },
452
+
453
+ /**
454
+ * Lit un bloc spécifique de l'IndexedDB locale sous format hexadécimal.
455
+ */
456
+ async readSpaceBlock(blockIdx) {
457
+ return new Promise((resolve, reject) => {
458
+ const request = indexedDB.open('pospace-db', 1);
459
+ request.onsuccess = (e) => {
460
+ const db = e.target.result;
461
+ const tx = db.transaction('blocks', 'readonly');
462
+ const store = tx.objectStore('blocks');
463
+ const getReq = store.get(blockIdx);
464
+ getReq.onsuccess = () => {
465
+ const block = getReq.result;
466
+ if (block) {
467
+ const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
468
+ resolve(hex);
469
+ } else {
470
+ reject(new Error("Block not found"));
471
+ }
472
+ };
473
+ getReq.onerror = () => reject(getReq.error);
474
+ };
475
+ request.onerror = () => reject(new Error("Failed to open pospace database"));
476
+ });
477
+ },
478
+
479
+ /**
480
+ * Résout le Proof-of-Space en combinant optionnellement le bloc du pair.
481
+ */
482
+ async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
483
+ const blocks = [];
484
+ for (const idx of queries) {
485
+ const blockHex = await this.readSpaceBlock(idx);
486
+ blocks.push(blockHex);
487
+ }
488
+ let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
489
+ const encoder = new TextEncoder();
490
+ const data = encoder.encode(finalPayload);
491
+ const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
492
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
493
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
494
+ },
495
+
496
+ /**
497
+ * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
498
+ * À appeler une fois sur la page.
499
+ */
500
+ startMouseEntropyTracker() {
501
+ // Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
502
+ if (this._mouseTrackerAttached) return;
503
+ this._mouseTrackerAttached = true;
504
+
505
+ document.addEventListener('mousemove', (e) => {
506
+ // NOUVEAU: Capturer une série de points {x, y, t}
507
+ if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
508
+ // Garder la taille de l'historique constante pour éviter une consommation mémoire excessive.
509
+ mouseMovementsHistory.shift();
510
+ }
511
+ mouseMovementsHistory.push({
512
+ x: e.clientX,
513
+ y: e.clientY,
514
+ t: performance.now()
515
+ });
516
+ }, {passive: true});
517
+ },
518
+
519
+ /**
520
+ * Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
521
+ * À appeler une fois sur la page.
522
+ */
523
+ startKeystrokeDynamicsTracker() {
524
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
525
+ if (this._keystrokeTrackerAttached) return;
526
+ this._keystrokeTrackerAttached = true;
527
+
528
+ const activeKeys = new Map();
529
+ let lastKeyDownTime = 0;
530
+ let lastKeyName = '';
531
+
532
+ document.addEventListener('keydown', (e) => {
533
+ const now = performance.now();
534
+ const key = e.key;
535
+ const code = e.code;
536
+ if (!key && !code) return;
537
+
538
+ const keyIdentifier = code || key;
539
+
540
+ // Prevent key repeat triggering multiple events
541
+ if (activeKeys.has(keyIdentifier)) return;
542
+ activeKeys.set(keyIdentifier, now);
543
+
544
+ if (keystrokeTimestamps.length > 0) {
545
+ const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
546
+ const latency = now - lastTimestamp;
547
+ if (latency > 10 && latency < 2000) {
548
+ if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
549
+ keystrokeLatencies.shift();
550
+ }
551
+ keystrokeLatencies.push(latency);
552
+ }
553
+ }
554
+ keystrokeTimestamps.push(now);
555
+
556
+ // Flight Time (KeyDown to KeyDown)
557
+ if (lastKeyDownTime > 0) {
558
+ const flightTime = now - lastKeyDownTime;
559
+ if (flightTime > 10 && flightTime < 2000) {
560
+ if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
561
+ keystrokeFlightTimes.shift();
562
+ }
563
+ const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
564
+ keystrokeFlightTimes.push({ digraph, time: flightTime });
565
+ }
566
+ }
567
+ lastKeyDownTime = now;
568
+ lastKeyName = keyIdentifier;
569
+ }, {passive: true});
570
+
571
+ document.addEventListener('keyup', (e) => {
572
+ const now = performance.now();
573
+ const key = e.key;
574
+ const code = e.code;
575
+ if (!key && !code) return;
576
+
577
+ const keyIdentifier = code || key;
578
+
579
+ if (activeKeys.has(keyIdentifier)) {
580
+ const pressTime = activeKeys.get(keyIdentifier);
581
+ const dwellTime = now - pressTime;
582
+ activeKeys.delete(keyIdentifier);
583
+
584
+ if (dwellTime > 5 && dwellTime < 1000) {
585
+ if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
586
+ keystrokeDwellTimes.shift();
587
+ }
588
+ keystrokeDwellTimes.push(dwellTime);
589
+ }
590
+ }
591
+ }, {passive: true});
592
+ },
593
+
594
+ /**
595
+ * Starts tracking click events to analyze position variance.
596
+ * @private
597
+ */
598
+ startClickTracker() {
599
+ if (this._clickTrackerAttached) return;
600
+ this._clickTrackerAttached = true;
601
+
602
+ document.addEventListener('click', (e) => {
603
+ if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
604
+ clicksHistory.shift();
605
+ }
606
+ // Generate a simple identifier for the target element
607
+ const target = e.target;
608
+ const targetId = target.id || target.name || target.tagName;
609
+
610
+ clicksHistory.push({
611
+ x: e.clientX,
612
+ y: e.clientY,
613
+ t: performance.now(),
614
+ targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
615
+ });
616
+ }, { passive: true });
617
+ },
618
+ /**
619
+ * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
620
+ * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
621
+ * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
622
+ */
623
+ initializeHoneypots(honeypotFieldNames) {
624
+ // 1. Nettoyer les anciens écouteurs
625
+ activeHoneypotListeners.forEach((listener, field) => {
626
+ field.removeEventListener('input', listener);
627
+ });
628
+ activeHoneypotListeners.clear();
629
+
630
+ // 2. Ajouter les nouveaux écouteurs sur le DOM classique
631
+ honeypotFieldNames.forEach(fieldName => {
632
+ const field = document.querySelector(`[name="${fieldName}"]`);
633
+ if (field) {
634
+ const listener = () => {
635
+ this.onHoneypotTrigger();
636
+ field.removeEventListener('input', listener);
637
+ };
638
+ field.addEventListener('input', listener);
639
+ activeHoneypotListeners.set(field, listener); // On stocke la référence
640
+ }
641
+ });
642
+
643
+ // 3. Générer des champs d'input pièges masqués dans un Shadow DOM fermé
644
+ if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
645
+ const host = document.createElement('div');
646
+ host.setAttribute('aria-hidden', 'true');
647
+ host.style.position = 'absolute';
648
+ host.style.width = '0';
649
+ host.style.height = '0';
650
+ host.style.overflow = 'hidden';
651
+
652
+ const shadow = host.attachShadow({ mode: 'closed' });
653
+
654
+ // Aléatisation des classes, variables CSS et balises de structure
655
+ const wrapperClass = genRandStr(10);
656
+ const posStateVar = `--${genRandStr(8)}`;
657
+ const offValVar = `--${genRandStr(8)}`;
658
+ const visStateVar = `--${genRandStr(8)}`;
659
+ const scaleValVar = `--${genRandStr(8)}`;
660
+
661
+ const style = document.createElement('style');
662
+ style.textContent = `
663
+ :host {
664
+ ${posStateVar}: absolute;
665
+ ${offValVar}: -9999px;
666
+ ${visStateVar}: hidden;
667
+ ${scaleValVar}: 0;
668
+ }
669
+ .${wrapperClass} {
670
+ position: var(${posStateVar});
671
+ left: var(${offValVar});
672
+ top: var(${offValVar});
673
+ visibility: var(${visStateVar});
674
+ transform: scale(var(${scaleValVar}));
675
+ }
676
+ `;
677
+ shadow.appendChild(style);
678
+
679
+ const wrapperTags = ['div', 'section', 'p', 'span', 'form', 'main'];
680
+ const selectedWrapperTag = wrapperTags[Math.floor(Math.random() * wrapperTags.length)];
681
+ const wrapper = document.createElement(selectedWrapperTag);
682
+ wrapper.className = wrapperClass;
683
+
684
+ honeypotFieldNames.forEach(fieldName => {
685
+ const label = document.createElement('label');
686
+ label.textContent = fieldName;
687
+ const input = document.createElement('input');
688
+ input.type = 'text';
689
+ input.name = fieldName;
690
+ input.tabIndex = -1;
691
+ input.className = genRandStr(6);
692
+ input.id = genRandStr(8);
693
+ input.autocomplete = 'off';
694
+
695
+ const trigger = () => {
696
+ this.onHoneypotTrigger();
697
+ };
698
+
699
+ input.addEventListener('input', trigger, { passive: true });
700
+ input.addEventListener('change', trigger, { passive: true });
701
+ input.addEventListener('focus', trigger, { passive: true });
702
+
703
+ // Imbrication polymorphique du label et de l'input
704
+ const nestingType = Math.floor(Math.random() * 3);
705
+ if (nestingType === 1) {
706
+ label.appendChild(input);
707
+ wrapper.appendChild(label);
708
+ } else if (nestingType === 2) {
709
+ const innerContainer = document.createElement(Math.random() > 0.5 ? 'span' : 'div');
710
+ innerContainer.className = genRandStr(5);
711
+ innerContainer.appendChild(label);
712
+ innerContainer.appendChild(input);
713
+ wrapper.appendChild(innerContainer);
714
+ } else {
715
+ wrapper.appendChild(label);
716
+ wrapper.appendChild(input);
717
+ }
718
+ });
719
+
720
+ shadow.appendChild(wrapper);
721
+ document.body.appendChild(host);
722
+ }
723
+ },
724
+
725
+ /**
726
+ * Récupère les métriques comportementales collectées.
727
+ * À appeler avant d'envoyer une requête sensible.
728
+ * @returns {ClientBehaviorMetrics}
729
+ */
730
+ getClientBehaviorMetrics() {
731
+ // Add history length as a behavioral signal.
732
+ metrics.historyLength = window.history.length;
733
+
734
+ // Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
735
+ metrics.clicksHistory = clicksHistory;
736
+ metrics.clientTimestamp = Date.now();
737
+
738
+ metrics.touchMovementsHistory = touchMovementsHistory;
739
+ // NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
740
+ metrics.mouseMovementsHistory = mouseMovementsHistory;
741
+
742
+ // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
743
+ metrics.keystrokeDwellTimes = keystrokeDwellTimes;
744
+ metrics.keystrokeFlightTimes = keystrokeFlightTimes;
745
+
746
+ // Calcule la latence moyenne des frappes
747
+ if (keystrokeLatencies.length > 0) {
748
+ const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
749
+ metrics.keystrokeLatency = sum / keystrokeLatencies.length;
750
+ } else {
751
+ metrics.keystrokeLatency = 0;
752
+ }
753
+ return metrics;
754
+ },
755
+
756
+ /**
757
+ * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
758
+ * @param {RequestInfo} resource
759
+ * @param {RequestInit} [options]
760
+ * @returns {Promise<Response>}
761
+ */
762
+ async protectedFetch(resource, options = {}) {
763
+ const fp = this.getDeviceFingerprint();
764
+ const behavior = this.getClientBehaviorMetrics();
765
+
766
+ const headers = new Headers(options.headers || {});
767
+ headers.set('X-Device-Fingerprint', fp);
768
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
769
+
770
+ options.headers = headers;
771
+ return fetch(resource, options);
772
+ },
773
+
774
+ // --- Système d'interception de Fetch robuste et anti-conflit ---
775
+
776
+ _isFetchPatched: false,
777
+ _interceptorChain: [],
778
+ // On stocke la fonction fetch originale et on la lie à son contexte (window)
779
+ // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
780
+ _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
781
+
782
+ /**
783
+ * Adds an interceptor function to the `fetch` chain.
784
+ * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
785
+ * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
786
+ * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
787
+ */
788
+ addFetchInterceptor(interceptor) {
789
+ if (!this._isFetchPatched) {
790
+ this.patchGlobalFetch();
791
+ }
792
+ this._interceptorChain.push(interceptor);
793
+ },
794
+
795
+ patchGlobalFetch() {
796
+ if (this._isFetchPatched || !this._originalFetch) return;
797
+
798
+ this._isFetchPatched = true;
799
+ window.fetch = (resource, options) => {
800
+ // Le "dispatcher" qui exécute la chaîne.
801
+ const dispatch = (index, res, opts) => {
802
+ if (index >= this._interceptorChain.length) {
803
+ // Fin de la chaîne, on appelle le fetch original.
804
+ return this._originalFetch(res, opts);
805
+ }
806
+ const nextInterceptor = this._interceptorChain[index];
807
+ // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
808
+ return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
809
+ };
810
+ return dispatch(0, resource, options || {});
811
+ };
812
+ },
813
+
814
+ /**
815
+ * La fonction qui est appelée lorsqu'un honeypot est déclenché.
816
+ * @private
817
+ */
818
+ onHoneypotTrigger() {
819
+ metrics.honeypotInteraction = true;
820
+ // Émettre un événement pour que l'application puisse réagir.
821
+ this._dispatchEvent('honeypotTriggered');
822
+ },
823
+
824
+ /**
825
+ * Initialise l'intercepteur de fingerprinting.
826
+ * Il s'ajoute à la chaîne d'interception sans écraser les autres.
827
+ * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
828
+ * Si non fourni, protège les requêtes de même origine.
829
+ */
830
+ initializeFetch(targetDomains = []) {
831
+ const fingerprintInterceptor = (resource, options, next) => {
832
+ const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
833
+ let shouldProtect = false;
834
+
835
+ try {
836
+ const url = new URL(requestUrl, window.location.origin);
837
+ // Protéger si la liste de domaines est vide ET que la requête est de même origine,
838
+ // OU si le domaine de la requête est dans la liste fournie.
839
+ shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
840
+ (targetDomains.length > 0 && targetDomains.includes(url.hostname));
841
+ } catch (e) {
842
+ // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
843
+ // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
844
+ // On protège par défaut si aucune liste de domaines n'est spécifiée.
845
+ shouldProtect = targetDomains.length === 0;
846
+ }
847
+
848
+ if (shouldProtect) {
849
+ const fp = this.getDeviceFingerprint();
850
+ const behavior = this.getClientBehaviorMetrics();
851
+ const headers = new Headers(options.headers || {});
852
+ headers.set('X-Device-Fingerprint', fp);
853
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
854
+ options.headers = headers;
855
+ }
856
+
857
+ // Passe la main à l'intercepteur suivant dans la chaîne.
858
+ return next(resource, options);
859
+ };
860
+
861
+ this.addFetchInterceptor(fingerprintInterceptor);
862
+ }, // <-- VIRGULE AJOUTÉE ICI
863
+
864
+ /**
865
+ * Injects visually hidden "honeypot" links into the DOM to trap bots.
866
+ * @param {string[]} urls - An array of trap URLs to inject.
867
+ * @private
868
+ */
869
+ injectTrapLinks(urls) {
870
+ if (!urls || urls.length === 0 || typeof document === 'undefined') {
871
+ return;
872
+ }
873
+
874
+ const host = document.createElement('div');
875
+ host.setAttribute('aria-hidden', 'true');
876
+ host.style.position = 'absolute';
877
+ host.style.width = '0';
878
+ host.style.height = '0';
879
+ host.style.overflow = 'hidden';
880
+
881
+ const shadow = host.attachShadow({ mode: 'closed' });
882
+
883
+ // Aléatisation des classes, variables CSS et balises de structure
884
+ const wrapperClass = genRandStr(10);
885
+ const layoutPosVar = `--${genRandStr(8)}`;
886
+ const offsetValVar = `--${genRandStr(8)}`;
887
+ const visibilityStateVar = `--${genRandStr(8)}`;
888
+ const scaleFactorVar = `--${genRandStr(8)}`;
889
+ const ptrEventsVar = `--${genRandStr(8)}`;
890
+ const linkColorClass = genRandStr(6);
891
+
892
+ const style = document.createElement('style');
893
+ style.textContent = `
894
+ :host {
895
+ ${layoutPosVar}: absolute;
896
+ ${offsetValVar}: -9999px;
897
+ ${visibilityStateVar}: hidden;
898
+ ${scaleFactorVar}: 0;
899
+ ${ptrEventsVar}: none;
900
+ }
901
+ .${wrapperClass} {
902
+ position: var(${layoutPosVar});
903
+ left: var(${offsetValVar});
904
+ top: var(${offsetValVar});
905
+ visibility: var(${visibilityStateVar});
906
+ transform: scale(var(${scaleFactorVar}));
907
+ pointer-events: var(${ptrEventsVar});
908
+ }
909
+ .${linkColorClass} {
910
+ color: transparent;
911
+ text-decoration: none;
912
+ }
913
+ `;
914
+ shadow.appendChild(style);
915
+
916
+ const wrapperTags = ['div', 'section', 'p', 'span', 'nav', 'aside'];
917
+ const selectedWrapperTag = wrapperTags[Math.floor(Math.random() * wrapperTags.length)];
918
+ const wrapper = document.createElement(selectedWrapperTag);
919
+ wrapper.className = wrapperClass;
920
+
921
+ urls.forEach((url, i) => {
922
+ const link = document.createElement('a');
923
+ link.href = url;
924
+ link.rel = 'nofollow';
925
+ link.tabIndex = -1;
926
+ link.className = linkColorClass;
927
+ link.id = genRandStr(8);
928
+
929
+ // Structure interne polymorphique du lien
930
+ const contentNestingType = Math.floor(Math.random() * 3);
931
+ if (contentNestingType === 1) {
932
+ const span = document.createElement('span');
933
+ span.className = genRandStr(5);
934
+ span.innerHTML = `&gt; ${i + 1}`;
935
+ link.appendChild(span);
936
+ } else if (contentNestingType === 2) {
937
+ const b = document.createElement('b');
938
+ b.className = genRandStr(5);
939
+ b.innerHTML = `&gt; ${i + 1}`;
940
+ link.appendChild(b);
941
+ } else {
942
+ link.innerHTML = `<span>&gt; ${i + 1}</span>`;
943
+ }
944
+
945
+ const trigger = () => {
946
+ this.onHoneypotTrigger();
947
+ };
948
+ link.addEventListener('click', trigger, { passive: true });
949
+ link.addEventListener('focus', trigger, { passive: true });
950
+ link.addEventListener('mouseover', trigger, { passive: true });
951
+
952
+ // Imbrication du lien de manière polymorphique
953
+ if (Math.random() > 0.5) {
954
+ const itemContainer = document.createElement('span');
955
+ itemContainer.className = genRandStr(5);
956
+ itemContainer.appendChild(link);
957
+ wrapper.appendChild(itemContainer);
958
+ } else {
959
+ wrapper.appendChild(link);
960
+ }
961
+ });
962
+
963
+ shadow.appendChild(wrapper);
964
+ document.body.appendChild(host);
965
+ },
966
+ /**
967
+ * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
968
+ * @param {Response} response - La réponse initiale (potentiellement 429).
969
+ * @param {RequestInfo} resource - La ressource de la requête originale.
970
+ * @param {RequestInit} options - Les options de la requête originale.
971
+ * @returns {Promise<Response>} - La réponse de la requête réessayée.
972
+ * @private
973
+ */
974
+ async solveChallengeAndRetry(response, resource, options) {
975
+ if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
976
+ return response;
977
+ }
978
+
979
+ try {
980
+ const challengeData = await response.json();
981
+ if (!challengeData.challenge || !challengeData.challenge.type) {
982
+ return response; // Pas un challenge JSON valide
983
+ }
984
+
985
+ console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
986
+ this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
987
+
988
+ // L'empreinte de l'appareil qui résout le challenge est cruciale.
989
+ const solverFp = this.getDeviceFingerprint();
990
+ const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
991
+ console.log('[Fingerprint] Challenge solved. Retrying original request.');
992
+
993
+ this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
994
+ // Ajouter la solution aux paramètres de la requête pour le nouvel essai
995
+ const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
996
+ // La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
997
+ solutionWrapper.applyToUrl(url);
998
+
999
+ // On ajoute l'empreinte du solveur à la requête de réessai.
1000
+ url.searchParams.set('pow_fp', solverFp);
1001
+
1002
+ // On utilise la chaîne d'intercepteurs pour la requête réessayée,
1003
+ // ce qui garantit que le fetch original est appelé avec le bon contexte.
1004
+ // Cela évite de réintroduire l'erreur "Illegal invocation".
1005
+ return window.fetch(url.toString(), options);
1006
+ } catch (e) {
1007
+ console.error('[Fingerprint] Failed to solve or retry challenge:', e);
1008
+ return response; // Retourne la réponse 429 originale en cas d'échec
1009
+ }
1010
+ }, // <-- VIRGULE AJOUTÉE ICI
1011
+
1012
+ /**
1013
+ * Initialise toutes les protections côté client en une seule fois.
1014
+ * Tente également de charger le module WASM si `wasmPath` est fourni.
1015
+ * C'est la méthode d'initialisation recommandée.
1016
+ * @param {ClientConfig} [config={}] - L'objet de configuration.
1017
+ */
1018
+ initializeClient(config = {}) {
1019
+ const {
1020
+ mouse = true,
1021
+ keystrokes = true,
1022
+ clicks = true, // Add new option
1023
+ touches = true, // Nouveau paramètre tactiles
1024
+ rendering = true,
1025
+ phantomTraps = true, // NOUVEAU
1026
+ honeypots = [],
1027
+ trapUrls = [], // Nouveau paramètre pour les URL pièges
1028
+ wasmPath, // Nouveau paramètre
1029
+ fetch: fetchConfig = {}
1030
+ } = config;
1031
+
1032
+ // Tentative de chargement du WASM si le chemin est fourni
1033
+ if (wasmPath) {
1034
+ this.initializeWasm(wasmPath);
1035
+ }
1036
+
1037
+ if (mouse) {
1038
+ this.startMouseEntropyTracker();
1039
+ }
1040
+ if (keystrokes) {
1041
+ this.startKeystrokeDynamicsTracker();
1042
+ }
1043
+ if (clicks) {
1044
+ this.startClickTracker();
1045
+ }
1046
+ if (touches) {
1047
+ this.startTouchEventTracker();
1048
+ }
1049
+ if (rendering) {
1050
+ this.startRenderingTracker();
1051
+ }
1052
+ if (phantomTraps) {
1053
+ this.injectPhantomTraps();
1054
+ }
1055
+ if (honeypots.length > 0) {
1056
+ this.initializeHoneypots(honeypots);
1057
+ }
1058
+
1059
+ // Injection dynamique des liens pièges au démarrage
1060
+ if (trapUrls.length > 0) {
1061
+ this.injectTrapLinks(trapUrls);
1062
+ }
1063
+ // On active l'interception si `fetch` est configuré, même avec un objet vide.
1064
+ if (config.fetch) {
1065
+ this.initializeFetch(fetchConfig.targetDomains);
1066
+
1067
+ // Ajoute l'intercepteur pour la résolution de challenge
1068
+ if (fetchConfig.handleChallenges !== false) {
1069
+ this.addFetchInterceptor(async (resource, options, next) => {
1070
+ const originalResponse = await next(resource, options);
1071
+ // On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
1072
+ return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
1073
+ });
1074
+ }
1075
+ }
1076
+ },
1077
+
1078
+ /**
1079
+ * Tente de charger et d'initialiser le module WebAssembly pour un hachage plus rapide.
1080
+ * Si le chargement échoue, il se rabat silencieusement sur l'implémentation JS.
1081
+ * @param {string} wasmPath - Le chemin vers le script de chargement du module WASM (ex: '/fp.js').
1082
+ */
1083
+ async initializeWasm(wasmPath) {
1084
+ try {
1085
+ // Direct standalone polymorphic WebAssembly loading
1086
+ if (wasmPath.endsWith('.wasm')) {
1087
+ const response = await fetch(wasmPath);
1088
+ const arrayBuffer = await response.arrayBuffer();
1089
+ const module = await WebAssembly.compile(arrayBuffer);
1090
+ const instance = await WebAssembly.instantiate(module, {});
1091
+ const exports = instance.exports;
1092
+ const memory = exports.memory;
1093
+ activeCyrb53 = (str) => {
1094
+ const encoder = new TextEncoder();
1095
+ const bytes = encoder.encode(str);
1096
+ const view = new Uint8Array(memory.buffer, 0, bytes.length);
1097
+ view.set(bytes);
1098
+ return exports.hash(0, bytes.length);
1099
+ };
1100
+ console.log('[Fingerprint] Standalone polymorphic WASM loaded successfully. Using fast dynamic hashing.');
1101
+ if (this._cachedBuilder) {
1102
+ this._cachedBuilder.addRaw('wasm', 'true');
1103
+ }
1104
+ return;
1105
+ }
1106
+ // 1. Injecter le script qui charge le module WASM
1107
+ const script = document.createElement('script');
1108
+ script.src = wasmPath;
1109
+ await new Promise((resolve, reject) => {
1110
+ script.onload = resolve;
1111
+ script.onerror = reject;
1112
+ document.head.appendChild(script);
1113
+ });
1114
+
1115
+ // 2. Attendre que la fonction globale `createFingerprintModule` soit disponible
1116
+ if (typeof window.createFingerprintModule !== 'function') {
1117
+ throw new Error('WASM loader script did not expose createFingerprintModule.');
1118
+ }
1119
+
1120
+ // 3. Initialiser le module
1121
+ const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
1122
+ const wasmModule = await window.createFingerprintModule({
1123
+ instantiateWasm: (imports, successCallback) => {
1124
+ (async () => {
1125
+ try {
1126
+ const cached = await getCachedWasm(wasmUrl);
1127
+ if (cached) {
1128
+ let instance;
1129
+ if (cached instanceof WebAssembly.Module) {
1130
+ instance = await WebAssembly.instantiate(cached, imports);
1131
+ } else {
1132
+ const result = await WebAssembly.instantiate(cached, imports);
1133
+ instance = result.instance;
1134
+ }
1135
+ successCallback(instance, cached);
1136
+ return;
1137
+ }
1138
+
1139
+ const response = await fetch(wasmUrl);
1140
+ const arrayBuffer = await response.arrayBuffer();
1141
+
1142
+ let cachedData = arrayBuffer;
1143
+ let isModuleCached = false;
1144
+ try {
1145
+ const compiledModule = await WebAssembly.compile(arrayBuffer);
1146
+ const success = await cacheWasm(wasmUrl, compiledModule);
1147
+ if (success) {
1148
+ cachedData = compiledModule;
1149
+ isModuleCached = true;
1150
+ }
1151
+ } catch (e) {
1152
+ // Fallback if browser doesn't allow structured cloning of Compiled Modules
1153
+ }
1154
+
1155
+ if (!isModuleCached) {
1156
+ await cacheWasm(wasmUrl, arrayBuffer);
1157
+ }
1158
+
1159
+ let instance;
1160
+ if (cachedData instanceof WebAssembly.Module) {
1161
+ instance = await WebAssembly.instantiate(cachedData, imports);
1162
+ } else {
1163
+ const result = await WebAssembly.instantiate(arrayBuffer, imports);
1164
+ instance = result.instance;
1165
+ }
1166
+ successCallback(instance, cachedData);
1167
+ } catch (err) {
1168
+ console.warn('[Fingerprint] Custom WASM instantiation failed, falling back to default Emscripten loader:', err);
1169
+ successCallback(null);
1170
+ }
1171
+ })();
1172
+ return {}; // Async instantiation indicator for Emscripten
1173
+ }
1174
+ });
1175
+ if (typeof wasmModule._hash_string !== 'function') {
1176
+ throw new Error('WASM module did not export _hash_string.');
1177
+ }
1178
+ window.wasmModule = wasmModule;
1179
+ ClientLibrary.wasmModule = wasmModule;
1180
+
1181
+ // 4. Remplacer la fonction de hachage par la version WASM
1182
+ activeCyrb53 = (str) => {
1183
+ // La fonction C++ attend un pointeur, Emscripten gère la conversion
1184
+ return wasmModule._hash_string(str);
1185
+ };
1186
+
1187
+ console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
1188
+ // NOUVEAU: Ajoute un indicateur à l'empreinte pour que le serveur sache que le WASM est actif.
1189
+ if (this._cachedBuilder) {
1190
+ this._cachedBuilder.addRaw('wasm', 'true');
1191
+ }
1192
+ } catch (error) {
1193
+ console.warn('[Fingerprint] WASM module failed to load. Falling back to JS implementation. Error:', error);
1194
+ }
1195
+ }
1196
+ };
1197
+
1198
+ /**
1199
+ * @typedef {object} ClientBehaviorMetrics
1200
+ * @property {number} mouseEntropy - Entropie des mouvements de la souris.
1201
+ * @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Historique des points de la souris.
1202
+ * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
1203
+ * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
1204
+ * @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Historique des clics.
1205
+ * @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Historique des glissements tactiles.
1206
+ * @property {number} historyLength - La longueur de l'historique de session du navigateur (`window.history.length`).
1207
+ * @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
1208
+ * @property {string[]} [trapUrls] - URLs pièges à injecter dynamiquement.
1209
+ */
1210
+ /** @type {ClientBehaviorMetrics} */
1211
+ const metrics = {
1212
+ mouseEntropy: 0, // Conservé pour la compatibilité, mais l'analyse se fait maintenant sur l'historique
1213
+ mouseMovementsHistory: [],
1214
+ touchMovementsHistory: [],
1215
+ clicksHistory: [],
1216
+ keystrokeLatency: 0,
1217
+ honeypotInteraction: false,
1218
+ historyLength: 0,
1219
+ clientTimestamp: 0,
1220
+ rendering: { fps: 0, jitter: 0, offscreenAnom: false },
1221
+ };
1222
+
1223
+ let lastMousePos = { x: 0, y: 0 };
1224
+ let mouseMovementsHistory = []; // NOUVEAU: Historique des points de la souris
1225
+ let touchMovementsHistory = []; // NOUVEAU: Historique des gestes tactiles
1226
+ const TOUCH_HISTORY_MAX = 100;
1227
+ const MOUSE_HISTORY_MAX = 100; // Limite le nombre de points stockés
1228
+ let clicksHistory = [];
1229
+ const CLICKS_HISTORY_MAX = 50;
1230
+ let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
1231
+ let keystrokeTimestamps = [];
1232
+ let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
1233
+ let keystrokeDwellTimes = [];
1234
+ let keystrokeFlightTimes = [];
1235
+ const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
1236
+
1237
+
1238
+
1239
+ // Exporter les fonctions individuellement pour la compatibilité ascendante
1240
+ export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
1241
+ export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
1242
+ export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
1243
+ export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
1244
+ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
1245
+ export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
1246
+ export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
1247
+ export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
1248
+ export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
1249
+ export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
1250
+ export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
1251
+ export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
1252
+ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
1253
+ export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
1254
+ export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
1255
+ export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
1256
+ export const initializeWasm = ClientLibrary.initializeWasm.bind(ClientLibrary);
1257
+ export const injectTrapLinks = ClientLibrary.injectTrapLinks.bind(ClientLibrary);
1258
+ export const injectPhantomTraps = ClientLibrary.injectPhantomTraps.bind(ClientLibrary);
1259
+ export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
1260
+ export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrary);
1261
+ export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
1262
+ export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
1263
+ export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
1264
+
1265
+ // Export the internal object for testing purposes
1266
+ export default ClientLibrary;
1267
+
1268
+ // --- Global Export for Browser ---
1269
+ // Attach the library to the window object to make it accessible from inline scripts.
1270
+ if (typeof window !== 'undefined') {
1271
+ window.ClientLibrary = ClientLibrary;
1192
1272
  }