@anonympins/fingerprint 0.0.8 → 0.1.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.
package/README.md CHANGED
@@ -25,7 +25,7 @@ The process unfolds in three steps:
25
25
  * **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
26
26
  * **High Suspicion**: For the most suspicious requests, the system issues a high-difficulty combined CPU/Memory challenge. The architecture allows for plugging in more complex challenges like CAPTCHAs if needed.
27
27
 
28
- Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period.
28
+ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges for a set period. For API clients, the challenge is delivered as a `429` JSON response, and the client is expected to solve it and retry the request.
29
29
 
30
30
  ## Features
31
31
 
@@ -81,21 +81,33 @@ const securityConfig = {
81
81
  headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
82
82
  requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
83
83
  inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
84
+ behaviorScore: 0.7, // Penalizes non-human interactions (no mouse/keyboard activity)
84
85
  honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
85
86
  },
87
+ // A new, non-suspicious device will always have its score adjusted to a minimum of 1, ensuring it receives a minimal, almost imperceptible challenge on its first visit.
86
88
  thresholds: {
87
89
  low: 20, // Score from which a CPU challenge is issued
88
90
  medium: 45, // Score for a more difficult combined CPU/Memory challenge
89
91
  high: 75, // Score for a very difficult challenge
90
92
  block: 95, // Score above which the request is blocked outright (HTTP 403)
91
- isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
93
+ isStaticResource: (req) => req.path.startsWith('/static/'), // Optional: Custom function to identify static resources
94
+ isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json') // Optional: Custom function to identify API requests
92
95
  },
96
+ // (Optional) Configure the duration (in milliseconds) for various temporary data.
97
+ ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
98
+ challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
99
+ deviceIdCookieMaxAge: undefined, // By default, it's a session cookie. Set a value in ms for a persistent cookie.
100
+
93
101
  patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
94
- velocityThreshold: 200, // ms between requests to be considered "fast"
95
- burstThreshold: 500, // ms for identical requests to be a "burst"
96
- scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
97
- historySize: 10, // Number of requests to keep for pattern analysis
98
- decayFactor: 0.9, // How quickly the pattern score decays over time
102
+ velocityThreshold: 200, // ms between requests to be considered "fast"
103
+ burstThreshold: 500, // ms for identical requests to be a "burst"
104
+ scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
105
+ scrapeBurstWeight: 40, // Additional weight for repeated scraping patterns
106
+ sequenceLength: 3, // Length of a request sequence to detect (e.g., A->B->C)
107
+ sequenceWeight: 60, // Penalty for repeating a sequence
108
+ historySize: 10, // Number of requests to keep for pattern analysis
109
+ decayFactor: 0.9, // How quickly the pattern score decays over time
110
+ inactivityReset: 30000, // ms of inactivity after which the pattern score is reset
99
111
  },
100
112
  honeypot: {
101
113
  // List of field names that are traps for bots.
@@ -140,10 +152,10 @@ const securityConfig = {
140
152
  // Useful for internal tools, trusted partners, or monitoring services.
141
153
  // This check is performed first for maximum efficiency.
142
154
  { type: 'allowlist', entries: [
143
- '192.168.1.100', // A specific internal IP
144
- '203.0.113.0/24', // A partner's network range
145
- '2001:db8::/32' // An IPv6 range
146
- ]},
155
+ '192.168.1.100', // A specific internal IP
156
+ '203.0.113.0/24', // A partner's network range
157
+ '2001:db8::/32' // An IPv6 range
158
+ ]},
147
159
  // Option 2: DNS-verified bots (e.g., search engine crawlers).
148
160
  // This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
149
161
  // The result is cached per IP to avoid repeated DNS lookups.
@@ -198,17 +210,41 @@ The main Express middleware. It orchestrates identification, suspicion calculati
198
210
 
199
211
  #### `configureStore(store)`
200
212
  Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
213
+ The library provides ready-to-use adapters for popular datastores like Redis and MongoDB, which automatically handle the Time-To-Live (TTL) required for temporary data like challenge secrets.
201
214
 
202
- See the Datastore Integration Guide for a complete example of creating a Redis store.
215
+ **Redis Example:**
203
216
 
