@anonympins/fingerprint 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/src/js/build-client.js +8 -10
  7. package/src/js/fingerprint.builder.js +37 -38
  8. package/src/js/fingerprint.client.js +1531 -1463
  9. package/src/js/fingerprint.client.obfuscated.js +1 -0
  10. package/src/js/fingerprint.js +190 -16
  11. package/src/js/fingerprint.utils.js +213 -213
  12. package/src/js/library.js +340 -376
  13. package/src/js/mongodb-store.js +1 -1
  14. package/src/js/optimization.worker.js +27 -27
  15. package/src/js/pow.solver.inline.js +154 -56
  16. package/src/js/pow.solver.js +180 -92
  17. package/src/js/pow.worker.js +48 -48
  18. package/src/js/redis-store.js +1 -1
  19. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  20. package/src/js/tests/fingerprint.test.js +124 -10
  21. package/src/js/tests/ip-reputation.test.js +1 -0
  22. package/src/js/upow-model-task.js +14 -15
  23. package/src/php/AutoTuner.php +34 -34
  24. package/src/php/Challenge/ChallengeUtils.php +57 -11
  25. package/src/php/Config/SecurityProfiles.php +20 -0
  26. package/src/php/DirectFingerprint.php +190 -145
  27. package/src/php/FingerprintBuilder.php +29 -30
  28. package/src/php/FingerprintClient.php +147 -148
  29. package/src/php/FingerprintEngine.php +44 -17
  30. package/src/php/Ja3AnomalyDetector.php +33 -32
  31. package/src/php/Optimization/FunctionRegistry.php +7 -7
  32. package/src/php/Optimization/Optimization.php +24 -24
  33. package/src/php/Optimization/OptimizationOperators.php +26 -25
  34. package/src/php/Optimization/ProblemInitializers.php +52 -52
  35. package/src/php/ProblemManager.php +31 -34
  36. package/src/php/RequestContext.php +8 -1
  37. package/src/php/Store/IStore.php +7 -8
  38. package/src/php/Store/InMemoryStore.php +66 -66
  39. package/src/php/Store/MongoDbStore.php +5 -5
  40. package/src/php/Store/RedisStore.php +5 -5
  41. package/src/php/Store/StoreManager.php +35 -35
  42. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  43. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  44. package/src/php/Tests/FingerprintEngineTest.php +38 -0
  45. package/src/php/Tests/IpReputationTest.php +10 -0
  46. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  47. package/src/php/Tests/MetricsTest.php +97 -97
  48. package/src/php/Tests/ProblemManagerTest.php +376 -376
  49. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  50. package/src/php/Tests/RequestUtilsTest.php +471 -471
  51. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  52. package/src/php/Tests/config/ed25519_key.json +3 -3
  53. package/src/php/Utils/BlockList.php +99 -99
  54. package/src/php/Utils/Env.php +49 -49
  55. package/src/php/Utils/MaliciousPatterns.php +74 -74
  56. package/src/php/Utils/MetricsManager.php +209 -209
  57. package/src/php/Utils/RequestUtils.php +129 -7
  58. package/src/php/Utils/TLSClientHelloParser.php +369 -369
  59. package/src/php/WordPress/WpDbStore.php +155 -0
  60. package/src/php/WordPress/fingerprint-wordpress.php +554 -0
  61. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  62. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  63. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  64. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  65. package/src/php/WordPress/package.php +218 -0
  66. package/src/php/bin/auto-tune.php +118 -118
