@anonympins/fingerprint 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -63,6 +63,8 @@ export POW_SECRET="your_secret_key_of_at_least_32_characters"
63
63
 
64
64
  The `powMiddleware` requires a configuration object defining the weights of suspicion indicators and the challenge trigger thresholds.
65
65
 
66
+ All following `securityConfig` parameters are optional.
67
+
66
68
  ```javascript
67
69
  import express from 'express';
68
70
  import bodyParser from 'body-parser';
@@ -98,6 +100,10 @@ const securityConfig = {
98
100
  high: 75, // Score for a very difficult challenge
99
101
  block: 95, // Score above which the request is blocked outright (HTTP 404)
100
102
  },
103
+ cpu: {
104
+ minDifficultyBits: 8,
105
+ maxDifficultyBits: 24,
106
+ },
101
107
  // (Optional) Configure the duration (in milliseconds) for various temporary data.
102
108
  ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
103
109
  challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
@@ -106,11 +112,15 @@ const securityConfig = {
106
112
  verbose: process.env.NODE_ENV !== 'production', // Log detailed info in development, but not in production.
107
113
  patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
108
114
  velocityThreshold: 800, // ms between requests to be considered "fast"
109
- burstThreshold: 1500, // ms for identical requests to be a "burst"
115
+ burstThreshold: 1500, // ms for identical requests to be a "burst"
110
116
  scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
111
117
  historySize: 10, // Number of requests to keep for pattern analysis
112
- decayFactor: 0.9, // How quickly the pattern score decays over time
113
- inactivityReset: 30000, // ms of inactivity after which the pattern score is reset
118
+ minSamples: 5, // Minimum number of timings to collect before statistical analysis.
119
+ regularityThreshold: 50, // Standard deviation (ms) below which behavior is "too regular".
120
+ benfordThreshold: 0.15, // Benford's Law deviation threshold above which the distribution is "unnatural".
121
+ patternWeight: 80, // Strong, one-time penalty when a pattern is detected.
122
+ decayFactor: 0.9, // Factor by which the pattern score decreases over time.
123
+ inactivityReset: 5000, // Time (ms) after which the pattern score is reset.
114
124
  },
115
125
  honeypot: {
116
126
  // List of field names that are traps for bots.
@@ -152,6 +162,9 @@ const securityConfig = {
152
162
  '203.0.113.0/24', // A partner's network range
153
163
  '2001:db8::/32' // An IPv6 range
154
164
  ]},
165
+ { type: 'hostname_allowlist', entries: [
166
+ 'google.com', // A specific hostname
167
+ ]},
155
168
  // Option 2: DNS-verified bots (e.g., search engine crawlers).
156
169
  // This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
157
170
  // The result is cached per IP to avoid repeated DNS lookups.
@@ -169,7 +182,8 @@ const securityConfig = {
169
182
  autotuning: {
170
183
  trafficData: trafficData, // The data source for the genetic algorithm.
171
184
  interval: 1800000, // Optimization cycle every 30 minutes (in ms).
172
- minDataPoints: 200 // Minimum requests before starting an optimization cycle.
185
+ minDataPoints: 200, // Minimum requests before starting an optimization cycle.
186
+ maxDataPoints: 20000 // Minimum requests before starting an optimization cycle.
173
187
  },
174
188
  // Enables problem solving for suspicious activity (configurable in problems.config.json)
175
189
  enableUsefulWork: true
@@ -536,6 +550,48 @@ While `powMiddleware` is convenient for Express, you can use the `FingerprintEng
536
550
 
537
551
  The engine is a named export from the main module.
538
552
 