204
217
  ```javascript
205
218
  import { configureStore } from './fingerprint.js';
206
- import { createRedisStore } from './redis-store.js'; // Assuming a redis store implementation exists
219
+ import { createRedisStore } from './redis-store.js';
220
+ import Redis from 'ioredis';
207
221
 
208
- const redisStore = createRedisStore(process.env.REDIS_URL);
222
+ const redisClient = new Redis(process.env.REDIS_URL);
223
+ const redisStore = createRedisStore(redisClient);
209
224
  configureStore(redisStore);
210
225
  ```
211
226
 
227
+ **MongoDB Example:**
228
+
229
+ ```javascript
230
+ import { configureStore } from './fingerprint.js';
231
+ import { createMongoDbStore } from './mongodb-store.js';
232
+ import { MongoClient } from 'mongodb';
233
+
234
+ const mongoClient = new MongoClient(process.env.MONGODB_URL);
235
+
236
+ // It's recommended to connect before your application starts listening.
237
+ await mongoClient.connect();
238
+
239
+ const mongoStore = createMongoDbStore(mongoClient.db('your-db-name'), 'sessions'); // 'sessions' is the collection name
240
+ configureStore(mongoStore);
241
+
242
+ // IMPORTANT: For automatic expiration of challenges and other temporary data to work,
243
+ // you must create a TTL index on the `expiresAt` field in your MongoDB collection.
244
+ // Run this command in the mongo shell:
245
+ // db.sessions.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })
246
+ ```
247
+
212
248
  #### `identifyRequest(req, res)`
213
249
  An asynchronous function that returns an identification string for a given request, based on its suspicion level (`device:<id>`, `suspicious_medium:<ip>`, etc.). Useful for integration with a custom rate-limiter.
214
250
 
@@ -300,34 +336,35 @@ To simplify setup, all client-side features can be enabled and configured throug
300
336
 
301
337
  ```javascript
302
338
  import { initializeClient } from './path/to/fingerprint.client.js';
303
-
304
- // --- EXAMPLES ---
305
-
306
- // Example 1: Enable all default protections and protect same-origin fetch requests.
339
+
340
+ /**
341
+ * Initializes all client-side protections.
342
+ * This is the recommended way to set up the client-side library.
343
+ */
307
344
  initializeClient({
308
- honeypots: ['email_confirm', 'user_nickname'], // Your honeypot field names
309
- fetch: {} // An empty object enables fetch protection for same-origin
310
- });
311
-
312
- // Example 2: Enable everything and protect a specific API domain.
313
- initializeClient({
314
- honeypots: ['email_confirm'],
345
+ // (Optional, default: true) Enable mouse movement tracking to detect non-human patterns.
346
+ // Set to `false` to disable.
347
+ mouse: true,
348
+
349
+ // (Optional, default: true) Enable keystroke dynamics tracking (timing between key presses).
350
+ // Set to `false` to disable.
351
+ keystrokes: true,
352
+
353
+ // (Optional) An array of `name` attributes for hidden form fields that act as bot traps.
354
+ honeypots: ['email_confirm', 'user_nickname', 'website_url'],
355
+
356
+ // (Optional) Enables automatic protection for `fetch` requests.
357
+ // If the `fetch` object is present, the protection is active.
315
358
  fetch: {
316
- targetDomains: ['api.yourdomain.com']
359
+ // (Optional) An array of domains to protect. If empty or not provided, it protects same-origin requests by default.
360
+ targetDomains: ['api.yourdomain.com', 'auth.yourdomain.com'],
361
+
362
+ // (Optional, default: true) If enabled, the client will automatically intercept 429 challenge responses,
363
+ // solve the PoW in the background, and retry the original request with the solution.
364
+ // This makes the protection seamless for API clients that use this library.
365
+ handleChallenges: true
317
366
  }
318
367
  });
319
-
320
- // Example 3: Enable only mouse and keystroke tracking, without patching fetch.
321
- initializeClient({
322
- mouse: true,
323
- keystrokes: true
324
- });
325
-
326
- // Example 4: Disable keystroke tracking but keep other defaults.
327
- initializeClient({
328
- keystrokes: false,
329
- fetch: {}
330
- });
331
368
  ```
332
369
 
333
370
  ### Client-Side Behavioral Analysis
