@anonympins/fingerprint 0.0.2 → 0.0.4
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 +57 -45
- package/fingerprint.js +245 -413
- package/library.js +1 -1
- package/package.json +47 -50
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# fingerprint
|
|
2
|
-

|
|
3
|
-

|
|
4
|
-
](https://github.com/anonympins/fingerprint/actions/workflows/ci.yml)
|
|
3
|
+
[](https://github.com/anonympins/fingerprint/releases)
|
|
4
|
+
[](https://github.com/anonympins/fingerprint/blob/main/LICENSE)
|
|
5
|
+

|
|
6
|
+
[](https://github.com/anonympins/fingerprint/watchers)
|
|
7
7
|
|
|
8
8
|
An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.
|
|
9
9
|
|
|
10
10
|
## How It Works
|
|
11
11
|
|
|
12
|
-
This system
|
|
12
|
+
This system identifies and slows down bots and automated scripts by evaluating the "suspicion" level of each incoming request. Instead of outright blocking, it imposes challenges with a difficulty proportional to the suspicion score, penalizing bots without significantly impacting legitimate users.
|
|
13
13
|
|
|
14
14
|
The process unfolds in three steps:
|
|
15
15
|
|
|
@@ -20,25 +20,19 @@ The process unfolds in three steps:
|
|
|
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
21
|
* **Inconsistency**: A low similarity score between the current fingerprint and the initial one associated with the `device_id` (cookie theft detection).
|
|
22
22
|
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:
|
|
23
|
-
* **
|
|
24
|
-
* **
|
|
25
|
-
* **Level 3 (High Suspicion)**: Complex challenge (e.g., TSP - Traveling Salesperson Problem) or a CAPTCHA.
|
|
23
|
+
* **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.
|
|
24
|
+
* **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.
|
|
26
25
|
|
|
27
|
-
Once the challenge is solved, a clearance "ticket" is issued via a cookie, exempting the user from new challenges for a set period.
|
|
26
|
+
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
27
|
|
|
29
28
|
## Features
|
|
30
29
|
|
|
31
30
|
- **Multi-Factor Fingerprinting**: Combines client-side data (`hardwareConcurrency`, `deviceMemory`, `screen`, `canvas`, `webgl`) and server-side data (`User-Agent`, `Client-Hints`).
|
|
32
|
-
- **Weighted Suspicion Engine**: Calculates a score based on behavioral and technical indicators.
|
|
33
|
-
- **Variable-Difficulty Proof-of-Work Challenges**:
|
|
34
|
-
- `cpu_target`: An "analog" CPU challenge where difficulty is finely tuned to the suspicion score.
|
|
35
|
-
- `memory`: A challenge that allocates an amount of memory proportional to the suspicion level.
|
|
36
|
-
- `tsp`: An optimization challenge (Traveling Salesperson Problem) for the most suspicious cases.
|
|
37
31
|
- **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
|
|
38
32
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
39
|
-
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`.
|
|
33
|
+
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
|
|
40
34
|
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
|
|
41
|
-
- **Automatic Threshold Tuning**:
|
|
35
|
+
- **Automatic Threshold Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust suspicion thresholds (`low`, `medium`, `high`), improving bot detection accuracy and reducing false positives over time.
|
|
42
36
|
|
|
43
37
|
## Installation and Usage
|
|
44
38
|
|
|
@@ -58,13 +52,11 @@ export POW_SECRET="your_secret_key_of_at_least_32_characters"
|
|
|
58
52
|
|
|
59
53
|
### Integration Example
|
|
60
54
|
|
|
61
|
-
|
|
55
|
+
The `powMiddleware` requires a configuration object defining the weights of suspicion indicators and the challenge trigger thresholds.
|
|
62
56
|
|
|
63
57
|
```javascript
|
|
64
58
|
import express from 'express';
|
|
65
59
|
import cookieParser from 'cookie-parser';
|
|
66
|
-
// The `configurePow` function is a conceptual example. In the actual implementation,
|
|
67
|
-
// you would pass the configuration to the middleware, for example, via a factory function.
|
|
68
60
|
import { powMiddleware /*, configurePow */ } from './fingerprint.js'; // Adjust the path
|
|
69
61
|
|
|
70
62
|
const app = express();
|
|
@@ -81,27 +73,34 @@ const securityConfig = {
|
|
|
81
73
|
},
|
|
82
74
|
thresholds: {
|
|
83
75
|
low: 20, // Score from which a CPU challenge is issued
|
|
84
|
-
medium: 45, // Score for a Memory challenge
|
|
85
|
-
high: 75
|
|
76
|
+
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
77
|
+
high: 75, // Score for a very difficult challenge
|
|
78
|
+
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
79
|
+
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
86
80
|
}
|
|
87
81
|
};
|
|
88
82
|
|
|
89
|
-
//
|
|
90
|
-
// For example: const configuredPowMiddleware = createPowMiddleware(securityConfig);
|
|
83
|
+
// Create an instance of the middleware with your security configuration.
|
|
91
84
|
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
92
85
|
|
|
93
86
|
// Enable trust proxy if your app is behind a reverse proxy (Nginx, etc.)
|
|
94
87
|
// to correctly retrieve the client's IP.
|
|
95
88
|
app.set('trust proxy', 1);
|
|
96
89
|
|
|
97
|
-
// Apply the protection middleware to all routes or to specific
|
|
98
|
-
// You would use the configured middleware here.
|
|
90
|
+
// Apply the protection middleware to all routes or to specific ones.
|
|
99
91
|
app.use(powMiddlewareInstance);
|
|
100
92
|
|
|
101
93
|
app.get('/', (req, res) => {
|
|
102
94
|
res.send('Welcome to the protected page!');
|
|
103
95
|
});
|
|
104
96
|
|
|
97
|
+
// Example of accessing the suspicion score in a subsequent middleware or route.
|
|
98
|
+
// The `fingerprint` object is attached to the request object by the middleware.
|
|
99
|
+
app.use((req, res, next) => {
|
|
100
|
+
console.log(`Request from ${req.ip} has a suspicion score of: ${req.fingerprint?.score}`);
|
|
101
|
+
next();
|
|
102
|
+
});
|
|
103
|
+
|
|
105
104
|
app.listen(3000, () => console.log('Server started on port 3000'));
|
|
106
105
|
```
|
|
107
106
|
|
|
@@ -111,7 +110,7 @@ In addition to the main middleware, several functions are exported to allow for
|
|
|
111
110
|
|
|
112
111
|
### Main Functions
|
|
113
112
|
|
|
114
|
-
#### `powMiddleware(
|
|
113
|
+
#### `powMiddleware(securityConfig)`
|
|
115
114
|
The main Express middleware. It orchestrates identification, suspicion calculation, and challenge issuance. It is the main entry point of the library.
|
|
116
115
|
|
|
117
116
|
#### `configureStore(store)`
|
|
@@ -119,7 +118,7 @@ Allows replacing the in-memory store with an external datastore (like Redis) for
|
|
|
119
118
|
|
|
120
119
|
```javascript
|
|
121
120
|
import { configureStore } from './fingerprint.js';
|
|
122
|
-
import { createRedisStore } from './redis-store.js'; // Assuming
|
|
121
|
+
import { createRedisStore } from './redis-store.js'; // Assuming a redis store implementation exists
|
|
123
122
|
|
|
124
123
|
const redisStore = createRedisStore(process.env.REDIS_URL);
|
|
125
124
|
configureStore(redisStore);
|
|
@@ -154,7 +153,7 @@ app.use(async (req, res, next) => {
|
|
|
154
153
|
#### `isTicketValid(ip, ticket)`
|
|
155
154
|
Checks the validity of a `pow_clearance` cookie. Returns `true` if the ticket is present, not expired, and correctly signed for the given IP.
|
|
156
155
|
|
|
157
|
-
#### `FingerprintBuilder`
|
|
156
|
+
#### `FingerprintBuilder`
|
|
158
157
|
A class for building granular server-side fingerprints.
|
|
159
158
|
|
|
160
159
|
```javascript
|
|
@@ -166,7 +165,22 @@ const fp = builder.toString(); // "os:hash1|ua:hash2"
|
|
|
166
165
|
|
|
167
166
|
#### `getDeviceFingerprint()`
|
|
168
167
|
*Client-side function only.* Generates a detailed browser fingerprint using APIs like Canvas, WebGL, etc.
|
|
168
|
+
This is the primary function for client-side identification.
|
|
169
|
+
|
|
170
|
+
#### `generateRequestSignature(payload)`
|
|
171
|
+
*Client-side function only.* Creates a signature for an outgoing request. It combines the device fingerprint with a hash of the request's `payload`. This can be used on the server-side to verify that a request comes from a recognized device and that its payload has not been trivially altered.
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// On the client
|
|
175
|
+
const signature = generateRequestSignature({ action: 'update', id: 123 });
|
|
176
|
+
// Send signature in headers...
|
|
177
|
+
```
|
|
169
178
|
|
|
179
|
+
#### `generateClientSideSignature(payload, secret)`
|
|
180
|
+
*Client-side function only.* Generates a secure HMAC-SHA256 signature for a given `payload` using a `secret`.
|
|
181
|
+
**Security Note:** This function is powerful but should be used with caution. The `secret` must be managed securely. It is typically used with a temporary, single-use secret provided by the server for a specific action, rather than a long-lived shared secret embedded in the client-side code.
|
|
182
|
+
|
|
183
|
+
---
|
|
170
184
|
## Advanced Features
|
|
171
185
|
|
|
172
186
|
### Architecture: `FingerprintEngine`
|
|
@@ -209,9 +223,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
209
223
|
clientIp: req.socket.remoteAddress,
|
|
210
224
|
path: req.url.split('?')[0],
|
|
211
225
|
cookies: {}, // Parse cookies from req.headers.cookie
|
|
212
|
-
query:
|
|
226
|
+
query: new URL(req.url, `http://${req.headers.host}`).searchParams,
|
|
213
227
|
headers: req.headers,
|
|
214
|
-
isStatic: /\.(js|css|png)$/.test(req.url),
|
|
215
228
|
rawReq: req, // Pass the raw request
|
|
216
229
|
rawRes: res, // Pass the raw response for cookie setting
|
|
217
230
|
};
|
|
@@ -219,6 +232,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
219
232
|
// 2. Process and get a decision
|
|
220
233
|
const decision = await engine.processRequest(requestContext);
|
|
221
234
|
|
|
235
|
+
// The decision object now contains the score and the raw suspicion vector.
|
|
236
|
+
// You can use it for logging or custom logic.
|
|
237
|
+
console.log(`Request from ${requestContext.clientIp} processed with score: ${decision.score}`);
|
|
238
|
+
|
|
222
239
|
// 3. Act on the decision
|
|
223
240
|
if (decision.action === 'challenge') {
|
|
224
241
|
res.writeHead(decision.status, { 'Content-Type': 'text/html' });
|
|
@@ -244,35 +261,30 @@ Manually setting the `low`, `medium`, and `high` thresholds can be challenging.
|
|
|
244
261
|
|
|
245
262
|
1. **Enable Logging**: The auto-tuner needs data. You must provide a `logger` function in your security configuration. This function will be called for significant events (`challenge_issued`, `challenge_solved`, etc.).
|
|
246
263
|
|
|
247
|
-
2. **
|
|
264
|
+
2. **Enable Auto-tuning**: Add an `autotuning` property to your security configuration. The middleware will automatically start the tuning process.
|
|
248
265
|
|
|
249
266
|
```javascript
|
|
250
|
-
import { powMiddleware
|
|
267
|
+
import { powMiddleware } from './fingerprint.js';
|
|
251
268
|
|
|
252
269
|
// Array to store traffic analysis data. In a real application, this could be
|
|
253
270
|
// a more robust logging system.
|
|
254
271
|
const trafficData = [];
|
|
255
272
|
|
|
256
273
|
const securityConfig = {
|
|
257
|
-
weights: { /* ...
|
|
274
|
+
weights: { /* ... */ },
|
|
258
275
|
thresholds: {
|
|
259
276
|
low: 20, // Initial values, will be optimized
|
|
260
277
|
medium: 45,
|
|
261
278
|
high: 75
|
|
262
279
|
},
|
|
263
|
-
// The logger is required for auto-tuning
|
|
264
|
-
|
|
280
|
+
logger: (log) => trafficData.push(log), // The logger is required for auto-tuning
|
|
281
|
+
autotune: {
|
|
282
|
+
trafficData: trafficData, // The data source for the algorithm
|
|
283
|
+
interval: 1800000, // Optimization cycle every 30 minutes (optional)
|
|
284
|
+
minDataPoints: 200 // Minimum requests before starting optimization (optional)
|
|
285
|
+
}
|
|
265
286
|
};
|
|
266
287
|
|
|
267
|
-
// Start the background optimization process.
|
|
268
|
-
// The `securityConfig.thresholds` object will be mutated with optimized values.
|
|
269
|
-
startThresholdAutoTuning({
|
|
270
|
-
securityConfig: securityConfig, // The config object to be updated
|
|
271
|
-
trafficData: trafficData, // The data source for the algorithm
|
|
272
|
-
interval: 1800000, // Optimization cycle every 30 minutes
|
|
273
|
-
minDataPoints: 200 // Minimum requests before starting optimization
|
|
274
|
-
});
|
|
275
|
-
|
|
276
288
|
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
277
289
|
app.use(powMiddlewareInstance);
|
|
278
290
|
```
|
package/fingerprint.js
CHANGED
|
@@ -1,258 +1,51 @@
|
|
|
1
1
|
// C:/Dev/games.primals.net/src/utils/fingerprint.js
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { Optimization } from "./library.js";
|
|
4
|
+
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
4
5
|
|
|
5
|
-
const POW_SECRET = process.env.POW_SECRET;
|
|
6
|
-
|
|
7
|
-
if (!POW_SECRET && process.env.NODE_ENV === 'production') {
|
|
8
|
-
throw new Error('POW_SECRET environment variable is not set. This is required for production.');
|
|
9
|
-
} else if (!POW_SECRET) {
|
|
10
|
-
console.warn('Warning: POW_SECRET environment variable not set. Using a default, insecure secret for development.');
|
|
11
|
-
}
|
|
12
6
|
/**
|
|
13
|
-
*
|
|
7
|
+
* Retrieves the POW_SECRET from environment variables with appropriate checks.
|
|
8
|
+
* @returns {string} The secret key.
|
|
14
9
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ch = str.charCodeAt(i);
|
|
20
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
21
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
10
|
+
const getPowSecret = () => {
|
|
11
|
+
const secret = process.env.POW_SECRET;
|
|
12
|
+
if (!secret && process.env.NODE_ENV === 'production') {
|
|
13
|
+
throw new Error('POW_SECRET environment variable is not set. This is required for production.');
|
|
22
14
|
}
|
|
23
|
-
|
|
24
|
-
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
25
|
-
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
26
|
-
h2 =
|
|
27
|
-
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
28
|
-
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
29
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
15
|
+
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
30
16
|
};
|
|
31
17
|
|
|
32
18
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
19
|
+
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
20
|
+
* This is our "level 2 fingerprint".
|
|
21
|
+
* @param {object} context - The request context.
|
|
22
|
+
* @returns {string} A hash representing the device.
|
|
35
23
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* @param {string} group - The group name (e.g., 'hw', 'screen', 'geo')
|
|
44
|
-
* @param {string|number|boolean} value - The raw value to be hashed
|
|
45
|
-
*/
|
|
46
|
-
add(group, value) {
|
|
47
|
-
if (value === undefined || value === null) return this;
|
|
48
|
-
// Hash the value individually to anonymize it and reduce its size
|
|
49
|
-
this.components.set(group, cyrb53(String(value)));
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generates the final signature string.
|
|
55
|
-
* Sorts keys to ensure a deterministic order.
|
|
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) => {
|
|
74
|
-
const map = new Map();
|
|
75
|
-
str.split("|").forEach((part) => {
|
|
76
|
-
const [k, v] = part.split(":");
|
|
77
|
-
if (k && v) map.set(k, v);
|
|
78
|
-
});
|
|
79
|
-
return map;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const map1 = parse(fpString1);
|
|
83
|
-
const map2 = parse(fpString2);
|
|
84
|
-
|
|
85
|
-
// "Veracity" weights (Entropy/Stability)
|
|
86
|
-
const weights = {
|
|
87
|
-
cvs: 4.0, // Canvas: Very high entropy (Unique rendering)
|
|
88
|
-
gpu: 3.0, // GPU: High entropy (Specific hardware)
|
|
89
|
-
hw: 1.5, // Hardware: Medium entropy
|
|
90
|
-
scr: 1.0, // Screen: Medium
|
|
91
|
-
geo: 0.5, // Geo: Low (VPN/Travel)
|
|
92
|
-
os: 0.5, // OS: Low (Generic)
|
|
93
|
-
bot: 0.0, // Bot: Informational
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
let weightedMatches = 0;
|
|
97
|
-
let totalWeight = 0;
|
|
98
|
-
|
|
99
|
-
const allKeys = new Set([...map1.keys(), ...map2.keys()]);
|
|
100
|
-
|
|
101
|
-
allKeys.forEach((key) => {
|
|
102
|
-
if (map1.has(key) && map2.has(key)) {
|
|
103
|
-
const weight = weights[key] || 1.0;
|
|
104
|
-
totalWeight += weight;
|
|
105
|
-
|
|
106
|
-
if (map1.get(key) === map2.get(key)) {
|
|
107
|
-
weightedMatches += weight;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
|
|
113
|
-
}
|
|
24
|
+
function getHeaderSignature(context) {
|
|
25
|
+
if (!context.rawHeaders) return '';
|
|
26
|
+
const headerKeys = [];
|
|
27
|
+
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
28
|
+
headerKeys.push(context.rawHeaders[i]);
|
|
29
|
+
}
|
|
30
|
+
return cyrb53(headerKeys.join(','));
|
|
114
31
|
}
|
|
32
|
+
export function getDeviceHash(context) {
|
|
33
|
+
// Prioritize the rich client-side fingerprint if provided.
|
|
34
|
+
const clientFp = context.headers['x-device-fingerprint'];
|
|
35
|
+
if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
|
|
36
|
+
// Basic validation to ensure it looks like our client-side fingerprint.
|
|
37
|
+
return clientFp;
|
|
38
|
+
}
|
|
115
39
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// The presence of `window` and `document` confirms this.
|
|
126
|
-
|
|
127
|
-
if (typeof window === "undefined") return "server-side";
|
|
128
|
-
|
|
129
|
-
if (!cachedBuilder) {
|
|
130
|
-
const nav = window.navigator;
|
|
131
|
-
const screen = window.screen;
|
|
132
|
-
|
|
133
|
-
cachedBuilder = new FingerprintBuilder();
|
|
134
|
-
|
|
135
|
-
// 1. Hardware (Very stable): Cores, RAM, GPU (if available via canvas), Touch
|
|
136
|
-
cachedBuilder.add(
|
|
137
|
-
"hw",
|
|
138
|
-
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// 2. Geo/Locale (Stable except for travel/VPN): Timezone, Language
|
|
142
|
-
cachedBuilder.add(
|
|
143
|
-
"geo",
|
|
144
|
-
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// 3. Screen (Stable except for monitor/zoom changes): Dimensions, ColorDepth
|
|
148
|
-
// Note: We use availWidth/Height which excludes the taskbar, sometimes more unique
|
|
149
|
-
cachedBuilder.add(
|
|
150
|
-
"scr",
|
|
151
|
-
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// 4. Platform (Stable): OS, Engine
|
|
155
|
-
cachedBuilder.add("os", nav.platform);
|
|
156
|
-
|
|
157
|
-
// 5. Graphics (WebGL Vendor/Renderer) - Strong hardware invariant
|
|
158
|
-
try {
|
|
159
|
-
const canvas = document.createElement("canvas");
|
|
160
|
-
const gl =
|
|
161
|
-
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
162
|
-
if (gl) {
|
|
163
|
-
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
164
|
-
if (debugInfo) {
|
|
165
|
-
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
166
|
-
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
167
|
-
cachedBuilder.add("gpu", `${vendor}_${renderer}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch (e) {}
|
|
171
|
-
|
|
172
|
-
// 6. Canvas Fingerprinting (Rendering quirks) - Adds ~5-10% uniqueness
|
|
173
|
-
// Exploits micro-differences in anti-aliasing and font rendering
|
|
174
|
-
try {
|
|
175
|
-
const canvas = document.createElement("canvas");
|
|
176
|
-
const ctx = canvas.getContext("2d");
|
|
177
|
-
if (ctx) {
|
|
178
|
-
canvas.width = 200;
|
|
179
|
-
canvas.height = 50;
|
|
180
|
-
ctx.textBaseline = "alphabetic";
|
|
181
|
-
ctx.font = "14px 'Arial'";
|
|
182
|
-
ctx.fillStyle = "#f60";
|
|
183
|
-
ctx.fillRect(125, 1, 62, 20);
|
|
184
|
-
ctx.fillStyle = "#069";
|
|
185
|
-
ctx.fillText("Primals", 2, 15);
|
|
186
|
-
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
187
|
-
ctx.fillText("Primals", 4, 17);
|
|
188
|
-
cachedBuilder.add("cvs", canvas.toDataURL());
|
|
189
|
-
}
|
|
190
|
-
} catch (e) {}
|
|
191
|
-
|
|
192
|
-
// 7. Bot Detection (Hidden indicator)
|
|
193
|
-
if (nav.webdriver) cachedBuilder.add("bot", "true");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Return a copy to allow adding dynamic fields if needed without polluting the cache
|
|
197
|
-
return cachedBuilder.toString();
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Generates a request signature including the context.
|
|
202
|
-
* @param {object} payload
|
|
203
|
-
*/
|
|
204
|
-
export const generateRequestSignature = (payload = {}) => {
|
|
205
|
-
const deviceFp = getDeviceFingerprint();
|
|
206
|
-
|
|
207
|
-
// Create a temporary builder that inherits from deviceFp
|
|
208
|
-
// Note: Here we keep it simple, just concatenating the payload hash
|
|
209
|
-
const sortedPayload = Object.keys(payload)
|
|
210
|
-
.sort()
|
|
211
|
-
.map((k) => `${k}=${payload[k]}`)
|
|
212
|
-
.join("&");
|
|
213
|
-
const payloadHash = cyrb53(sortedPayload);
|
|
214
|
-
|
|
215
|
-
return `${deviceFp}|req:${payloadHash}`;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generates an HMAC-SHA256 signature for combat data.
|
|
220
|
-
* @param {object} payload - The data to sign (e.g., { opponentId, victory, damageDealt }).
|
|
221
|
-
* @param {string} secret - The shared secret key.
|
|
222
|
-
* @returns {Promise<string>} The hexadecimal signature.
|
|
223
|
-
*/
|
|
224
|
-
export const generateCombatSignature = async (payload, secret) => {
|
|
225
|
-
// NOTE: This is client-side code using the Web Crypto API (`window.crypto`).
|
|
226
|
-
// It should be moved to a client-side script file.
|
|
227
|
-
|
|
228
|
-
// 1. Create a stable string from the payload.
|
|
229
|
-
const sortedPayload = Object.keys(payload)
|
|
230
|
-
.sort()
|
|
231
|
-
.map((k) => `${k}=${payload[k]}`)
|
|
232
|
-
.join("&");
|
|
233
|
-
|
|
234
|
-
// 2. Use the Web Crypto API for HMAC
|
|
235
|
-
const encoder = new TextEncoder();
|
|
236
|
-
const key = await window.crypto.subtle.importKey(
|
|
237
|
-
"raw",
|
|
238
|
-
encoder.encode(secret),
|
|
239
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
240
|
-
false,
|
|
241
|
-
["sign"],
|
|
242
|
-
);
|
|
243
|
-
const signatureBuffer = await window.crypto.subtle.sign(
|
|
244
|
-
"HMAC",
|
|
245
|
-
key,
|
|
246
|
-
encoder.encode(sortedPayload),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
// 3. Convert the signature to a hexadecimal string.
|
|
250
|
-
const hashArray = Array.from(new Uint8Array(signatureBuffer));
|
|
251
|
-
const hexString = hashArray
|
|
252
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
253
|
-
.join("");
|
|
254
|
-
return hexString;
|
|
255
|
-
};
|
|
40
|
+
// Fallback to server-side only fingerprinting if the header is missing.
|
|
41
|
+
const srv = new FingerprintBuilder();
|
|
42
|
+
srv.add("ua", context.headers["user-agent"]);
|
|
43
|
+
if (context.headers["sec-ch-ua-platform"])
|
|
44
|
+
srv.add("os", context.headers["sec-ch-ua-platform"]);
|
|
45
|
+
if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
|
|
46
|
+
srv.add("h_ord", getHeaderSignature(context));
|
|
47
|
+
return srv.toString();
|
|
48
|
+
}
|
|
256
49
|
|
|
257
50
|
/**
|
|
258
51
|
* Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
|
|
@@ -531,7 +324,7 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
531
324
|
// 2. Generate an HMAC ticket so the client doesn't have to do it again for 1 hour
|
|
532
325
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
533
326
|
const signature = crypto
|
|
534
|
-
.createHmac("sha256",
|
|
327
|
+
.createHmac("sha256", getPowSecret())
|
|
535
328
|
.update(`${ip}:${expiry}`)
|
|
536
329
|
.digest("hex");
|
|
537
330
|
|
|
@@ -542,18 +335,22 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
542
335
|
* Verifies a memory PoW solution.
|
|
543
336
|
* The server performs the same calculation to validate.
|
|
544
337
|
*/
|
|
545
|
-
export const verifyMemoryPoW = (nonce, solution, difficulty = 16) => {
|
|
338
|
+
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
|
|
546
339
|
const size = difficulty * 1024 * 1024;
|
|
547
340
|
const iterations = size / 16;
|
|
548
341
|
const buffer = new Uint32Array(size / 4);
|
|
549
|
-
|
|
342
|
+
const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
|
|
343
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
344
|
+
|
|
550
345
|
for (let i = 0; i < buffer.length; i++) {
|
|
551
346
|
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
552
347
|
}
|
|
348
|
+
|
|
553
349
|
let finalHash = 0;
|
|
350
|
+
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
554
351
|
for (let i = 0; i < iterations; i++) {
|
|
555
|
-
|
|
556
|
-
finalHash ^=
|
|
352
|
+
addr = buffer[addr] % buffer.length;
|
|
353
|
+
finalHash ^= addr;
|
|
557
354
|
}
|
|
558
355
|
return finalHash === parseInt(solution, 10);
|
|
559
356
|
};
|
|
@@ -562,58 +359,42 @@ export const isTicketValid = (ip, ticket) => {
|
|
|
562
359
|
const [expiry, sig] = ticket.split(":");
|
|
563
360
|
if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
|
|
564
361
|
const expectedSig = crypto
|
|
565
|
-
.createHmac("sha256",
|
|
362
|
+
.createHmac("sha256", getPowSecret())
|
|
566
363
|
.update(`${ip}:${expiry}`)
|
|
567
364
|
.digest("hex");
|
|
568
365
|
|
|
569
366
|
// Use timingSafeEqual to prevent timing attacks
|
|
570
|
-
|
|
367
|
+
try {
|
|
368
|
+
return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expectedSig, 'hex'));
|
|
369
|
+
} catch (e) {
|
|
370
|
+
// This can happen if the buffers have different lengths, which is a failure case.
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
571
373
|
};
|
|
572
374
|
|
|
573
|
-
/**
|
|
574
|
-
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
575
|
-
* This is our "level 2 fingerprint".
|
|
576
|
-
* @param {object} req - The Express request object.
|
|
577
|
-
* @returns {string} A hash representing the device.
|
|
578
|
-
*/
|
|
579
|
-
function getDeviceHash(req) {
|
|
580
|
-
const srv = new FingerprintBuilder();
|
|
581
|
-
srv.add("ua", req.headers["user-agent"]);
|
|
582
|
-
if (req.headers["sec-ch-ua-platform"])
|
|
583
|
-
srv.add("os", req.headers["sec-ch-ua-platform"]);
|
|
584
|
-
if (req.headers["sec-ch-ua"]) srv.add("ch", req.headers["sec-ch-ua"]);
|
|
585
|
-
return srv.toString(); // Returns the full fingerprint string for detailed comparison.
|
|
586
|
-
}
|
|
587
375
|
|
|
588
376
|
/**
|
|
589
377
|
* Calculates suspicion indicators related to HTTP header anomalies.
|
|
590
|
-
* @param {object}
|
|
378
|
+
* @param {object} context - The request context.
|
|
591
379
|
* @returns {{headerAnomalyScore: number}}
|
|
592
380
|
*/
|
|
593
|
-
function getHeaderAnomalies(
|
|
594
|
-
// FIX: consistencyScore est maintenant passé
|
|
381
|
+
function getHeaderAnomalies(context) {
|
|
595
382
|
let anomalyScore = 0;
|
|
596
383
|
// Strong penalty if User-Agent is missing or very short (sign of a simple script)
|
|
597
|
-
if (!
|
|
384
|
+
if (!context.headers["user-agent"] || context.headers["user-agent"].length < 10) {
|
|
598
385
|
anomalyScore += 60;
|
|
599
386
|
}
|
|
600
387
|
// Penalty if Accept-Language header is missing
|
|
601
|
-
if (!
|
|
388
|
+
if (!context.headers["accept-language"]) {
|
|
602
389
|
anomalyScore += 25;
|
|
603
390
|
}
|
|
604
391
|
// Penalty for HTTP/1.0 requests, often used by old tools or bots
|
|
605
|
-
if (
|
|
392
|
+
if (context.httpVersion === "1.0") {
|
|
606
393
|
anomalyScore += 15;
|
|
607
394
|
}
|
|
608
395
|
|
|
609
|
-
// NEW: Inconsistency score (stolen cookie?)
|
|
610
|
-
// If the consistency score is low, add a massive penalty.
|
|
611
|
-
// A score of 0.2 means a huge difference.
|
|
612
|
-
const inconsistencyScore = Math.max(0, (1 - consistencyScore) * 200);
|
|
613
|
-
|
|
614
396
|
return {
|
|
615
397
|
headerAnomalyScore: Math.min(100, anomalyScore),
|
|
616
|
-
inconsistencyScore: Math.min(100, inconsistencyScore),
|
|
617
398
|
};
|
|
618
399
|
}
|
|
619
400
|
|
|
@@ -627,7 +408,7 @@ function getHeaderAnomalies(req, consistencyScore) {
|
|
|
627
408
|
|
|
628
409
|
/**
|
|
629
410
|
* Default in-memory store implementation.
|
|
630
|
-
* @type {IStore}
|
|
411
|
+
* @type {IStore}
|
|
631
412
|
*/
|
|
632
413
|
const inMemoryStore = {
|
|
633
414
|
_map: new Map(),
|
|
@@ -652,16 +433,16 @@ export const configureStore = (externalStore) => {
|
|
|
652
433
|
/**
|
|
653
434
|
* Orchestrates request identification using a persistent anchor (cookie)
|
|
654
435
|
* and fingerprint verification.
|
|
655
|
-
* @param {object}
|
|
656
|
-
* @
|
|
657
|
-
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number}>}
|
|
436
|
+
* @param {object} context - The request context.
|
|
437
|
+
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
|
|
658
438
|
*/
|
|
659
|
-
async function resolveRequestIdentity(
|
|
660
|
-
const existingDeviceId =
|
|
661
|
-
const currentDeviceHash = getDeviceHash(
|
|
439
|
+
async function resolveRequestIdentity(context) {
|
|
440
|
+
const existingDeviceId = context.cookies?.device_id;
|
|
441
|
+
const currentDeviceHash = getDeviceHash(context);
|
|
662
442
|
let deviceId = existingDeviceId;
|
|
663
443
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
664
444
|
let deviceData = null;
|
|
445
|
+
let newCookie = null;
|
|
665
446
|
|
|
666
447
|
if (deviceId) {
|
|
667
448
|
deviceData = await store.get(`device:${deviceId}`);
|
|
@@ -680,13 +461,14 @@ async function resolveRequestIdentity(req, res) {
|
|
|
680
461
|
// Case 2: New user or lost/invalid cookie.
|
|
681
462
|
deviceId = crypto.randomUUID(); // Generate a new "passport".
|
|
682
463
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
464
|
+
// Return the intention to set a cookie.
|
|
465
|
+
newCookie = {
|
|
466
|
+
name: "device_id",
|
|
467
|
+
value: deviceId,
|
|
468
|
+
options: {
|
|
469
|
+
httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 31536000000, // 1 year
|
|
470
|
+
}
|
|
471
|
+
};
|
|
690
472
|
|
|
691
473
|
// Initialize tracking for this new device.
|
|
692
474
|
deviceData = {
|
|
@@ -700,24 +482,24 @@ async function resolveRequestIdentity(req, res) {
|
|
|
700
482
|
// The write will happen in getSuspicionVector after all modifications.
|
|
701
483
|
}
|
|
702
484
|
|
|
703
|
-
return { deviceId, deviceData, consistencyScore };
|
|
485
|
+
return { deviceId, deviceData, consistencyScore, newCookie };
|
|
704
486
|
}
|
|
705
487
|
|
|
706
488
|
/*
|
|
707
489
|
* Calcule les indicateurs de suspicion liés au comportement de l'appareil (historique, rotation).
|
|
708
|
-
* @param {object}
|
|
490
|
+
* @param {object} context - The request context.
|
|
709
491
|
* @param {object} deviceData - The device's activity data.
|
|
710
492
|
* @returns {Promise<{historyScore: number, rotationScore: number}>}
|
|
711
493
|
*/
|
|
712
|
-
async function getBehavioralIndicators(
|
|
494
|
+
async function getBehavioralIndicators(context, deviceData) {
|
|
713
495
|
const now = Date.now();
|
|
714
|
-
const clientIp =
|
|
496
|
+
const clientIp = context.clientIp;
|
|
715
497
|
|
|
716
498
|
// Get the IP type to modulate the score
|
|
717
499
|
const ipProfile = (await store.get(`ip:${clientIp}`)) || { type: "residential" };
|
|
718
500
|
const isSharedIp = ipProfile.type === "shared";
|
|
719
501
|
|
|
720
|
-
const currentFpHash = getDeviceHash(
|
|
502
|
+
const currentFpHash = getDeviceHash(context); // Use the device hash
|
|
721
503
|
|
|
722
504
|
// --- Behavior analysis (Change frequency) ---
|
|
723
505
|
if (deviceData.lastFpHash && currentFpHash !== deviceData.lastFpHash) {
|
|
@@ -727,7 +509,7 @@ async function getBehavioralIndicators(req, deviceData) {
|
|
|
727
509
|
deviceData.rapidChangeCount = Math.min(
|
|
728
510
|
deviceData.rapidChangeCount + 1,
|
|
729
511
|
MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
|
|
730
|
-
);
|
|
512
|
+
);
|
|
731
513
|
} else {
|
|
732
514
|
deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
|
|
733
515
|
}
|
|
@@ -762,13 +544,20 @@ async function getBehavioralIndicators(req, deviceData) {
|
|
|
762
544
|
|
|
763
545
|
/**
|
|
764
546
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
765
|
-
* @param {object}
|
|
547
|
+
* @param {object} context - The request context object.
|
|
766
548
|
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number}>}
|
|
767
549
|
*/
|
|
768
|
-
export const getSuspicionVector = async (
|
|
769
|
-
const { deviceId, deviceData, consistencyScore } = await resolveRequestIdentity(
|
|
550
|
+
export const getSuspicionVector = async (context) => {
|
|
551
|
+
const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context);
|
|
770
552
|
|
|
771
|
-
const clientIp =
|
|
553
|
+
const clientIp = context.clientIp;
|
|
554
|
+
|
|
555
|
+
// If a new cookie needs to be set, attach it to the request object
|
|
556
|
+
// so the middleware can handle it. This is a temporary state holder.
|
|
557
|
+
if (newCookie) {
|
|
558
|
+
context._newCookies = context._newCookies || [];
|
|
559
|
+
context._newCookies.push(newCookie);
|
|
560
|
+
}
|
|
772
561
|
await store.set(`ip-device:${clientIp}`, deviceId); // Link the IP to the device
|
|
773
562
|
|
|
774
563
|
// Periodically clean up device data
|
|
@@ -778,13 +567,16 @@ export const getSuspicionVector = async (req, res) => {
|
|
|
778
567
|
}
|
|
779
568
|
deviceData.lastUpdate = Date.now();
|
|
780
569
|
|
|
781
|
-
const behavioral = await getBehavioralIndicators(
|
|
782
|
-
const
|
|
570
|
+
const behavioral = await getBehavioralIndicators(context, deviceData);
|
|
571
|
+
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
572
|
+
// Calculate the inconsistency score here, separately.
|
|
573
|
+
const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200));
|
|
574
|
+
|
|
783
575
|
|
|
784
576
|
// Save the updated device state to the store
|
|
785
577
|
await store.set(`device:${deviceId}`, deviceData);
|
|
786
578
|
|
|
787
|
-
return { ...behavioral,
|
|
579
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore };
|
|
788
580
|
};
|
|
789
581
|
|
|
790
582
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -804,61 +596,29 @@ const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Number of rapid fingerprint changes a
|
|
|
804
596
|
* and IP, making spoofing more complex (requires changing the entire stack).
|
|
805
597
|
*/
|
|
806
598
|
export const identifyRequest = async (req, res) => {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
type: "residential",
|
|
813
|
-
deviceIds: new Set(),
|
|
814
|
-
statelessCount: 0,
|
|
815
|
-
lastSeen: 0,
|
|
599
|
+
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
600
|
+
// It requires a default configuration to work.
|
|
601
|
+
const defaultConfig = {
|
|
602
|
+
weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8 },
|
|
603
|
+
thresholds: { low: 20, medium: 40, high: 75 }
|
|
816
604
|
};
|
|
817
|
-
|
|
818
|
-
if (deviceId) {
|
|
819
|
-
ipProfile.deviceIds.add(deviceId);
|
|
820
|
-
} else {
|
|
821
|
-
// Improved anti-"Amnesiac Bot" logic
|
|
822
|
-
ipProfile.statelessCount++;
|
|
823
|
-
}
|
|
605
|
+
const engine = new FingerprintEngine(defaultConfig);
|
|
824
606
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
607
|
+
const requestContext = {
|
|
608
|
+
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
609
|
+
cookies: req.cookies,
|
|
610
|
+
headers: req.headers,
|
|
611
|
+
rawHeaders: req.rawHeaders,
|
|
612
|
+
httpVersion: req.httpVersion,
|
|
613
|
+
};
|
|
829
614
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return `suspicious_high:${clientIp}`;
|
|
835
|
-
}
|
|
836
|
-
await store.set(`ip:${clientIp}`, ipProfile);
|
|
837
|
-
|
|
838
|
-
// For compatibility with the rate-limiter, calculate a simple score.
|
|
839
|
-
// The PoW will use the more complex weighted system.
|
|
840
|
-
const vector = await getSuspicionVector(req, res);
|
|
841
|
-
const score =
|
|
842
|
-
vector.historyScore * 0.3 +
|
|
843
|
-
vector.rotationScore * 0.5 +
|
|
844
|
-
vector.headerAnomalyScore * 0.1 +
|
|
845
|
-
vector.inconsistencyScore * 0.8; // Inconsistency is a very strong signal
|
|
846
|
-
|
|
847
|
-
// Return a string for compatibility with rate limiters,
|
|
848
|
-
// but based on suspicion thresholds.
|
|
849
|
-
// NOTE: These thresholds are fixed here, but the PoW will use dynamic thresholds.
|
|
850
|
-
if (score >= 75) {
|
|
851
|
-
return `suspicious_high:${clientIp}`;
|
|
852
|
-
}
|
|
853
|
-
if (score >= 40) {
|
|
854
|
-
return `suspicious_medium:${clientIp}`;
|
|
615
|
+
const key = await engine.identifyRequest(requestContext);
|
|
616
|
+
|
|
617
|
+
if (requestContext._newCookies && res) {
|
|
618
|
+
requestContext._newCookies.forEach(c => res.cookie(c.name, c.value, c.options));
|
|
855
619
|
}
|
|
856
620
|
|
|
857
|
-
|
|
858
|
-
// Use the device hash so the rate-limit follows the device, not the IP.
|
|
859
|
-
const deviceIdForIp = await store.get(`ip-device:${clientIp}`);
|
|
860
|
-
const finalDeviceId = deviceId || deviceIdForIp || clientIp;
|
|
861
|
-
return `device:${finalDeviceId}`;
|
|
621
|
+
return key;
|
|
862
622
|
};
|
|
863
623
|
// --- NOUVEAU CHALLENGE CPU "ANALOGIQUE" ---
|
|
864
624
|
|
|
@@ -952,7 +712,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
952
712
|
* @param {string} clientIp - The client's IP address.
|
|
953
713
|
* @returns {string} HTML content.
|
|
954
714
|
*/
|
|
955
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
|
|
715
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
|
|
956
716
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
957
717
|
return `
|
|
958
718
|
<html><head><title>Advanced Security Check</title></head>
|
|
@@ -964,13 +724,14 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
964
724
|
async function solve() {
|
|
965
725
|
const nonce = "${nonce}";
|
|
966
726
|
const path = "${path}";
|
|
727
|
+
const clientSecret = "${clientSecret}"; // Secret is now available to the client
|
|
967
728
|
|
|
968
729
|
// --- CPU Challenge ---
|
|
969
730
|
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
970
731
|
const cpuTarget = BigInt("0x${target}");
|
|
971
732
|
let cpuSolution = 0;
|
|
972
733
|
while (true) {
|
|
973
|
-
const msg = "${clientIp}:${nonce}:" + cpuSolution;
|
|
734
|
+
const msg = "${clientIp}:${nonce}:" + cpuSolution + ":" + clientSecret;
|
|
974
735
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
975
736
|
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
976
737
|
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
@@ -983,14 +744,16 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
983
744
|
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
984
745
|
|
|
985
746
|
let memSolution = 0;
|
|
747
|
+
const iterations = size / 16;
|
|
986
748
|
try {
|
|
987
749
|
const size = ${memoryDifficulty} * 1024 * 1024;
|
|
988
750
|
const buffer = new Uint32Array(size / 4);
|
|
989
|
-
|
|
751
|
+
const seed = nonce + ":" + clientSecret;
|
|
752
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
990
753
|
for (let i = 0; i < buffer.length; i++) {
|
|
991
|
-
buffer[i] =
|
|
754
|
+
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
992
755
|
}
|
|
993
|
-
for(let i = 0; i <
|
|
756
|
+
for(let i = 0; i < iterations; i++) {
|
|
994
757
|
const addr = buffer[i % buffer.length] % buffer.length;
|
|
995
758
|
memSolution ^= buffer[addr];
|
|
996
759
|
}
|
|
@@ -1013,11 +776,13 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1013
776
|
nonce,
|
|
1014
777
|
solution,
|
|
1015
778
|
suspicionFactor,
|
|
779
|
+
clientSecret, // Le secret est maintenant requis
|
|
1016
780
|
) {
|
|
1017
781
|
const target = calculateTarget(suspicionFactor);
|
|
782
|
+
const message = clientSecret ? `${clientIp}:${nonce}:${solution}:${clientSecret}` : `${clientIp}:${nonce}:${solution}`;
|
|
1018
783
|
const hash = crypto
|
|
1019
784
|
.createHash("sha256")
|
|
1020
|
-
.update(
|
|
785
|
+
.update(message)
|
|
1021
786
|
.digest("hex");
|
|
1022
787
|
const hashAsInt = BigInt("0x" + hash);
|
|
1023
788
|
|
|
@@ -1026,7 +791,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1026
791
|
// The proof is valid, generate the ticket
|
|
1027
792
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
1028
793
|
const signature = crypto
|
|
1029
|
-
.createHmac("sha256",
|
|
794
|
+
.createHmac("sha256", getPowSecret())
|
|
1030
795
|
.update(`${clientIp}:${expiry}`)
|
|
1031
796
|
.digest("hex");
|
|
1032
797
|
return `${expiry}:${signature}`;
|
|
@@ -1035,9 +800,11 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1035
800
|
return null;
|
|
1036
801
|
}
|
|
1037
802
|
|
|
1038
|
-
const staticExtensions =
|
|
1039
|
-
|
|
1040
|
-
|
|
803
|
+
const staticExtensions = new RegExp(
|
|
804
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$",
|
|
805
|
+
"i",
|
|
806
|
+
);
|
|
807
|
+
const isStaticResource = (path) => staticExtensions.test(path);
|
|
1041
808
|
|
|
1042
809
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
1043
810
|
class FingerprintEngine {
|
|
@@ -1052,12 +819,11 @@ class FingerprintEngine {
|
|
|
1052
819
|
const { weights, thresholds, logger } = this.securityConfig;
|
|
1053
820
|
|
|
1054
821
|
if (isStatic) {
|
|
1055
|
-
return { action: 'next' };
|
|
822
|
+
return { action: 'next', score: 0, vector: {} };
|
|
1056
823
|
}
|
|
1057
824
|
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1060
|
-
const suspicionVector = await __internal.getSuspicionVector(requestContext.rawReq, requestContext.rawRes);
|
|
825
|
+
// The engine now works with the context directly, no more rawReq dependency here.
|
|
826
|
+
const suspicionVector = await __internal.getSuspicionVector(requestContext);
|
|
1061
827
|
|
|
1062
828
|
const finalScore =
|
|
1063
829
|
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
@@ -1076,18 +842,15 @@ class FingerprintEngine {
|
|
|
1076
842
|
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
1077
843
|
)
|
|
1078
844
|
: 0;
|
|
1079
|
-
const powCookie = cookies?.pow_clearance;
|
|
845
|
+
const powCookie = cookies?.pow_clearance;
|
|
1080
846
|
const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
1081
847
|
|
|
1082
|
-
// Basic log for each non-static request
|
|
1083
|
-
if (logger && !isSuspicious) {
|
|
1084
|
-
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
848
|
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
1088
849
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
1089
850
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
1090
851
|
let isValid = false,
|
|
852
|
+
// Retrieve the client-side secret associated with this nonce
|
|
853
|
+
clientSecret = await store.get(`secret:${pow_nonce}`),
|
|
1091
854
|
ticket = null;
|
|
1092
855
|
if (pow_type === "cpu_target") {
|
|
1093
856
|
// Verify the new type
|
|
@@ -1095,25 +858,20 @@ class FingerprintEngine {
|
|
|
1095
858
|
clientIp,
|
|
1096
859
|
pow_nonce,
|
|
1097
860
|
pow_solution,
|
|
1098
|
-
suspicionFactor, // Pass the analog factor
|
|
861
|
+
suspicionFactor, // Pass the analog factor
|
|
862
|
+
clientSecret,
|
|
1099
863
|
);
|
|
1100
|
-
isValid = ticket !== null;
|
|
1101
|
-
} else if (pow_type === "mem") {
|
|
1102
|
-
const minDifficulty = 16; // 16Mo
|
|
1103
|
-
const maxDifficulty = 48; // 48Mo
|
|
1104
|
-
const difficulty =
|
|
1105
|
-
minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
|
|
1106
|
-
isValid = verifyMemoryPoW(pow_nonce, pow_solution, difficulty);
|
|
1107
|
-
} else if (pow_type === "cpu_mem") {
|
|
864
|
+
isValid = ticket !== null; } else if (pow_type === "cpu_mem") {
|
|
1108
865
|
// Verify combined challenge
|
|
1109
866
|
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
|
|
1110
|
-
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
|
|
867
|
+
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor, clientSecret
|
|
1111
868
|
);
|
|
1112
|
-
|
|
1113
869
|
const minDifficulty = 16; // 16Mo
|
|
1114
870
|
const maxDifficulty = 48; // 48Mo
|
|
1115
|
-
const
|
|
1116
|
-
const
|
|
871
|
+
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
872
|
+
const memDifficulty = Math.round(minDifficulty + memActivationFactor * (maxDifficulty - minDifficulty));
|
|
873
|
+
|
|
874
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty, clientSecret);
|
|
1117
875
|
|
|
1118
876
|
isValid = cpuTicket !== null && isMemValid;
|
|
1119
877
|
if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
|
|
@@ -1123,11 +881,16 @@ class FingerprintEngine {
|
|
|
1123
881
|
}
|
|
1124
882
|
|
|
1125
883
|
if (isValid) {
|
|
884
|
+
// The secret has been used, delete it to prevent replay.
|
|
885
|
+
if (clientSecret) {
|
|
886
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
1126
889
|
if (!ticket) {
|
|
1127
890
|
// If the ticket has not already been generated (CPU case)
|
|
1128
891
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
1129
892
|
const signature = crypto
|
|
1130
|
-
.createHmac("sha256",
|
|
893
|
+
.createHmac("sha256", getPowSecret())
|
|
1131
894
|
.update(`${clientIp}:${expiry}`)
|
|
1132
895
|
.digest("hex");
|
|
1133
896
|
ticket = `${expiry}:${signature}`;
|
|
@@ -1140,6 +903,8 @@ class FingerprintEngine {
|
|
|
1140
903
|
return {
|
|
1141
904
|
action: 'redirect',
|
|
1142
905
|
path: path,
|
|
906
|
+
score: finalScore,
|
|
907
|
+
vector: suspicionVector,
|
|
1143
908
|
cookie: {
|
|
1144
909
|
name: 'pow_clearance',
|
|
1145
910
|
value: ticket,
|
|
@@ -1155,6 +920,10 @@ class FingerprintEngine {
|
|
|
1155
920
|
|
|
1156
921
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
1157
922
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
923
|
+
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
924
|
+
|
|
925
|
+
// Store the secret with a short TTL (e.g., 5 minutes)
|
|
926
|
+
await store.set(`secret:${nonce}`, clientSecret, 300);
|
|
1158
927
|
|
|
1159
928
|
if (logger) {
|
|
1160
929
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
@@ -1165,23 +934,7 @@ class FingerprintEngine {
|
|
|
1165
934
|
// ... logic for TSP/Captcha challenge
|
|
1166
935
|
}
|
|
1167
936
|
|
|
1168
|
-
//
|
|
1169
|
-
if (isSuspiciousMedium) {
|
|
1170
|
-
// Utilisons notre nouveau challenge combiné !
|
|
1171
|
-
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
1172
|
-
|
|
1173
|
-
const minMemDifficulty = 16; // 16Mo
|
|
1174
|
-
const maxMemDifficulty = 48; // 48Mo
|
|
1175
|
-
const memDifficulty = minMemDifficulty + suspicionFactor * (maxMemDifficulty - minMemDifficulty);
|
|
1176
|
-
|
|
1177
|
-
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
|
|
1178
|
-
return {
|
|
1179
|
-
action: 'challenge',
|
|
1180
|
-
status: 429, body: page
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// NOUVELLE LOGIQUE UNIFIÉE POUR TOUS LES NIVEAUX DE SUSPICION (low et medium)
|
|
937
|
+
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
1185
938
|
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1186
939
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
1187
940
|
|
|
@@ -1191,15 +944,70 @@ class FingerprintEngine {
|
|
|
1191
944
|
|
|
1192
945
|
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
1193
946
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
1194
|
-
const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
|
|
947
|
+
const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
|
|
1195
948
|
|
|
1196
|
-
//
|
|
1197
|
-
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
|
|
1198
|
-
return { action: 'challenge', status: 429, body: page };
|
|
949
|
+
// Always use the combined page, even if memory difficulty is 0 (it will be almost instant).
|
|
950
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret);
|
|
951
|
+
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
|
|
1199
952
|
}
|
|
1200
953
|
}
|
|
1201
954
|
|
|
1202
|
-
|
|
955
|
+
// Basic log for each non-static request that passed without a challenge
|
|
956
|
+
if (logger) {
|
|
957
|
+
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return { action: 'next', score: finalScore, vector: suspicionVector };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Identifies a request in a granular way for non-Express environments.
|
|
965
|
+
* @param {object} requestContext - The request context object.
|
|
966
|
+
* @returns {Promise<string>} An identification string (e.g., "device:<id>", "suspicious_high:<ip>").
|
|
967
|
+
*/
|
|
968
|
+
async identifyRequest(requestContext) {
|
|
969
|
+
const { clientIp, cookies, rawReq, rawRes } = requestContext;
|
|
970
|
+
|
|
971
|
+
// --- Update IP reputation ---
|
|
972
|
+
const ipProfile = (await store.get(`ip:${clientIp}`)) || {
|
|
973
|
+
type: "residential",
|
|
974
|
+
deviceIds: new Set(),
|
|
975
|
+
statelessCount: 0,
|
|
976
|
+
lastSeen: 0,
|
|
977
|
+
};
|
|
978
|
+
ipProfile.lastSeen = Date.now();
|
|
979
|
+
if (cookies?.device_id) {
|
|
980
|
+
ipProfile.deviceIds.add(cookies.device_id);
|
|
981
|
+
} else {
|
|
982
|
+
ipProfile.statelessCount++;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (ipProfile.deviceIds.size > SHARED_IP_DEVICE_THRESHOLD) {
|
|
986
|
+
ipProfile.type = "shared";
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const statelessLimit = ipProfile.type === "shared" ? 50 : 10;
|
|
990
|
+
if (ipProfile.statelessCount > statelessLimit) {
|
|
991
|
+
return `suspicious_high:${clientIp}`;
|
|
992
|
+
}
|
|
993
|
+
await store.set(`ip:${clientIp}`, ipProfile);
|
|
994
|
+
|
|
995
|
+
const vector = await __internal.getSuspicionVector(requestContext);
|
|
996
|
+
const score =
|
|
997
|
+
vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
|
|
998
|
+
vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
|
|
999
|
+
vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
|
|
1000
|
+
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8);
|
|
1001
|
+
|
|
1002
|
+
if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
|
|
1003
|
+
if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
|
|
1004
|
+
if (score >= this.securityConfig.thresholds.medium) return `suspicious_medium:${clientIp}`;
|
|
1005
|
+
|
|
1006
|
+
// If a new device_id was created, it's in the context.
|
|
1007
|
+
const newDeviceId = requestContext._newCookies?.find(c => c.name === 'device_id')?.value;
|
|
1008
|
+
const finalDeviceId = cookies?.device_id || newDeviceId || clientIp;
|
|
1009
|
+
|
|
1010
|
+
return `device:${finalDeviceId}`;
|
|
1203
1011
|
}
|
|
1204
1012
|
}
|
|
1205
1013
|
|
|
@@ -1207,6 +1015,13 @@ class FingerprintEngine {
|
|
|
1207
1015
|
export const powMiddleware = (securityConfig) => {
|
|
1208
1016
|
const engine = new FingerprintEngine(securityConfig);
|
|
1209
1017
|
|
|
1018
|
+
if (securityConfig.autotuning) {
|
|
1019
|
+
startThresholdAutoTuning({
|
|
1020
|
+
securityConfig: securityConfig,
|
|
1021
|
+
...securityConfig.autotuning,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1210
1025
|
return async (req, res, next) => {
|
|
1211
1026
|
const requestContext = {
|
|
1212
1027
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
@@ -1214,15 +1029,29 @@ export const powMiddleware = (securityConfig) => {
|
|
|
1214
1029
|
cookies: req.cookies,
|
|
1215
1030
|
query: req.query,
|
|
1216
1031
|
headers: req.headers,
|
|
1217
|
-
isStatic: isStaticResource(req),
|
|
1218
|
-
//
|
|
1219
|
-
|
|
1220
|
-
|
|
1032
|
+
isStatic: isStaticResource(req.path),
|
|
1033
|
+
// Add the newly required properties for full decoupling
|
|
1034
|
+
rawHeaders: req.rawHeaders,
|
|
1035
|
+
httpVersion: req.httpVersion,
|
|
1221
1036
|
};
|
|
1222
1037
|
|
|
1223
1038
|
const decision = await engine.processRequest(requestContext);
|
|
1224
1039
|
|
|
1040
|
+
// Attach the fingerprinting result to the request object for downstream middlewares.
|
|
1041
|
+
req.fingerprint = {
|
|
1042
|
+
score: decision.score,
|
|
1043
|
+
vector: decision.vector,
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// After getSuspicionVector runs, it might have attached cookies to be set.
|
|
1047
|
+
if (requestContext._newCookies) {
|
|
1048
|
+
requestContext._newCookies.forEach(c => res.cookie(c.name, c.value, c.options));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1225
1051
|
switch (decision.action) {
|
|
1052
|
+
case 'block':
|
|
1053
|
+
return res.status(decision.status).send(decision.body);
|
|
1054
|
+
|
|
1226
1055
|
case 'challenge':
|
|
1227
1056
|
return res.status(decision.status).send(decision.body);
|
|
1228
1057
|
|
|
@@ -1245,7 +1074,10 @@ export const powMiddleware = (securityConfig) => {
|
|
|
1245
1074
|
* This is a common pattern to allow mocking of ES module functions.
|
|
1246
1075
|
*/
|
|
1247
1076
|
export const __internal = {
|
|
1077
|
+
getDeviceHash,
|
|
1248
1078
|
getSuspicionVector,
|
|
1079
|
+
cyrb53, // Export for testing
|
|
1080
|
+
FingerprintBuilder, // Export for testing
|
|
1249
1081
|
calculateTarget,
|
|
1250
1082
|
FingerprintEngine, // Expose for advanced testing
|
|
1251
1083
|
};
|
|
@@ -1284,7 +1116,7 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
1284
1116
|
const fitnessFunction = (solution) => {
|
|
1285
1117
|
const [low, medium, high] = solution;
|
|
1286
1118
|
// Constraints: thresholds must be ordered and within a reasonable range.
|
|
1287
|
-
if (low >= medium || medium >= high || low
|
|
1119
|
+
if (low >= medium || medium >= high || low <= 10 || high >= 90) return Infinity;
|
|
1288
1120
|
|
|
1289
1121
|
let falsePositives = 0; // Humans challenged unnecessarily.
|
|
1290
1122
|
let falseNegatives = 0; // Undetected bots.
|
package/library.js
CHANGED
package/package.json
CHANGED
|
@@ -1,50 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
-
"main": "fingerprint.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "vitest run"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"fingerprint.js",
|
|
15
|
-
"library.js",
|
|
16
|
-
"README.md",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
],
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"fingerprint",
|
|
25
|
-
"bot",
|
|
26
|
-
"anti-bot",
|
|
27
|
-
"security",
|
|
28
|
-
"express",
|
|
29
|
-
"middleware",
|
|
30
|
-
"proof-of-work",
|
|
31
|
-
"pow",
|
|
32
|
-
"rate-limiting",
|
|
33
|
-
"mitigation",
|
|
34
|
-
"captcha"
|
|
35
|
-
],
|
|
36
|
-
"author": "anonympins",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"bugs": {
|
|
39
|
-
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"cookie-parser": "^1.4.6",
|
|
44
|
-
"express": "^4.18.2",
|
|
45
|
-
"vitest": "^1.
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@anonympins/fingerprint",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
+
"main": "fingerprint.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.0.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"fingerprint.js",
|
|
15
|
+
"library.js",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"fingerprint",
|
|
25
|
+
"bot",
|
|
26
|
+
"anti-bot",
|
|
27
|
+
"security",
|
|
28
|
+
"express",
|
|
29
|
+
"middleware",
|
|
30
|
+
"proof-of-work",
|
|
31
|
+
"pow",
|
|
32
|
+
"rate-limiting",
|
|
33
|
+
"mitigation",
|
|
34
|
+
"captcha"
|
|
35
|
+
],
|
|
36
|
+
"author": "anonympins",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"cookie-parser": "^1.4.6",
|
|
44
|
+
"express": "^4.18.2",
|
|
45
|
+
"vitest": "^4.1.11"
|
|
46
|
+
}
|
|
47
|
+
}
|