553
+ ### Useful Proof-of-Work (`ProblemManager`)
554
+
555
+ Instead of issuing a generic Proof-of-Work, the system can dispatch a "useful" computational problem to a suspicious client. This allows harnessing the client's CPU cycles to solve complex problems (like optimization tasks) over time. This feature is managed by the `ProblemManager` class, which is enabled via the `enableUsefulWork: true` flag in the security configuration.
556
+
557
+ The `ProblemManager` reads its configuration from `problems.config.json`, which defines the problems to be solved, the type of work units, and the current state of the solutions.
558
+
559
+ While you typically won't interact with it directly, its methods are exported and can be used for monitoring or manual administration. The main instance is exported as `problemManager`.
560
+
561
+ #### `problemManager.dispatchWork(suspicionFactor)`
562
+
563
+ Selects a problem and generates a work unit for a client. The difficulty of the task (e.g., number of iterations) is scaled based on the client's `suspicionFactor`.
564
+
565
+ * **`suspicionFactor`** (`number`): A factor to adjust the difficulty of the work unit.
566
+ * **Returns**: (`object|null`) An object containing the `problemId` and the `task` to be sent to the client, or `null` if no problems are available.
567
+
568
+ #### `problemManager.integrateSolution(problemId, solutionData)`
569
+
570
+ Integrates a solution returned by a client into the problem's state. If the new solution is better than the existing one, it is saved as the new best solution.
571
+
572
+ * **`problemId`** (`string`): The ID of the problem being updated.
573
+ * **`solutionData`** (`object`): The solution data returned by the client (e.g., `{ solution, energy }`).
574
+
575
+ #### `problemManager.getBestSolutions([problemId])`
576
+
577
+ Retrieves the best solution currently known for one or all problems. This is useful for creating an API endpoint to view the progress of the distributed computation.
578
+
579
+ * **`problemId`** (`string`, optional): The ID of a specific problem.
580
+ * **Returns**: (`object|Array<object>|null`)
581
+ * If a `problemId` is provided, it returns an object with the best solution for that problem (`{ id, solution, score, lastUpdate }`).
582
+ * If no `problemId` is provided, it returns an array of these objects for all problems.
583
+
584
+ **Example: Creating an API endpoint to view solutions**
585
+
586
+ ```javascript
587
+ import { problemManager } from './fingerprint.js'; // Adjust path
588
+
589
+ app.get('/api/problems/solutions', (req, res) => {
590
+ const solutions = problemManager.getBestSolutions();
591
+ res.json(solutions);
592
+ });
593
+ ```
594
+
539
595
  **Workflow:**
540
596
 
541
597
  1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
