@anonympins/fingerprint 0.1.4 → 0.2.1
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 +100 -36
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +21 -4
- package/fingerprint.js +491 -295
- package/library.js +25 -19
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +205 -22
- package/pow.worker.js +27 -0
- package/problem-manager.js +175 -0
package/README.md
CHANGED
|
@@ -18,14 +18,20 @@ The process unfolds in three steps:
|
|
|
18
18
|
* **Header Anomalies**: Missing `User-Agent`, `Accept-Language`, etc.
|
|
19
19
|
* **Device Behavior**: Rapid fingerprint changes (User-Agent rotation).
|
|
20
20
|
* **IP Behavior**: An excessive number of different devices seen from the same IP, or a single device using a large number of IPs (proxy rotation).
|
|
21
|
-
* **Inconsistency**: A low similarity score between the current fingerprint and the
|
|
21
|
+
* **Inconsistency**: A low similarity score between the current fingerprint and the one initially associated with the `device_id` (cookie theft detection).
|
|
22
|
+
* **Cross-Layer Inconsistency**: Mismatches between client-side data (e.g., OS reported by the browser) and server-side headers (e.g., `User-Agent`).
|
|
22
23
|
* **Request Patterns**: Repetitive, rapid-fire, or sequential requests typical of scraping bots. The parameters for detecting these patterns (e.g., request velocity, burst detection) are dynamically adjusted by the auto-tuner for optimal performance.
|
|
23
24
|
* **Honeypot Trap**: Detection of bots that automatically fill hidden form fields or probe for common but unused URL parameters (e.g., `?debug=true`).
|
|
24
25
|
3. **Dynamic Challenge**: If the suspicion score exceeds a certain threshold, a challenge is presented to the user. The difficulty and type of challenge depend on the score:
|
|
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
|
+
* **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
27
|
* **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.
|
|
28
|
+
* **New Devices**: To increase the cost for bots that simply clear their cookies, new (unseen) devices are systematically presented with a minimal, almost imperceptible challenge on their first visit, even if their suspicion score is low.
|
|
27
29
|
|
|
28
|
-
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges
|
|
30
|
+
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges. The duration of this ticket is dynamic:
|
|
31
|
+
- **Probationary Ticket**: If the request was moderately suspicious, a very short-lived "probationary" ticket (e.g., 30 seconds) is issued. This forces the client to be re-evaluated quickly, increasing security.
|
|
32
|
+
- **Optimal TTL Ticket**: For less suspicious requests, a genetic algorithm calculates the optimal ticket duration, balancing security (shorter TTL for higher risk) and user experience (longer TTL for lower risk).
|
|
33
|
+
|
|
34
|
+
For API clients, the challenge is delivered as a `404` JSON response, and the client library can automatically solve it and retry the original request.
|
|
29
35
|
|
|
30
36
|
## Features
|
|
31
37
|
|
|
@@ -33,7 +39,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
33
39
|
- **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
|
|
34
40
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
35
41
|
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
|
|
36
|
-
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure
|
|
42
|
+
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure validation of tickets and other signatures.
|
|
37
43
|
- **Bot Whitelisting**: Includes a DNS-based verification mechanism to reliably identify and whitelist legitimate crawlers like Googlebot and Bingbot, preventing them from being challenged. The results are cached for optimal performance.
|
|
38
44
|
- **Automatic Parameter Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust not only suspicion thresholds (`low`, `medium`, `high`) but also the parameters for behavioral pattern detection, improving accuracy and reducing false positives over time.
|
|
39
45
|
|
|
@@ -61,7 +67,7 @@ The `powMiddleware` requires a configuration object defining the weights of susp
|
|
|
61
67
|
import express from 'express';
|
|
62
68
|
import bodyParser from 'body-parser';
|
|
63
69
|
import cookieParser from 'cookie-parser';
|
|
64
|
-
import { powMiddleware, default_whitelist } from './fingerprint.js'; // Adjust the path
|
|
70
|
+
import { powMiddleware, default_whitelist, default_analyzers } from './fingerprint.js'; // Adjust the path
|
|
65
71
|
|
|
66
72
|
const app = express();
|
|
67
73
|
app.use(cookieParser());
|
|
@@ -81,17 +87,20 @@ const securityConfig = {
|
|
|
81
87
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
82
88
|
requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
|
|
83
89
|
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
84
|
-
behaviorScore: 0.7,
|
|
85
|
-
honeypotScore: 1.0
|
|
90
|
+
behaviorScore: 0.7, // Penalizes non-human interactions (no mouse/keyboard activity)
|
|
91
|
+
honeypotScore: 1.0, // Strongly penalizes bots filling hidden form fields
|
|
92
|
+
crossLayerInconsistencyScore: 0.4, // Penalizes mismatches between client-side data (e.g., OS) and server-side headers (e.g., User-Agent)
|
|
93
|
+
timeInconsistencyScore: 0.9 // Strongly penalizes large time gaps between client metric collection and server reception (replay attack)
|
|
86
94
|
},
|
|
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.
|
|
88
95
|
thresholds: {
|
|
89
96
|
low: 20, // Score from which a CPU challenge is issued
|
|
90
97
|
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
91
98
|
high: 75, // Score for a very difficult challenge
|
|
92
|
-
block: 95, // Score above which the request is blocked outright (HTTP
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
block: 95, // Score above which the request is blocked outright (HTTP 404)
|
|
100
|
+
},
|
|
101
|
+
cpu: {
|
|
102
|
+
minDifficultyBits: 8,
|
|
103
|
+
maxDifficultyBits: 24,
|
|
95
104
|
},
|
|
96
105
|
// (Optional) Configure the duration (in milliseconds) for various temporary data.
|
|
97
106
|
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
@@ -114,27 +123,20 @@ const securityConfig = {
|
|
|
114
123
|
// List of URL paths that should never be accessed by a legitimate user.
|
|
115
124
|
// A request to one of these paths will immediately flag the device as malicious.
|
|
116
125
|
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
117
|
-
// Automatically detect common
|
|
118
|
-
|
|
119
|
-
//
|
|
126
|
+
// Automatically detect common injection patterns. Can be a boolean or an array of specific types.
|
|
127
|
+
// - `true`: Enables all available detections (default).
|
|
128
|
+
// - `false`: Disables injection detection.
|
|
129
|
+
// - `['sql', 'rce']`: Enables only SQL injection and Remote Command Execution detection.
|
|
130
|
+
detectInjections: ['sql', 'rce', 'traversal', 'xxe', 'ssti', 'log4shell'], // (Optional, default: true)
|
|
131
|
+
// (Optional) Plug in external analyzers. This allows you to extend detection with specialized libraries or custom logic.
|
|
120
132
|
// Each function receives an object with all query and body data and should return `true` if a threat is detected.
|
|
121
133
|
analyzers: [
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return waf.isMalicious(JSON.stringify(data));
|
|
129
|
-
},
|
|
130
|
-
// Example 2: Using a specialized library for XSS detection.
|
|
131
|
-
// (npm install xss)
|
|
132
|
-
(data) => {
|
|
133
|
-
const xss = require('xss');
|
|
134
|
-
const originalData = JSON.stringify(data);
|
|
135
|
-
// If the sanitized string is different from the original, it means malicious HTML/JS was found and removed.
|
|
136
|
-
return xss(originalData) !== originalData;
|
|
137
|
-
},
|
|
134
|
+
...default_analyzers(), // Includes the default XSS analyzer.
|
|
135
|
+
|
|
136
|
+
// Example 2: Enable a powerful WAF with ModSecurity and the OWASP Core Rule Set.
|
|
137
|
+
// Requires `npm install modsecurity-nodejs` and downloading the OWASP CRS rules.
|
|
138
|
+
// modsecurity_analyzer('/path/to/owasp-crs/crs-setup.conf'),
|
|
139
|
+
|
|
138
140
|
// Example 3: A custom function to detect specific keywords (e.g., for anti-spam).
|
|
139
141
|
(data) => {
|
|
140
142
|
const spamKeywords = ['viagra', 'free money', 'crypto pump'];
|
|
@@ -161,8 +163,10 @@ const securityConfig = {
|
|
|
161
163
|
...default_whitelist(), // Use the defaults
|
|
162
164
|
{ userAgent: 'MyIndustrySpecificBot', hostnameSuffix: '.my-bot-verifier.com' }, // Add a custom bot
|
|
163
165
|
],
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
+
// Optional: Custom function to identify static resources
|
|
167
|
+
isStaticResource: (req) => req.path.startsWith('/static/'),
|
|
168
|
+
// Optional: Custom function to identify API requests
|
|
169
|
+
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
|
|
166
170
|
// The logger is required for auto-tuning. It collects data on requests.
|
|
167
171
|
logger: (log) => trafficData.push(log),
|
|
168
172
|
// (Optional) Configuration for the automatic threshold and pattern tuning.
|
|
@@ -171,6 +175,8 @@ const securityConfig = {
|
|
|
171
175
|
interval: 1800000, // Optimization cycle every 30 minutes (in ms).
|
|
172
176
|
minDataPoints: 200 // Minimum requests before starting an optimization cycle.
|
|
173
177
|
},
|
|
178
|
+
// Enables problem solving for suspicious activity (configurable in problems.config.json)
|
|
179
|
+
enableUsefulWork: true
|
|
174
180
|
};
|
|
175
181
|
|
|
176
182
|
// Create an instance of the middleware with your security configuration.
|
|
@@ -196,7 +202,39 @@ app.use((req, res, next) => {
|
|
|
196
202
|
|
|
197
203
|
app.listen(3000, () => console.log('Server started on port 3000'));
|
|
198
204
|
```
|
|
199
|
-
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Advanced Behavioral Analysis
|
|
210
|
+
|
|
211
|
+
The FingerprintEngine includes sophisticated behavioral analysis to detect non-human patterns. This analysis is performed by the `getRequestPatternScore` function, which is a stateful check that looks for repetitive or unnaturally fast requests from a single device.
|
|
212
|
+
|
|
213
|
+
This function uses several configurable parameters to identify suspicious behavior:
|
|
214
|
+
|
|
215
|
+
### Core Pattern Detection
|
|
216
|
+
|
|
217
|
+
These parameters form the basis of the request pattern analysis:
|
|
218
|
+
|
|
219
|
+
* `velocityThreshold`: (Default: 800ms) Penalizes requests that are too fast to be humanly possible. If the time since the last request from a device is less than this value, the suspicion score increases.
|
|
220
|
+
* `burstThreshold`: (Default: 1500ms) Adds a significant penalty for multiple identical requests (same path and query parameters) occurring in a very short time frame. This is a strong indicator of automated retries or brute-force attacks.
|
|
221
|
+
* `scrapeThreshold`: (Default: 1000ms) Penalizes sequential requests to the same path but with different query parameters. This pattern is typical of scraping bots that iterate through pages or product IDs.
|
|
222
|
+
* `sequenceLength`: (Default: 3) Detects repetitive sequences of requests (e.g., A -> B -> C -> A -> B -> C), which is a common pattern for scripted bots navigating a site.
|
|
223
|
+
|
|
224
|
+
### Statistical Analysis (Benford's Law)
|
|
225
|
+
|
|
226
|
+
To counter more advanced bots that might try to randomize their request timings, the engine employs statistical analysis based on Benford's Law.
|
|
227
|
+
|
|
228
|
+
* **How it works**: Benford's Law states that in many naturally occurring sets of numbers, the leading digit is more likely to be small. For example, the number 1 appears as the leading digit about 30% of the time, while 9 appears less than 5% of the time. The timings between a human's requests tend to follow this natural distribution, whereas a bot's randomized delays often do not.
|
|
229
|
+
|
|
230
|
+
* `benfordMinSamples`: (Default: 15) The minimum number of request timings to collect before performing a Benford's Law test.
|
|
231
|
+
* `benfordWeight`: (Default: 50) The weight applied to the suspicion score if the distribution of timings significantly deviates from Benford's Law.
|
|
232
|
+
|
|
233
|
+
### Configuration and Auto-Tuning
|
|
234
|
+
|
|
235
|
+
All these parameters are part of the `patterns` object within the main security configuration and can be fine-tuned.
|
|
236
|
+
|
|
237
|
+
## Customizing the Challenge Page
|
|
200
238
|
|
|
201
239
|
You can provide your own HTML template for the Proof-of-Work challenge page to maintain a consistent user experience with your brand.
|
|
202
240
|
|
|
@@ -251,7 +289,7 @@ The main Express middleware. It orchestrates identification, suspicion calculati
|
|
|
251
289
|
|
|
252
290
|
#### `configureStore(store)`
|
|
253
291
|
Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
|
|
254
|
-
The library provides ready-to-use adapters for popular datastores like Redis and
|
|
292
|
+
The library provides ready-to-use adapters for popular datastores like **Redis**, **MongoDB**, and any **SQL database** supported by Knex.js. These adapters automatically handle the Time-To-Live (TTL) required for temporary data like challenge secrets.
|
|
255
293
|
|
|
256
294
|
**Redis Example:**
|
|
257
295
|
|
|
@@ -286,6 +324,32 @@ configureStore(mongoStore);
|
|
|
286
324
|
// db.sessions.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })
|
|
287
325
|
```
|
|
288
326
|
|
|
327
|
+
**SQL Example (with Knex.js):**
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
import { configureStore } from './fingerprint.js';
|
|
331
|
+
import { createSqlStore } from './sql-store.js';
|
|
332
|
+
import knex from 'knex';
|
|
333
|
+
|
|
334
|
+
const knexClient = knex({
|
|
335
|
+
client: 'pg', // or 'mysql', 'sqlite3', etc.
|
|
336
|
+
connection: process.env.DATABASE_URL,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const sqlStore = createSqlStore(knexClient, 'fingerprint_sessions'); // 'fingerprint_sessions' is the table name
|
|
340
|
+
configureStore(sqlStore);
|
|
341
|
+
|
|
342
|
+
// IMPORTANT: For automatic expiration of challenges and other temporary data to work,
|
|
343
|
+
// your table must have an `expiresAt` column. The store will handle cleanup of expired rows,
|
|
344
|
+
// but you must create the table yourself.
|
|
345
|
+
// Example schema for PostgreSQL:
|
|
346
|
+
// CREATE TABLE fingerprint_sessions (
|
|
347
|
+
// "key" VARCHAR(255) PRIMARY KEY,
|
|
348
|
+
// "value" TEXT NOT NULL,
|
|
349
|
+
// "expiresAt" TIMESTAMPTZ
|
|
350
|
+
// );
|
|
351
|
+
```
|
|
352
|
+
|
|
289
353
|
#### `identifyRequest(req, res)`
|
|
290
354
|
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.
|
|
291
355
|
|
|
@@ -472,7 +536,7 @@ Although not exported for direct public use, understanding its role can be usefu
|
|
|
472
536
|
|
|
473
537
|
While `powMiddleware` is convenient for Express, you can use the `FingerprintEngine` directly in any Node.js server environment (e.g., native `http`, Fastify, Koa). This gives you full control over the request/response cycle.
|
|
474
538
|
|
|
475
|
-
**For concrete examples with Koa and Fastify, see our Framework Integration Guide.**
|
|
539
|
+
**For concrete examples with Koa and Fastify, see our [Framework Integration Guide](https://github.com/anonympins/fingerprint/blob/main/INTEGRATION.md).**
|
|
476
540
|
|
|
477
541
|
The engine is a named export from the main module.
|
|
478
542
|
|
|
@@ -498,7 +562,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
498
562
|
clientIp: req.socket.remoteAddress,
|
|
499
563
|
path: req.url.split('?')[0],
|
|
500
564
|
cookies: {}, // Parse cookies from req.headers.cookie
|
|
501
|
-
query: new URL(req.url, `http://${req.headers.host}`).searchParams,
|
|
565
|
+
query: Object.fromEntries(new URL(req.url, `http://${req.headers.host}`).searchParams),
|
|
502
566
|
headers: req.headers,
|
|
503
567
|
rawReq: req, // Pass the raw request
|
|
504
568
|
rawRes: res, // Pass the raw response for cookie setting
|
package/fingerprint.builder.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
}
|
package/fingerprint.client.js
CHANGED
|
@@ -202,6 +202,9 @@ const ClientLibrary = {
|
|
|
202
202
|
* @returns {ClientBehaviorMetrics}
|
|
203
203
|
*/
|
|
204
204
|
getClientBehaviorMetrics() {
|
|
205
|
+
// Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
|
|
206
|
+
metrics.clientTimestamp = Date.now();
|
|
207
|
+
|
|
205
208
|
// Normalise l'entropie de la souris
|
|
206
209
|
if (mouseMovements > 10) {
|
|
207
210
|
metrics.mouseEntropy /= mouseMovements;
|
|
@@ -332,7 +335,7 @@ const ClientLibrary = {
|
|
|
332
335
|
* @private
|
|
333
336
|
*/
|
|
334
337
|
async solveChallengeAndRetry(response, resource, options) {
|
|
335
|
-
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) {
|
|
336
339
|
return response;
|
|
337
340
|
}
|
|
338
341
|
|
|
@@ -343,7 +346,9 @@ const ClientLibrary = {
|
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
346
|
-
|
|
349
|
+
// L'empreinte de l'appareil qui résout le challenge est cruciale.
|
|
350
|
+
const solverFp = this.getDeviceFingerprint();
|
|
351
|
+
const solution = await solveChallenge(challengeData.challenge, solverFp);
|
|
347
352
|
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
348
353
|
|
|
349
354
|
// Ajouter la solution aux paramètres de la requête pour le nouvel essai
|
|
@@ -357,6 +362,15 @@ const ClientLibrary = {
|
|
|
357
362
|
url.searchParams.set(`pow_solution_${key}`, String(value));
|
|
358
363
|
});
|
|
359
364
|
|
|
365
|
+
// Pour le challenge de travail utile
|
|
366
|
+
if (solution.work_result) {
|
|
367
|
+
url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
|
|
368
|
+
url.searchParams.set('pow_problem_id', solution.problem_id);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// On ajoute l'empreinte du solveur à la requête de réessai.
|
|
372
|
+
url.searchParams.set('pow_fp', solverFp);
|
|
373
|
+
|
|
360
374
|
// On utilise la chaîne d'intercepteurs pour la requête réessayée,
|
|
361
375
|
// ce qui garantit que le fetch original est appelé avec le bon contexte.
|
|
362
376
|
// Cela évite de réintroduire l'erreur "Illegal invocation".
|
|
@@ -404,8 +418,9 @@ const ClientLibrary = {
|
|
|
404
418
|
// Ajoute l'intercepteur pour la résolution de challenge
|
|
405
419
|
if (fetchConfig.handleChallenges !== false) {
|
|
406
420
|
this.addFetchInterceptor(async (resource, options, next) => {
|
|
407
|
-
const
|
|
408
|
-
|
|
421
|
+
const originalResponse = await next(resource, options);
|
|
422
|
+
// On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
|
|
423
|
+
return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
|
|
409
424
|
});
|
|
410
425
|
}
|
|
411
426
|
}
|
|
@@ -417,6 +432,7 @@ const ClientLibrary = {
|
|
|
417
432
|
* @property {number} mouseEntropy - Entropie des mouvements de la souris.
|
|
418
433
|
* @property {number} keystrokeLatency - Latence moyenne entre les frappes.
|
|
419
434
|
* @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
|
|
435
|
+
* @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
|
|
420
436
|
*/
|
|
421
437
|
|
|
422
438
|
/** @type {ClientBehaviorMetrics} */
|
|
@@ -424,6 +440,7 @@ const metrics = {
|
|
|
424
440
|
mouseEntropy: 0,
|
|
425
441
|
keystrokeLatency: 0,
|
|
426
442
|
honeypotInteraction: false,
|
|
443
|
+
clientTimestamp: 0,
|
|
427
444
|
};
|
|
428
445
|
|
|
429
446
|
let lastMousePos = { x: 0, y: 0 };
|