@anonympins/fingerprint 0.1.3 → 0.2.0

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.
@@ -1,459 +1,471 @@
1
- import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
- import { solveChallenge } from './pow.solver.js';
3
-
4
- const ClientLibrary = {
5
- // Cache pour éviter de recalculer les constantes (Hardware, etc.)
6
- _cachedBuilder: null,
7
- /**
8
- * Génère l'empreinte de l'appareil actuel.
9
- */
10
- getDeviceFingerprint() {
11
- if (typeof window === "undefined") {
12
- console.error("getDeviceFingerprint can only be called on the client-side.");
13
- return "";
14
- }
15
-
16
- if (!this._cachedBuilder) {
17
- const nav = window.navigator;
18
- const screen = window.screen;
19
-
20
- this._cachedBuilder = new FingerprintBuilder();
21
-
22
- // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
23
- this._cachedBuilder.add(
24
- "hw",
25
- `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
26
- );
27
-
28
- // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
29
- this._cachedBuilder.add(
30
- "geo",
31
- `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
32
- );
33
-
34
- // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
35
- this._cachedBuilder.add(
36
- "scr",
37
- `${screen.width}x${screen.height}_${screen.colorDepth}`,
38
- );
39
-
40
- // 4. Platform (Stable) : OS, Engine
41
- this._cachedBuilder.add("os", nav.platform);
42
-
43
- // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
44
- try {
45
- const canvas = document.createElement("canvas");
46
- const gl =
47
- canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
48
- if (gl) {
49
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
50
- if (debugInfo) {
51
- const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
52
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
53
- this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
54
- }
55
- }
56
- } catch (e) {
57
- }
58
-
59
- // 6. Canvas Fingerprinting (Rendering quirks)
60
- try {
61
- const canvas = document.createElement("canvas");
62
- const ctx = canvas.getContext("2d");
63
- if (ctx) {
64
- canvas.width = 200;
65
- canvas.height = 50;
66
- ctx.textBaseline = "alphabetic";
67
- ctx.font = "14px 'Arial'";
68
- ctx.fillStyle = "#f60";
69
- ctx.fillRect(125, 1, 62, 20);
70
- ctx.fillStyle = "#069";
71
- ctx.fillText("fingerprint", 2, 15);
72
- ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
73
- ctx.fillText("fingerprint", 4, 17);
74
- this._cachedBuilder.add("cvs", canvas.toDataURL());
75
- }
76
- } catch (e) {
77
- }
78
-
79
- // 7. Bot Detection (Indication cachée)
80
- if (nav.webdriver) this._cachedBuilder.add("bot", "true");
81
- }
82
-
83
- return this._cachedBuilder.toString();
84
- },
85
-
86
- /**
87
- * Génère une signature de requête incluant le contexte.
88
- * @param {object} payload
89
- */
90
- generateRequestSignature(payload = {}) {
91
- const deviceFp = this.getDeviceFingerprint();
92
- const sortedPayload = Object.keys(payload)
93
- .sort()
94
- .map((k) => `${k}=${payload[k]}`)
95
- .join("&");
96
- const payloadHash = cyrb53(sortedPayload);
97
- return `${deviceFp}|req:${payloadHash}`;
98
- },
99
-
100
- /**
101
- * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
102
- * @param {object} payload - Les données à signer.
103
- * @param {string} secret - La clé secrète partagée.
104
- * @returns {Promise<string>} La signature hexadécimale.
105
- */
106
- async generateClientSideSignature(payload, secret) {
107
- const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
108
- const encoder = new TextEncoder();
109
- const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
110
- name: "HMAC",
111
- hash: "SHA-256"
112
- }, false, ["sign"]);
113
- const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
114
- const hashArray = Array.from(new Uint8Array(signatureBuffer));
115
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
116
- },
117
-
118
- /**
119
- * @internal
120
- * Resets the cached fingerprint builder. Used for testing purposes.
121
- */
122
- _resetCache() {
123
- this._cachedBuilder = null;
124
- },
125
-
126
- /**
127
- * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
128
- * À appeler une fois sur la page.
129
- */
130
- startMouseEntropyTracker() {
131
- // S'assurer de ne pas attacher l'écouteur plusieurs fois
132
- if (mouseMovements > 0) return;
133
-
134
- document.addEventListener('mousemove', (e) => {
135
- const dx = e.clientX - lastMousePos.x;
136
- const dy = e.clientY - lastMousePos.y;
137
- // Une métrique simple : la somme des distances. Un bot aura souvent 0.
138
- metrics.mouseEntropy += Math.sqrt(dx * dx + dy * dy);
139
- lastMousePos = {x: e.clientX, y: e.clientY};
140
- mouseMovements++;
141
- }, {passive: true});
142
- },
143
-
144
- /**
145
- * Démarre le suivi de la dynamique de frappe pour calculer la latence.
146
- * À appeler une fois sur la page.
147
- */
148
- startKeystrokeDynamicsTracker() {
149
- // S'assurer de ne pas attacher l'écouteur plusieurs fois
150
- if (keystrokeTimestamps.length > 0) return;
151
-
152
- document.addEventListener('keydown', () => {
153
- const now = performance.now();
154
- if (keystrokeTimestamps.length > 0) {
155
- const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
156
- const latency = now - lastTimestamp;
157
- // On ignore les latences irréalistes (trop longues ou trop courtes)
158
- if (latency > 10 && latency < 2000) { // Augmenté à 2s
159
- if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
160
- keystrokeLatencies.shift(); // Garder la taille de l'historique
161
- }
162
- keystrokeLatencies.push(latency);
163
- }
164
- }
165
- keystrokeTimestamps.push(now);
166
- }, {passive: true});
167
- },
168
-
169
- /**
170
- * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
171
- * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
172
- * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
173
- */
174
- initializeHoneypots(honeypotFieldNames) {
175
- // 1. Nettoyer les anciens écouteurs
176
- activeHoneypotListeners.forEach((listener, field) => {
177
- field.removeEventListener('input', listener);
178
- });
179
- activeHoneypotListeners.clear();
180
-
181
- // 2. Ajouter les nouveaux écouteurs
182
- honeypotFieldNames.forEach(fieldName => {
183
- const field = document.querySelector(`[name="${fieldName}"]`);
184
- if (field) {
185
- // On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
186
- // L'option { once: true } est excellente, mais pour une réinitialisation complète,
187
- // il est plus propre de gérer le nettoyage nous-mêmes.
188
- const listener = () => {
189
- this.onHoneypotTrigger();
190
- // Se supprime lui-même après exécution, comme { once: true }
191
- field.removeEventListener('input', listener);
192
- };
193
- field.addEventListener('input', listener);
194
- activeHoneypotListeners.set(field, listener); // On stocke la référence
195
- }
196
- });
197
- },
198
-
199
- /**
200
- * Récupère les métriques comportementales collectées.
201
- * À appeler avant d'envoyer une requête sensible.
202
- * @returns {ClientBehaviorMetrics}
203
- */
204
- getClientBehaviorMetrics() {
205
- // Normalise l'entropie de la souris
206
- if (mouseMovements > 10) {
207
- metrics.mouseEntropy /= mouseMovements;
208
- }
209
- // Calcule la latence moyenne des frappes
210
- if (keystrokeLatencies.length > 0) {
211
- const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
212
- metrics.keystrokeLatency = sum / keystrokeLatencies.length;
213
- } else {
214
- metrics.keystrokeLatency = 0;
215
- }
216
- return metrics;
217
- },
218
-
219
- /**
220
- * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
221
- * @param {RequestInfo} resource
222
- * @param {RequestInit} [options]
223
- * @returns {Promise<Response>}
224
- */
225
- async protectedFetch(resource, options = {}) {
226
- const fp = this.getDeviceFingerprint();
227
- const behavior = this.getClientBehaviorMetrics();
228
-
229
- const headers = new Headers(options.headers || {});
230
- headers.set('X-Device-Fingerprint', fp);
231
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
232
-
233
- options.headers = headers;
234
- return fetch(resource, options);
235
- },
236
-
237
- // --- Système d'interception de Fetch robuste et anti-conflit ---
238
-
239
- _isFetchPatched: false,
240
- _interceptorChain: [],
241
- // On stocke la fonction fetch originale et on la lie à son contexte (window)
242
- // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
243
- _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
244
-
245
- /**
246
- * Adds an interceptor function to the `fetch` chain.
247
- * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
248
- * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
249
- * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
250
- */
251
- addFetchInterceptor(interceptor) {
252
- if (!this._isFetchPatched) {
253
- this.patchGlobalFetch();
254
- }
255
- this._interceptorChain.push(interceptor);
256
- },
257
-
258
- patchGlobalFetch() {
259
- if (this._isFetchPatched || !this._originalFetch) return;
260
-
261
- this._isFetchPatched = true;
262
- window.fetch = (resource, options) => {
263
- // Le "dispatcher" qui exécute la chaîne.
264
- const dispatch = (index, res, opts) => {
265
- if (index >= this._interceptorChain.length) {
266
- // Fin de la chaîne, on appelle le fetch original.
267
- return this._originalFetch(res, opts);
268
- }
269
- const nextInterceptor = this._interceptorChain[index];
270
- // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
271
- return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
272
- };
273
- return dispatch(0, resource, options || {});
274
- };
275
- },
276
-
277
- /**
278
- * La fonction qui est appelée lorsqu'un honeypot est déclenché.
279
- * @private
280
- */
281
- onHoneypotTrigger : () => {
282
- metrics.honeypotInteraction = true;
283
- // On pourrait même envoyer un signalement au serveur immédiatement.
284
- },
285
-
286
- /**
287
- * Initialise l'intercepteur de fingerprinting.
288
- * Il s'ajoute à la chaîne d'interception sans écraser les autres.
289
- * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
290
- * Si non fourni, protège les requêtes de même origine.
291
- */
292
- initializeFetch(targetDomains = []) {
293
- const fingerprintInterceptor = (resource, options, next) => {
294
- const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
295
- let shouldProtect = false;
296
-
297
- try {
298
- const url = new URL(requestUrl, window.location.origin);
299
- // Protéger si la liste de domaines est vide ET que la requête est de même origine,
300
- // OU si le domaine de la requête est dans la liste fournie.
301
- shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
302
- (targetDomains.length > 0 && targetDomains.includes(url.hostname));
303
- } catch (e) {
304
- // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
305
- // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
306
- // On protège par défaut si aucune liste de domaines n'est spécifiée.
307
- shouldProtect = targetDomains.length === 0;
308
- }
309
-
310
- if (shouldProtect) {
311
- const fp = this.getDeviceFingerprint();
312
- const behavior = this.getClientBehaviorMetrics();
313
- const headers = new Headers(options.headers || {});
314
- headers.set('X-Device-Fingerprint', fp);
315
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
316
- options.headers = headers;
317
- }
318
-
319
- // Passe la main à l'intercepteur suivant dans la chaîne.
320
- return next(resource, options);
321
- };
322
-
323
- this.addFetchInterceptor(fingerprintInterceptor);
324
- },
325
-
326
- /**
327
- * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
328
- * @param {Response} response - La réponse initiale (potentiellement 429).
329
- * @param {RequestInfo} resource - La ressource de la requête originale.
330
- * @param {RequestInit} options - Les options de la requête originale.
331
- * @returns {Promise<Response>} - La réponse de la requête réessayée.
332
- * @private
333
- */
334
- async solveChallengeAndRetry(response, resource, options) {
335
- if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
336
- return response;
337
- }
338
-
339
- try {
340
- const challengeData = await response.json();
341
- if (!challengeData.challenge || !challengeData.challenge.type) {
342
- return response; // Pas un challenge JSON valide
343
- }
344
-
345
- console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
346
- const solution = await solveChallenge(challengeData.challenge);
347
- console.log('[Fingerprint] Challenge solved. Retrying original request.');
348
-
349
- // Ajouter la solution aux paramètres de la requête pour le nouvel essai
350
- const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
351
- // Le type de challenge est maintenant `cpu_mem` pour les API
352
- url.searchParams.set('pow_type', 'cpu_mem');
353
- url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
354
-
355
- // La solution peut être un objet (pour les challenges combinés) ou une valeur simple
356
- Object.entries(solution).forEach(([key, value]) => {
357
- // Le serveur attend pow_solution_cpu et pow_solution_mem
358
- // On s'assure que la clé est bien 'cpu' ou 'mem' avant de l'ajouter
359
- if (key === 'cpu' || key === 'mem') {
360
- url.searchParams.set(`pow_solution_${key}`, String(value));
361
- }
362
- });
363
-
364
- // On utilise la chaîne d'intercepteurs pour la requête réessayée,
365
- // ce qui garantit que le fetch original est appelé avec le bon contexte.
366
- // Cela évite de réintroduire l'erreur "Illegal invocation".
367
- return window.fetch(url.toString(), options);
368
- } catch (e) {
369
- console.error('[Fingerprint] Failed to solve or retry challenge:', e);
370
- return response; // Retourne la réponse 429 originale en cas d'échec
371
- }
372
- },
373
- /**
374
- * @typedef {object} ClientConfig
375
- * @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
376
- * @property {boolean} [keystrokes=true] - Activer le suivi de la dynamique de frappe.
377
- * @property {string[]} [honeypots] - Noms des champs de formulaire honeypot à initialiser.
378
- * @property {object} [fetch] - Configuration pour l'interception de fetch.
379
- * @property {string[]} [fetch.targetDomains] - Domaines à protéger. Si non fourni, protège les requêtes de même origine.
380
- */
381
-
382
- /**
383
- * Initialise toutes les protections côté client en une seule fois.
384
- * C'est la méthode d'initialisation recommandée.
385
- * @param {ClientConfig} [config={}] - L'objet de configuration.
386
- */
387
- initializeClient(config = {}) {
388
- const {
389
- mouse = true,
390
- keystrokes = true,
391
- honeypots = [],
392
- fetch: fetchConfig = {},
393
- } = config;
394
-
395
- if (mouse) {
396
- this.startMouseEntropyTracker();
397
- }
398
- if (keystrokes) {
399
- this.startKeystrokeDynamicsTracker();
400
- }
401
- if (honeypots.length > 0) {
402
- this.initializeHoneypots(honeypots);
403
- }
404
- // On active l'interception si `fetch` est configuré, même avec un objet vide.
405
- if (config.fetch) {
406
- this.initializeFetch(fetchConfig.targetDomains);
407
-
408
- // Ajoute l'intercepteur pour la résolution de challenge
409
- if (fetchConfig.handleChallenges !== false) {
410
- this.addFetchInterceptor(async (resource, options, next) => {
411
- const response = await next(resource, options);
412
- return this.solveChallengeAndRetry(response, resource, options);
413
- });
414
- }
415
- }
416
- }
417
- };
418
-
419
- /**
420
- * @typedef {object} ClientBehaviorMetrics
421
- * @property {number} mouseEntropy - Entropie des mouvements de la souris.
422
- * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
423
- * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
424
- */
425
-
426
- /** @type {ClientBehaviorMetrics} */
427
- const metrics = {
428
- mouseEntropy: 0,
429
- keystrokeLatency: 0,
430
- honeypotInteraction: false,
431
- };
432
-
433
- let lastMousePos = { x: 0, y: 0 };
434
- let mouseMovements = 0;
435
- let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
436
- let keystrokeTimestamps = [];
437
- let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
438
- const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
439
-
440
-
441
-
442
- // Exporter les fonctions individuellement pour la compatibilité ascendante
443
- export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
444
- export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
445
- export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
446
- export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
447
- export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
448
- export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
449
- export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
450
- export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
451
- export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
452
- export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
453
- export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
454
- export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
455
- export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
456
- export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
457
-
458
- // Export the internal object for testing purposes
1
+ import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
+ import { solveChallenge } from './pow.solver.js';
3
+
4
+ const ClientLibrary = {
5
+ // Cache pour éviter de recalculer les constantes (Hardware, etc.)
6
+ _cachedBuilder: null,
7
+ /**
8
+ * Génère l'empreinte de l'appareil actuel.
9
+ */
10
+ getDeviceFingerprint() {
11
+ if (typeof window === "undefined") {
12
+ console.error("getDeviceFingerprint can only be called on the client-side.");
13
+ return "";
14
+ }
15
+
16
+ if (!this._cachedBuilder) {
17
+ const nav = window.navigator;
18
+ const screen = window.screen;
19
+
20
+ this._cachedBuilder = new FingerprintBuilder();
21
+
22
+ // 1. Hardware (Très stable) : Cœurs, RAM, GPU (si dispo via canvas), Touch
23
+ this._cachedBuilder.add(
24
+ "hw",
25
+ `${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
26
+ );
27
+
28
+ // 2. Geo/Locale (Stable sauf voyage/VPN) : Timezone, Langue
29
+ this._cachedBuilder.add(
30
+ "geo",
31
+ `${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
32
+ );
33
+
34
+ // 3. Screen (Stable sauf changement moniteur/zoom) : Dimensions, ColorDepth
35
+ this._cachedBuilder.add(
36
+ "scr",
37
+ `${screen.width}x${screen.height}_${screen.colorDepth}`,
38
+ );
39
+
40
+ // 4. Platform (Stable) : OS, Engine
41
+ this._cachedBuilder.add("os", nav.platform);
42
+
43
+ // 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
44
+ try {
45
+ const canvas = document.createElement("canvas");
46
+ const gl =
47
+ canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
48
+ if (gl) {
49
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
50
+ if (debugInfo) {
51
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
52
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
53
+ this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
54
+ }
55
+ }
56
+ } catch (e) {
57
+ }
58
+
59
+ // 6. Canvas Fingerprinting (Rendering quirks)
60
+ try {
61
+ const canvas = document.createElement("canvas");
62
+ const ctx = canvas.getContext("2d");
63
+ if (ctx) {
64
+ canvas.width = 200;
65
+ canvas.height = 50;
66
+ ctx.textBaseline = "alphabetic";
67
+ ctx.font = "14px 'Arial'";
68
+ ctx.fillStyle = "#f60";
69
+ ctx.fillRect(125, 1, 62, 20);
70
+ ctx.fillStyle = "#069";
71
+ ctx.fillText("fingerprint", 2, 15);
72
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
73
+ ctx.fillText("fingerprint", 4, 17);
74
+ this._cachedBuilder.add("cvs", canvas.toDataURL());
75
+ }
76
+ } catch (e) {
77
+ }
78
+
79
+ // 7. Bot Detection (Indication cachée)
80
+ if (nav.webdriver) this._cachedBuilder.add("bot", "true");
81
+ }
82
+
83
+ return this._cachedBuilder.toString();
84
+ },
85
+
86
+ /**
87
+ * Génère une signature de requête incluant le contexte.
88
+ * @param {object} payload
89
+ */
90
+ generateRequestSignature(payload = {}) {
91
+ const deviceFp = this.getDeviceFingerprint();
92
+ const sortedPayload = Object.keys(payload)
93
+ .sort()
94
+ .map((k) => `${k}=${payload[k]}`)
95
+ .join("&");
96
+ const payloadHash = cyrb53(sortedPayload);
97
+ return `${deviceFp}|req:${payloadHash}`;
98
+ },
99
+
100
+ /**
101
+ * Génère une signature HMAC-SHA256 en utilisant l'API Web Crypto.
102
+ * @param {object} payload - Les données à signer.
103
+ * @param {string} secret - La clé secrète partagée.
104
+ * @returns {Promise<string>} La signature hexadécimale.
105
+ */
106
+ async generateClientSideSignature(payload, secret) {
107
+ const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
108
+ const encoder = new TextEncoder();
109
+ const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
110
+ name: "HMAC",
111
+ hash: "SHA-256"
112
+ }, false, ["sign"]);
113
+ const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
114
+ const hashArray = Array.from(new Uint8Array(signatureBuffer));
115
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
116
+ },
117
+
118
+ /**
119
+ * @internal
120
+ * Resets the cached fingerprint builder. Used for testing purposes.
121
+ */
122
+ _resetCache() {
123
+ this._cachedBuilder = null;
124
+ },
125
+
126
+ /**
127
+ * Démarre le suivi des mouvements de la souris pour calculer l'entropie.
128
+ * À appeler une fois sur la page.
129
+ */
130
+ startMouseEntropyTracker() {
131
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
132
+ if (mouseMovements > 0) return;
133
+
134
+ document.addEventListener('mousemove', (e) => {
135
+ const dx = e.clientX - lastMousePos.x;
136
+ const dy = e.clientY - lastMousePos.y;
137
+ // Une métrique simple : la somme des distances. Un bot aura souvent 0.
138
+ metrics.mouseEntropy += Math.sqrt(dx * dx + dy * dy);
139
+ lastMousePos = {x: e.clientX, y: e.clientY};
140
+ mouseMovements++;
141
+ }, {passive: true});
142
+ },
143
+
144
+ /**
145
+ * Démarre le suivi de la dynamique de frappe pour calculer la latence.
146
+ * À appeler une fois sur la page.
147
+ */
148
+ startKeystrokeDynamicsTracker() {
149
+ // S'assurer de ne pas attacher l'écouteur plusieurs fois
150
+ if (keystrokeTimestamps.length > 0) return;
151
+
152
+ document.addEventListener('keydown', () => {
153
+ const now = performance.now();
154
+ if (keystrokeTimestamps.length > 0) {
155
+ const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
156
+ const latency = now - lastTimestamp;
157
+ // On ignore les latences irréalistes (trop longues ou trop courtes)
158
+ if (latency > 10 && latency < 2000) { // Augmenté à 2s
159
+ if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
160
+ keystrokeLatencies.shift(); // Garder la taille de l'historique
161
+ }
162
+ keystrokeLatencies.push(latency);
163
+ }
164
+ }
165
+ keystrokeTimestamps.push(now);
166
+ }, {passive: true});
167
+ },
168
+
169
+ /**
170
+ * Initialise ou réinitialise les honeypots côté client pour une détection immédiate.
171
+ * Les anciens écouteurs sont supprimés avant d'en ajouter de nouveaux.
172
+ * @param {string[]} honeypotFieldNames - Noms des champs de formulaire cachés.
173
+ */
174
+ initializeHoneypots(honeypotFieldNames) {
175
+ // 1. Nettoyer les anciens écouteurs
176
+ activeHoneypotListeners.forEach((listener, field) => {
177
+ field.removeEventListener('input', listener);
178
+ });
179
+ activeHoneypotListeners.clear();
180
+
181
+ // 2. Ajouter les nouveaux écouteurs
182
+ honeypotFieldNames.forEach(fieldName => {
183
+ const field = document.querySelector(`[name="${fieldName}"]`);
184
+ if (field) {
185
+ // On utilise une fonction nommée (ou une référence) pour pouvoir la supprimer plus tard.
186
+ // L'option { once: true } est excellente, mais pour une réinitialisation complète,
187
+ // il est plus propre de gérer le nettoyage nous-mêmes.
188
+ const listener = () => {
189
+ this.onHoneypotTrigger();
190
+ // Se supprime lui-même après exécution, comme { once: true }
191
+ field.removeEventListener('input', listener);
192
+ };
193
+ field.addEventListener('input', listener);
194
+ activeHoneypotListeners.set(field, listener); // On stocke la référence
195
+ }
196
+ });
197
+ },
198
+
199
+ /**
200
+ * Récupère les métriques comportementales collectées.
201
+ * À appeler avant d'envoyer une requête sensible.
202
+ * @returns {ClientBehaviorMetrics}
203
+ */
204
+ getClientBehaviorMetrics() {
205
+ // Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
206
+ metrics.clientTimestamp = Date.now();
207
+
208
+ // Normalise l'entropie de la souris
209
+ if (mouseMovements > 10) {
210
+ metrics.mouseEntropy /= mouseMovements;
211
+ }
212
+ // Calcule la latence moyenne des frappes
213
+ if (keystrokeLatencies.length > 0) {
214
+ const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
215
+ metrics.keystrokeLatency = sum / keystrokeLatencies.length;
216
+ } else {
217
+ metrics.keystrokeLatency = 0;
218
+ }
219
+ return metrics;
220
+ },
221
+
222
+ /**
223
+ * Enrichit une requête fetch avec les en-têtes de fingerprinting et de comportement.
224
+ * @param {RequestInfo} resource
225
+ * @param {RequestInit} [options]
226
+ * @returns {Promise<Response>}
227
+ */
228
+ async protectedFetch(resource, options = {}) {
229
+ const fp = this.getDeviceFingerprint();
230
+ const behavior = this.getClientBehaviorMetrics();
231
+
232
+ const headers = new Headers(options.headers || {});
233
+ headers.set('X-Device-Fingerprint', fp);
234
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
235
+
236
+ options.headers = headers;
237
+ return fetch(resource, options);
238
+ },
239
+
240
+ // --- Système d'interception de Fetch robuste et anti-conflit ---
241
+
242
+ _isFetchPatched: false,
243
+ _interceptorChain: [],
244
+ // On stocke la fonction fetch originale et on la lie à son contexte (window)
245
+ // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
246
+ _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
247
+
248
+ /**
249
+ * Adds an interceptor function to the `fetch` chain.
250
+ * Chaque intercepteur reçoit `resource`, `options`, et une fonction `next`.
251
+ * Il DOIT appeler `next(resource, options)` pour continuer la chaîne.
252
+ * @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
253
+ */
254
+ addFetchInterceptor(interceptor) {
255
+ if (!this._isFetchPatched) {
256
+ this.patchGlobalFetch();
257
+ }
258
+ this._interceptorChain.push(interceptor);
259
+ },
260
+
261
+ patchGlobalFetch() {
262
+ if (this._isFetchPatched || !this._originalFetch) return;
263
+
264
+ this._isFetchPatched = true;
265
+ window.fetch = (resource, options) => {
266
+ // Le "dispatcher" qui exécute la chaîne.
267
+ const dispatch = (index, res, opts) => {
268
+ if (index >= this._interceptorChain.length) {
269
+ // Fin de la chaîne, on appelle le fetch original.
270
+ return this._originalFetch(res, opts);
271
+ }
272
+ const nextInterceptor = this._interceptorChain[index];
273
+ // Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
274
+ return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
275
+ };
276
+ return dispatch(0, resource, options || {});
277
+ };
278
+ },
279
+
280
+ /**
281
+ * La fonction qui est appelée lorsqu'un honeypot est déclenché.
282
+ * @private
283
+ */
284
+ onHoneypotTrigger : () => {
285
+ metrics.honeypotInteraction = true;
286
+ // On pourrait même envoyer un signalement au serveur immédiatement.
287
+ },
288
+
289
+ /**
290
+ * Initialise l'intercepteur de fingerprinting.
291
+ * Il s'ajoute à la chaîne d'interception sans écraser les autres.
292
+ * @param {string[]} [targetDomains] - Optionnel. Liste de domaines à protéger.
293
+ * Si non fourni, protège les requêtes de même origine.
294
+ */
295
+ initializeFetch(targetDomains = []) {
296
+ const fingerprintInterceptor = (resource, options, next) => {
297
+ const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
298
+ let shouldProtect = false;
299
+
300
+ try {
301
+ const url = new URL(requestUrl, window.location.origin);
302
+ // Protéger si la liste de domaines est vide ET que la requête est de même origine,
303
+ // OU si le domaine de la requête est dans la liste fournie.
304
+ shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
305
+ (targetDomains.length > 0 && targetDomains.includes(url.hostname));
306
+ } catch (e) {
307
+ // Si l'URL est relative (ex: '/api/data'), new URL() ne lèvera pas d'erreur.
308
+ // Ce bloc est une sécurité pour les cas où l'URL serait malformée.
309
+ // On protège par défaut si aucune liste de domaines n'est spécifiée.
310
+ shouldProtect = targetDomains.length === 0;
311
+ }
312
+
313
+ if (shouldProtect) {
314
+ const fp = this.getDeviceFingerprint();
315
+ const behavior = this.getClientBehaviorMetrics();
316
+ const headers = new Headers(options.headers || {});
317
+ headers.set('X-Device-Fingerprint', fp);
318
+ headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
319
+ options.headers = headers;
320
+ }
321
+
322
+ // Passe la main à l'intercepteur suivant dans la chaîne.
323
+ return next(resource, options);
324
+ };
325
+
326
+ this.addFetchInterceptor(fingerprintInterceptor);
327
+ },
328
+
329
+ /**
330
+ * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
331
+ * @param {Response} response - La réponse initiale (potentiellement 429).
332
+ * @param {RequestInfo} resource - La ressource de la requête originale.
333
+ * @param {RequestInit} options - Les options de la requête originale.
334
+ * @returns {Promise<Response>} - La réponse de la requête réessayée.
335
+ * @private
336
+ */
337
+ async solveChallengeAndRetry(response, resource, options) {
338
+ if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
339
+ return response;
340
+ }
341
+
342
+ try {
343
+ const challengeData = await response.json();
344
+ if (!challengeData.challenge || !challengeData.challenge.type) {
345
+ return response; // Pas un challenge JSON valide
346
+ }
347
+
348
+ console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
349
+ const solution = await solveChallenge(challengeData.challenge);
350
+ console.log('[Fingerprint] Challenge solved. Retrying original request.');
351
+
352
+ // Ajouter la solution aux paramètres de la requête pour le nouvel essai
353
+ const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
354
+ // Le type de challenge est maintenant `cpu_mem` pour les API
355
+ url.searchParams.set('pow_type', 'cpu_mem');
356
+ url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
357
+
358
+ // La solution est un objet { cpu: ..., mem: ... }. Le serveur attend pow_solution_cpu et pow_solution_mem.
359
+ Object.entries(solution).forEach(([key, value]) => {
360
+ url.searchParams.set(`pow_solution_${key}`, String(value));
361
+ });
362
+
363
+ // Pour le challenge d'optimisation, la solution est un tableau d'objets
364
+ if (solution.population) {
365
+ url.searchParams.set('pow_solution_population', JSON.stringify(solution.population));
366
+ }
367
+
368
+ // Pour le challenge de travail utile
369
+ if (solution.work_result) {
370
+ url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
371
+ url.searchParams.set('pow_problem_id', solution.problem_id);
372
+ }
373
+
374
+ // On utilise la chaîne d'intercepteurs pour la requête réessayée,
375
+ // ce qui garantit que le fetch original est appelé avec le bon contexte.
376
+ // Cela évite de réintroduire l'erreur "Illegal invocation".
377
+ return window.fetch(url.toString(), options);
378
+ } catch (e) {
379
+ console.error('[Fingerprint] Failed to solve or retry challenge:', e);
380
+ return response; // Retourne la réponse 429 originale en cas d'échec
381
+ }
382
+ },
383
+ /**
384
+ * @typedef {object} ClientConfig
385
+ * @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
386
+ * @property {boolean} [keystrokes=true] - Activer le suivi de la dynamique de frappe.
387
+ * @property {string[]} [honeypots] - Noms des champs de formulaire honeypot à initialiser.
388
+ * @property {object} [fetch] - Configuration pour l'interception de fetch.
389
+ * @property {string[]} [fetch.targetDomains] - Domaines à protéger. Si non fourni, protège les requêtes de même origine.
390
+ */
391
+
392
+ /**
393
+ * Initialise toutes les protections côté client en une seule fois.
394
+ * C'est la méthode d'initialisation recommandée.
395
+ * @param {ClientConfig} [config={}] - L'objet de configuration.
396
+ */
397
+ initializeClient(config = {}) {
398
+ const {
399
+ mouse = true,
400
+ keystrokes = true,
401
+ honeypots = [],
402
+ fetch: fetchConfig = {},
403
+ } = config;
404
+
405
+ if (mouse) {
406
+ this.startMouseEntropyTracker();
407
+ }
408
+ if (keystrokes) {
409
+ this.startKeystrokeDynamicsTracker();
410
+ }
411
+ if (honeypots.length > 0) {
412
+ this.initializeHoneypots(honeypots);
413
+ }
414
+ // On active l'interception si `fetch` est configuré, même avec un objet vide.
415
+ if (config.fetch) {
416
+ this.initializeFetch(fetchConfig.targetDomains);
417
+
418
+ // Ajoute l'intercepteur pour la résolution de challenge
419
+ if (fetchConfig.handleChallenges !== false) {
420
+ this.addFetchInterceptor(async (resource, options, next) => {
421
+ const response = await next(resource, options);
422
+ return this.solveChallengeAndRetry(response, resource, options);
423
+ });
424
+ }
425
+ }
426
+ }
427
+ };
428
+
429
+ /**
430
+ * @typedef {object} ClientBehaviorMetrics
431
+ * @property {number} mouseEntropy - Entropie des mouvements de la souris.
432
+ * @property {number} keystrokeLatency - Latence moyenne entre les frappes.
433
+ * @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
434
+ * @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
435
+ */
436
+
437
+ /** @type {ClientBehaviorMetrics} */
438
+ const metrics = {
439
+ mouseEntropy: 0,
440
+ keystrokeLatency: 0,
441
+ honeypotInteraction: false,
442
+ clientTimestamp: 0,
443
+ };
444
+
445
+ let lastMousePos = { x: 0, y: 0 };
446
+ let mouseMovements = 0;
447
+ let activeHoneypotListeners = new Map(); // Garde une trace des écouteurs actifs
448
+ let keystrokeTimestamps = [];
449
+ let keystrokeLatencies = []; // NOUVEAU: Tableau dédié pour les latences
450
+ const KEYSTROKE_HISTORY_MAX = 20; // On garde l'historique des 20 dernières frappes
451
+
452
+
453
+
454
+ // Exporter les fonctions individuellement pour la compatibilité ascendante
455
+ export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
456
+ export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
457
+ export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
458
+ export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
459
+ export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
460
+ export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
461
+ export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
462
+ export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
463
+ export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
464
+ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
465
+ export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
466
+ export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
467
+ export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
468
+ export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
469
+
470
+ // Export the internal object for testing purposes
459
471
  export default ClientLibrary;