@anonympins/fingerprint 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -22
- package/fingerprint.js +502 -230
- package/library.js +0 -824
- package/package.json +7 -3
package/fingerprint.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// C:/Dev/games.primals.net/src/utils/fingerprint.js
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import { Optimization } from "./library.js";
|
|
3
4
|
|
|
4
5
|
const POW_SECRET = process.env.POW_SECRET;
|
|
5
6
|
|
|
@@ -9,7 +10,7 @@ if (!POW_SECRET && process.env.NODE_ENV === 'production') {
|
|
|
9
10
|
console.warn('Warning: POW_SECRET environment variable not set. Using a default, insecure secret for development.');
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
+
* cyrb53 hash algorithm (fast with a low collision rate). Exported for reuse.
|
|
13
14
|
*/
|
|
14
15
|
export const cyrb53 = (str, seed = 0) => {
|
|
15
16
|
let h1 = 0xdeadbeef ^ seed,
|
|
@@ -29,8 +30,8 @@ export const cyrb53 = (str, seed = 0) => {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
33
|
+
* Class to build a composite fingerprint (Multi-Hash).
|
|
34
|
+
* Output format: "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
34
35
|
*/
|
|
35
36
|
export class FingerprintBuilder {
|
|
36
37
|
constructor() {
|
|
@@ -38,20 +39,20 @@ export class FingerprintBuilder {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @param {string} group -
|
|
43
|
-
* @param {string|number|boolean} value -
|
|
42
|
+
* Adds a component to the global hash.
|
|
43
|
+
* @param {string} group - The group name (e.g., 'hw', 'screen', 'geo')
|
|
44
|
+
* @param {string|number|boolean} value - The raw value to be hashed
|
|
44
45
|
*/
|
|
45
46
|
add(group, value) {
|
|
46
47
|
if (value === undefined || value === null) return this;
|
|
47
|
-
//
|
|
48
|
+
// Hash the value individually to anonymize it and reduce its size
|
|
48
49
|
this.components.set(group, cyrb53(String(value)));
|
|
49
50
|
return this;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* Generates the final signature string.
|
|
55
|
+
* Sorts keys to ensure a deterministic order.
|
|
55
56
|
*/
|
|
56
57
|
toString() {
|
|
57
58
|
return Array.from(this.components.entries())
|
|
@@ -61,10 +62,10 @@ export class FingerprintBuilder {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* @param {string} fpString1 -
|
|
67
|
-
* @param {string} fpString2 -
|
|
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
|
|
68
69
|
*/
|
|
69
70
|
static compare(fpString1, fpString2) {
|
|
70
71
|
if (!fpString1 || !fpString2) return 0;
|
|
@@ -81,15 +82,15 @@ export class FingerprintBuilder {
|
|
|
81
82
|
const map1 = parse(fpString1);
|
|
82
83
|
const map2 = parse(fpString2);
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
+
// "Veracity" weights (Entropy/Stability)
|
|
85
86
|
const weights = {
|
|
86
|
-
cvs: 4.0, // Canvas:
|
|
87
|
-
gpu: 3.0, // GPU:
|
|
88
|
-
hw: 1.5, // Hardware:
|
|
89
|
-
scr: 1.0, // Screen:
|
|
90
|
-
geo: 0.5, // Geo:
|
|
91
|
-
os: 0.5, // OS:
|
|
92
|
-
bot: 0.0, // Bot:
|
|
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
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
let weightedMatches = 0;
|
|
@@ -112,11 +113,11 @@ export class FingerprintBuilder {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
// Cache
|
|
116
|
+
// Cache to avoid recalculating constants (Hardware, etc.)
|
|
116
117
|
let cachedBuilder = null;
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
|
-
*
|
|
120
|
+
* Generates the fingerprint of the current device.
|
|
120
121
|
*/
|
|
121
122
|
export const getDeviceFingerprint = () => {
|
|
122
123
|
// NOTE: This is client-side code and should be in a separate file.
|
|
@@ -131,29 +132,29 @@ export const getDeviceFingerprint = () => {
|
|
|
131
132
|
|
|
132
133
|
cachedBuilder = new FingerprintBuilder();
|
|
133
134
|
|
|
134
|
-
// 1. Hardware (
|
|
135
|
+
// 1. Hardware (Very stable): Cores, RAM, GPU (if available via canvas), Touch
|
|
135
136
|
cachedBuilder.add(
|
|
136
137
|
"hw",
|
|
137
138
|
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
138
139
|
);
|
|
139
140
|
|
|
140
|
-
// 2. Geo/Locale (Stable
|
|
141
|
+
// 2. Geo/Locale (Stable except for travel/VPN): Timezone, Language
|
|
141
142
|
cachedBuilder.add(
|
|
142
143
|
"geo",
|
|
143
144
|
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
144
145
|
);
|
|
145
146
|
|
|
146
|
-
// 3. Screen (Stable
|
|
147
|
-
// Note
|
|
147
|
+
// 3. Screen (Stable except for monitor/zoom changes): Dimensions, ColorDepth
|
|
148
|
+
// Note: We use availWidth/Height which excludes the taskbar, sometimes more unique
|
|
148
149
|
cachedBuilder.add(
|
|
149
150
|
"scr",
|
|
150
151
|
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
151
152
|
);
|
|
152
153
|
|
|
153
|
-
// 4. Platform (Stable)
|
|
154
|
+
// 4. Platform (Stable): OS, Engine
|
|
154
155
|
cachedBuilder.add("os", nav.platform);
|
|
155
156
|
|
|
156
|
-
// 5. Graphics (WebGL Vendor/Renderer) -
|
|
157
|
+
// 5. Graphics (WebGL Vendor/Renderer) - Strong hardware invariant
|
|
157
158
|
try {
|
|
158
159
|
const canvas = document.createElement("canvas");
|
|
159
160
|
const gl =
|
|
@@ -168,8 +169,8 @@ export const getDeviceFingerprint = () => {
|
|
|
168
169
|
}
|
|
169
170
|
} catch (e) {}
|
|
170
171
|
|
|
171
|
-
// 6. Canvas Fingerprinting (Rendering quirks) -
|
|
172
|
-
//
|
|
172
|
+
// 6. Canvas Fingerprinting (Rendering quirks) - Adds ~5-10% uniqueness
|
|
173
|
+
// Exploits micro-differences in anti-aliasing and font rendering
|
|
173
174
|
try {
|
|
174
175
|
const canvas = document.createElement("canvas");
|
|
175
176
|
const ctx = canvas.getContext("2d");
|
|
@@ -188,23 +189,23 @@ export const getDeviceFingerprint = () => {
|
|
|
188
189
|
}
|
|
189
190
|
} catch (e) {}
|
|
190
191
|
|
|
191
|
-
// 7. Bot Detection (
|
|
192
|
+
// 7. Bot Detection (Hidden indicator)
|
|
192
193
|
if (nav.webdriver) cachedBuilder.add("bot", "true");
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
//
|
|
196
|
+
// Return a copy to allow adding dynamic fields if needed without polluting the cache
|
|
196
197
|
return cachedBuilder.toString();
|
|
197
198
|
};
|
|
198
199
|
|
|
199
200
|
/**
|
|
200
|
-
*
|
|
201
|
+
* Generates a request signature including the context.
|
|
201
202
|
* @param {object} payload
|
|
202
203
|
*/
|
|
203
204
|
export const generateRequestSignature = (payload = {}) => {
|
|
204
205
|
const deviceFp = getDeviceFingerprint();
|
|
205
206
|
|
|
206
|
-
//
|
|
207
|
-
// Note:
|
|
207
|
+
// Create a temporary builder that inherits from deviceFp
|
|
208
|
+
// Note: Here we keep it simple, just concatenating the payload hash
|
|
208
209
|
const sortedPayload = Object.keys(payload)
|
|
209
210
|
.sort()
|
|
210
211
|
.map((k) => `${k}=${payload[k]}`)
|
|
@@ -215,22 +216,22 @@ export const generateRequestSignature = (payload = {}) => {
|
|
|
215
216
|
};
|
|
216
217
|
|
|
217
218
|
/**
|
|
218
|
-
*
|
|
219
|
-
* @param {object} payload -
|
|
220
|
-
* @param {string} secret -
|
|
221
|
-
* @returns {Promise<string>}
|
|
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.
|
|
222
223
|
*/
|
|
223
224
|
export const generateCombatSignature = async (payload, secret) => {
|
|
224
225
|
// NOTE: This is client-side code using the Web Crypto API (`window.crypto`).
|
|
225
226
|
// It should be moved to a client-side script file.
|
|
226
227
|
|
|
227
|
-
// 1.
|
|
228
|
+
// 1. Create a stable string from the payload.
|
|
228
229
|
const sortedPayload = Object.keys(payload)
|
|
229
230
|
.sort()
|
|
230
231
|
.map((k) => `${k}=${payload[k]}`)
|
|
231
232
|
.join("&");
|
|
232
233
|
|
|
233
|
-
// 2.
|
|
234
|
+
// 2. Use the Web Crypto API for HMAC
|
|
234
235
|
const encoder = new TextEncoder();
|
|
235
236
|
const key = await window.crypto.subtle.importKey(
|
|
236
237
|
"raw",
|
|
@@ -245,7 +246,7 @@ export const generateCombatSignature = async (payload, secret) => {
|
|
|
245
246
|
encoder.encode(sortedPayload),
|
|
246
247
|
);
|
|
247
248
|
|
|
248
|
-
// 3.
|
|
249
|
+
// 3. Convert the signature to a hexadecimal string.
|
|
249
250
|
const hashArray = Array.from(new Uint8Array(signatureBuffer));
|
|
250
251
|
const hexString = hashArray
|
|
251
252
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
@@ -254,13 +255,13 @@ export const generateCombatSignature = async (payload, secret) => {
|
|
|
254
255
|
};
|
|
255
256
|
|
|
256
257
|
/**
|
|
257
|
-
*
|
|
258
|
-
* @param {string} nonce -
|
|
259
|
-
* @param {number} numCities -
|
|
260
|
-
* @param {number} targetMaxDistance -
|
|
261
|
-
* @param {Array<{x: number, y: number}>} cities -
|
|
262
|
-
* @param {string} path -
|
|
263
|
-
* @returns {string} HTML
|
|
258
|
+
* Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
|
|
259
|
+
* @param {string} nonce - Unique nonce for the challenge.
|
|
260
|
+
* @param {number} numCities - Number of cities to include in the problem.
|
|
261
|
+
* @param {number} targetMaxDistance - Maximum acceptable distance for the solution.
|
|
262
|
+
* @param {Array<{x: number, y: number}>} cities - Coordinates of the cities.
|
|
263
|
+
* @param {string} path - Redirect path after solving.
|
|
264
|
+
* @returns {string} HTML of the challenge page.
|
|
264
265
|
*/
|
|
265
266
|
const generateTspChallenge = (
|
|
266
267
|
nonce,
|
|
@@ -272,28 +273,28 @@ const generateTspChallenge = (
|
|
|
272
273
|
const citiesJson = JSON.stringify(cities);
|
|
273
274
|
return `
|
|
274
275
|
<html>
|
|
275
|
-
<head><title>
|
|
276
|
+
<head><title>Advanced Security Check (Level 3)</title></head>
|
|
276
277
|
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
277
|
-
<h1>
|
|
278
|
-
<p>
|
|
279
|
-
<div id="loader" style="margin:20px;">⚙️
|
|
278
|
+
<h1>Ultimate Verification (Level 3)</h1>
|
|
279
|
+
<p>Please solve this small optimization problem to prove you are human.</p>
|
|
280
|
+
<div id="loader" style="margin:20px;">⚙️ Calculating route... (${numCities} cities)</div>
|
|
280
281
|
<script>
|
|
281
282
|
const cities = ${citiesJson};
|
|
282
283
|
const nonce = "${nonce}";
|
|
283
284
|
const targetMaxDistance = ${targetMaxDistance};
|
|
284
285
|
|
|
285
|
-
//
|
|
286
|
+
// Utility function to calculate the distance between two cities
|
|
286
287
|
function distance(city1, city2) {
|
|
287
288
|
return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
//
|
|
291
|
+
// Utility function to evaluate the total distance of a path
|
|
291
292
|
function evaluatePathDistance(cities, path) {
|
|
292
293
|
let totalDistance = 0;
|
|
293
294
|
for (let i = 0; i < path.length - 1; i++) {
|
|
294
295
|
totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
|
|
295
296
|
}
|
|
296
|
-
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); //
|
|
297
|
+
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
|
|
297
298
|
return totalDistance;
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -305,7 +306,7 @@ const generateTspChallenge = (
|
|
|
305
306
|
let currentPath = [];
|
|
306
307
|
let visited = new Array(numCities).fill(false);
|
|
307
308
|
|
|
308
|
-
let currentCityIndex = 0; //
|
|
309
|
+
let currentCityIndex = 0; // Always start with the first city for reproducibility
|
|
309
310
|
currentPath.push(currentCityIndex);
|
|
310
311
|
visited[currentCityIndex] = true;
|
|
311
312
|
|
|
@@ -330,7 +331,7 @@ const generateTspChallenge = (
|
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
async function solve() {
|
|
333
|
-
//
|
|
334
|
+
// To avoid freezing the browser, yield the thread from time to time
|
|
334
335
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
335
336
|
const solutionPath = solveTspNearestNeighbor(cities);
|
|
336
337
|
const solutionDistance = evaluatePathDistance(cities, solutionPath);
|
|
@@ -338,7 +339,7 @@ const generateTspChallenge = (
|
|
|
338
339
|
if (solutionDistance <= targetMaxDistance) {
|
|
339
340
|
window.location.href = "${path}" + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(solutionPath);
|
|
340
341
|
} else {
|
|
341
|
-
document.getElementById('loader').innerText = "
|
|
342
|
+
document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
|
|
342
343
|
}
|
|
343
344
|
}
|
|
344
345
|
solve();
|
|
@@ -348,13 +349,13 @@ const generateTspChallenge = (
|
|
|
348
349
|
};
|
|
349
350
|
|
|
350
351
|
/**
|
|
351
|
-
*
|
|
352
|
-
* @param {string} nonce -
|
|
353
|
-
* @param {string} solutionPathJson -
|
|
354
|
-
* @param {number} numCities -
|
|
355
|
-
* @param {number} targetMaxDistance -
|
|
356
|
-
* @param {Array<{x: number, y: number}>} cities -
|
|
357
|
-
* @returns {boolean} True
|
|
352
|
+
* Verifies a TSP PoW solution.
|
|
353
|
+
* @param {string} nonce - The challenge nonce.
|
|
354
|
+
* @param {string} solutionPathJson - The path proposed by the client (stringified JSON).
|
|
355
|
+
* @param {number} numCities - The number of cities in the challenge.
|
|
356
|
+
* @param {number} targetMaxDistance - The maximum acceptable distance.
|
|
357
|
+
* @param {Array<{x: number, y: number}>} cities - The coordinates of the cities.
|
|
358
|
+
* @returns {boolean} True if the solution is valid.
|
|
358
359
|
*/
|
|
359
360
|
export const verifyTspChallenge = (
|
|
360
361
|
nonce,
|
|
@@ -368,7 +369,7 @@ export const verifyTspChallenge = (
|
|
|
368
369
|
if (!Array.isArray(solutionPath) || solutionPath.length !== numCities)
|
|
369
370
|
return false;
|
|
370
371
|
|
|
371
|
-
//
|
|
372
|
+
// Verify that the path is a valid permutation of the cities
|
|
372
373
|
const uniqueCities = new Set(solutionPath);
|
|
373
374
|
if (
|
|
374
375
|
uniqueCities.size !== numCities ||
|
|
@@ -377,11 +378,11 @@ export const verifyTspChallenge = (
|
|
|
377
378
|
)
|
|
378
379
|
return false;
|
|
379
380
|
|
|
380
|
-
//
|
|
381
|
+
// Recalculate the distance on the server side
|
|
381
382
|
let totalDistance = 0;
|
|
382
383
|
let totalPenalty = 0;
|
|
383
384
|
|
|
384
|
-
//
|
|
385
|
+
// Function to calculate the angle between 3 points (p1 -> p2 -> p3)
|
|
385
386
|
const calculateAngle = (p1, p2, p3) => {
|
|
386
387
|
const v1 = { x: p1.x - p2.x, y: p1.y - p2.y };
|
|
387
388
|
const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
|
@@ -398,31 +399,31 @@ export const verifyTspChallenge = (
|
|
|
398
399
|
const p2_idx = solutionPath[(i + 1) % numCities];
|
|
399
400
|
const p3_idx = solutionPath[(i + 2) % numCities];
|
|
400
401
|
|
|
401
|
-
// 1.
|
|
402
|
+
// 1. Calculate segment distance
|
|
402
403
|
totalDistance += Math.sqrt(Math.pow(cities[p1_idx].x - cities[p2_idx].x, 2) + Math.pow(cities[p1_idx].y - cities[p2_idx].y, 2));
|
|
403
404
|
|
|
404
|
-
// 2.
|
|
405
|
+
// 2. Calculate turn penalty
|
|
405
406
|
const angle = calculateAngle(
|
|
406
407
|
cities[p1_idx],
|
|
407
408
|
cities[p2_idx],
|
|
408
409
|
cities[p3_idx],
|
|
409
410
|
);
|
|
410
411
|
if (angle < 45) {
|
|
411
|
-
//
|
|
412
|
-
totalPenalty += (45 - angle) * 5; //
|
|
412
|
+
// Penalty for very sharp turns (< 45 degrees)
|
|
413
|
+
totalPenalty += (45 - angle) * 5; // The penalty is proportional to the sharpness of the angle
|
|
413
414
|
}
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
const finalScore = totalDistance + totalPenalty;
|
|
417
418
|
return finalScore <= targetMaxDistance;
|
|
418
419
|
} catch (e) {
|
|
419
|
-
console.error("
|
|
420
|
+
console.error("Error during TSP challenge verification:", e);
|
|
420
421
|
return false;
|
|
421
422
|
}
|
|
422
423
|
};
|
|
423
424
|
|
|
424
425
|
/**
|
|
425
|
-
*
|
|
426
|
+
* Generates the HTML content for the CPU PoW challenge (SHA-256).
|
|
426
427
|
*/
|
|
427
428
|
const generateCpuPoWChallenge = (
|
|
428
429
|
clientIp,
|
|
@@ -432,11 +433,11 @@ const generateCpuPoWChallenge = (
|
|
|
432
433
|
) => {
|
|
433
434
|
return `
|
|
434
435
|
<html>
|
|
435
|
-
<head><title>
|
|
436
|
+
<head><title>Security Check</title></head>
|
|
436
437
|
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
437
|
-
<h1>
|
|
438
|
-
<p>
|
|
439
|
-
<div id="loader" style="margin:20px;">⚙️
|
|
438
|
+
<h1>One moment... (Level 1)</h1>
|
|
439
|
+
<p>We are verifying that you are not a bot. This takes a few seconds.</p>
|
|
440
|
+
<div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
|
|
440
441
|
<script>
|
|
441
442
|
async function solve() {
|
|
442
443
|
const ip = "${clientIp}";
|
|
@@ -451,7 +452,7 @@ const generateCpuPoWChallenge = (
|
|
|
451
452
|
const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
452
453
|
if (hash.startsWith(target)) break;
|
|
453
454
|
solution++;
|
|
454
|
-
if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); //
|
|
455
|
+
if (solution % 100000 === 0) await new Promise(resolve => setTimeout(resolve, 0)); // To avoid freezing the browser
|
|
455
456
|
}
|
|
456
457
|
window.location.href = "${path}" + "?pow_type=cpu&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
457
458
|
}
|
|
@@ -463,7 +464,7 @@ const generateCpuPoWChallenge = (
|
|
|
463
464
|
};
|
|
464
465
|
|
|
465
466
|
/**
|
|
466
|
-
*
|
|
467
|
+
* Generates the HTML content for a memory-intensive PoW challenge.
|
|
467
468
|
*/
|
|
468
469
|
const generateMemoryPoWChallenge = (
|
|
469
470
|
clientIp,
|
|
@@ -471,14 +472,14 @@ const generateMemoryPoWChallenge = (
|
|
|
471
472
|
difficulty = 16,
|
|
472
473
|
path = "",
|
|
473
474
|
) => {
|
|
474
|
-
// difficulty
|
|
475
|
+
// difficulty here is the buffer size in MB.
|
|
475
476
|
return `
|
|
476
477
|
<html>
|
|
477
|
-
<head><title>
|
|
478
|
+
<head><title>Advanced Security Check</title></head>
|
|
478
479
|
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
479
|
-
<h1>
|
|
480
|
-
<p>
|
|
481
|
-
<div id="loader" style="margin:20px;">⚙️
|
|
480
|
+
<h1>Enhanced Verification... (Level 2)</h1>
|
|
481
|
+
<p>Your activity requires an additional security check.</p>
|
|
482
|
+
<div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
|
|
482
483
|
<script>
|
|
483
484
|
async function solve() {
|
|
484
485
|
const nonce = "${nonce}";
|
|
@@ -499,7 +500,7 @@ const generateMemoryPoWChallenge = (
|
|
|
499
500
|
}
|
|
500
501
|
window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
501
502
|
} catch(e) {
|
|
502
|
-
document.getElementById('loader').innerText = "
|
|
503
|
+
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
solve();
|
|
@@ -509,7 +510,7 @@ const generateMemoryPoWChallenge = (
|
|
|
509
510
|
};
|
|
510
511
|
|
|
511
512
|
/**
|
|
512
|
-
*
|
|
513
|
+
* Verifies if a PoW solution is valid and generates a clearance ticket.
|
|
513
514
|
*/
|
|
514
515
|
export const verifyPoWAndGenerateTicket = (
|
|
515
516
|
ip,
|
|
@@ -517,7 +518,7 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
517
518
|
solution,
|
|
518
519
|
difficulty = 4,
|
|
519
520
|
) => {
|
|
520
|
-
// 1.
|
|
521
|
+
// 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
|
|
521
522
|
const hash = crypto
|
|
522
523
|
.createHash("sha256")
|
|
523
524
|
.update(`${ip}:${nonce}:${solution}`)
|
|
@@ -527,7 +528,7 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
527
528
|
return null;
|
|
528
529
|
}
|
|
529
530
|
|
|
530
|
-
// 2.
|
|
531
|
+
// 2. Generate an HMAC ticket so the client doesn't have to do it again for 1 hour
|
|
531
532
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
532
533
|
const signature = crypto
|
|
533
534
|
.createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
|
|
@@ -538,8 +539,8 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
538
539
|
};
|
|
539
540
|
|
|
540
541
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
542
|
+
* Verifies a memory PoW solution.
|
|
543
|
+
* The server performs the same calculation to validate.
|
|
543
544
|
*/
|
|
544
545
|
export const verifyMemoryPoW = (nonce, solution, difficulty = 16) => {
|
|
545
546
|
const size = difficulty * 1024 * 1024;
|
|
@@ -565,15 +566,15 @@ export const isTicketValid = (ip, ticket) => {
|
|
|
565
566
|
.update(`${ip}:${expiry}`)
|
|
566
567
|
.digest("hex");
|
|
567
568
|
|
|
568
|
-
//
|
|
569
|
+
// Use timingSafeEqual to prevent timing attacks
|
|
569
570
|
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig));
|
|
570
571
|
};
|
|
571
572
|
|
|
572
573
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
* @param {object} req -
|
|
576
|
-
* @returns {string}
|
|
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.
|
|
577
578
|
*/
|
|
578
579
|
function getDeviceHash(req) {
|
|
579
580
|
const srv = new FingerprintBuilder();
|
|
@@ -581,33 +582,33 @@ function getDeviceHash(req) {
|
|
|
581
582
|
if (req.headers["sec-ch-ua-platform"])
|
|
582
583
|
srv.add("os", req.headers["sec-ch-ua-platform"]);
|
|
583
584
|
if (req.headers["sec-ch-ua"]) srv.add("ch", req.headers["sec-ch-ua"]);
|
|
584
|
-
return srv.toString(); //
|
|
585
|
+
return srv.toString(); // Returns the full fingerprint string for detailed comparison.
|
|
585
586
|
}
|
|
586
587
|
|
|
587
588
|
/**
|
|
588
|
-
*
|
|
589
|
-
* @param {object} req -
|
|
589
|
+
* Calculates suspicion indicators related to HTTP header anomalies.
|
|
590
|
+
* @param {object} req - The Express request object.
|
|
590
591
|
* @returns {{headerAnomalyScore: number}}
|
|
591
592
|
*/
|
|
592
593
|
function getHeaderAnomalies(req, consistencyScore) {
|
|
593
594
|
// FIX: consistencyScore est maintenant passé
|
|
594
595
|
let anomalyScore = 0;
|
|
595
|
-
//
|
|
596
|
+
// Strong penalty if User-Agent is missing or very short (sign of a simple script)
|
|
596
597
|
if (!req.headers["user-agent"] || req.headers["user-agent"].length < 10) {
|
|
597
598
|
anomalyScore += 60;
|
|
598
599
|
}
|
|
599
|
-
//
|
|
600
|
+
// Penalty if Accept-Language header is missing
|
|
600
601
|
if (!req.headers["accept-language"]) {
|
|
601
602
|
anomalyScore += 25;
|
|
602
603
|
}
|
|
603
|
-
//
|
|
604
|
+
// Penalty for HTTP/1.0 requests, often used by old tools or bots
|
|
604
605
|
if (req.httpVersion === "1.0") {
|
|
605
606
|
anomalyScore += 15;
|
|
606
607
|
}
|
|
607
608
|
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
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.
|
|
611
612
|
const inconsistencyScore = Math.max(0, (1 - consistencyScore) * 200);
|
|
612
613
|
|
|
613
614
|
return {
|
|
@@ -625,8 +626,8 @@ function getHeaderAnomalies(req, consistencyScore) {
|
|
|
625
626
|
*/
|
|
626
627
|
|
|
627
628
|
/**
|
|
628
|
-
*
|
|
629
|
-
* @type {IStore}
|
|
629
|
+
* Default in-memory store implementation.
|
|
630
|
+
* @type {IStore}
|
|
630
631
|
*/
|
|
631
632
|
const inMemoryStore = {
|
|
632
633
|
_map: new Map(),
|
|
@@ -640,26 +641,26 @@ const inMemoryStore = {
|
|
|
640
641
|
let store = inMemoryStore;
|
|
641
642
|
|
|
642
643
|
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* @param {IStore} externalStore -
|
|
644
|
+
* Allows configuring an external datastore (e.g., Redis).
|
|
645
|
+
* Must be called before the middleware is used.
|
|
646
|
+
* @param {IStore} externalStore - An implementation of the IStore interface.
|
|
646
647
|
*/
|
|
647
648
|
export const configureStore = (externalStore) => {
|
|
648
649
|
store = externalStore;
|
|
649
650
|
};
|
|
650
651
|
|
|
651
652
|
/**
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
* @param {object} req -
|
|
655
|
-
* @param {object} res -
|
|
656
|
-
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number}>}
|
|
653
|
+
* Orchestrates request identification using a persistent anchor (cookie)
|
|
654
|
+
* and fingerprint verification.
|
|
655
|
+
* @param {object} req - The Express request object.
|
|
656
|
+
* @param {object} res - The Express response object (to set the cookie).
|
|
657
|
+
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number}>}
|
|
657
658
|
*/
|
|
658
659
|
async function resolveRequestIdentity(req, res) {
|
|
659
660
|
const existingDeviceId = req.cookies?.device_id;
|
|
660
661
|
const currentDeviceHash = getDeviceHash(req);
|
|
661
662
|
let deviceId = existingDeviceId;
|
|
662
|
-
let consistencyScore = 1.0; // 1.0 =
|
|
663
|
+
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
663
664
|
let deviceData = null;
|
|
664
665
|
|
|
665
666
|
if (deviceId) {
|
|
@@ -667,36 +668,36 @@ async function resolveRequestIdentity(req, res) {
|
|
|
667
668
|
}
|
|
668
669
|
|
|
669
670
|
if (deviceData) {
|
|
670
|
-
//
|
|
671
|
+
// Case 1: The user has a "passport" and we know them.
|
|
671
672
|
const storedHash = deviceData.initialDeviceHash;
|
|
672
673
|
|
|
673
|
-
//
|
|
674
|
+
// Compare the current fingerprint with the reference one.
|
|
674
675
|
consistencyScore = FingerprintBuilder.compare(
|
|
675
676
|
storedHash,
|
|
676
677
|
currentDeviceHash,
|
|
677
678
|
);
|
|
678
679
|
} else {
|
|
679
|
-
//
|
|
680
|
-
deviceId = crypto.randomUUID(); //
|
|
680
|
+
// Case 2: New user or lost/invalid cookie.
|
|
681
|
+
deviceId = crypto.randomUUID(); // Generate a new "passport".
|
|
681
682
|
|
|
682
|
-
//
|
|
683
|
+
// Set the cookie securely.
|
|
683
684
|
res.cookie("device_id", deviceId, {
|
|
684
685
|
httpOnly: true,
|
|
685
686
|
secure: process.env.NODE_ENV === "production",
|
|
686
687
|
sameSite: "strict",
|
|
687
|
-
maxAge: 31536000000, // 1
|
|
688
|
+
maxAge: 31536000000, // 1 year
|
|
688
689
|
});
|
|
689
690
|
|
|
690
|
-
//
|
|
691
|
+
// Initialize tracking for this new device.
|
|
691
692
|
deviceData = {
|
|
692
|
-
initialDeviceHash: currentDeviceHash, //
|
|
693
|
+
initialDeviceHash: currentDeviceHash, // Anchor the initial fingerprint.
|
|
693
694
|
ips: new Set(),
|
|
694
695
|
lastUpdate: Date.now(),
|
|
695
696
|
lastFpHash: currentDeviceHash,
|
|
696
697
|
lastChangeTimestamp: 0,
|
|
697
698
|
rapidChangeCount: 0,
|
|
698
699
|
};
|
|
699
|
-
//
|
|
700
|
+
// The write will happen in getSuspicionVector after all modifications.
|
|
700
701
|
}
|
|
701
702
|
|
|
702
703
|
return { deviceId, deviceData, consistencyScore };
|
|
@@ -704,37 +705,37 @@ async function resolveRequestIdentity(req, res) {
|
|
|
704
705
|
|
|
705
706
|
/*
|
|
706
707
|
* Calcule les indicateurs de suspicion liés au comportement de l'appareil (historique, rotation).
|
|
707
|
-
* @param {object} req -
|
|
708
|
-
* @param {object} deviceData -
|
|
708
|
+
* @param {object} req - The Express request object.
|
|
709
|
+
* @param {object} deviceData - The device's activity data.
|
|
709
710
|
* @returns {Promise<{historyScore: number, rotationScore: number}>}
|
|
710
711
|
*/
|
|
711
712
|
async function getBehavioralIndicators(req, deviceData) {
|
|
712
713
|
const now = Date.now();
|
|
713
714
|
const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
|
|
714
715
|
|
|
715
|
-
//
|
|
716
|
+
// Get the IP type to modulate the score
|
|
716
717
|
const ipProfile = (await store.get(`ip:${clientIp}`)) || { type: "residential" };
|
|
717
718
|
const isSharedIp = ipProfile.type === "shared";
|
|
718
719
|
|
|
719
|
-
const currentFpHash = getDeviceHash(req); //
|
|
720
|
+
const currentFpHash = getDeviceHash(req); // Use the device hash
|
|
720
721
|
|
|
721
|
-
// ---
|
|
722
|
+
// --- Behavior analysis (Change frequency) ---
|
|
722
723
|
if (deviceData.lastFpHash && currentFpHash !== deviceData.lastFpHash) {
|
|
723
724
|
const timeSinceLastChange = now - deviceData.lastChangeTimestamp;
|
|
724
725
|
|
|
725
726
|
if (timeSinceLastChange < RAPID_CHANGE_THRESHOLD_MS) {
|
|
726
727
|
deviceData.rapidChangeCount = Math.min(
|
|
727
728
|
deviceData.rapidChangeCount + 1,
|
|
728
|
-
MAX_RAPID_CHANGES_PER_DEVICE * 2,
|
|
729
|
-
);
|
|
729
|
+
MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
|
|
730
|
+
);
|
|
730
731
|
} else {
|
|
731
|
-
deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); //
|
|
732
|
+
deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
|
|
732
733
|
}
|
|
733
734
|
deviceData.lastChangeTimestamp = now;
|
|
734
735
|
}
|
|
735
736
|
|
|
736
737
|
deviceData.lastFpHash = currentFpHash;
|
|
737
|
-
deviceData.ips.add(clientIp); //
|
|
738
|
+
deviceData.ips.add(clientIp); // Record the IP used by this device
|
|
738
739
|
|
|
739
740
|
// NOUVELLE LOGIQUE : Le score d'historique est basé sur le nombre d'IPs utilisées par l'appareil.
|
|
740
741
|
// Très efficace contre la rotation de proxy.
|
|
@@ -750,7 +751,7 @@ async function getBehavioralIndicators(req, deviceData) {
|
|
|
750
751
|
100,
|
|
751
752
|
);
|
|
752
753
|
|
|
753
|
-
// Score
|
|
754
|
+
// Score based on rapid identity rotation (0-100)
|
|
754
755
|
const rotationScore = Math.min(
|
|
755
756
|
100,
|
|
756
757
|
(deviceData.rapidChangeCount / MAX_RAPID_CHANGES_PER_DEVICE) * 100,
|
|
@@ -760,17 +761,17 @@ async function getBehavioralIndicators(req, deviceData) {
|
|
|
760
761
|
}
|
|
761
762
|
|
|
762
763
|
/**
|
|
763
|
-
*
|
|
764
|
-
* @param {object} req -
|
|
764
|
+
* Returns a vector of raw (unweighted) suspicion scores.
|
|
765
|
+
* @param {object} req - The Express request object.
|
|
765
766
|
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number}>}
|
|
766
767
|
*/
|
|
767
768
|
export const getSuspicionVector = async (req, res) => {
|
|
768
769
|
const { deviceId, deviceData, consistencyScore } = await resolveRequestIdentity(req, res);
|
|
769
770
|
|
|
770
771
|
const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
|
|
771
|
-
await store.set(`ip-device:${clientIp}`, deviceId); //
|
|
772
|
+
await store.set(`ip-device:${clientIp}`, deviceId); // Link the IP to the device
|
|
772
773
|
|
|
773
|
-
//
|
|
774
|
+
// Periodically clean up device data
|
|
774
775
|
if (Date.now() - deviceData.lastUpdate > 10 * 60 * 1000) { // 10 minutes
|
|
775
776
|
deviceData.ips.clear();
|
|
776
777
|
deviceData.rapidChangeCount = 0;
|
|
@@ -780,13 +781,13 @@ export const getSuspicionVector = async (req, res) => {
|
|
|
780
781
|
const behavioral = await getBehavioralIndicators(req, deviceData);
|
|
781
782
|
const anomalies = getHeaderAnomalies(req, consistencyScore);
|
|
782
783
|
|
|
783
|
-
//
|
|
784
|
+
// Save the updated device state to the store
|
|
784
785
|
await store.set(`device:${deviceId}`, deviceData);
|
|
785
786
|
|
|
786
787
|
return { ...behavioral, ...anomalies };
|
|
787
788
|
};
|
|
788
789
|
|
|
789
|
-
//
|
|
790
|
+
// A residential user can change networks (home, 4G, public wifi).
|
|
790
791
|
const MAX_DISTINCT_IPS_PER_DEVICE = 15;
|
|
791
792
|
// Un utilisateur derrière un NAT/proxy ne devrait pas utiliser BEAUCOUP d'autres IPs.
|
|
792
793
|
const MAX_DISTINCT_IPS_FOR_SHARED_USER = 5;
|
|
@@ -795,18 +796,18 @@ const MAX_DISTINCT_IPS_FOR_SHARED_USER = 5;
|
|
|
795
796
|
const SHARED_IP_DEVICE_THRESHOLD = 50;
|
|
796
797
|
|
|
797
798
|
const RAPID_CHANGE_THRESHOLD_MS = 2000; // 2 secondes
|
|
798
|
-
const MAX_RAPID_CHANGES_PER_DEVICE = 3; //
|
|
799
|
+
const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Number of rapid fingerprint changes allowed per device.
|
|
799
800
|
|
|
800
801
|
/**
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
802
|
+
* Identifies a request on the server side in a granular way.
|
|
803
|
+
* Uses FingerprintBuilder to create a fingerprint based on headers
|
|
804
|
+
* and IP, making spoofing more complex (requires changing the entire stack).
|
|
804
805
|
*/
|
|
805
806
|
export const identifyRequest = async (req, res) => {
|
|
806
807
|
const clientIp = req.ip || req.socket?.remoteAddress || "unknown";
|
|
807
808
|
const deviceId = req.cookies?.device_id;
|
|
808
809
|
|
|
809
|
-
// ---
|
|
810
|
+
// --- Update IP reputation ---
|
|
810
811
|
const ipProfile = (await store.get(`ip:${clientIp}`)) || {
|
|
811
812
|
type: "residential",
|
|
812
813
|
deviceIds: new Set(),
|
|
@@ -817,35 +818,35 @@ export const identifyRequest = async (req, res) => {
|
|
|
817
818
|
if (deviceId) {
|
|
818
819
|
ipProfile.deviceIds.add(deviceId);
|
|
819
820
|
} else {
|
|
820
|
-
//
|
|
821
|
+
// Improved anti-"Amnesiac Bot" logic
|
|
821
822
|
ipProfile.statelessCount++;
|
|
822
823
|
}
|
|
823
824
|
|
|
824
|
-
//
|
|
825
|
+
// If an IP sees too many different devices, classify it as "shared".
|
|
825
826
|
if (ipProfile.deviceIds.size > SHARED_IP_DEVICE_THRESHOLD) {
|
|
826
827
|
ipProfile.type = "shared";
|
|
827
828
|
}
|
|
828
829
|
|
|
829
|
-
//
|
|
830
|
-
//
|
|
830
|
+
// If a residential IP makes too many requests without a cookie, it's a bot.
|
|
831
|
+
// For a shared IP, we are more tolerant because new users are constantly arriving.
|
|
831
832
|
const statelessLimit = ipProfile.type === "shared" ? 50 : 10;
|
|
832
833
|
if (ipProfile.statelessCount > statelessLimit) {
|
|
833
834
|
return `suspicious_high:${clientIp}`;
|
|
834
835
|
}
|
|
835
836
|
await store.set(`ip:${clientIp}`, ipProfile);
|
|
836
837
|
|
|
837
|
-
//
|
|
838
|
-
//
|
|
838
|
+
// For compatibility with the rate-limiter, calculate a simple score.
|
|
839
|
+
// The PoW will use the more complex weighted system.
|
|
839
840
|
const vector = await getSuspicionVector(req, res);
|
|
840
841
|
const score =
|
|
841
842
|
vector.historyScore * 0.3 +
|
|
842
843
|
vector.rotationScore * 0.5 +
|
|
843
844
|
vector.headerAnomalyScore * 0.1 +
|
|
844
|
-
vector.inconsistencyScore * 0.8; //
|
|
845
|
+
vector.inconsistencyScore * 0.8; // Inconsistency is a very strong signal
|
|
845
846
|
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
// NOTE
|
|
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.
|
|
849
850
|
if (score >= 75) {
|
|
850
851
|
return `suspicious_high:${clientIp}`;
|
|
851
852
|
}
|
|
@@ -853,8 +854,8 @@ export const identifyRequest = async (req, res) => {
|
|
|
853
854
|
return `suspicious_medium:${clientIp}`;
|
|
854
855
|
}
|
|
855
856
|
|
|
856
|
-
//
|
|
857
|
-
//
|
|
857
|
+
// For normal requests, return a hash of the fingerprint for rate limiting.
|
|
858
|
+
// Use the device hash so the rate-limit follows the device, not the IP.
|
|
858
859
|
const deviceIdForIp = await store.get(`ip-device:${clientIp}`);
|
|
859
860
|
const finalDeviceId = deviceId || deviceIdForIp || clientIp;
|
|
860
861
|
return `device:${finalDeviceId}`;
|
|
@@ -862,34 +863,37 @@ export const identifyRequest = async (req, res) => {
|
|
|
862
863
|
// --- NOUVEAU CHALLENGE CPU "ANALOGIQUE" ---
|
|
863
864
|
|
|
864
865
|
// Le plus grand nombre possible avec SHA-256 (2^256 - 1)
|
|
866
|
+
// The largest possible number with SHA-256 (2^256 - 1)
|
|
865
867
|
const MAX_DIFFICULTY_TARGET = 2n ** 256n - 1n;
|
|
866
868
|
// Une difficulté de base, ex: nécessite que les 16 premiers bits soient à 0
|
|
867
869
|
// (équivalent à 4 zéros en hexadécimal)
|
|
870
|
+
// A base difficulty, e.g., requires the first 16 bits to be 0
|
|
871
|
+
// (equivalent to 4 zeros in hexadecimal)
|
|
868
872
|
const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
|
|
869
873
|
|
|
870
874
|
/**
|
|
871
|
-
*
|
|
872
|
-
* @param {number} suspicionFactor -
|
|
873
|
-
* @returns {BigInt}
|
|
875
|
+
* Calculates the difficulty target based on the suspicion factor.
|
|
876
|
+
* @param {number} suspicionFactor - A number from 0 to 1.
|
|
877
|
+
* @returns {BigInt} The target number.
|
|
874
878
|
*/
|
|
875
879
|
function calculateTarget(suspicionFactor) {
|
|
876
|
-
//
|
|
877
|
-
// MIN_DIFFICULTY:
|
|
878
|
-
// MAX_DIFFICULTY:
|
|
879
|
-
const MIN_DIFFICULTY_BITS = 18; //
|
|
880
|
-
const MAX_DIFFICULTY_BITS = 26; //
|
|
880
|
+
// Difficulty range adjusted to be realistic.
|
|
881
|
+
// MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
|
|
882
|
+
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
883
|
+
const MIN_DIFFICULTY_BITS = 18; // Default value, should be configurable
|
|
884
|
+
const MAX_DIFFICULTY_BITS = 26; // Default value, should be configurable
|
|
881
885
|
|
|
882
|
-
//
|
|
886
|
+
// Use linear interpolation between min and max difficulty.
|
|
883
887
|
const totalDifficultyBits =
|
|
884
888
|
MIN_DIFFICULTY_BITS +
|
|
885
889
|
suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
|
|
886
890
|
|
|
887
|
-
//
|
|
891
|
+
// The target is max / 2^bits
|
|
888
892
|
return MAX_DIFFICULTY_TARGET >> BigInt(Math.floor(totalDifficultyBits));
|
|
889
893
|
}
|
|
890
894
|
|
|
891
895
|
/**
|
|
892
|
-
*
|
|
896
|
+
* Generates a CPU challenge based on a target.
|
|
893
897
|
*/
|
|
894
898
|
export function generateCpuTargetChallenge(
|
|
895
899
|
clientIp,
|
|
@@ -901,7 +905,7 @@ export function generateCpuTargetChallenge(
|
|
|
901
905
|
return {
|
|
902
906
|
type: "cpu_target",
|
|
903
907
|
nonce: nonce,
|
|
904
|
-
target: target.toString(16), //
|
|
908
|
+
target: target.toString(16), // Send the target in hexadecimal
|
|
905
909
|
path: originalUrl,
|
|
906
910
|
};
|
|
907
911
|
}
|
|
@@ -942,7 +946,67 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
942
946
|
}
|
|
943
947
|
|
|
944
948
|
/**
|
|
945
|
-
*
|
|
949
|
+
* Generates the HTML content for a combined CPU + Memory PoW challenge.
|
|
950
|
+
* @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
|
|
951
|
+
* @param {number} memoryDifficulty - Memory allocation in MB.
|
|
952
|
+
* @param {string} clientIp - The client's IP address.
|
|
953
|
+
* @returns {string} HTML content.
|
|
954
|
+
*/
|
|
955
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
|
|
956
|
+
const { nonce, target, path } = cpuChallengeDetails;
|
|
957
|
+
return `
|
|
958
|
+
<html><head><title>Advanced Security Check</title></head>
|
|
959
|
+
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
960
|
+
<h1>Enhanced Verification... (Level 2)</h1>
|
|
961
|
+
<p>Your activity requires an additional security check. This may take a few moments.</p>
|
|
962
|
+
<div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div>
|
|
963
|
+
<script>
|
|
964
|
+
async function solve() {
|
|
965
|
+
const nonce = "${nonce}";
|
|
966
|
+
const path = "${path}";
|
|
967
|
+
|
|
968
|
+
// --- CPU Challenge ---
|
|
969
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
970
|
+
const cpuTarget = BigInt("0x${target}");
|
|
971
|
+
let cpuSolution = 0;
|
|
972
|
+
while (true) {
|
|
973
|
+
const msg = "${clientIp}:${nonce}:" + cpuSolution;
|
|
974
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
975
|
+
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
976
|
+
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
977
|
+
cpuSolution++;
|
|
978
|
+
if (cpuSolution % 100000 === 0) await new Promise(r => setTimeout(r, 0));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// --- Memory Challenge ---
|
|
982
|
+
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (${memoryDifficulty} MB)';
|
|
983
|
+
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
984
|
+
|
|
985
|
+
let memSolution = 0;
|
|
986
|
+
try {
|
|
987
|
+
const size = ${memoryDifficulty} * 1024 * 1024;
|
|
988
|
+
const buffer = new Uint32Array(size / 4);
|
|
989
|
+
let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
|
|
990
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
991
|
+
buffer[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
992
|
+
}
|
|
993
|
+
for(let i = 0; i < (size / 16); i++) {
|
|
994
|
+
const addr = buffer[i % buffer.length] % buffer.length;
|
|
995
|
+
memSolution ^= buffer[addr];
|
|
996
|
+
}
|
|
997
|
+
} catch(e) {
|
|
998
|
+
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1002
|
+
}
|
|
1003
|
+
solve();
|
|
1004
|
+
</script>
|
|
1005
|
+
</body></html>`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Verifies a PoW solution based on a target and generates a ticket.
|
|
946
1010
|
*/
|
|
947
1011
|
export function verifyCpuTargetPoWAndGenerateTicket(
|
|
948
1012
|
clientIp,
|
|
@@ -958,8 +1022,8 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
958
1022
|
const hashAsInt = BigInt("0x" + hash);
|
|
959
1023
|
|
|
960
1024
|
if (hashAsInt < target) {
|
|
961
|
-
//
|
|
962
|
-
//
|
|
1025
|
+
// The comparison is direct with native BigInts
|
|
1026
|
+
// The proof is valid, generate the ticket
|
|
963
1027
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
964
1028
|
const signature = crypto
|
|
965
1029
|
.createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
|
|
@@ -976,51 +1040,62 @@ const staticExtensions =
|
|
|
976
1040
|
const isStaticResource = (req) => staticExtensions.test(req.path);
|
|
977
1041
|
|
|
978
1042
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (isStaticResource(req)) {
|
|
982
|
-
return next();
|
|
983
|
-
}
|
|
984
|
-
|
|
1043
|
+
class FingerprintEngine {
|
|
1044
|
+
constructor(securityConfig) {
|
|
985
1045
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
986
|
-
|
|
1046
|
+
this.securityConfig = securityConfig;
|
|
1047
|
+
this.isProduction = isProduction;
|
|
1048
|
+
}
|
|
987
1049
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const
|
|
1050
|
+
async processRequest(requestContext) {
|
|
1051
|
+
const { clientIp, path, cookies, query, isStatic } = requestContext;
|
|
1052
|
+
const { weights, thresholds, logger } = this.securityConfig;
|
|
1053
|
+
|
|
1054
|
+
if (isStatic) {
|
|
1055
|
+
return { action: 'next' };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// We need to pass `req` and `res` to getSuspicionVector for cookie handling.
|
|
1059
|
+
// This is a remaining coupling point that could be refactored further.
|
|
1060
|
+
const suspicionVector = await __internal.getSuspicionVector(requestContext.rawReq, requestContext.rawRes);
|
|
991
1061
|
|
|
992
1062
|
const finalScore =
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1063
|
+
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
1064
|
+
suspicionVector.rotationScore * (weights.rotationScore || 0) +
|
|
1065
|
+
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
|
|
1066
|
+
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0);
|
|
997
1067
|
|
|
998
1068
|
const isSuspiciousHigh = finalScore >= thresholds.high;
|
|
999
1069
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
1000
1070
|
const isSuspicious = finalScore >= thresholds.low;
|
|
1001
1071
|
|
|
1002
|
-
//
|
|
1072
|
+
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
1003
1073
|
const suspicionFactor = isSuspicious
|
|
1004
1074
|
? Math.min(
|
|
1005
1075
|
1,
|
|
1006
1076
|
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
1007
1077
|
)
|
|
1008
1078
|
: 0;
|
|
1009
|
-
const powCookie =
|
|
1010
|
-
const { pow_type, pow_nonce, pow_solution,
|
|
1079
|
+
const powCookie = cookies?.pow_clearance;
|
|
1080
|
+
const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
1081
|
+
|
|
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
|
+
}
|
|
1011
1086
|
|
|
1012
1087
|
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
1013
|
-
// ---
|
|
1014
|
-
if (pow_nonce && pow_solution) {
|
|
1088
|
+
// --- CHALLENGE SOLUTION HANDLING ---
|
|
1089
|
+
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
1015
1090
|
let isValid = false,
|
|
1016
1091
|
ticket = null;
|
|
1017
1092
|
if (pow_type === "cpu_target") {
|
|
1018
|
-
//
|
|
1093
|
+
// Verify the new type
|
|
1019
1094
|
ticket = verifyCpuTargetPoWAndGenerateTicket(
|
|
1020
1095
|
clientIp,
|
|
1021
1096
|
pow_nonce,
|
|
1022
1097
|
pow_solution,
|
|
1023
|
-
suspicionFactor, //
|
|
1098
|
+
suspicionFactor, // Pass the analog factor directly
|
|
1024
1099
|
);
|
|
1025
1100
|
isValid = ticket !== null;
|
|
1026
1101
|
} else if (pow_type === "mem") {
|
|
@@ -1029,14 +1104,27 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
|
|
|
1029
1104
|
const difficulty =
|
|
1030
1105
|
minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
|
|
1031
1106
|
isValid = verifyMemoryPoW(pow_nonce, pow_solution, difficulty);
|
|
1107
|
+
} else if (pow_type === "cpu_mem") {
|
|
1108
|
+
// Verify combined challenge
|
|
1109
|
+
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
|
|
1110
|
+
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
const minDifficulty = 16; // 16Mo
|
|
1114
|
+
const maxDifficulty = 48; // 48Mo
|
|
1115
|
+
const memDifficulty = minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
|
|
1116
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty);
|
|
1117
|
+
|
|
1118
|
+
isValid = cpuTicket !== null && isMemValid;
|
|
1119
|
+
if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
|
|
1032
1120
|
} else if (pow_type === "tsp") {
|
|
1033
|
-
//
|
|
1121
|
+
// Logic for TSP remains the same
|
|
1034
1122
|
// ...
|
|
1035
1123
|
}
|
|
1036
1124
|
|
|
1037
1125
|
if (isValid) {
|
|
1038
1126
|
if (!ticket) {
|
|
1039
|
-
//
|
|
1127
|
+
// If the ticket has not already been generated (CPU case)
|
|
1040
1128
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
1041
1129
|
const signature = crypto
|
|
1042
1130
|
.createHmac("sha256", POW_SECRET || "fallback-dev-secret-32-chars-minimum")
|
|
@@ -1045,50 +1133,110 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
|
|
|
1045
1133
|
ticket = `${expiry}:${signature}`;
|
|
1046
1134
|
}
|
|
1047
1135
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1136
|
+
if (logger) {
|
|
1137
|
+
logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: finalScore, challengeType: pow_type, timestamp: Date.now() });
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
action: 'redirect',
|
|
1142
|
+
path: path,
|
|
1143
|
+
cookie: {
|
|
1144
|
+
name: 'pow_clearance',
|
|
1145
|
+
value: ticket,
|
|
1146
|
+
options: {
|
|
1147
|
+
httpOnly: true,
|
|
1148
|
+
secure: this.isProduction,
|
|
1149
|
+
maxAge: 3600000,
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1054
1153
|
}
|
|
1055
1154
|
}
|
|
1056
1155
|
|
|
1057
|
-
// ---
|
|
1156
|
+
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
1058
1157
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
1059
1158
|
|
|
1060
|
-
|
|
1159
|
+
if (logger) {
|
|
1160
|
+
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// LEVEL 3: CAPTCHA (the highest)
|
|
1061
1164
|
if (isSuspiciousHigh) {
|
|
1062
|
-
// ...
|
|
1165
|
+
// ... logic for TSP/Captcha challenge
|
|
1063
1166
|
}
|
|
1064
1167
|
|
|
1065
|
-
//
|
|
1168
|
+
// LEVEL 2: Memory-Intensive PoW
|
|
1066
1169
|
if (isSuspiciousMedium) {
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
+
};
|
|
1076
1182
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1183
|
+
|
|
1184
|
+
// NOUVELLE LOGIQUE UNIFIÉE POUR TOUS LES NIVEAUX DE SUSPICION (low et medium)
|
|
1185
|
+
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1186
|
+
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
1187
|
+
|
|
1188
|
+
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
1189
|
+
// Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
|
|
1190
|
+
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
1191
|
+
|
|
1192
|
+
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
1193
|
+
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
1194
|
+
const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
|
|
1195
|
+
|
|
1196
|
+
// On utilise toujours la page combinée, même si la difficulté mémoire est 0 (le calcul sera quasi instantané).
|
|
1197
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
|
|
1198
|
+
return { action: 'challenge', status: 429, body: page };
|
|
1089
1199
|
}
|
|
1090
1200
|
}
|
|
1091
|
-
|
|
1201
|
+
|
|
1202
|
+
return { action: 'next' };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
1207
|
+
export const powMiddleware = (securityConfig) => {
|
|
1208
|
+
const engine = new FingerprintEngine(securityConfig);
|
|
1209
|
+
|
|
1210
|
+
return async (req, res, next) => {
|
|
1211
|
+
const requestContext = {
|
|
1212
|
+
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
1213
|
+
path: req.path,
|
|
1214
|
+
cookies: req.cookies,
|
|
1215
|
+
query: req.query,
|
|
1216
|
+
headers: req.headers,
|
|
1217
|
+
isStatic: isStaticResource(req),
|
|
1218
|
+
// Pass raw req/res for now to handle cookie setting in resolveRequestIdentity
|
|
1219
|
+
rawReq: req,
|
|
1220
|
+
rawRes: res,
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const decision = await engine.processRequest(requestContext);
|
|
1224
|
+
|
|
1225
|
+
switch (decision.action) {
|
|
1226
|
+
case 'challenge':
|
|
1227
|
+
return res.status(decision.status).send(decision.body);
|
|
1228
|
+
|
|
1229
|
+
case 'redirect':
|
|
1230
|
+
if (decision.cookie) {
|
|
1231
|
+
res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
|
|
1232
|
+
}
|
|
1233
|
+
return res.redirect(decision.path);
|
|
1234
|
+
|
|
1235
|
+
case 'next':
|
|
1236
|
+
default:
|
|
1237
|
+
return next();
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1092
1240
|
};
|
|
1093
1241
|
|
|
1094
1242
|
/**
|
|
@@ -1099,4 +1247,128 @@ export const powMiddleware = (securityConfig) => async (req, res, next) => {
|
|
|
1099
1247
|
export const __internal = {
|
|
1100
1248
|
getSuspicionVector,
|
|
1101
1249
|
calculateTarget,
|
|
1250
|
+
FingerprintEngine, // Expose for advanced testing
|
|
1102
1251
|
};
|
|
1252
|
+
|
|
1253
|
+
// --- THRESHOLD AUTO-TUNING SECTION ---
|
|
1254
|
+
|
|
1255
|
+
let autoTuningJobId = null;
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Executes a threshold optimization pass using collected traffic data.
|
|
1259
|
+
* @private
|
|
1260
|
+
* @param {object} securityConfig - The security configuration object to update.
|
|
1261
|
+
* @param {Array<object>} trafficData - The array containing traffic logs.
|
|
1262
|
+
* @param {number} minDataPoints - The minimum number of data points required to start optimization.
|
|
1263
|
+
*/
|
|
1264
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
1265
|
+
if (trafficData.length < minDataPoints) {
|
|
1266
|
+
console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
|
|
1270
|
+
|
|
1271
|
+
// Identify "bots" (those who received a challenge but never solved it)
|
|
1272
|
+
// and "humans" (those who passed the challenge or never received one).
|
|
1273
|
+
const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
|
|
1274
|
+
const historicalRequests = trafficData.map(log => {
|
|
1275
|
+
let isBot = false;
|
|
1276
|
+
if (log.type === 'challenge_issued' && !solvedDevices.has(log.deviceId)) {
|
|
1277
|
+
isBot = true; // Assumption: a challenge issued and not solved is a bot.
|
|
1278
|
+
}
|
|
1279
|
+
return { score: log.score, isBot };
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// The "fitness" function evaluates the quality of a set of thresholds.
|
|
1283
|
+
// A lower score is better.
|
|
1284
|
+
const fitnessFunction = (solution) => {
|
|
1285
|
+
const [low, medium, high] = solution;
|
|
1286
|
+
// Constraints: thresholds must be ordered and within a reasonable range.
|
|
1287
|
+
if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
|
|
1288
|
+
|
|
1289
|
+
let falsePositives = 0; // Humans challenged unnecessarily.
|
|
1290
|
+
let falseNegatives = 0; // Undetected bots.
|
|
1291
|
+
|
|
1292
|
+
for (const req of historicalRequests) {
|
|
1293
|
+
if (req.isBot) {
|
|
1294
|
+
if (req.score < low) falseNegatives++;
|
|
1295
|
+
} else { // Human
|
|
1296
|
+
if (req.score >= low) falsePositives++;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Penalize passing bots 2x more than inconvenienced humans.
|
|
1300
|
+
return (falsePositives * 1.0) + (falseNegatives * 2.0);
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// Functions for the genetic algorithm.
|
|
1304
|
+
const createIndividual = () => [10 + Math.random() * 20, 30 + Math.random() * 30, 60 + Math.random() * 30];
|
|
1305
|
+
const crossover = (p1, p2) => [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2];
|
|
1306
|
+
const mutate = (s) => {
|
|
1307
|
+
const n = [...s];
|
|
1308
|
+
const i = Math.floor(Math.random() * 3);
|
|
1309
|
+
n[i] += (Math.random() - 0.5) * 5;
|
|
1310
|
+
return n;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// Start optimization.
|
|
1314
|
+
const result = Optimization.geneticAlgorithm(createIndividual, fitnessFunction, crossover, mutate, {
|
|
1315
|
+
generations: 50,
|
|
1316
|
+
populationSize: 40
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
const [newLow, newMedium, newHigh] = result.solution;
|
|
1320
|
+
|
|
1321
|
+
// Update the configuration live.
|
|
1322
|
+
securityConfig.thresholds = {
|
|
1323
|
+
low: Math.round(newLow),
|
|
1324
|
+
medium: Math.round(newMedium),
|
|
1325
|
+
high: Math.round(newHigh)
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
console.log("[AutoTuning] Nouveaux seuils optimisés appliqués :", securityConfig.thresholds);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Starts the background process for auto-tuning security thresholds.
|
|
1333
|
+
* @export
|
|
1334
|
+
* @param {object} options - Configuration options for auto-tuning.
|
|
1335
|
+
* @param {object} options.securityConfig - The live security configuration object that will be mutated.
|
|
1336
|
+
* @param {Array<object>} options.trafficData - The array where the logger pushes traffic data.
|
|
1337
|
+
* @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
|
|
1338
|
+
* @param {number} [options.minDataPoints=200] - The minimum number of requests to analyze before starting a cycle (default: 200).
|
|
1339
|
+
*/
|
|
1340
|
+
export function startThresholdAutoTuning(options) {
|
|
1341
|
+
if (autoTuningJobId) {
|
|
1342
|
+
console.warn("[AutoTuning] Le job est déjà en cours d'exécution.");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const {
|
|
1347
|
+
securityConfig,
|
|
1348
|
+
trafficData,
|
|
1349
|
+
interval = 1800000,
|
|
1350
|
+
minDataPoints = 200
|
|
1351
|
+
} = options;
|
|
1352
|
+
|
|
1353
|
+
if (!securityConfig || !trafficData) {
|
|
1354
|
+
throw new Error("[AutoTuning] `securityConfig` et `trafficData` sont requis.");
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
1358
|
+
|
|
1359
|
+
autoTuningJobId = setInterval(() => {
|
|
1360
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints);
|
|
1361
|
+
}, interval);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Stops the threshold auto-tuning process.
|
|
1366
|
+
* @export
|
|
1367
|
+
*/
|
|
1368
|
+
export function stopThresholdAutoTuning() {
|
|
1369
|
+
if (autoTuningJobId) {
|
|
1370
|
+
clearInterval(autoTuningJobId);
|
|
1371
|
+
autoTuningJobId = null;
|
|
1372
|
+
console.log("[AutoTuning] Job d'optimisation des seuils arrêté.");
|
|
1373
|
+
}
|
|
1374
|
+
}
|