@@ -1,1464 +1,1532 @@
1
- import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
2
- import {solveChallenge} from './pow.solver.js';
3
-
4
- function secureRandom() {
5
- if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
6
- const array = new Uint32Array(1);
7
- globalThis.crypto.getRandomValues(array);
8
- return array[0] / 0xffffffff;
9
- }
10
- return Math.random();
11
- }
12
-
13
- // Variable pour stocker la fonction de hachage active.
14
- // Par défaut, c'est l'implémentation JavaScript.
15
- let activeCyrb53 = jsCyrb53;
16
- let derivedKey = null;
17
-
18
- async function negotiateSessionKey() {
19
- if (typeof window === 'undefined' || !window.crypto || !window.crypto.subtle) return;
20
- try {
21
- const keyPair = await window.crypto.subtle.generateKey(
22
- { name: "ECDH", namedCurve: "P-256" },
23
- false,
24
- ["deriveKey"]
25
- );
26
- const clientPubKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
27
- const clientPubKeyHex = Array.from(new Uint8Array(clientPubKeyBuffer))
28
- .map(b => b.toString(16).padStart(2, '0'))
29
- .join('');
30
-
31
- const response = await fetch(window.location.pathname + '?fp_handshake=1', {
32
- method: 'POST',
33
- headers: {
34
- 'Content-Type': 'application/json',
35
- 'x-client-ephemeral-key': clientPubKeyHex
36
- },
37
- body: JSON.stringify({ fp: ClientLibrary.getDeviceFingerprint() })
38
- });
39
-
40
- const serverPubKeyHex = response.headers.get('x-server-ephemeral-key');
41
- if (serverPubKeyHex) {
42
- const serverPubKeyBuffer = new Uint8Array(
43
- serverPubKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
44
- );
45
- const serverPubKey = await window.crypto.subtle.importKey(
46
- "raw",
47
- serverPubKeyBuffer,
48
- { name: "ECDH", namedCurve: "P-256" },
49
- true,
50
- []
51
- );
52
- derivedKey = await window.crypto.subtle.deriveKey(
53
- { name: "ECDH", public: serverPubKey },
54
- keyPair.privateKey,
55
- { name: "HMAC", hash: "SHA-256", length: 256 },
56
- true,
57
- ["sign"]
58
- );
59
- console.log('[Fingerprint] Cryptographic session negotiated successfully.');
60
- }
61
- } catch (e) {
62
- console.warn('[Fingerprint] Cryptographic session negotiation failed:', e);
63
- }
64
- }
65
-
66
- async function signMetrics(metricsObj) {
67
- if (!derivedKey) return metricsObj;
68
- try {
69
- const copy = JSON.parse(JSON.stringify(metricsObj));
70
- delete copy.signature;
71
- const dataToSign = new TextEncoder().encode(JSON.stringify(copy));
72
- const signatureBuffer = await window.crypto.subtle.sign(
73
- "HMAC",
74
- derivedKey,
75
- dataToSign
76
- );
77
- copy.signature = Array.from(new Uint8Array(signatureBuffer))
78
- .map(b => b.toString(16).padStart(2, '0'))
79
- .join('');
80
- return copy;
81
- } catch (e) {
82
- return metricsObj;
83
- }
84
- }
85
-
86
- const DB_NAME = 'wasm-cache-db';
87
- const DB_VERSION = 1;
88
- const STORE_NAME = 'wasm-modules';
89
-
90
- const genRandStr = (len = 8) => {
91
- return Array.from({ length: len }, () => String.fromCharCode(97 + Math.floor(secureRandom() * 26))).join('');
92
- };
93
-
94
- function getCachedWasm(url) {
95
- return new Promise((resolve) => {
96
- if (typeof indexedDB === 'undefined') return resolve(null);
97
- const request = indexedDB.open(DB_NAME, DB_VERSION);
98
- request.onupgradeneeded = (e) => {
99
- const db = e.target.result;
100
- if (!db.objectStoreNames.contains(STORE_NAME)) {
101
- db.createObjectStore(STORE_NAME);
102
- }
103
- };
104
- request.onsuccess = (e) => {
105
- const db = e.target.result;
106
- try {
107
- const transaction = db.transaction(STORE_NAME, 'readonly');
108
- const store = transaction.objectStore(STORE_NAME);
109
- const getReq = store.get(url);
110
- getReq.onsuccess = () => resolve(getReq.result);
111
- getReq.onerror = () => resolve(null);
112
- } catch (err) {
113
- resolve(null);
114
- }
115
- };
116
- request.onerror = () => resolve(null);
117
- });
118
- }
119
-
120
- function cacheWasm(url, data) {
121
- return new Promise((resolve) => {
122
- if (typeof indexedDB === 'undefined') return resolve(false);
123
- const request = indexedDB.open(DB_NAME, DB_VERSION);
124
- request.onupgradeneeded = (e) => {
125
- const db = e.target.result;
126
- if (!db.objectStoreNames.contains(STORE_NAME)) {
127
- db.createObjectStore(STORE_NAME);
128
- }
129
- };
130
- request.onsuccess = (e) => {
131
- const db = e.target.result;
132
- try {
133
- const transaction = db.transaction(STORE_NAME, 'readwrite');
134
- const store = transaction.objectStore(STORE_NAME);
135
- store.put(data, url);
136
- transaction.oncomplete = () => resolve(true);
137
- transaction.onerror = () => resolve(false);
138
- } catch (err) {
139
- resolve(false);
140
- }
141
- };
142
- request.onerror = () => resolve(false);
143
- });
144
- }
145
-
146
- const ClientLibrary = {
147
- // Cache pour éviter de recalculer les constantes (Hardware, etc.)
148
- _cachedBuilder: null,
149
- /**
150
- * @private
151
- * Dispatches a custom event from the window object.
152
- * @param {string} eventName - The name of the event.
153
- * @param {object} [detail={}] - The data to include in the event's detail property.
154
- */
155
- _dispatchEvent(eventName, detail = {}) {
156
- if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return;
157
- const event = new CustomEvent(`fingerprint:${eventName}`, { detail });
158
- window.dispatchEvent(event);
159
- },
160
-
161
- /**
162
- * Wrapper interne pour la fonction de hachage.
163
- * @private
164
- */
165
- _hasher: (str, seed) => activeCyrb53(str, seed),
166
-
167
- /**
168
- * Génère une preuve de connaissance à divulgation nulle (ZKP) de Schnorr pour l'empreinte de l'appareil.
169
- * Rend le tout stable et compatible avec les vérifications JS, PHP et Python.
170
- * @param {string} fingerprint - L'empreinte de l'appareil.
171
- * @returns {Promise<string>} La preuve sous format "y:t:s" en hexadécimal.
172
- */
173
- async generateZkpProof(fingerprint) {
174
- const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
175
- const ZKP_G = 2n;
176
- const cryptoObj = window.crypto || window.msCrypto;
177
-
178
- const sha256Hex = async (str) => {
179
- const encoder = new TextEncoder();
180
- const data = encoder.encode(str);
181
- const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
182
- const hashArray = Array.from(new Uint8Array(hashBuffer));
183
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
184
- };
185
-
186
- const modPow = (base, exponent, modulus) => {
187
- if (modulus === 1n) return 0n;
188
- let result = 1n;
189
- base = base % modulus;
190
- while (exponent > 0n) {
191
- if (exponent % 2n === 1n) {
192
- result = (result * base) % modulus;
193
- }
194
- exponent = exponent >> 1n;
195
- base = (base * base) % modulus;
196
- }
197
- return result;
198
- };
199
-
200
- const xHex = await sha256Hex(fingerprint);
201
- const x = BigInt('0x' + xHex) % ZKP_P;
202
- const y = modPow(ZKP_G, x, ZKP_P);
203
- const randomBytes = new Uint8Array(32);
204
- cryptoObj.getRandomValues(randomBytes);
205
- let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
206
- let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
207
- if (v === 0n) v = 1n;
208
- const t = modPow(ZKP_G, v, ZKP_P);
209
- const cStr = ZKP_G.toString() + y.toString() + t.toString();
210
- const cHex = await sha256Hex(cStr);
211
- const c = BigInt('0x' + cHex) % ZKP_P;
212
- const s = (v + c * x) % (ZKP_P - 1n);
213
-
214
- return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
215
- },
216
-
217
- /**
218
- * Génère l'empreinte de l'appareil actuel.
219
- */
220
- getDeviceFingerprint() {
221
- if (typeof window === "undefined") {
222
- console.error("getDeviceFingerprint can only be called on the client-side.");
223
- return "";
224
- }
225
-
226
- if (!this._cachedBuilder) {
227
- const nav = window.navigator;
228
- const screen = window.screen;
229
-
230
- this._cachedBuilder = new FingerprintBuilder();
231
-
232
- // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
233
- this._cachedBuilder.add(
234
- "hw", // Utilise maintenant le hasher actif
235
- `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
236
- );
237
-
238
- // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
239
- this._cachedBuilder.add(
240
- "geo",
241
- `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
242
- );
243
-
244
- // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
245
- this._cachedBuilder.add(
246
- "scr",
247
- `${screen.width}x${screen.height}_${screen.colorDepth}`,
248
- );
249
-
250
- // 4. Platform (Stable) : OS, Engine
251
- this._cachedBuilder.add("os", nav.platform);
252
-
253
- // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
254
- try {
255
- const canvas = document.createElement("canvas");
256
- const gl =
257
- canvas.getContext("webgl2") ||
258
- canvas.getContext("webgl") ||
259
- canvas.getContext("experimental-webgl");
260
- if (gl) {
261
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
262
- if (debugInfo) {
263
- const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
264
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
265
- this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
266
- }
267
- }
268
- } catch (e) {
269
- }
270
-
271
- // 6. Canvas Fingerprinting (Rendering quirks)
272
- try {
273
- const canvas = document.createElement("canvas");
274
- const ctx = canvas.getContext("2d");
275
- if (ctx) {
276
- canvas.width = 200;
277
- canvas.height = 50;
278
- ctx.textBaseline = "alphabetic";
279
- ctx.font = "14px 'Arial'";
280
- ctx.fillStyle = "#f60";
281
- ctx.fillRect(125, 1, 62, 20);
282
- ctx.fillStyle = "#069";
283
- ctx.fillText("fingerprint", 2, 15);
284
- ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
285
- ctx.fillText("fingerprint", 4, 17);
286
- this._cachedBuilder.add("cvs", canvas.toDataURL());
287
- }
288
- } catch (e) {
289
- }
290
-
291
- // 7. Détection des artefacts du Chrome DevTools Protocol (CDP)
292
- // Ces variables sont souvent injectées par les outils d'automatisation.
293
- const cdpFootprints = [
294
- 'cdc_adoQpoasnfa76pfcZLmcfl_Array',
295
- 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
296
- 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
297
- '$cdc_asdjflasutopfhvcZLmcfl_',
298
- '_selenium',
299
- '_driver'
300
- ];
301
- if (cdpFootprints.some(fp => window[fp])) {
302
- this._cachedBuilder.add("cdp", "true");
303
- }
304
-
305
- // 7. Bot Detection (Indication cachée)
306
- if (nav.webdriver) this._cachedBuilder.add("bot", "true");
307
- }
308
-
309
- return this._cachedBuilder.toString();
310
- },
311
-
312
- /**
313
- * Génère une signature de requête incluant le contexte.
314
- * @param {object} payload
315
- */
316
- /**
317
- * Génère une signature de requête incluant le contexte.
318
- * @param {object} payload
319
- */
320
- generateRequestSignature(payload = {}) {
321
- const deviceFp = this.getDeviceFingerprint();
322
- const sortedPayload = Object.keys(payload)
323
- .sort()
324
- .map((k) => `${k}=${payload[k]}`)
325
- .join("&");
326
- const payloadHash = this._hasher(sortedPayload);
327
- return `${deviceFp}|req:${payloadHash}`;
328
- },
329
-
330
- /**
331
- * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
332
- * @param {object} payload - Les données à signer.
333
- * @param {string} secret - La clé secrète partagée.
334
- * @returns {Promise<string>} La signature hexadécimale.
335
- */
336
- async generateClientSideSignature(payload, secret) {
337
- const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
338
- const encoder = new TextEncoder();
339
- const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
340
- name: "HMAC",
341
- hash: "SHA-256"
342
- }, false, ["sign"]);
343
- const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
344
- const hashArray = Array.from(new Uint8Array(signatureBuffer));
345
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
346
- },
347
-
348
- /**
349
- * @internal
350
- * Resets the cached fingerprint builder. Used for testing purposes.
351
- */
352
- _resetCache() {
353
- // Réinitialise le hasher à l'implémentation JS par défaut.
354
- activeCyrb53 = jsCyrb53;
355
- this._cachedBuilder = null;
356
- },
357
-
358
- /**
359
- * Injecte des éléments interactifs fantômes invisibles pour piéger les bots (focus/hover).
360
- */
361
- injectPhantomTraps() {
362
- if (typeof document === 'undefined') return;
363
-
364
- // Création d'un élément interactif fantôme
365
- const phantom = document.createElement('a');
366
- phantom.href = '#';
367
- // Nom trompeur aléatoire pour attirer les analyseurs automatiques de liens / formulaires
368
- 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'];
369
- phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
370
- phantom.className = genRandStr(8);
371
- phantom.tabIndex = 0; // Dans le flux naturel de tabulation
372
- phantom.setAttribute('aria-hidden', 'true'); // Masqué pour les screen readers légitimes
373
-
374
- // Style invisible mais interactif (1px x 1px, presque transparent)
375
- phantom.style.position = 'fixed';
376
- phantom.style.top = '1px';
377
- phantom.style.left = '1px';
378
- phantom.style.width = '1px';
379
- phantom.style.height = '1px';
380
- phantom.style.opacity = '0.001';
381
- phantom.style.zIndex = '99999';
382
- phantom.style.overflow = 'hidden';
383
- phantom.style.pointerEvents = 'auto';
384
-
385
- const triggerTrap = () => {
386
- this.onHoneypotTrigger();
387
- };
388
-
389
- phantom.addEventListener('focus', triggerTrap, { passive: true });
390
- phantom.addEventListener('mouseover', triggerTrap, { passive: true });
391
-
392
- // Imbrication polymorphique du piège fantôme dans le corps du document
393
- const nestingOptions = [
394
- () => document.body.appendChild(phantom),
395
- () => {
396
- const wrapper = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
397
- wrapper.className = genRandStr(8);
398
- wrapper.style.position = 'absolute';
399
- wrapper.style.width = '0';
400
- wrapper.style.height = '0';
401
- wrapper.style.overflow = 'hidden';
402
- wrapper.appendChild(phantom);
403
- document.body.appendChild(wrapper);
404
- }
405
- ];
406
- nestingOptions[Math.floor(secureRandom() * nestingOptions.length)]();
407
- },
408
-
409
- /**
410
- * Démarre le suivi des événements tactiles sur mobile/tablette.
411
- */
412
- startTouchEventTracker() {
413
- if (this._touchTrackerAttached) return;
414
- this._touchTrackerAttached = true;
415
-
416
- const handleTouch = (e) => {
417
- if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
418
- touchMovementsHistory.shift();
419
- }
420
- const touch = e.touches[0] || e.changedTouches[0];
421
- if (!touch) return;
422
-
423
- const radiusX = touch.radiusX || 0;
424
- const radiusY = touch.radiusY || 0;
425
- const radius = (radiusX + radiusY) / 2;
426
- const force = touch.force || touch.webkitForce || 0;
427
-
428
- touchMovementsHistory.push({
429
- x: touch.clientX,
430
- y: touch.clientY,
431
- t: performance.now(),
432
- p: force,
433
- r: radius,
434
- num: e.touches.length
435
- });
436
- };
437
-
438
- document.addEventListener('touchstart', handleTouch, { passive: true });
439
- document.addEventListener('touchmove', handleTouch, { passive: true });
440
- document.addEventListener('touchend', handleTouch, { passive: true });
441
- },
442
-
443
- /**
444
- * Démarre le suivi de la régularité d'affichage (V-Sync/rAF) pour détecter les framebuffers logiciels sans V-Sync.
445
- */
446
- startRenderingTracker() {
447
- if (this._renderingTrackerAttached) return;
448
- this._renderingTrackerAttached = true;
449
-
450
- if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
451
-
452
- const rAfTimestamps = [];
453
- let lastTime = performance.now();
454
- const maxSamples = 15;
455
-
456
- const checkOffscreenAnom = () => {
457
- try {
458
- if ('OffscreenCanvas' in window && HTMLCanvasElement.prototype.transferControlToOffscreen) {
459
- const nativeToString = Function.prototype.toString.call(HTMLCanvasElement.prototype.transferControlToOffscreen);
460
- return !nativeToString.includes('[native code]');
461
- }
462
- } catch (e) {}
463
- return false;
464
- };
465
-
466
- const loop = (time) => {
467
- const delta = time - lastTime;
468
- lastTime = time;
469
- if (rAfTimestamps.length < maxSamples) {
470
- if (rAfTimestamps.length > 0) { // Skip first delta
471
- rAfTimestamps.push(delta);
472
- }
473
- window.requestAnimationFrame(loop);
474
- } else {
475
- const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
476
- const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
477
- const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
478
-
479
- metrics.rendering = {
480
- fps: Math.round((1000 / avg) * 100) / 100,
481
- jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
482
- offscreenAnom: checkOffscreenAnom()
483
- };
484
- }
485
- };
486
- window.requestAnimationFrame(loop);
487
- },
488
-
489
- /**
490
- * Initialise l'espace Proof-of-Space persistant dans l'IndexedDB locale.
491
- */
492
- async initializeSpace(seed, sizeMb) {
493
- return new Promise((resolve, reject) => {
494
- const request = indexedDB.open('pospace-db', 1);
495
- request.onupgradeneeded = (e) => {
496
- const db = e.target.result;
497
- if (!db.objectStoreNames.contains('blocks')) {
498
- db.createObjectStore('blocks');
499
- }
500
- };
501
- request.onsuccess = async (e) => {
502
- const db = e.target.result;
503
- const tx = db.transaction('blocks', 'readwrite');
504
- const store = tx.objectStore('blocks');
505
-
506
- const maxBlocks = sizeMb * 1024;
507
- const countReq = store.count();
508
- countReq.onsuccess = async () => {
509
- if (countReq.result < maxBlocks) {
510
- for (let i = 0; i < maxBlocks; i++) {
511
- const block = new Uint8Array(1024);
512
- let h = 5381;
513
- for (let j = 0; j < seed.length; j++) {
514
- h = (h << 5) + h + seed.charCodeAt(j);
515
- }
516
- h = (h << 5) + h + i;
517
- for (let k = 0; k < 1024; k++) {
518
- h = Math.imul(h ^ k, 1597334677);
519
- block[k] = h & 0xff;
520
- }
521
- store.put(block, i);
522
- }
523
- }
524
- resolve();
525
- };
526
- };
527
- request.onerror = () => reject(new Error("Failed to open pospace database"));
528
- });
529
- },
530
-
531
- /**
532
- * Lit un bloc spécifique de l'IndexedDB locale sous format hexadécimal.
533
- */
534
- async readSpaceBlock(blockIdx) {
535
- return new Promise((resolve, reject) => {
536
- const request = indexedDB.open('pospace-db', 1);
537
- request.onsuccess = (e) => {
538
- const db = e.target.result;
539
- const tx = db.transaction('blocks', 'readonly');
540
- const store = tx.objectStore('blocks');
541
- const getReq = store.get(blockIdx);
542
- getReq.onsuccess = () => {
543
- const block = getReq.result;
544
- if (block) {
545
- const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
546
- resolve(hex);
547
- } else {
548
- reject(new Error("Block not found"));
549
- }
550
- };
551
- getReq.onerror = () => reject(getReq.error);
552
- };
553
- request.onerror = () => reject(new Error("Failed to open pospace database"));
554
- });
555
- },
556
-
557
- /**
558
- * Résout le Proof-of-Space en combinant optionnellement le bloc du pair.
559
- */
560
- async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
561
- const blocks = [];
562
- for (const idx of queries) {
563
- const blockHex = await this.readSpaceBlock(idx);
564
- blocks.push(blockHex);
565
- }
566
- let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
567
- const encoder = new TextEncoder();
568
- const data = encoder.encode(finalPayload);
569
- const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
570
- const hashArray = Array.from(new Uint8Array(hashBuffer));
571
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
572
- },
573
-
574
- /**
575
- * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
576
- * À appeler une fois sur la page.
577
- */
578
- startMouseEntropyTracker() {
579
- // Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
580
- if (this._mouseTrackerAttached) return;
581
- this._mouseTrackerAttached = true;
582
-
583
- document.addEventListener('mousemove', (e) => {
584
- // NOUVEAU: Capturer une série de points {x, y, t}
585
- if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
586
- // Garder la taille de l'historique constante pour éviter une consommation mémoire excessive.
587
- mouseMovementsHistory.shift();
588
- }
589
- mouseMovementsHistory.push({
590
- x: e.clientX,
591
- y: e.clientY,
592
- t: performance.now()
593
- });
594
- }, {passive: true});
595
- },
596
-
597
- /**
598
- * Démarre le suivi de la dynamique de frappe pour calculer le dwell time et le flight time (digraphie/trigraphie).
599
- * À appeler une fois sur la page.
600
- */
601
- startKeystrokeDynamicsTracker() {
602
- // S'assurer de ne pas attacher l'écouteur plusieurs fois
603
- if (this._keystrokeTrackerAttached) return;
604
- this._keystrokeTrackerAttached = true;
605
-
606
- const activeKeys = new Map();
607
- let lastKeyDownTime = 0;
608
- let lastKeyName = '';
609
-
610
- document.addEventListener('keydown', (e) => {
611
- const now = performance.now();
612
- const key = e.key;
613
- const code = e.code;
614
- if (!key && !code) return;
615
-
616
- const keyIdentifier = code || key;
617
-
618
- // Prevent key repeat triggering multiple events
619
- if (activeKeys.has(keyIdentifier)) return;
620
- activeKeys.set(keyIdentifier, now);
621
-
622
- if (keystrokeTimestamps.length > 0) {
623
- const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
624
- const latency = now - lastTimestamp;
625
- if (latency > 10 && latency < 2000) {
626
- if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
627
- keystrokeLatencies.shift();
628
- }
629
- keystrokeLatencies.push(latency);
630
- }
631
- }
632
- keystrokeTimestamps.push(now);
633
-
634
- // Flight Time (KeyDown to KeyDown)
635
- if (lastKeyDownTime > 0) {
636
- const flightTime = now - lastKeyDownTime;
637
- if (flightTime > 10 && flightTime < 2000) {
638
- if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
639
- keystrokeFlightTimes.shift();
640
- }
641
- const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
642
- keystrokeFlightTimes.push({ digraph, time: flightTime });
643
- }
644
- }
645
- lastKeyDownTime = now;
646
- lastKeyName = keyIdentifier;
647
- }, {passive: true});
648
-
649
- document.addEventListener('keyup', (e) => {
650
- const now = performance.now();
651
- const key = e.key;
652
- const code = e.code;
653
- if (!key && !code) return;
654
-
655
- const keyIdentifier = code || key;
656
-
657
- if (activeKeys.has(keyIdentifier)) {
658
- const pressTime = activeKeys.get(keyIdentifier);
659
- const dwellTime = now - pressTime;
660
- activeKeys.delete(keyIdentifier);
661
-
662
- if (dwellTime > 5 && dwellTime < 1000) {
663
- if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
664
- keystrokeDwellTimes.shift();
665
- }
666
- keystrokeDwellTimes.push(dwellTime);
667
- }
668
- }
669
- }, {passive: true});
670
- },
671
-
672
- /**
673
- * Starts tracking click events to analyze position variance.
674
- * @private
675
- */
676
- startClickTracker() {
677
- if (this._clickTrackerAttached) return;
678
- this._clickTrackerAttached = true;
679
-
680
- document.addEventListener('click', (e) => {
681
- if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
682
- clicksHistory.shift();
683
- }
684
- // Generate a simple identifier for the target element
685
- const target = e.target;
686
- const targetId = target.id || target.name || target.tagName;
687
-
688
- clicksHistory.push({
689
- x: e.clientX,
690
- y: e.clientY,
691
- t: performance.now(),
692
- targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
693
- });
694
- }, { passive: true });
695
- },
696
- /**
697
- * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
698
- * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
699
- * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
700
- */
701
- initializeHoneypots(honeypotFieldNames) {
702
- // 1. Nettoyer les anciens écouteurs
703
- activeHoneypotListeners.forEach((listener, field) => {
704
- field.removeEventListener('input', listener);
705
- });
706
- activeHoneypotListeners.clear();
707
-
708
- // 2. Ajouter les nouveaux écouteurs sur le DOM classique
709
- honeypotFieldNames.forEach(fieldName => {
710
- const field = document.querySelector(`[name="${fieldName}"]`);
711
- if (field) {
712
- const listener = () => {
713
- this.onHoneypotTrigger();
714
- field.removeEventListener('input', listener);
715
- };
716
- field.addEventListener('input', listener);
717
- activeHoneypotListeners.set(field, listener); // On stocke la référence
718
- }
719
- });
720
-
721
- // 3. Générer des champs d'input pièges masqués dans un Shadow DOM fermé
722
- if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
723
- const host = document.createElement('div');
724
- host.setAttribute('aria-hidden', 'true');
725
- host.style.position = 'absolute';
726
- host.style.width = '0';
727
- host.style.height = '0';
728
- host.style.overflow = 'hidden';
729
-
730
- const shadow = host.attachShadow({ mode: 'closed' });
731
-
732
- // Aléatisation des classes, variables CSS et balises de structure
733
- const wrapperClass = genRandStr(10);
734
- const posStateVar = `--${genRandStr(8)}`;
735
- const offValVar = `--${genRandStr(8)}`;
736
- const visStateVar = `--${genRandStr(8)}`;
737
- const scaleValVar = `--${genRandStr(8)}`;
738
-
739
- const style = document.createElement('style');
740
- style.textContent = `
741
- :host {
742
- ${posStateVar}: absolute;
743
- ${offValVar}: -9999px;
744
- ${visStateVar}: hidden;
745
- ${scaleValVar}: 0;
746
- }
747
- .${wrapperClass} {
748
- position: var(${posStateVar});
749
- left: var(${offValVar});
750
- top: var(${offValVar});
751
- visibility: var(${visStateVar});
752
- transform: scale(var(${scaleValVar}));
753
- }
754
- `;
755
- shadow.appendChild(style);
756
-
757
- const wrapperTags = ['div', 'section', 'p', 'span', 'form', 'main'];
758
- const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
759
- const wrapper = document.createElement(selectedWrapperTag);
760
- wrapper.className = wrapperClass;
761
-
762
- honeypotFieldNames.forEach(fieldName => {
763
- const label = document.createElement('label');
764
- label.textContent = fieldName;
765
- const input = document.createElement('input');
766
- input.type = 'text';
767
- input.name = fieldName;
768
- input.tabIndex = -1;
769
- input.className = genRandStr(6);
770
- input.id = genRandStr(8);
771
- input.autocomplete = 'off';
772
-
773
- const trigger = () => {
774
- this.onHoneypotTrigger();
775
- };
776
-
777
- input.addEventListener('input', trigger, { passive: true });
778
- input.addEventListener('change', trigger, { passive: true });
779
- input.addEventListener('focus', trigger, { passive: true });
780
-
781
- // Imbrication polymorphique du label et de l'input
782
- const nestingType = Math.floor(secureRandom() * 3);
783
- if (nestingType === 1) {
784
- label.appendChild(input);
785
- wrapper.appendChild(label);
786
- } else if (nestingType === 2) {
787
- const innerContainer = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
788
- innerContainer.className = genRandStr(5);
789
- innerContainer.appendChild(label);
790
- innerContainer.appendChild(input);
791
- wrapper.appendChild(innerContainer);
792
- } else {
793
- wrapper.appendChild(label);
794
- wrapper.appendChild(input);
795
- }
796
- });
797
-
798
- shadow.appendChild(wrapper);
799
- document.body.appendChild(host);
800
- }
801
- },
802
- /**
803
- * Sollicite silencieusement l'attestation matérielle FIDO2 (WebAuthn)
804
- * pour ancrer l'identité physique de l'appareil via TPM / Secure Enclave.
805
- * @returns {Promise<object|null>}
806
- */
807
- async getWebAuthnAnchor() {
808
- if (typeof window === 'undefined' || !window.PublicKeyCredential) return null;
809
- try {
810
- const isPlatformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
811
- if (!isPlatformAvailable) return null;
812
-
813
- const storedCredIdBase64 = localStorage.getItem('fp_webauthn_cred_id');
814
- const challenge = new Uint8Array([109, 101, 116, 114, 105, 99, 115, 95, 97, 110, 99, 104, 111, 114, 95, 115, 101, 99, 117, 114, 101]); // Static stable challenge
815
-
816
- if (storedCredIdBase64) {
817
- // Tentative d'assertion silencieuse (Authentification)
818
- const credentialId = Uint8Array.from(atob(storedCredIdBase64), c => c.charCodeAt(0));
819
- const assertion = await navigator.credentials.get({
820
- publicKey: {
821
- challenge,
822
- allowCredentials: [{
823
- id: credentialId,
824
- type: 'public-key'
825
- }],
826
- userVerification: 'discouraged',
827
- timeout: 1000
828
- }
829
- });
830
- if (assertion) {
831
- return {
832
- type: 'assertion',
833
- credentialId: storedCredIdBase64,
834
- signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
835
- authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
836
- clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON)))
837
- };
838
- }
839
- } else {
840
- // Création silencieuse (Enregistrement initial)
841
- const options = {
842
- publicKey: {
843
- challenge,
844
- rp: { name: window.location.hostname, id: window.location.hostname },
845
- user: {
846
- id: new Uint8Array([102, 112, 105, 100]),
847
- name: 'silent-device-anchor',
848
- displayName: 'Hardware Device Anchor'
849
- },
850
- pubKeyCredParams: [
851
- { type: 'public-key', alg: -7 }, // ES256 (P-256) - Secure Enclave / TPM / Android Keystore
852
- { type: 'public-key', alg: -257 } // RS256 - Windows Hello TPM
853
- ],
854
- authenticatorSelection: {
855
- authenticatorAttachment: 'platform',
856
- userVerification: 'discouraged',
857
- residentKey: 'preferred'
858
- },
859
- timeout: 1500,
860
- attestation: 'indirect'
861
- }
862
- };
863
- const credential = await navigator.credentials.create(options);
864
- if (credential) {
865
- const credIdBase64 = btoa(String.fromCharCode(...new Uint8Array(credential.rawId)));
866
- localStorage.setItem('fp_webauthn_cred_id', credIdBase64);
867
- const publicKeyDer = credential.response.getPublicKey ? credential.response.getPublicKey() : null;
868
- return {
869
- type: 'registration',
870
- credentialId: credIdBase64,
871
- publicKey: publicKeyDer ? btoa(String.fromCharCode(...new Uint8Array(publicKeyDer))) : null,
872
- attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.getAttestationObject()))),
873
- clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
874
- };
875
- }
876
- }
877
- } catch (e) {
878
- // Ignore et bypass silencieusement pour garder l'invisibilité en cas de non-support
879
- console.log('[WebAuthn-Anchor] Silent attestation bypassed:', e.message);
880
- }
881
- return null;
882
- },
883
-
884
- /**
885
- * Récupère les métriques comportementales collectées.
886
- * À appeler avant d'envoyer une requête sensible.
887
- * @returns {ClientBehaviorMetrics}
888
- */
889
- getClientBehaviorMetrics() {
890
- // Add history length as a behavioral signal.
891
- metrics.historyLength = window.history.length;
892
-
893
- // Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
894
- metrics.clicksHistory = clicksHistory;
895
- metrics.clientTimestamp = Date.now();
896
-
897
- metrics.touchMovementsHistory = touchMovementsHistory;
898
- // NOUVEAU: Inclure l'historique des mouvements de la souris pour une analyse côté serveur.
899
- metrics.mouseMovementsHistory = mouseMovementsHistory;
900
-
901
- // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
902
- metrics.keystrokeDwellTimes = keystrokeDwellTimes;
903
- metrics.keystrokeFlightTimes = keystrokeFlightTimes;
904
-
905
- // Calcule la latence moyenne des frappes
906
- if (keystrokeLatencies.length > 0) {
907
- const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
908
- metrics.keystrokeLatency = sum / keystrokeLatencies.length;
909
- } else {
910
- metrics.keystrokeLatency = 0;
911
- }
912
- return metrics;
913
- },
914
-
915
- /**
916
- * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
917
- * @param {RequestInfo} resource
918
- * @param {RequestInit} [options]
919
- * @returns {Promise<Response>}
920
- */
921
- async protectedFetch(resource, options = {}) {
922
- const fp = this.getDeviceFingerprint();
923
- const rawBehavior = this.getClientBehaviorMetrics();
924
- const zkpProof = await this.generateZkpProof(fp); // Generate ZKP proof
925
- const behavior = await signMetrics(rawBehavior);
926
-
927
- const headers = new Headers(options.headers || {});
928
- headers.set('X-ZKP-Proof', zkpProof); // Add ZKP proof to headers
929
- headers.set('X-Device-Fingerprint', fp);
930
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
931
-
932
- options.headers = headers;
933
- return fetch(resource, options);
934
- },
935
-
936
- // --- Système d'interception de Fetch robuste et anti-conflit ---
937
-
938
- _isFetchPatched: false,
939
- _interceptorChain: [],
940
- // On stocke la fonction fetch originale et on la lie à son contexte (window)
941
- // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
942
- _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
943
-
944
- /**
945
- * Adds an interceptor function to the `fetch` chain.
946
- * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
947
- * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
948
- * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
949
- */
950
- addFetchInterceptor(interceptor) {
951
- if (!this._isFetchPatched) {
952
- this.patchGlobalFetch();
953
- }
954
- this._interceptorChain.push(interceptor);
955
- },
956
-
957
- patchGlobalFetch() {
958
- if (this._isFetchPatched || !this._originalFetch) return;
959
-
960
- this._isFetchPatched = true;
961
- window.fetch = (resource, options) => {
962
- // Le "dispatcher" qui exécute la chaîne.
963
- const dispatch = (index, res, opts) => {
964
- if (index >= this._interceptorChain.length) {
965
- // Fin de la chaîne, on appelle le fetch original.
966
- return this._originalFetch(res, opts);
967
- }
968
- const nextInterceptor = this._interceptorChain[index];
969
- // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
970
- return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
971
- };
972
- return dispatch(0, resource, options || {});
973
- };
974
- },
975
-
976
- /**
977
- * La fonction qui est appelée lorsqu'un honeypot est déclenché.
978
- * @private
979
- */
980
- onHoneypotTrigger() {
981
- metrics.honeypotInteraction = true;
982
- // Émettre un événement pour que l'application puisse réagir.
983
- this._dispatchEvent('honeypotTriggered');
984
- },
985
-
986
- /**
987
- * Initialise l'intercepteur de fingerprinting.
988
- * Il s'ajoute à la chaîne d'interception sans écraser les autres.
989
- * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
990
- * Si non fourni, protège les requêtes de même origine.
991
- */
992
- initializeFetch(targetDomains = []) {
993
- const fingerprintInterceptor = async (resource, options, next) => {
994
- const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
995
- let shouldProtect = false;
996
-
997
- try {
998
- const url = new URL(requestUrl, window.location.origin);
999
- // Protéger si la liste de domaines est vide ET que la requête est de même origine,
1000
- // OU si le domaine de la requête est dans la liste fournie.
1001
- shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
1002
- (targetDomains.length > 0 && targetDomains.includes(url.hostname));
1003
- } catch (e) {
1004
- // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
1005
- // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
1006
- // On protège par défaut si aucune liste de domaines n'est spécifiée.
1007
- shouldProtect = targetDomains.length === 0;
1008
- }
1009
-
1010
- if (shouldProtect) {
1011
- const fp = this.getDeviceFingerprint();
1012
- const rawBehavior = this.getClientBehaviorMetrics();
1013
- const behavior = await signMetrics(rawBehavior);
1014
- const headers = new Headers(options.headers || {});
1015
- headers.set('X-Device-Fingerprint', fp);
1016
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
1017
- options.headers = headers;
1018
- }
1019
-
1020
- // Passe la main à l'intercepteur suivant dans la chaîne.
1021
- return next(resource, options);
1022
- };
1023
-
1024
- this.addFetchInterceptor(fingerprintInterceptor);
1025
- }, // <-- VIRGULE AJOUTÉE ICI
1026
-
1027
- /**
1028
- * Injects visually hidden "honeypot" links into the DOM to trap bots.
1029
- * @param {string[]} urls - An array of trap URLs to inject.
1030
- * @private
1031
- */
1032
- injectTrapLinks(urls) {
1033
- if (!urls || urls.length === 0 || typeof document === 'undefined') {
1034
- return;
1035
- }
1036
-
1037
- const host = document.createElement('div');
1038
- host.setAttribute('aria-hidden', 'true');
1039
- host.style.position = 'absolute';
1040
- host.style.width = '0';
1041
- host.style.height = '0';
1042
- host.style.overflow = 'hidden';
1043
-
1044
- const shadow = host.attachShadow({ mode: 'closed' });
1045
-
1046
- // Aléatisation des classes, variables CSS et balises de structure
1047
- const wrapperClass = genRandStr(10);
1048
- const layoutPosVar = `--${genRandStr(8)}`;
1049
- const offsetValVar = `--${genRandStr(8)}`;
1050
- const visibilityStateVar = `--${genRandStr(8)}`;
1051
- const scaleFactorVar = `--${genRandStr(8)}`;
1052
- const ptrEventsVar = `--${genRandStr(8)}`;
1053
- const linkColorClass = genRandStr(6);
1054
-
1055
- const style = document.createElement('style');
1056
- style.textContent = `
1057
- :host {
1058
- ${layoutPosVar}: absolute;
1059
- ${offsetValVar}: -9999px;
1060
- ${visibilityStateVar}: hidden;
1061
- ${scaleFactorVar}: 0;
1062
- ${ptrEventsVar}: none;
1063
- }
1064
- .${wrapperClass} {
1065
- position: var(${layoutPosVar});
1066
- left: var(${offsetValVar});
1067
- top: var(${offsetValVar});
1068
- visibility: var(${visibilityStateVar});
1069
- transform: scale(var(${scaleFactorVar}));
1070
- pointer-events: var(${ptrEventsVar});
1071
- }
1072
- .${linkColorClass} {
1073
- color: transparent;
1074
- text-decoration: none;
1075
- }
1076
- `;
1077
- shadow.appendChild(style);
1078
-
1079
- const wrapperTags = ['div', 'section', 'p', 'span', 'nav', 'aside'];
1080
- const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
1081
- const wrapper = document.createElement(selectedWrapperTag);
1082
- wrapper.className = wrapperClass;
1083
-
1084
- urls.forEach((url, i) => {
1085
- const link = document.createElement('a');
1086
- link.href = url;
1087
- link.rel = 'nofollow';
1088
- link.tabIndex = -1;
1089
- link.className = linkColorClass;
1090
- link.id = genRandStr(8);
1091
-
1092
- // Structure interne polymorphique du lien
1093
- const contentNestingType = Math.floor(secureRandom() * 3);
1094
- if (contentNestingType === 1) {
1095
- const span = document.createElement('span');
1096
- span.className = genRandStr(5);
1097
- span.innerHTML = `&gt; ${i + 1}`;
1098
- link.appendChild(span);
1099
- } else if (contentNestingType === 2) {
1100
- const b = document.createElement('b');
1101
- b.className = genRandStr(5);
1102
- b.innerHTML = `&gt; ${i + 1}`;
1103
- link.appendChild(b);
1104
- } else {
1105
- link.innerHTML = `<span>&gt; ${i + 1}</span>`;
1106
- }
1107
-
1108
- const trigger = () => {
1109
- this.onHoneypotTrigger();
1110
- };
1111
- link.addEventListener('click', trigger, { passive: true });
1112
- link.addEventListener('focus', trigger, { passive: true });
1113
- link.addEventListener('mouseover', trigger, { passive: true });
1114
-
1115
- // Imbrication du lien de manière polymorphique
1116
- if (secureRandom() > 0.5) {
1117
- const itemContainer = document.createElement('span');
1118
- itemContainer.className = genRandStr(5);
1119
- itemContainer.appendChild(link);
1120
- wrapper.appendChild(itemContainer);
1121
- } else {
1122
- wrapper.appendChild(link);
1123
- }
1124
- });
1125
-
1126
- shadow.appendChild(wrapper);
1127
- document.body.appendChild(host);
1128
- },
1129
- /**
1130
- * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
1131
- * @param {Response} response - La réponse initiale (potentiellement 429).
1132
- * @param {RequestInfo} resource - La ressource de la requête originale.
1133
- * @param {RequestInit} options - Les options de la requête originale.
1134
- * @returns {Promise<Response>} - La réponse de la requête réessayée.
1135
- * @private
1136
- */
1137
- async solveChallengeAndRetry(response, resource, options) {
1138
- if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
1139
- return response;
1140
- }
1141
-
1142
- try {
1143
- const challengeData = await response.json();
1144
- if (!challengeData.challenge || !challengeData.challenge.type) {
1145
- return response; // Pas un challenge JSON valide
1146
- }
1147
-
1148
- console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
1149
- this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
1150
-
1151
- // L'empreinte de l'appareil qui résout le challenge est cruciale.
1152
- const solverFp = this.getDeviceFingerprint();
1153
- const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
1154
- console.log('[Fingerprint] Challenge solved. Retrying original request.');
1155
-
1156
- this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
1157
- // Ajouter la solution aux paramètres de la requête pour le nouvel essai
1158
- const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
1159
- // La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
1160
- solutionWrapper.applyToUrl(url);
1161
-
1162
- // On ajoute l'empreinte du solveur à la requête de réessai.
1163
- url.searchParams.set('pow_fp', solverFp);
1164
-
1165
- // On utilise la chaîne d'intercepteurs pour la requête réessayée,
1166
- // ce qui garantit que le fetch original est appelé avec le bon contexte.
1167
- // Cela évite de réintroduire l'erreur "Illegal invocation".
1168
- return window.fetch(url.toString(), options);
1169
- } catch (e) {
1170
- console.error('[Fingerprint] Failed to solve or retry challenge:', e);
1171
- return response; // Retourne la réponse 429 originale en cas d'échec
1172
- }
1173
- }, // <-- VIRGULE AJOUTÉE ICI
1174
-
1175
- /**
1176
- * Initialise toutes les protections côté client en une seule fois.
1177
- * Tente également de charger le module WASM si `wasmPath` est fourni.
1178
- * C'est la méthode d'initialisation recommandée.
1179
- * @param {ClientConfig} [config={}] - L'objet de configuration.
1180
- */
1181
- initializeClient(config = {}) {
1182
- const {
1183
- mouse = true,
1184
- keystrokes = true,
1185
- clicks = true, // Add new option
1186
- touches = true, // Nouveau paramètre tactiles
1187
- rendering = true,
1188
- phantomTraps = true, // NOUVEAU
1189
- honeypots = [],
1190
- trapUrls = [], // Nouveau paramètre pour les URL pièges
1191
- wasmPath, // Nouveau paramètre
1192
- workerPath, // NOUVEAU
1193
- fetch: fetchConfig = {}
1194
- } = config;
1195
-
1196
- negotiateSessionKey();
1197
- // Tentative de chargement du WASM si le chemin est fourni
1198
- if (wasmPath) {
1199
- this.initializeWasm(wasmPath);
1200
- }
1201
-
1202
- if (mouse) {
1203
- this.startMouseEntropyTracker();
1204
- }
1205
- if (workerPath) {
1206
- this.workerPath = workerPath;
1207
- }
1208
- if (keystrokes) {
1209
- this.startKeystrokeDynamicsTracker();
1210
- }
1211
- if (clicks) {
1212
- this.startClickTracker();
1213
- }
1214
- if (touches) {
1215
- this.startTouchEventTracker();
1216
- }
1217
- if (rendering) {
1218
- this.startRenderingTracker();
1219
- }
1220
- if (phantomTraps) {
1221
- this.injectPhantomTraps();
1222
- }
1223
- if (honeypots.length > 0) {
1224
- this.initializeHoneypots(honeypots);
1225
- }
1226
-
1227
- // Injection dynamique des liens pièges au démarrage
1228
- if (trapUrls.length > 0) {
1229
- this.injectTrapLinks(trapUrls);
1230
- }
1231
- // On active l'interception si `fetch` est configuré, même avec un objet vide.
1232
- if (config.fetch) {
1233
- this.initializeFetch(fetchConfig.targetDomains);
1234
-
1235
- // Ajoute l'intercepteur pour la résolution de challenge
1236
- if (fetchConfig.handleChallenges !== false) {
1237
- this.addFetchInterceptor(async (resource, options, next) => {
1238
- const originalResponse = await next(resource, options);
1239
- // On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
1240
- return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
1241
- });
1242
- }
1243
- }
1244
- },
1245
-
1246
- /**
1247
- * Tente de charger et d'initialiser le module WebAssembly pour un hachage plus rapide.
1248
- * Si le chargement échoue, il se rabat silencieusement sur l'implémentation JS.
1249
- * @param {string} wasmPath - Le chemin vers le script de chargement du module WASM (ex: '/fp.js').
1250
- */
1251
- async initializeWasm(wasmPath) {
1252
- try {
1253
- // Direct standalone polymorphic WebAssembly loading
1254
- if (wasmPath.endsWith('.wasm')) {
1255
- let instance;
1256
- if (typeof WebAssembly.instantiateStreaming === 'function') {
1257
- try {
1258
- const response = await fetch(wasmPath);
1259
- const result = await WebAssembly.instantiateStreaming(response, {});
1260
- instance = result.instance;
1261
- } catch (streamingError) {
1262
- console.warn('[Fingerprint] WebAssembly.instantiateStreaming failed, falling back to compile:', streamingError);
1263
- }
1264
- }
1265
- if (!instance) {
1266
- const response = await fetch(wasmPath);
1267
- const arrayBuffer = await response.arrayBuffer();
1268
- const module = await WebAssembly.compile(arrayBuffer);
1269
- instance = await WebAssembly.instantiate(module, {});
1270
- }
1271
- const exports = instance.exports;
1272
- const memory = exports.memory;
1273
- activeCyrb53 = (str) => {
1274
- const encoder = new TextEncoder();
1275
- const bytes = encoder.encode(str);
1276
- const view = new Uint8Array(memory.buffer, 0, bytes.length);
1277
- view.set(bytes);
1278
- return exports.hash(0, bytes.length);
1279
- };
1280
- console.log('[Fingerprint] Standalone polymorphic WASM loaded successfully. Using fast dynamic hashing.');
1281
- if (this._cachedBuilder) {
1282
- this._cachedBuilder.addRaw('wasm', 'true');
1283
- }
1284
- return;
1285
- }
1286
- // 1. Injecter le script qui charge le module WASM
1287
- const script = document.createElement('script');
1288
- script.src = wasmPath;
1289
- await new Promise((resolve, reject) => {
1290
- script.onload = resolve;
1291
- script.onerror = reject;
1292
- document.head.appendChild(script);
1293
- });
1294
-
1295
- // 2. Attendre que la fonction globale `createFingerprintModule` soit disponible
1296
- if (typeof window.createFingerprintModule !== 'function') {
1297
- throw new Error('WASM loader script did not expose createFingerprintModule.');
1298
- }
1299
-
1300
- // 3. Initialiser le module
1301
- const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
1302
- const wasmModule = await window.createFingerprintModule({
1303
- instantiateWasm: (imports, successCallback) => {
1304
- (async () => {
1305
- try {
1306
- const cached = await getCachedWasm(wasmUrl);
1307
- if (cached) {
1308
- let instance;
1309
- if (cached instanceof WebAssembly.Module) {
1310
- instance = await WebAssembly.instantiate(cached, imports);
1311
- } else {
1312
- const result = await WebAssembly.instantiate(cached, imports);
1313
- instance = result.instance;
1314
- }
1315
- successCallback(instance, cached);
1316
- return;
1317
- }
1318
-
1319
- const response = await fetch(wasmUrl);
1320
- const arrayBuffer = await response.arrayBuffer();
1321
-
1322
- let cachedData = arrayBuffer;
1323
- let isModuleCached = false;
1324
- try {
1325
- const compiledModule = await WebAssembly.compile(arrayBuffer);
1326
- const success = await cacheWasm(wasmUrl, compiledModule);
1327
- if (success) {
1328
- cachedData = compiledModule;
1329
- isModuleCached = true;
1330
- }
1331
- } catch (e) {
1332
- // Fallback if browser doesn't allow structured cloning of Compiled Modules
1333
- }
1334
-
1335
- if (!isModuleCached) {
1336
- await cacheWasm(wasmUrl, arrayBuffer);
1337
- }
1338
-
1339
- let instance;
1340
- if (cachedData instanceof WebAssembly.Module) {
1341
- instance = await WebAssembly.instantiate(cachedData, imports);
1342
- } else {
1343
- const result = await WebAssembly.instantiate(arrayBuffer, imports);
1344
- instance = result.instance;
1345
- }
1346
- successCallback(instance, cachedData);
1347
- } catch (err) {
1348
- console.warn('[Fingerprint] Custom WASM instantiation failed, falling back to default Emscripten loader:', err);
1349
- successCallback(null);
1350
- }
1351
- })();
1352
- return {}; // Async instantiation indicator for Emscripten
1353
- }
1354
- });
1355
- if (typeof wasmModule._hash_string !== 'function') {
1356
- throw new Error('WASM module did not export _hash_string.');
1357
- }
1358
- window.wasmModule = wasmModule;
1359
- ClientLibrary.wasmModule = wasmModule;
1360
-
1361
- // Pooling de mémoire statique : pré-allocation d'un buffer réutilisable de 4 Ko
1362
- // Évite d'allouer/libérer de la mémoire sur le tas Emscripten lors des frappes clavier/mouvements souris rapides
1363
- const staticBufferSize = 4096;
1364
- const staticBufferPtr = wasmModule._malloc(staticBufferSize);
1365
- const encoder = new TextEncoder();
1366
-
1367
- // 4. Remplacer la fonction de hachage par la version WASM optimisée via le buffer statique
1368
- activeCyrb53 = (str) => {
1369
- const bytes = encoder.encode(str);
1370
- if (bytes.length < staticBufferSize) {
1371
- wasmModule.HEAPU8.set(bytes, staticBufferPtr);
1372
- wasmModule.HEAPU8[staticBufferPtr + bytes.length] = 0; // null-terminator
1373
- return wasmModule._hash_string(staticBufferPtr);
1374
- }
1375
- // Fallback dynamique sécurisé si la chaîne dépasse la taille du buffer statique
1376
- return wasmModule._hash_string(str);
1377
- };
1378
-
1379
- console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
1380
- // NOUVEAU: Ajoute un indicateur à l'empreinte pour que le serveur sache que le WASM est actif.
1381
- if (this._cachedBuilder) {
1382
- this._cachedBuilder.addRaw('wasm', 'true');
1383
- }
1384
- } catch (error) {
1385
- console.warn('[Fingerprint] WASM module failed to load. Falling back to JS implementation. Error:', error);
1386
- }
1387
- }
1388
- };
1389
-
1390
- /**
1391
- * @typedef {object} ClientBehaviorMetrics
1392
- * @property {number} mouseEntropy - Entropie des mouvements de la souris.
1393
- * @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Historique des points de la souris.
1394
- * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
1395
- * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
1396
- * @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Historique des clics.
1397
- * @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Historique des glissements tactiles.
1398
- * @property {number} historyLength - La longueur de l'historique de session du navigateur (`window.history.length`).
1399
- * @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
1400
- * @property {string[]} [trapUrls] - URLs pièges à injecter dynamiquement.
1401
- */
1402
- /** @type {ClientBehaviorMetrics} */
1403
- const metrics = {
1404
- mouseEntropy: 0, // Conservé pour la compatibilité, mais l'analyse se fait maintenant sur l'historique
1405
- mouseMovementsHistory: [],
1406
- touchMovementsHistory: [],
1407
- clicksHistory: [],
1408
- keystrokeLatency: 0,
1409
- honeypotInteraction: false,
1410
- historyLength: 0,
1411
- clientTimestamp: 0,
1412
- rendering: { fps: 0, jitter: 0, offscreenAnom: false },
1413
- };
1414
-
1415
- let lastMousePos = { x: 0, y: 0 };
1416
- let mouseMovementsHistory = []; // NOUVEAU: Historique des points de la souris
1417
- let touchMovementsHistory = []; // NOUVEAU: Historique des gestes tactiles
1418
- const TOUCH_HISTORY_MAX = 100;
1419
- const MOUSE_HISTORY_MAX = 100; // Limite le nombre de points stockés
1420
- let clicksHistory = [];
1421
- const CLICKS_HISTORY_MAX = 50;
1422
- let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
1423
- let keystrokeTimestamps = [];
1424
- let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
1425
- let keystrokeDwellTimes = [];
1426
- let keystrokeFlightTimes = [];
1427
- const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
1428
-
1429
-
1430
-
1431
- // Exporter les fonctions individuellement pour la compatibilité ascendante
1432
- export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
1433
- export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
1434
- export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
1435
- export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
1436
- export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
1437
- export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
1438
- export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
1439
- export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
1440
- export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
1441
- export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
1442
- export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
1443
- export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
1444
- export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
1445
- export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
1446
- export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
1447
- export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
1448
- export const initializeWasm = ClientLibrary.initializeWasm.bind(ClientLibrary);
1449
- export const injectTrapLinks = ClientLibrary.injectTrapLinks.bind(ClientLibrary);
1450
- export const injectPhantomTraps = ClientLibrary.injectPhantomTraps.bind(ClientLibrary);
1451
- export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
1452
- export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrary);
1453
- export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
1454
- export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
1455
- export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
1456
-
1457
- // Export the internal object for testing purposes
1458
- export default ClientLibrary;
1459
-
1460
- // --- Global Export for Browser ---
1461
- // Attach the library to the window object to make it accessible from inline scripts.
1462
- if (typeof window !== 'undefined') {
1463
- window.ClientLibrary = ClientLibrary;
1
+ import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
2
+ import {solveChallenge} from './pow.solver.js';
3
+
4
+ function secureRandom() {
5
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
6
+ const array = new Uint32Array(1);
7
+ globalThis.crypto.getRandomValues(array);
8
+ return array[0] / 0xffffffff;
9
+ }
10
+ return Math.random();
11
+ }
12
+
13
+ // Variable storing the active hash function.
14
+ // Defaults to the JavaScript implementation.
15
+ let activeCyrb53 = jsCyrb53;
16
+ let derivedKey = null;
17
+
18
+ async function negotiateSessionKey() {
19
+ if (typeof window === 'undefined' || !window.crypto || !window.crypto.subtle) return;
20
+ try {
21
+ const keyPair = await window.crypto.subtle.generateKey(
22
+ { name: "ECDH", namedCurve: "P-256" },
23
+ false,
24
+ ["deriveKey"]
25
+ );
26
+ const clientPubKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
27
+ const clientPubKeyHex = Array.from(new Uint8Array(clientPubKeyBuffer))
28
+ .map(b => b.toString(16).padStart(2, '0'))
29
+ .join('');
30
+
31
+ const response = await fetch(window.location.pathname + '?fp_handshake=1', {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'x-client-ephemeral-key': clientPubKeyHex
36
+ },
37
+ body: JSON.stringify({ fp: ClientLibrary.getDeviceFingerprint() })
38
+ });
39
+
40
+ if (!response || !response.headers) return;
41
+ let serverPubKeyHex = null;
42
+ if (typeof response.headers.get === 'function') {
43
+ serverPubKeyHex = response.headers.get('x-server-ephemeral-key');
44
+ } else if (typeof response.headers === 'object') {
45
+ serverPubKeyHex = response.headers['x-server-ephemeral-key'] || response.headers['X-Server-Ephemeral-Key'];
46
+ }
47
+ if (serverPubKeyHex) {
48
+ const serverPubKeyBuffer = new Uint8Array(
49
+ serverPubKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
50
+ );
51
+ const serverPubKey = await window.crypto.subtle.importKey(
52
+ "raw",
53
+ serverPubKeyBuffer,
54
+ { name: "ECDH", namedCurve: "P-256" },
55
+ true,
56
+ []
57
+ );
58
+ derivedKey = await window.crypto.subtle.deriveKey(
59
+ { name: "ECDH", public: serverPubKey },
60
+ keyPair.privateKey,
61
+ { name: "HMAC", hash: "SHA-256", length: 256 },
62
+ true,
63
+ ["sign"]
64
+ );
65
+ console.log('[Fingerprint] Cryptographic session negotiated successfully.');
66
+ }
67
+ } catch (e) {
68
+ console.warn('[Fingerprint] Cryptographic session negotiation failed:', e);
69
+ }
70
+ }
71
+
72
+ async function signMetrics(metricsObj) {
73
+ if (!derivedKey) return metricsObj;
74
+ try {
75
+ const copy = JSON.parse(JSON.stringify(metricsObj));
76
+ delete copy.signature;
77
+ const dataToSign = new TextEncoder().encode(JSON.stringify(copy));
78
+ const signatureBuffer = await window.crypto.subtle.sign(
79
+ "HMAC",
80
+ derivedKey,
81
+ dataToSign
82
+ );
83
+ copy.signature = Array.from(new Uint8Array(signatureBuffer))
84
+ .map(b => b.toString(16).padStart(2, '0'))
85
+ .join('');
86
+ return copy;
87
+ } catch (e) {
88
+ return metricsObj;
89
+ }
90
+ }
91
+
92
+ const DB_NAME = 'wasm-cache-db';
93
+ const DB_VERSION = 1;
94
+ const STORE_NAME = 'wasm-modules';
95
+
96
+ const genRandStr = (len = 8) => {
97
+ return Array.from({ length: len }, () => String.fromCharCode(97 + Math.floor(secureRandom() * 26))).join('');
98
+ };
99
+
100
+ function getCachedWasm(url) {
101
+ return new Promise((resolve) => {
102
+ if (typeof indexedDB === 'undefined') return resolve(null);
103
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
104
+ request.onupgradeneeded = (e) => {
105
+ const db = e.target.result;
106
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
107
+ db.createObjectStore(STORE_NAME);
108
+ }
109
+ };
110
+ request.onsuccess = (e) => {
111
+ const db = e.target.result;
112
+ try {
113
+ const transaction = db.transaction(STORE_NAME, 'readonly');
114
+ const store = transaction.objectStore(STORE_NAME);
115
+ const getReq = store.get(url);
116
+ getReq.onsuccess = () => resolve(getReq.result);
117
+ getReq.onerror = () => resolve(null);
118
+ } catch (err) {
119
+ resolve(null);
120
+ }
121
+ };
122
+ request.onerror = () => resolve(null);
123
+ });
124
+ }
125
+
126
+ function cacheWasm(url, data) {
127
+ return new Promise((resolve) => {
128
+ if (typeof indexedDB === 'undefined') return resolve(false);
129
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
130
+ request.onupgradeneeded = (e) => {
131
+ const db = e.target.result;
132
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
133
+ db.createObjectStore(STORE_NAME);
134
+ }
135
+ };
136
+ request.onsuccess = (e) => {
137
+ const db = e.target.result;
138
+ try {
139
+ const transaction = db.transaction(STORE_NAME, 'readwrite');
140
+ const store = transaction.objectStore(STORE_NAME);
141
+ store.put(data, url);
142
+ transaction.oncomplete = () => resolve(true);
143
+ transaction.onerror = () => resolve(false);
144
+ } catch (err) {
145
+ resolve(false);
146
+ }
147
+ };
148
+ request.onerror = () => resolve(false);
149
+ });
150
+ }
151
+
152
+ const ClientLibrary = {
153
+ // Cache to avoid recalculating hardware constants
154
+ _cachedBuilder: null,
155
+ /**
156
+ * @private
157
+ * Dispatches a custom event from the window object.
158
+ * @param {string} eventName - The name of the event.
159
+ * @param {object} [detail={}] - The data to include in the event's detail property.
160
+ */
161
+ _dispatchEvent(eventName, detail = {}) {
162
+ if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return;
163
+ const event = new CustomEvent(`fingerprint:${eventName}`, { detail });
164
+ window.dispatchEvent(event);
165
+ },
166
+
167
+ /**
168
+ * Checks if native JavaScript prototypes have been tampered with or hooked (Frida, Puppeteer Stealth).
169
+ */
170
+ detectTamperedPrototypes() {
171
+ const checkNative = (obj, method) => {
172
+ try {
173
+ if (!obj) return false;
174
+ const fn = obj[method];
175
+ if (!fn) return false;
176
+ const str = Function.prototype.toString.call(fn);
177
+ if (!str.includes('[native code]')) return true; // Standard JS hook
178
+ const desc = Object.getOwnPropertyDescriptor(obj, method);
179
+ if (desc && (!desc.writable && !desc.configurable && desc.value)) return false;
180
+ return false;
181
+ } catch (e) {
182
+ return true;
183
+ }
184
+ };
185
+
186
+ const win = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
187
+ if (!win) return false;
188
+
189
+ const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : win.HTMLCanvasElement;
190
+ const canvas2d = typeof CanvasRenderingContext2D !== 'undefined' ? CanvasRenderingContext2D : win.CanvasRenderingContext2D;
191
+ const webgl = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext : win.WebGLRenderingContext;
192
+
193
+ return checkNative(htmlCanvas?.prototype, 'toDataURL') ||
194
+ checkNative(canvas2d?.prototype, 'getImageData') ||
195
+ checkNative(webgl?.prototype, 'getParameter') ||
196
+ checkNative(win, 'fetch');
197
+ },
198
+
199
+ /**
200
+ * Internal wrapper for the active hash function.
201
+ * @private
202
+ */
203
+ _hasher: (str, seed) => activeCyrb53(str, seed),
204
+
205
+ /**
206
+ * Generates a Schnorr Zero-Knowledge Proof (ZKP) of the device fingerprint.
207
+ * Ensures compatibility across JS, PHP, and Java verification engines.
208
+ * @param {string} fingerprint - Device fingerprint string.
209
+ * @returns {Promise<string>} Proof in hex format "y:t:s".
210
+ */
211
+ async generateZkpProof(fingerprint) {
212
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
213
+ const ZKP_G = 2n;
214
+ const cryptoObj = window.crypto || window.msCrypto;
215
+
216
+ const sha256Hex = async (str) => {
217
+ const encoder = new TextEncoder();
218
+ const data = encoder.encode(str);
219
+ const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
220
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
221
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
222
+ };
223
+
224
+ const modPow = (base, exponent, modulus) => {
225
+ if (modulus === 1n) return 0n;
226
+ let result = 1n;
227
+ base = base % modulus;
228
+ while (exponent > 0n) {
229
+ if (exponent % 2n === 1n) {
230
+ result = (result * base) % modulus;
231
+ }
232
+ exponent = exponent >> 1n;
233
+ base = (base * base) % modulus;
234
+ }
235
+ return result;
236
+ };
237
+
238
+ const xHex = await sha256Hex(fingerprint);
239
+ const x = BigInt('0x' + xHex) % ZKP_P;
240
+ const y = modPow(ZKP_G, x, ZKP_P);
241
+ const randomBytes = new Uint8Array(32);
242
+ cryptoObj.getRandomValues(randomBytes);
243
+ let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
244
+ let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
245
+ if (v === 0n) v = 1n;
246
+ const t = modPow(ZKP_G, v, ZKP_P);
247
+ const cStr = ZKP_G.toString() + y.toString() + t.toString();
248
+ const cHex = await sha256Hex(cStr);
249
+ const c = BigInt('0x' + cHex) % ZKP_P;
250
+ const s = (v + c * x) % (ZKP_P - 1n);
251
+
252
+ return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
253
+ },
254
+
255
+ /**
256
+ * Generates the current device fingerprint.
257
+ */
258
+ getDeviceFingerprint() {
259
+ if (typeof window === "undefined") {
260
+ console.error("getDeviceFingerprint can only be called on the client-side.");
261
+ return "";
262
+ }
263
+
264
+ if (!this._cachedBuilder) {
265
+ const nav = window.navigator;
266
+ const screen = window.screen;
267
+
268
+ this._cachedBuilder = new FingerprintBuilder();
269
+
270
+ // 1. Hardware (Very stable): Cores, RAM, GPU, Touch
271
+ this._cachedBuilder.add(
272
+ "hw",
273
+ `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
274
+ );
275
+
276
+ // 2. Geo/Locale (Stable unless traveling/VPN): Timezone, Language
277
+ this._cachedBuilder.add(
278
+ "geo",
279
+ `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
280
+ );
281
+
282
+ // 3. Screen (Stable unless monitor/zoom changes): Dimensions, ColorDepth
283
+ this._cachedBuilder.add(
284
+ "scr",
285
+ `${screen.width}x${screen.height}_${screen.colorDepth}`,
286
+ );
287
+
288
+ // 4. Platform (Stable) : OS, Engine
289
+ this._cachedBuilder.add("os", nav.platform);
290
+
291
+ // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
292
+ try {
293
+ const canvas = document.createElement("canvas");
294
+ const gl =
295
+ canvas.getContext("webgl2") ||
296
+ canvas.getContext("webgl") ||
297
+ canvas.getContext("experimental-webgl");
298
+ if (gl) {
299
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
300
+ if (debugInfo) {
301
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
302
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
303
+ this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
304
+ }
305
+ }
306
+ } catch (e) {
307
+ }
308
+
309
+ // 6. Canvas Fingerprinting (Rendering quirks)
310
+ try {
311
+ const canvas = document.createElement("canvas");
312
+ const ctx = canvas.getContext("2d");
313
+ if (ctx) {
314
+ canvas.width = 200;
315
+ canvas.height = 50;
316
+ ctx.textBaseline = "alphabetic";
317
+ ctx.font = "14px 'Arial'";
318
+ ctx.fillStyle = "#f60";
319
+ ctx.fillRect(125, 1, 62, 20);
320
+ ctx.fillStyle = "#069";
321
+ ctx.fillText("fingerprint", 2, 15);
322
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
323
+ ctx.fillText("fingerprint", 4, 17);
324
+ this._cachedBuilder.add("cvs", canvas.toDataURL());
325
+ }
326
+ } catch (e) {
327
+ }
328
+
329
+ // 7. Detection of Chrome DevTools Protocol (CDP) artifacts
330
+ // Injected by automation tools
331
+ const cdpFootprints = [
332
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Array',
333
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
334
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
335
+ '$cdc_asdjflasutopfhvcZLmcfl_',
336
+ '_selenium',
337
+ '_driver'
338
+ ];
339
+ if (cdpFootprints.some(fp => window[fp])) {
340
+ this._cachedBuilder.add("cdp", "true");
341
+ }
342
+
343
+ // 8. Bot Detection (Hidden indicators)
344
+ if (nav.webdriver) this._cachedBuilder.add("bot", "true");
345
+ if (ClientLibrary.detectTamperedPrototypes()) {
346
+ this._cachedBuilder.add("tampered", "true");
347
+ }
348
+ }
349
+
350
+ return this._cachedBuilder.toString();
351
+ },
352
+
353
+ /**
354
+ * Generates a request signature including context.
355
+ * @param {object} payload
356
+ */
357
+ /**
358
+ * Génère une signature de requête incluant le contexte.
359
+ * @param {object} payload
360
+ */
361
+ generateRequestSignature(payload = {}) {
362
+ const deviceFp = this.getDeviceFingerprint();
363
+ const sortedPayload = Object.keys(payload)
364
+ .sort()
365
+ .map((k) => `${k}=${payload[k]}`)
366
+ .join("&");
367
+ const payloadHash = this._hasher(sortedPayload);
368
+ return `${deviceFp}|req:${payloadHash}`;
369
+ },
370
+
371
+ /**
372
+ * Generates an HMAC-SHA256 signature using the Web Crypto API.
373
+ * @param {object} payload - Data payload to sign.
374
+ * @param {string} secret - Shared secret key.
375
+ * @returns {Promise<string>} Hexadecimal signature.
376
+ */
377
+ async generateClientSideSignature(payload, secret) {
378
+ const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
379
+ const encoder = new TextEncoder();
380
+ const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
381
+ name: "HMAC",
382
+ hash: "SHA-256"
383
+ }, false, ["sign"]);
384
+ const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
385
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
386
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
387
+ },
388
+
389
+ /**
390
+ * @internal
391
+ * Resets the cached fingerprint builder. Used for testing purposes.
392
+ */
393
+ _resetCache() {
394
+ // Reset hasher to default JS implementation.
395
+ activeCyrb53 = jsCyrb53;
396
+ this._cachedBuilder = null;
397
+ },
398
+
399
+ /**
400
+ * Injects invisible phantom interactive elements to trap bots (focus/hover).
401
+ */
402
+ injectPhantomTraps() {
403
+ if (typeof document === 'undefined') return;
404
+
405
+ // Create a phantom interactive element
406
+ const phantom = document.createElement('a');
407
+ phantom.href = '#';
408
+ // Random deceptive name to attract automated link/form parsers
409
+ 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'];
410
+ phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
411
+ phantom.className = genRandStr(8);
412
+ phantom.tabIndex = 0; // In natural tab flow
413
+ phantom.setAttribute('aria-hidden', 'true'); // Hidden from legitimate screen readers
414
+
415
+ // Invisible yet interactive style (1px x 1px, nearly transparent)
416
+ phantom.style.position = 'fixed';
417
+ phantom.style.top = '1px';
418
+ phantom.style.left = '1px';
419
+ phantom.style.width = '1px';
420
+ phantom.style.height = '1px';
421
+ phantom.style.opacity = '0.001';
422
+ phantom.style.zIndex = '99999';
423
+ phantom.style.overflow = 'hidden';
424
+ phantom.style.pointerEvents = 'auto';
425
+
426
+ const triggerTrap = () => {
427
+ this.onHoneypotTrigger();
428
+ };
429
+
430
+ phantom.addEventListener('focus', triggerTrap, { passive: true });
431
+ phantom.addEventListener('mouseover', triggerTrap, { passive: true });
432
+
433
+ // Polymorphic DOM insertion of the phantom trap
434
+ const nestingOptions = [
435
+ () => document.body.appendChild(phantom),
436
+ () => {
437
+ const wrapper = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
438
+ wrapper.className = genRandStr(8);
439
+ wrapper.style.position = 'absolute';
440
+ wrapper.style.width = '0';
441
+ wrapper.style.height = '0';
442
+ wrapper.style.overflow = 'hidden';
443
+ wrapper.appendChild(phantom);
444
+ document.body.appendChild(wrapper);
445
+ }
446
+ ];
447
+ nestingOptions[Math.floor(secureRandom() * nestingOptions.length)]();
448
+ },
449
+
450
+ /**
451
+ * Measures gyroscope/accelerometer noise to detect phone farm racks.
452
+ */
453
+ startMotionTracker() {
454
+ if (this._motionTrackerAttached || typeof window === 'undefined') return;
455
+ this._motionTrackerAttached = true;
456
+ const motionSamples = [];
457
+ const handleMotion = (e) => {
458
+ const acc = e.accelerationIncludingGravity || e.acceleration;
459
+ if (!acc) return;
460
+ const mag = Math.sqrt((acc.x || 0)**2 + (acc.y || 0)**2 + (acc.z || 0)**2);
461
+ motionSamples.push(mag);
462
+ if (motionSamples.length > 20) motionSamples.shift();
463
+ if (motionSamples.length >= 10) {
464
+ const avg = motionSamples.reduce((a, b) => a + b, 0) / motionSamples.length;
465
+ const variance = motionSamples.reduce((a, b) => a + (b - avg)**2, 0) / motionSamples.length;
466
+ metrics.motionVariance = Math.round(variance * 10000) / 10000;
467
+ }
468
+ };
469
+ try {
470
+ window.addEventListener('devicemotion', handleMotion, { passive: true });
471
+ } catch (e) {}
472
+ },
473
+
474
+ /**
475
+ * Starts tracking touch events on mobile/tablet devices.
476
+ */
477
+ startTouchEventTracker() {
478
+ if (this._touchTrackerAttached) return;
479
+ this._touchTrackerAttached = true;
480
+
481
+ const handleTouch = (e) => {
482
+ if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
483
+ touchMovementsHistory.shift();
484
+ }
485
+ const touch = e.touches[0] || e.changedTouches[0];
486
+ if (!touch) return;
487
+
488
+ const radiusX = touch.radiusX || 0;
489
+ const radiusY = touch.radiusY || 0;
490
+ const radius = (radiusX + radiusY) / 2;
491
+ const force = touch.force || touch.webkitForce || 0;
492
+
493
+ touchMovementsHistory.push({
494
+ x: touch.clientX,
495
+ y: touch.clientY,
496
+ t: performance.now(),
497
+ p: force,
498
+ r: radius,
499
+ num: e.touches.length
500
+ });
501
+ };
502
+
503
+ document.addEventListener('touchstart', handleTouch, { passive: true });
504
+ document.addEventListener('touchmove', handleTouch, { passive: true });
505
+ document.addEventListener('touchend', handleTouch, { passive: true });
506
+ },
507
+
508
+ /**
509
+ * Starts tracking display regularity (V-Sync/rAF) to detect software framebuffers lacking V-Sync.
510
+ */
511
+ startRenderingTracker() {
512
+ if (this._renderingTrackerAttached) return;
513
+ this._renderingTrackerAttached = true;
514
+
515
+ if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
516
+
517
+ const rAfTimestamps = [];
518
+ let lastTime = performance.now();
519
+ const maxSamples = 15;
520
+
521
+ const checkOffscreenAnom = () => {
522
+ try {
523
+ const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : window.HTMLCanvasElement;
524
+ if ('OffscreenCanvas' in window && htmlCanvas?.prototype?.transferControlToOffscreen) {
525
+ const nativeToString = Function.prototype.toString.call(htmlCanvas.prototype.transferControlToOffscreen);
526
+ return !nativeToString.includes('[native code]');
527
+ }
528
+ } catch (e) {}
529
+ return false;
530
+ };
531
+
532
+ const loop = (time) => {
533
+ const delta = time - lastTime;
534
+ lastTime = time;
535
+ if (rAfTimestamps.length < maxSamples) {
536
+ if (rAfTimestamps.length > 0) { // Skip first delta
537
+ rAfTimestamps.push(delta);
538
+ }
539
+ window.requestAnimationFrame(loop);
540
+ } else {
541
+ const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
542
+ const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
543
+ const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
544
+
545
+ metrics.rendering = {
546
+ fps: Math.round((1000 / avg) * 100) / 100,
547
+ jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
548
+ offscreenAnom: checkOffscreenAnom()
549
+ };
550
+ }
551
+ };
552
+ window.requestAnimationFrame(loop);
553
+ },
554
+
555
+ /**
556
+ * Initializes persistent Proof-of-Space allocation in local IndexedDB.
557
+ */
558
+ async initializeSpace(seed, sizeMb) {
559
+ return new Promise((resolve, reject) => {
560
+ const request = indexedDB.open('pospace-db', 1);
561
+ request.onupgradeneeded = (e) => {
562
+ const db = e.target.result;
563
+ if (!db.objectStoreNames.contains('blocks')) {
564
+ db.createObjectStore('blocks');
565
+ }
566
+ };
567
+ request.onsuccess = async (e) => {
568
+ const db = e.target.result;
569
+ const tx = db.transaction('blocks', 'readwrite');
570
+ const store = tx.objectStore('blocks');
571
+
572
+ const maxBlocks = sizeMb * 1024;
573
+ const countReq = store.count();
574
+ countReq.onsuccess = async () => {
575
+ if (countReq.result < maxBlocks) {
576
+ for (let i = 0; i < maxBlocks; i++) {
577
+ const block = new Uint8Array(1024);
578
+ let h = 5381;
579
+ for (let j = 0; j < seed.length; j++) {
580
+ h = (h << 5) + h + seed.charCodeAt(j);
581
+ }
582
+ h = (h << 5) + h + i;
583
+ for (let k = 0; k < 1024; k++) {
584
+ h = Math.imul(h ^ k, 1597334677);
585
+ block[k] = h & 0xff;
586
+ }
587
+ store.put(block, i);
588
+ }
589
+ }
590
+ resolve();
591
+ };
592
+ };
593
+ request.onerror = () => reject(new Error("Failed to open pospace database"));
594
+ });
595
+ },
596
+
597
+ /**
598
+ * Reads a specific block from local IndexedDB formatted as hex.
599
+ */
600
+ async readSpaceBlock(blockIdx) {
601
+ return new Promise((resolve, reject) => {
602
+ const request = indexedDB.open('pospace-db', 1);
603
+ request.onsuccess = (e) => {
604
+ const db = e.target.result;
605
+ const tx = db.transaction('blocks', 'readonly');
606
+ const store = tx.objectStore('blocks');
607
+ const getReq = store.get(blockIdx);
608
+ getReq.onsuccess = () => {
609
+ const block = getReq.result;
610
+ if (block) {
611
+ const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
612
+ resolve(hex);
613
+ } else {
614
+ reject(new Error("Block not found"));
615
+ }
616
+ };
617
+ getReq.onerror = () => reject(getReq.error);
618
+ };
619
+ request.onerror = () => reject(new Error("Failed to open pospace database"));
620
+ });
621
+ },
622
+
623
+ /**
624
+ * Solves Proof-of-Space by optionally combining a peer's block.
625
+ */
626
+ async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
627
+ const blocks = [];
628
+ for (const idx of queries) {
629
+ const blockHex = await this.readSpaceBlock(idx);
630
+ blocks.push(blockHex);
631
+ }
632
+ let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
633
+ const encoder = new TextEncoder();
634
+ const data = encoder.encode(finalPayload);
635
+ const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
636
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
637
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
638
+ },
639
+
640
+ /**
641
+ * Starts tracking mouse movements to evaluate entropy.
642
+ * Call once per page lifecycle.
643
+ */
644
+ startMouseEntropyTracker() {
645
+ // Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
646
+ if (this._mouseTrackerAttached) return;
647
+ this._mouseTrackerAttached = true;
648
+
649
+ document.addEventListener('mousemove', (e) => {
650
+ // Capture trajectory points {x, y, t}
651
+ if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
652
+ // Cap history size to prevent unbounded memory growth
653
+ mouseMovementsHistory.shift();
654
+ }
655
+ mouseMovementsHistory.push({
656
+ x: e.clientX,
657
+ y: e.clientY,
658
+ t: performance.now()
659
+ });
660
+ }, {passive: true});
661
+ },
662
+
663
+ /**
664
+ * Starts tracking keystroke dynamics to evaluate dwell and flight times.
665
+ * Call once per page lifecycle.
666
+ */
667
+ startKeystrokeDynamicsTracker() {
668
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
669
+ if (this._keystrokeTrackerAttached) return;
670
+ this._keystrokeTrackerAttached = true;
671
+
672
+ const activeKeys = new Map();
673
+ let lastKeyDownTime = 0;
674
+ let lastKeyName = '';
675
+
676
+ document.addEventListener('keydown', (e) => {
677
+ const now = performance.now();
678
+ const key = e.key;
679
+ const code = e.code;
680
+ if (!key && !code) return;
681
+
682
+ const keyIdentifier = code || key;
683
+
684
+ // Prevent key repeat triggering multiple events
685
+ if (activeKeys.has(keyIdentifier)) return;
686
+ activeKeys.set(keyIdentifier, now);
687
+
688
+ if (keystrokeTimestamps.length > 0) {
689
+ const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
690
+ const latency = now - lastTimestamp;
691
+ if (latency > 10 && latency < 2000) {
692
+ if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
693
+ keystrokeLatencies.shift();
694
+ }
695
+ keystrokeLatencies.push(latency);
696
+ }
697
+ }
698
+ keystrokeTimestamps.push(now);
699
+
700
+ // Flight Time (KeyDown to KeyDown)
701
+ if (lastKeyDownTime > 0) {
702
+ const flightTime = now - lastKeyDownTime;
703
+ if (flightTime > 10 && flightTime < 2000) {
704
+ if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
705
+ keystrokeFlightTimes.shift();
706
+ }
707
+ const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
708
+ keystrokeFlightTimes.push({ digraph, time: flightTime });
709
+ }
710
+ }
711
+ lastKeyDownTime = now;
712
+ lastKeyName = keyIdentifier;
713
+ }, {passive: true});
714
+
715
+ document.addEventListener('keyup', (e) => {
716
+ const now = performance.now();
717
+ const key = e.key;
718
+ const code = e.code;
719
+ if (!key && !code) return;
720
+
721
+ const keyIdentifier = code || key;
722
+
723
+ if (activeKeys.has(keyIdentifier)) {
724
+ const pressTime = activeKeys.get(keyIdentifier);
725
+ const dwellTime = now - pressTime;
726
+ activeKeys.delete(keyIdentifier);
727
+
728
+ if (dwellTime > 5 && dwellTime < 1000) {
729
+ if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
730
+ keystrokeDwellTimes.shift();
731
+ }
732
+ keystrokeDwellTimes.push(dwellTime);
733
+ }
734
+ }
735
+ }, {passive: true});
736
+ },
737
+
738
+ /**
739
+ * Starts tracking click events to analyze position variance.
740
+ * @private
741
+ */
742
+ startClickTracker() {
743
+ if (this._clickTrackerAttached) return;
744
+ this._clickTrackerAttached = true;
745
+
746
+ document.addEventListener('click', (e) => {
747
+ if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
748
+ clicksHistory.shift();
749
+ }
750
+ // Generate a simple identifier for the target element
751
+ const target = e.target;
752
+ const targetId = target.id || target.name || target.tagName;
753
+
754
+ clicksHistory.push({
755
+ x: e.clientX,
756
+ y: e.clientY,
757
+ t: performance.now(),
758
+ targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
759
+ });
760
+ }, { passive: true });
761
+ },
762
+ /**
763
+ * Initializes or resets client-side honeypots for immediate detection.
764
+ * Previous listeners are removed before attaching new ones.
765
+ * @param {string[]} honeypotFieldNames - Hidden form field names.
766
+ */
767
+ initializeHoneypots(honeypotFieldNames) {
768
+ // 1. Clean up existing listeners
769
+ activeHoneypotListeners.forEach((listener, field) => {
770
+ field.removeEventListener('input', listener);
771
+ });
772
+ activeHoneypotListeners.clear();
773
+
774
+ // 2. Attach new listeners to existing DOM elements
775
+ honeypotFieldNames.forEach(fieldName => {
776
+ const field = document.querySelector(`[name="${fieldName}"]`);
777
+ if (field) {
778
+ const listener = () => {
779
+ this.onHoneypotTrigger();
780
+ field.removeEventListener('input', listener);
781
+ };
782
+ field.addEventListener('input', listener);
783
+ activeHoneypotListeners.set(field, listener);
784
+ }
785
+ });
786
+
787
+ // 3. Generate trap inputs inside a closed Shadow DOM
788
+ if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
789
+ const host = document.createElement('div');
790
+ host.setAttribute('aria-hidden', 'true');
791
+ host.style.position = 'absolute';
792
+ host.style.width = '0';
793
+ host.style.height = '0';
794
+ host.style.overflow = 'hidden';
795
+
796
+ const shadow = host.attachShadow({ mode: 'closed' });
797
+
798
+ // Randomize classes, CSS variables, and layout tags
799
+ const wrapperClass = genRandStr(10);
800
+ const posStateVar = `--${genRandStr(8)}`;
801
+ const offValVar = `--${genRandStr(8)}`;
802
+ const visStateVar = `--${genRandStr(8)}`;
803
+ const scaleValVar = `--${genRandStr(8)}`;
804
+
805
+ const style = document.createElement('style');
806
+ style.textContent = `
807
+ :host {
808
+ ${posStateVar}: absolute;
809
+ ${offValVar}: -9999px;
810
+ ${visStateVar}: hidden;
811
+ ${scaleValVar}: 0;
812
+ }
813
+ .${wrapperClass} {
814
+ position: var(${posStateVar});
815
+ left: var(${offValVar});
816
+ top: var(${offValVar});
817
+ visibility: var(${visStateVar});
818
+ transform: scale(var(${scaleValVar}));
819
+ }
820
+ `;
821
+ shadow.appendChild(style);
822
+
823
+ const wrapperTags = ['div', 'section', 'p', 'span', 'form', 'main'];
824
+ const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
825
+ const wrapper = document.createElement(selectedWrapperTag);
826
+ wrapper.className = wrapperClass;
827
+
828
+ honeypotFieldNames.forEach(fieldName => {
829
+ const label = document.createElement('label');
830
+ label.textContent = fieldName;
831
+ const input = document.createElement('input');
832
+ input.type = 'text';
833
+ input.name = fieldName;
834
+ input.tabIndex = -1;
835
+ input.className = genRandStr(6);
836
+ input.id = genRandStr(8);
837
+ input.autocomplete = 'off';
838
+
839
+ const trigger = () => {
840
+ this.onHoneypotTrigger();
841
+ };
842
+
843
+ input.addEventListener('input', trigger, { passive: true });
844
+ input.addEventListener('change', trigger, { passive: true });
845
+ input.addEventListener('focus', trigger, { passive: true });
846
+
847
+ // Polymorphic nesting of label and input
848
+ const nestingType = Math.floor(secureRandom() * 3);
849
+ if (nestingType === 1) {
850
+ label.appendChild(input);
851
+ wrapper.appendChild(label);
852
+ } else if (nestingType === 2) {
853
+ const innerContainer = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
854
+ innerContainer.className = genRandStr(5);
855
+ innerContainer.appendChild(label);
856
+ innerContainer.appendChild(input);
857
+ wrapper.appendChild(innerContainer);
858
+ } else {
859
+ wrapper.appendChild(label);
860
+ wrapper.appendChild(input);
861
+ }
862
+ });
863
+
864
+ shadow.appendChild(wrapper);
865
+ document.body.appendChild(host);
866
+ }
867
+ },
868
+ /**
869
+ * Silently requests FIDO2 hardware attestation (WebAuthn)
870
+ * to anchor physical device identity via TPM / Secure Enclave.
871
+ * @returns {Promise<object|null>}
872
+ */
873
+ async getWebAuthnAnchor() {
874
+ const win = typeof window !== 'undefined' ? window : null;
875
+ if (!win || !win.PublicKeyCredential) return null;
876
+ try {
877
+ const isPlatformAvailable = await win.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
878
+ if (!isPlatformAvailable) return null;
879
+
880
+ const storedCredIdBase64 = localStorage.getItem('fp_webauthn_cred_id');
881
+ const challenge = new Uint8Array([109, 101, 116, 114, 105, 99, 115, 95, 97, 110, 99, 104, 111, 114, 95, 115, 101, 99, 117, 114, 101]); // Static stable challenge
882
+
883
+ if (storedCredIdBase64) {
884
+ // Silent assertion attempt (Authentication)
885
+ const credentialId = Uint8Array.from(atob(storedCredIdBase64), c => c.charCodeAt(0));
886
+ const assertion = await navigator.credentials.get({
887
+ publicKey: {
888
+ challenge,
889
+ allowCredentials: [{
890
+ id: credentialId,
891
+ type: 'public-key'
892
+ }],
893
+ userVerification: 'discouraged',
894
+ timeout: 1000
895
+ }
896
+ });
897
+ if (assertion) {
898
+ return {
899
+ type: 'assertion',
900
+ credentialId: storedCredIdBase64,
901
+ signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
902
+ authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
903
+ clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON)))
904
+ };
905
+ }
906
+ } else {
907
+ // Silent creation attempt (Registration)
908
+ const options = {
909
+ publicKey: {
910
+ challenge,
911
+ rp: { name: window.location.hostname, id: window.location.hostname },
912
+ user: {
913
+ id: new Uint8Array([102, 112, 105, 100]),
914
+ name: 'silent-device-anchor',
915
+ displayName: 'Hardware Device Anchor'
916
+ },
917
+ pubKeyCredParams: [
918
+ { type: 'public-key', alg: -7 }, // ES256 (P-256) - Secure Enclave / TPM / Android Keystore
919
+ { type: 'public-key', alg: -257 } // RS256 - Windows Hello TPM
920
+ ],
921
+ authenticatorSelection: {
922
+ authenticatorAttachment: 'platform',
923
+ userVerification: 'discouraged',
924
+ residentKey: 'preferred'
925
+ },
926
+ timeout: 1500,
927
+ attestation: 'indirect'
928
+ }
929
+ };
930
+ const credential = await navigator.credentials.create(options);
931
+ if (credential) {
932
+ const credIdBase64 = btoa(String.fromCharCode(...new Uint8Array(credential.rawId)));
933
+ localStorage.setItem('fp_webauthn_cred_id', credIdBase64);
934
+ const publicKeyDer = credential.response.getPublicKey ? credential.response.getPublicKey() : null;
935
+ return {
936
+ type: 'registration',
937
+ credentialId: credIdBase64,
938
+ publicKey: publicKeyDer ? btoa(String.fromCharCode(...new Uint8Array(publicKeyDer))) : null,
939
+ attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.getAttestationObject()))),
940
+ clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
941
+ };
942
+ }
943
+ }
944
+ } catch (e) {
945
+ // Bypass silently when unsupported to remain non-intrusive
946
+ console.log('[WebAuthn-Anchor] Silent attestation bypassed:', e.message);
947
+ }
948
+ return null;
949
+ },
950
+
951
+ /**
952
+ * Retrieves collected client behavioral metrics.
953
+ * Call before submitting sensitive requests.
954
+ * @returns {ClientBehaviorMetrics}
955
+ */
956
+ getClientBehaviorMetrics() {
957
+ // Add history length as a behavioral signal.
958
+ metrics.historyLength = window.history.length;
959
+
960
+ // Attach collection timestamp for replay detection
961
+ metrics.clicksHistory = clicksHistory;
962
+ metrics.clientTimestamp = Date.now();
963
+
964
+ metrics.touchMovementsHistory = touchMovementsHistory;
965
+ if (ClientLibrary.detectTamperedPrototypes()) {
966
+ metrics.prototypeTampered = true;
967
+ }
968
+ metrics.motionVariance = metrics.motionVariance ?? -1;
969
+ // Include mouse trajectory history for server-side evaluation
970
+ metrics.mouseMovementsHistory = mouseMovementsHistory;
971
+
972
+ // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
973
+ metrics.keystrokeDwellTimes = keystrokeDwellTimes;
974
+ metrics.keystrokeFlightTimes = keystrokeFlightTimes;
975
+
976
+ // Calculate mean keystroke latency
977
+ if (keystrokeLatencies.length > 0) {
978
+ const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
979
+ metrics.keystrokeLatency = sum / keystrokeLatencies.length;
980
+ } else {
981
+ metrics.keystrokeLatency = 0;
982
+ }
983
+ return metrics;
984
+ },
985
+
986
+ /**
987
+ * Enriches a fetch request with fingerprinting and behavioral headers.
988
+ * @param {RequestInfo} resource
989
+ * @param {RequestInit} [options]
990
+ * @returns {Promise<Response>}
991
+ */
992
+ async protectedFetch(resource, options = {}) {
993
+ const fp = this.getDeviceFingerprint();
994
+ const rawBehavior = this.getClientBehaviorMetrics();
995
+ const zkpProof = await this.generateZkpProof(fp); // Generate ZKP proof
996
+ const behavior = await signMetrics(rawBehavior);
997
+
998
+ const headers = new Headers(options.headers || {});
999
+ headers.set('X-ZKP-Proof', zkpProof); // Add ZKP proof to headers
1000
+ headers.set('X-Device-Fingerprint', fp);
1001
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
1002
+
1003
+ options.headers = headers;
1004
+ return fetch(resource, options);
1005
+ },
1006
+
1007
+ // --- Fetch Interception Chain ---
1008
+ _isFetchPatched: false,
1009
+ _interceptorChain: [],
1010
+ // Store original fetch bound to window to prevent illegal invocation errors
1011
+ _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
1012
+
1013
+ /**
1014
+ * Adds an interceptor function to the `fetch` chain.
1015
+ * Each interceptor receives `resource`, `options`, and a `next` function.
1016
+ * Must call `next(resource, options)` to continue the chain.
1017
+ * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
1018
+ */
1019
+ addFetchInterceptor(interceptor) {
1020
+ if (!this._isFetchPatched) {
1021
+ this.patchGlobalFetch();
1022
+ }
1023
+ this._interceptorChain.push(interceptor);
1024
+ },
1025
+
1026
+ patchGlobalFetch() {
1027
+ if (this._isFetchPatched || !this._originalFetch) return;
1028
+
1029
+ this._isFetchPatched = true;
1030
+ window.fetch = (resource, options) => {
1031
+ const dispatch = (index, res, opts) => {
1032
+ if (index >= this._interceptorChain.length) {
1033
+ return this._originalFetch(res, opts);
1034
+ }
1035
+ const nextInterceptor = this._interceptorChain[index];
1036
+ return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
1037
+ };
1038
+ return dispatch(0, resource, options || {});
1039
+ };
1040
+ },
1041
+
1042
+ /**
1043
+ * Invoked when a honeypot trigger occurs.
1044
+ * @private
1045
+ */
1046
+ onHoneypotTrigger() {
1047
+ metrics.honeypotInteraction = true;
1048
+ // Dispatch event allowing host applications to react
1049
+ this._dispatchEvent('honeypotTriggered');
1050
+ },
1051
+
1052
+ /**
1053
+ * Initializes the fingerprinting fetch interceptor.
1054
+ * Appends to interceptor chain without clobbering existing handlers.
1055
+ * @param {string[]} [targetDomains] - Domains to protect. Defaults to same-origin.
1056
+ */
1057
+ initializeFetch(targetDomains = []) {
1058
+ const fingerprintInterceptor = async (resource, options, next) => {
1059
+ const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
1060
+ let shouldProtect = false;
1061
+
1062
+ try {
1063
+ const url = new URL(requestUrl, window.location.origin);
1064
+ // Protect if targetDomains is empty and same-origin, or host matches targetDomains
1065
+ shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
1066
+ (targetDomains.length > 0 && targetDomains.includes(url.hostname));
1067
+ } catch (e) {
1068
+ // Fallback for malformed URLs
1069
+ shouldProtect = targetDomains.length === 0;
1070
+ }
1071
+
1072
+ if (shouldProtect) {
1073
+ const fp = this.getDeviceFingerprint();
1074
+ const rawBehavior = this.getClientBehaviorMetrics();
1075
+ const behavior = await signMetrics(rawBehavior);
1076
+ const headers = new Headers(options.headers || {});
1077
+ headers.set('X-Device-Fingerprint', fp);
1078
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
1079
+ options.headers = headers;
1080
+ }
1081
+
1082
+ // Forward to next interceptor in chain
1083
+ return next(resource, options);
1084
+ };
1085
+
1086
+ this.addFetchInterceptor(fingerprintInterceptor);
1087
+ },
1088
+
1089
+ /**
1090
+ * Injects visually hidden "honeypot" links into the DOM to trap bots.
1091
+ * @param {string[]} urls - An array of trap URLs to inject.
1092
+ * @private
1093
+ */
1094
+ injectTrapLinks(urls) {
1095
+ if (!urls || urls.length === 0 || typeof document === 'undefined') {
1096
+ return;
1097
+ }
1098
+
1099
+ const host = document.createElement('div');
1100
+ host.setAttribute('aria-hidden', 'true');
1101
+ host.style.position = 'absolute';
1102
+ host.style.width = '0';
1103
+ host.style.height = '0';
1104
+ host.style.overflow = 'hidden';
1105
+
1106
+ const shadow = host.attachShadow({ mode: 'closed' });
1107
+
1108
+ // Randomize classes, CSS variables, and layout tags
1109
+ const wrapperClass = genRandStr(10);
1110
+ const layoutPosVar = `--${genRandStr(8)}`;
1111
+ const offsetValVar = `--${genRandStr(8)}`;
1112
+ const visibilityStateVar = `--${genRandStr(8)}`;
1113
+ const scaleFactorVar = `--${genRandStr(8)}`;
1114
+ const ptrEventsVar = `--${genRandStr(8)}`;
1115
+ const linkColorClass = genRandStr(6);
1116
+
1117
+ const style = document.createElement('style');
1118
+ style.textContent = `
1119
+ :host {
1120
+ ${layoutPosVar}: absolute;
1121
+ ${offsetValVar}: -9999px;
1122
+ ${visibilityStateVar}: hidden;
1123
+ ${scaleFactorVar}: 0;
1124
+ ${ptrEventsVar}: none;
1125
+ }
1126
+ .${wrapperClass} {
1127
+ position: var(${layoutPosVar});
1128
+ left: var(${offsetValVar});
1129
+ top: var(${offsetValVar});
1130
+ visibility: var(${visibilityStateVar});
1131
+ transform: scale(var(${scaleFactorVar}));
1132
+ pointer-events: var(${ptrEventsVar});
1133
+ }
1134
+ .${linkColorClass} {
1135
+ color: transparent;
1136
+ text-decoration: none;
1137
+ }
1138
+ `;
1139
+ shadow.appendChild(style);
1140
+
1141
+ const wrapperTags = ['div', 'section', 'p', 'span', 'nav', 'aside'];
1142
+ const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
1143
+ const wrapper = document.createElement(selectedWrapperTag);
1144
+ wrapper.className = wrapperClass;
1145
+
1146
+ urls.forEach((url, i) => {
1147
+ const link = document.createElement('a');
1148
+ link.href = url;
1149
+ link.rel = 'nofollow';
1150
+ link.tabIndex = -1;
1151
+ link.className = linkColorClass;
1152
+ link.id = genRandStr(8);
1153
+
1154
+ // Polymorphic link content structure
1155
+ const contentNestingType = Math.floor(secureRandom() * 3);
1156
+ if (contentNestingType === 1) {
1157
+ const span = document.createElement('span');
1158
+ span.className = genRandStr(5);
1159
+ span.innerHTML = `&gt; ${i + 1}`;
1160
+ link.appendChild(span);
1161
+ } else if (contentNestingType === 2) {
1162
+ const b = document.createElement('b');
1163
+ b.className = genRandStr(5);
1164
+ b.innerHTML = `&gt; ${i + 1}`;
1165
+ link.appendChild(b);
1166
+ } else {
1167
+ link.innerHTML = `<span>&gt; ${i + 1}</span>`;
1168
+ }
1169
+
1170
+ const trigger = () => {
1171
+ this.onHoneypotTrigger();
1172
+ };
1173
+ link.addEventListener('click', trigger, { passive: true });
1174
+ link.addEventListener('focus', trigger, { passive: true });
1175
+ link.addEventListener('mouseover', trigger, { passive: true });
1176
+
1177
+ // Polymorphic link nesting
1178
+ if (secureRandom() > 0.5) {
1179
+ const itemContainer = document.createElement('span');
1180
+ itemContainer.className = genRandStr(5);
1181
+ itemContainer.appendChild(link);
1182
+ wrapper.appendChild(itemContainer);
1183
+ } else {
1184
+ wrapper.appendChild(link);
1185
+ }
1186
+ });
1187
+
1188
+ shadow.appendChild(wrapper);
1189
+ document.body.appendChild(host);
1190
+ },
1191
+ /**
1192
+ * Intercepts a JSON challenge response, solves it, and retries the request.
1193
+ * @param {Response} response - Initial response (status 404/challenge).
1194
+ * @param {RequestInfo} resource - Original request resource.
1195
+ * @param {RequestInit} options - Original request options.
1196
+ * @returns {Promise<Response>} - Retried response.
1197
+ * @private
1198
+ */
1199
+ async solveChallengeAndRetry(response, resource, options) {
1200
+ if (!response || response.status !== 404 || !response.headers?.get?.('content-type')?.includes('application/json') || response.bodyUsed) {
1201
+ return response;
1202
+ }
1203
+
1204
+ try {
1205
+ const challengeData = await response.json();
1206
+ if (!challengeData.challenge || !challengeData.challenge.type) {
1207
+ return response; // Not a valid JSON challenge
1208
+ }
1209
+
1210
+ console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
1211
+ this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
1212
+
1213
+ // The device fingerprint solving the challenge is crucial
1214
+ const solverFp = this.getDeviceFingerprint();
1215
+ const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
1216
+ console.log('[Fingerprint] Challenge solved. Retrying original request.');
1217
+
1218
+ this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
1219
+ // Append solution parameters to retry request URL
1220
+ const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
1221
+ solutionWrapper.applyToUrl(url);
1222
+
1223
+ // Attach solver fingerprint to retry request
1224
+ url.searchParams.set('pow_fp', solverFp);
1225
+
1226
+ // Use fetch to retry with appropriate execution context
1227
+ return window.fetch(url.toString(), options);
1228
+ } catch (e) {
1229
+ console.error('[Fingerprint] Failed to solve or retry challenge:', e);
1230
+ return response; // Retourne la réponse 429 originale en cas d'échec
1231
+ }
1232
+ },
1233
+
1234
+ /**
1235
+ * Initializes all client-side protections in one call.
1236
+ * Also loads WASM module if `wasmPath` is provided.
1237
+ * Recommended initialization approach.
1238
+ * @param {ClientConfig} [config={}] - Client configuration options.
1239
+ */
1240
+ initializeClient(config = {}) {
1241
+ const {
1242
+ mouse = true,
1243
+ keystrokes = true,
1244
+ clicks = true, // Add new option
1245
+ touches = true, // Touch event tracking
1246
+ motion = true,
1247
+ rendering = true,
1248
+ phantomTraps = true,
1249
+ honeypots = [],
1250
+ trapUrls = [], // Trap URLs
1251
+ wasmPath,
1252
+ workerPath, // NOUVEAU
1253
+ fetch: fetchConfig = {}
1254
+ } = config;
1255
+
1256
+ negotiateSessionKey();
1257
+ // Load WASM module if path provided
1258
+ if (wasmPath) {
1259
+ this.initializeWasm(wasmPath);
1260
+ }
1261
+
1262
+ if (mouse) {
1263
+ this.startMouseEntropyTracker();
1264
+ }
1265
+ if (workerPath) {
1266
+ this.workerPath = workerPath;
1267
+ }
1268
+ if (keystrokes) {
1269
+ this.startKeystrokeDynamicsTracker();
1270
+ }
1271
+ if (clicks) {
1272
+ this.startClickTracker();
1273
+ }
1274
+ if (touches) {
1275
+ this.startTouchEventTracker();
1276
+ }
1277
+ if (motion) {
1278
+ this.startMotionTracker();
1279
+ }
1280
+ if (ClientLibrary.detectTamperedPrototypes()) {
1281
+ metrics.prototypeTampered = true;
1282
+ }
1283
+ if (rendering) {
1284
+ this.startRenderingTracker();
1285
+ }
1286
+ if (phantomTraps) {
1287
+ this.injectPhantomTraps();
1288
+ }
1289
+ if (honeypots.length > 0) {
1290
+ this.initializeHoneypots(honeypots);
1291
+ }
1292
+
1293
+ // Dynamically inject trap links on startup
1294
+ if (trapUrls.length > 0) {
1295
+ this.injectTrapLinks(trapUrls);
1296
+ }
1297
+ // Enable fetch interception if configured
1298
+ if (config.fetch) {
1299
+ this.initializeFetch(fetchConfig.targetDomains);
1300
+
1301
+ // Add challenge resolution interceptor
1302
+ if (fetchConfig.handleChallenges !== false) {
1303
+ this.addFetchInterceptor(async (resource, options, next) => {
1304
+ const originalResponse = await next(resource, options);
1305
+ // Clone response to prevent draining the body stream for original caller
1306
+ return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
1307
+ });
1308
+ }
1309
+ }
1310
+ },
1311
+
1312
+ /**
1313
+ * Attempts to load and initialize WebAssembly module for accelerated hashing.
1314
+ * Falls back to JavaScript implementation on error.
1315
+ * @param {string} wasmPath - Path to WASM loader script or binary.
1316
+ */
1317
+ async initializeWasm(wasmPath) {
1318
+ try {
1319
+ // Direct standalone polymorphic WebAssembly loading
1320
+ if (wasmPath.endsWith('.wasm')) {
1321
+ let instance;
1322
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
1323
+ try {
1324
+ const response = await fetch(wasmPath);
1325
+ const result = await WebAssembly.instantiateStreaming(response, {});
1326
+ instance = result.instance;
1327
+ } catch (streamingError) {
1328
+ console.warn('[Fingerprint] WebAssembly.instantiateStreaming failed, falling back to compile:', streamingError);
1329
+ }
1330
+ }
1331
+ if (!instance) {
1332
+ const response = await fetch(wasmPath);
1333
+ const arrayBuffer = await response.arrayBuffer();
1334
+ const module = await WebAssembly.compile(arrayBuffer);
1335
+ instance = await WebAssembly.instantiate(module, {});
1336
+ }
1337
+ const exports = instance.exports;
1338
+ const memory = exports.memory;
1339
+ activeCyrb53 = (str) => {
1340
+ const encoder = new TextEncoder();
1341
+ const bytes = encoder.encode(str);
1342
+ const view = new Uint8Array(memory.buffer, 0, bytes.length);
1343
+ view.set(bytes);
1344
+ return exports.hash(0, bytes.length);
1345
+ };
1346
+ console.log('[Fingerprint] Standalone polymorphic WASM loaded successfully. Using fast dynamic hashing.');
1347
+ if (this._cachedBuilder) {
1348
+ this._cachedBuilder.addRaw('wasm', 'true');
1349
+ }
1350
+ return;
1351
+ }
1352
+ // 1. Inject WASM loader script
1353
+ const script = document.createElement('script');
1354
+ script.src = wasmPath;
1355
+ await new Promise((resolve, reject) => {
1356
+ script.onload = resolve;
1357
+ script.onerror = reject;
1358
+ document.head.appendChild(script);
1359
+ });
1360
+
1361
+ // 2. Await global module factory
1362
+ if (typeof window.createFingerprintModule !== 'function') {
1363
+ throw new Error('WASM loader script did not expose createFingerprintModule.');
1364
+ }
1365
+
1366
+ // 3. Initialize module
1367
+ const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
1368
+ const wasmModule = await window.createFingerprintModule({
1369
+ instantiateWasm: (imports, successCallback) => {
1370
+ (async () => {
1371
+ try {
1372
+ const cached = await getCachedWasm(wasmUrl);
1373
+ if (cached) {
1374
+ let instance;
1375
+ if (cached instanceof WebAssembly.Module) {
1376
+ instance = await WebAssembly.instantiate(cached, imports);
1377
+ } else {
1378
+ const result = await WebAssembly.instantiate(cached, imports);
1379
+ instance = result.instance;
1380
+ }
1381
+ successCallback(instance, cached);
1382
+ return;
1383
+ }
1384
+
1385
+ const response = await fetch(wasmUrl);
1386
+ const arrayBuffer = await response.arrayBuffer();
1387
+
1388
+ let cachedData = arrayBuffer;
1389
+ let isModuleCached = false;
1390
+ try {
1391
+ const compiledModule = await WebAssembly.compile(arrayBuffer);
1392
+ const success = await cacheWasm(wasmUrl, compiledModule);
1393
+ if (success) {
1394
+ cachedData = compiledModule;
1395
+ isModuleCached = true;
1396
+ }
1397
+ } catch (e) {
1398
+ // Fallback if browser doesn't allow structured cloning of Compiled Modules
1399
+ }
1400
+
1401
+ if (!isModuleCached) {
1402
+ await cacheWasm(wasmUrl, arrayBuffer);
1403
+ }
1404
+
1405
+ let instance;
1406
+ if (cachedData instanceof WebAssembly.Module) {
1407
+ instance = await WebAssembly.instantiate(cachedData, imports);
1408
+ } else {
1409
+ const result = await WebAssembly.instantiate(arrayBuffer, imports);
1410
+ instance = result.instance;
1411
+ }
1412
+ successCallback(instance, cachedData);
1413
+ } catch (err) {
1414
+ console.warn('[Fingerprint] Custom WASM instantiation failed, falling back to default Emscripten loader:', err);
1415
+ successCallback(null);
1416
+ }
1417
+ })();
1418
+ return {}; // Async instantiation indicator for Emscripten
1419
+ }
1420
+ });
1421
+ if (typeof wasmModule._hash_string !== 'function') {
1422
+ throw new Error('WASM module did not export _hash_string.');
1423
+ }
1424
+ window.wasmModule = wasmModule;
1425
+ ClientLibrary.wasmModule = wasmModule;
1426
+
1427
+ // Static memory pool: 4KB pre-allocated buffer
1428
+ // Avoids heap malloc/free churn on rapid mouse and keyboard tracking
1429
+ const staticBufferSize = 4096;
1430
+ const staticBufferPtr = wasmModule._malloc(staticBufferSize);
1431
+ const encoder = new TextEncoder();
1432
+
1433
+ // 4. Swap hash function with WASM implementation
1434
+ activeCyrb53 = (str) => {
1435
+ const bytes = encoder.encode(str);
1436
+ if (bytes.length < staticBufferSize) {
1437
+ wasmModule.HEAPU8.set(bytes, staticBufferPtr);
1438
+ wasmModule.HEAPU8[staticBufferPtr + bytes.length] = 0; // null-terminator
1439
+ return wasmModule._hash_string(staticBufferPtr);
1440
+ }
1441
+ // Fallback if string exceeds static buffer size
1442
+ return wasmModule._hash_string(str);
1443
+ };
1444
+
1445
+ console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
1446
+ // Tag fingerprint with wasm marker
1447
+ if (this._cachedBuilder) {
1448
+ this._cachedBuilder.addRaw('wasm', 'true');
1449
+ }
1450
+ } catch (error) {
1451
+ console.warn('[Fingerprint] WASM module failed to load. Falling back to JS implementation. Error:', error);
1452
+ }
1453
+ }
1454
+ };
1455
+
1456
+ /**
1457
+ * @typedef {object} ClientBehaviorMetrics
1458
+ * @property {number} mouseEntropy - Mouse entropy estimate.
1459
+ * @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Mouse movement coordinates.
1460
+ * @property {number} keystrokeLatency - Average latency between keystrokes.
1461
+ * @property {boolean} honeypotInteraction - True if honeypot was triggered.
1462
+ * @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Click interaction history.
1463
+ * @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Touch movement trajectory.
1464
+ * @property {number} historyLength - Browser window.history.length.
1465
+ * @property {number} clientTimestamp - Metrics collection timestamp.
1466
+ * @property {string[]} [trapUrls] - Dynamically injected trap URLs.
1467
+ */
1468
+ /** @type {ClientBehaviorMetrics} */
1469
+ const metrics = {
1470
+ mouseEntropy: 0, // Retained for compatibility; analysis primarily evaluates trajectory history
1471
+ mouseMovementsHistory: [],
1472
+ touchMovementsHistory: [],
1473
+ clicksHistory: [],
1474
+ keystrokeLatency: 0,
1475
+ honeypotInteraction: false,
1476
+ historyLength: 0,
1477
+ clientTimestamp: 0,
1478
+ rendering: { fps: 0, jitter: 0, offscreenAnom: false },
1479
+ };
1480
+
1481
+ let lastMousePos = { x: 0, y: 0 };
1482
+ let mouseMovementsHistory = []; // Mouse movement trajectory
1483
+ let touchMovementsHistory = []; // Touch movement trajectory
1484
+ const TOUCH_HISTORY_MAX = 100;
1485
+ const MOUSE_HISTORY_MAX = 100;
1486
+ let clicksHistory = [];
1487
+ const CLICKS_HISTORY_MAX = 50;
1488
+ let activeHoneypotListeners = new Map();
1489
+ let keystrokeTimestamps = [];
1490
+ let keystrokeLatencies = [];
1491
+ let keystrokeDwellTimes = [];
1492
+ let keystrokeFlightTimes = [];
1493
+ const KEYSTROKE_HISTORY_MAX = 20; // Keep the last 20 keystroke events
1494
+
1495
+
1496
+
1497
+ // Exporter les fonctions individuellement pour la compatibilité ascendante
1498
+ export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
1499
+ export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
1500
+ export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
1501
+ export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
1502
+ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
1503
+ export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
1504
+ export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
1505
+ export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
1506
+ export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
1507
+ export const startMotionTracker = ClientLibrary.startMotionTracker.bind(ClientLibrary);
1508
+ export const detectTamperedPrototypes = ClientLibrary.detectTamperedPrototypes.bind(ClientLibrary);
1509
+ export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
1510
+ export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
1511
+ export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
1512
+ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
1513
+ export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
1514
+ export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
1515
+ export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
1516
+ export const initializeWasm = ClientLibrary.initializeWasm.bind(ClientLibrary);
1517
+ export const injectTrapLinks = ClientLibrary.injectTrapLinks.bind(ClientLibrary);
1518
+ export const injectPhantomTraps = ClientLibrary.injectPhantomTraps.bind(ClientLibrary);
1519
+ export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
1520
+ export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrary);
1521
+ export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
1522
+ export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
1523
+ export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
1524
+
1525
+ // Export the internal object for testing purposes
1526
+ export default ClientLibrary;
1527
+
1528
+ // --- Global Export for Browser ---
1529
+ // Attach the library to the window object to make it accessible from inline scripts.
1530
+ if (typeof window !== 'undefined') {
1531
+ window.ClientLibrary = ClientLibrary;
1464
1532
  }