@@ -352,15 +389,15 @@ This provides a proactive, first-line defense against simple bots. By setting up
352
389
  If you prefer not to modify global functions or need fine-grained control over which requests are protected, you can use the `protectedFetch` wrapper. You must use this function instead of the standard `fetch` for your API calls.
353
390
 
354
391
  - **`protectedFetch(resource, options)`**: A wrapper around the native `fetch` API that automatically enriches requests with security headers. It adds:
355
- - `X-Device-Fingerprint`: The client's device fingerprint.
356
- - `X-Behavior-Metrics`: A JSON string containing the metrics collected by the behavioral trackers (e.g., mouse entropy, honeypot interaction).
392
+ - `X-Device-Fingerprint`: The client's device fingerprint.
393
+ - `X-Behavior-Metrics`: A JSON string containing the metrics collected by the behavioral trackers (e.g., mouse entropy, honeypot interaction).
357
394
 
358
395
  **Example:**
359
396
 
360
397
  ```javascript
361
- import {
362
- initializeClient,
363
- protectedFetch
398
+ import {
399
+ initializeClient,
400
+ protectedFetch
364
401
  } from './path/to/fingerprint.client.js';
365
402
 
366
403
  // Start tracking user behavior as soon as the app loads.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
3
+ */
4
+ export const cyrb53 = (str, seed = 0) => {
5
+ let h1 = 0xdeadbeef ^ seed,
6
+ h2 = 0x41c6ce57 ^ seed;
7
+ for (let i = 0, ch; i < str.length; i++) {
8
+ ch = str.charCodeAt(i);
9
+ h1 = Math.imul(h1 ^ ch, 2654435761);
10
+ h2 = Math.imul(h2 ^ ch, 1597334677);
11
+ }
12
+ h1 =
13
+ Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
14
+ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
15
+ h2 =
16
+ Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
17
+ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
18
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
19
+ };
20
+
21
+ /**
22
+ * Classe pour construire une empreinte composite (Multi-Hash).
23
+ * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
24
+ */
25
+ export class FingerprintBuilder {
26
+ constructor() {
27
+ this.components = new Map();
28
+ }
29
+
30
+ /**
31
+ * Ajoute un composant au hash global.
32
+ * @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
33
+ * @param {string|number|boolean} value - La valeur brute à hasher
34
+ */
35
+ add(group, value) {
36
+ if (value === undefined || value === null) return this;
37
+ // On hash la valeur individuellement pour l'anonymiser et réduire sa taille
38
+ this.components.set(group, cyrb53(String(value)));
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Adds a raw component without hashing it.
44
+ * Useful for metrics that need to be read on the server.
45
+ * @param {string} group - The name of the group.
46
+ * @param {string|number} value - The raw value.
47
+ */
48
+ addRaw(group, value) {
49
+ if (value === undefined || value === null) return this;
50
+ this.components.set(group, value);
51
+ return this;
52
+ }
53
+ /**
54
+ * Génère la chaîne de signature finale.
55
+ * Trie les clés pour garantir un ordre déterministe.
56
+ */
57
+ toString() {
58
+ return Array.from(this.components.entries())
59
+ .sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
60
+ .map(([key, hash]) => `${key}:${hash}`)
61
+ .join("|");
62
+ }
63
+
64
+ /**
65
+ * Compares two fingerprints and returns a similarity score (0 to 1).
66
+ * Uses weights to give more importance to strong invariants (Canvas, GPU).
67
+ * @param {string} fpString1 - Fingerprint A
68
+ * @param {string} fpString2 - Fingerprint B
69
+ */
70
+ static compare(fpString1, fpString2) {
71
+ if (!fpString1 || !fpString2) return 0;
72
+
73
+ const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
74
+
75
+ const map1 = parse(fpString1);
76
+ const map2 = parse(fpString2);
77
+
78
+ // Poids de "véracité" (Entropie/Stabilité)
79
+ const weights = {
80
+ cvs: 4.0, // Canvas: Très haute entropie (Rendu unique)
81
+ gpu: 3.0, // GPU: Haute entropie (Matériel spécifique)
82
+ hw: 1.5, // Hardware: Moyenne entropie
83
+ scr: 1.0, // Screen: Moyenne
84
+ geo: 0.5, // Geo: Faible (VPN/Voyage)
85
+ os: 0.5, // OS: Faible (Générique)
86
+ bot: 0.0, // Bot: Informatif
87
+ };
88
+
89
+ let weightedMatches = 0;
90
+ let totalWeight = 0;
91
+
92
+ const allKeys = new Set([...map1.keys(), ...map2.keys()]);
93
+
94
+ allKeys.forEach((key) => {
95
+ const weight = weights[key] || 1.0;
96
+ totalWeight += weight;
97
+ if (map1.get(key) === map2.get(key)) {
98
+ weightedMatches += weight;
99
+ }
100
+ });
101
+
102
+ return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
103
+ }
104
+ }
@@ -1,4 +1,5 @@
1
1
  import { cyrb53, FingerprintBuilder } from './fingerprint.builder.js';
2
+ import { solveChallenge } from './pow.solver.js';
2
3
 
3
4
  const ClientLibrary = {
4
5
  // Cache pour éviter de recalculer les constantes (Hardware, etc.)
@@ -320,6 +321,44 @@ const ClientLibrary = {
320
321
  this.addFetchInterceptor(fingerprintInterceptor);
321
322
  },
322
323
 
324
+ /**
325
+ * Intercepte une réponse de challenge JSON, le résout, et réessaie la requête.
326
+ * @param {Response} response - La réponse initiale (potentiellement 429).
327
+ * @param {RequestInfo} resource - La ressource de la requête originale.
328
+ * @param {RequestInit} options - Les options de la requête originale.
329
+ * @returns {Promise<Response>} - La réponse de la requête réessayée.
330
+ * @private
331
+ */
332
+ async solveChallengeAndRetry(response, resource, options) {
333
+ if (response.status !== 429) {
334
+ return response;
335
+ }
336
+
337
+ try {
338
+ const challengeData = await response.json();
339
+ if (!challengeData.challenge || !challengeData.challenge.type) {
340
+ return response; // Pas un challenge JSON valide
341
+ }
342
+
343
+ console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
344
+ const solution = await solveChallenge(challengeData.challenge);
345
+ console.log('[Fingerprint] Challenge solved. Retrying original request.');
346
+
347
+ // Ajouter la solution aux paramètres de la requête pour le nouvel essai
348
+ const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
349
+ url.searchParams.set('pow_type', challengeData.challenge.type);
350
+ url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
351
+ // La solution peut être un objet (pour les challenges combinés) ou une valeur simple
352
+ Object.entries(solution).forEach(([key, value]) => {
353
+ url.searchParams.set(`pow_solution_${key}`, value);
354
+ });
355
+
356
+ return this._originalFetch(url.toString(), options);
357
+ } catch (e) {
358
+ console.error('[Fingerprint] Failed to solve or retry challenge:', e);
359
+ return response; // Retourne la réponse 429 originale en cas d'échec
360
+ }
361
+ },
323
362
  /**
324
363
  * @typedef {object} ClientConfig
325
364
  * @property {boolean} [mouse=true] - Activer le suivi de l'entropie de la souris.
@@ -339,7 +378,7 @@ const ClientLibrary = {
339
378
  mouse = true,
340
379
  keystrokes = true,
341
380
  honeypots = [],
342
- fetch: fetchConfig,
381
+ fetch: fetchConfig = {},
343
382
  } = config;
344
383
 
345
384
  if (mouse) {
@@ -351,8 +390,17 @@ const ClientLibrary = {
351
390
  if (honeypots.length > 0) {
352
391
  this.initializeHoneypots(honeypots);
353
392
  }
354
- if (fetchConfig !== undefined) {
393
+ // On vérifie si l'objet fetch est présent pour activer l'interception
394
+ if (Object.keys(fetchConfig).length > 0 || config.fetch) {
355
395
  this.initializeFetch(fetchConfig.targetDomains);
396
+
397
+ // Ajoute l'intercepteur pour la résolution de challenge
398
+ if (fetchConfig.handleChallenges !== false) {
399
+ this.addFetchInterceptor(async (resource, options, next) => {
400
+ const response = await next(resource, options);
401
+ return this.solveChallengeAndRetry(response, resource, options);
402
+ });
403
+ }
356
404
  }
357
405
  }
358
406
  };
@@ -394,6 +442,7 @@ export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(Client
394
442
  export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
395
443
  export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
396
444
  export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
445
+ export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
397
446
 
398
447
  // Export the internal object for testing purposes
399
448
  export default ClientLibrary;