@@ -1,104 +1,161 @@
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
- }
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
+ /**
55
+ * Affiche les composants actuels dans la console.
56
+ * @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
57
+ */
58
+ log(title = 'FingerprintBuilder Components') {
59
+ console.log(`--- ${title} ---`);
60
+ const sortedComponents = Array.from(this.components.entries())
61
+ .sort((a, b) => a[0].localeCompare(b[0]));
62
+
63
+ console.table(Object.fromEntries(sortedComponents));
64
+ console.log(`Final string: ${this.toString()}`);
65
+ console.log(`---------------------------------${'-'.repeat(title.length)}`);
66
+ }
67
+
68
+ /**
69
+ * Génère la chaîne de signature finale.
70
+ * Trie les clés pour garantir un ordre déterministe.
71
+ */
72
+ toString() {
73
+ return Array.from(this.components.entries())
74
+ .sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
75
+ .map(([key, hash]) => `${key}:${hash}`)
76
+ .join("|");
77
+ }
78
+ /**
79
+ * Adds a raw component without hashing it.
80
+ * Useful for metrics that need to be read on the server.
81
+ * @param {string} group - The name of the group.
82
+ * @param {string|number} value - The raw value.
83
+ */
84
+ addRaw(group, value) {
85
+ if (value === undefined || value === null) return this;
86
+ this.components.set(group, value);
87
+ return this;
88
+ }
89
+
90
+ /**
91
+ * Compares two fingerprints and returns a similarity score (0 to 1).
92
+ * Uses weights to give more importance to strong invariants (Canvas, GPU).
93
+ * @param {string} fpString1 - Fingerprint A
94
+ * @param {string} fpString2 - Fingerprint B
95
+ */
96
+ static compare(fpString1, fpString2) {
97
+ if (!fpString1 || !fpString2) return 0;
98
+
99
+ const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
100
+
101
+ const map1 = parse(fpString1);
102
+ const map2 = parse(fpString2);
103
+
104
+ // Keys to ignore when comparing the initial request fingerprint with the challenge solver's fingerprint.
105
+ // Headers like Client-Hints (ch_*), cookie presence (cookie_keys), and upgrade-insecure-requests
106
+ // can vary or be absent on the subsequent request that submits the solution, especially after a redirect.
107
+ // By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
108
+ const volatileKeys = new Set([
109
+ 'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
110
+ 'cookie_keys', 'upgrade',
111
+ // Also ignore network and http version as they can change between requests (e.g., proxy, protocol upgrade)
112
+ 'network', 'http_ver',
113
+ // Ignore proxy-related headers as they are not stable client identifiers
114
+ 'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
115
+ ]);
116
+
117
+ // Poids de "véracité" (Entropie/Stabilité)
118
+ // Les poids sont augmentés pour donner plus d'importance aux signaux forts.
119
+ const weights = {
120
+ // --- Signaux très forts (difficiles à usurper) ---
121
+ cvs: 5.0, // Canvas: Très haute entropie (Rendu unique du GPU/driver)
122
+ gpu: 4.0, // GPU: Haute entropie (Matériel spécifique)
123
+ ja3: 3.5, // JA3: Identifie la librairie TLS (très stable pour un client donné)
124
+ ua: 2.0, // User-Agent: Signal fort, bien que modifiable
125
+
126
+ // --- Signaux composites et dérivés ---
127
+ client_fp_hash: 3.0, // Le hash de l'empreinte client est un signal très fort.
128
+ browser: 1.5, // Le navigateur extrait du UA.
129
+ os_version: 1.5, // L'OS extrait du UA.
130
+ device_type: 1.0, // Le type d'appareil extrait du UA.
131
+
132
+ // --- Signaux moyens ---
133
+ hw: 1.5, // Hardware (CPU, RAM): Stabilité moyenne
134
+ scr: 1.0, // Screen: Stabilité moyenne
135
+ // 'os' est souvent la même chose que 'ch_platform', on peut le déprécier ou lui donner un poids faible.
136
+ os: 0.8, // OS (nav.platform): Assez stable
137
+ geo: 0.5, // Geo/Langue: Peut changer (VPN, voyage)
138
+ };
139
+
140
+ let weightedMatches = 0;
141
+ let totalWeight = 0;
142
+
143
+ const allKeys = new Set([...map1.keys(), ...map2.keys()]);
144
+
145
+ allKeys.forEach((key) => {
146
+ // On ignore les clés volatiles pour cette comparaison spécifique.
147
+ if (volatileKeys.has(key)) return;
148
+
149
+ // On ne compare que les clés qui ont un poids défini.
150
+ const weight = weights[key];
151
+ if (!weight) return;
152
+
153
+ totalWeight += weight; // N'incrémenter que si la clé est pertinente.
154
+ if (map1.get(key) === map2.get(key)) {
155
+ weightedMatches += weight;
156
+ }
157
+ });
158
+
159
+ return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
160
+ }
104
161
  }
@@ -335,7 +335,7 @@ const ClientLibrary = {
335
335
  * @private
336
336
  */
337
337
  async solveChallengeAndRetry(response, resource, options) {
338
- if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
338
+ if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
339
339
  return response;
340
340
  }
341
341
 
@@ -346,30 +346,18 @@ const ClientLibrary = {
346
346
  }
347
347
 
348
348
  console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
349
- const solution = await solveChallenge(challengeData.challenge);
349
+ // L'empreinte de l'appareil qui résout le challenge est cruciale.
350
+ const solverFp = this.getDeviceFingerprint();
351
+ const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
350
352
  console.log('[Fingerprint] Challenge solved. Retrying original request.');
351
353
 
352
354
  // Ajouter la solution aux paramètres de la requête pour le nouvel essai
353
355
  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
- }
356
+ // La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
357
+ solutionWrapper.applyToUrl(url);
367
358
 
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
- }
359
+ // On ajoute l'empreinte du solveur à la requête de réessai.
360
+ url.searchParams.set('pow_fp', solverFp);
373
361
 
374
362
  // On utilise la chaîne d'intercepteurs pour la requête réessayée,
375
363
  // ce qui garantit que le fetch original est appelé avec le bon contexte.
@@ -418,8 +406,9 @@ const ClientLibrary = {
418
406
  // Ajoute l'intercepteur pour la résolution de challenge
419
407
  if (fetchConfig.handleChallenges !== false) {
420
408
  this.addFetchInterceptor(async (resource, options, next) => {
421
- const response = await next(resource, options);
422
- return this.solveChallengeAndRetry(response, resource, options);
409
+ const originalResponse = await next(resource, options);
410
+ // On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
411
+ return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
423
412
  });
424
413
  }
425
414
  }