@anonympins/fingerprint 0.4.5 → 0.4.6
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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.js +442 -59
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/fingerprint.test.js +59 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +97 -0
- package/src/php/FingerprintEngine.php +136 -8
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +45 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -15,6 +15,148 @@ const __dirname = dirname(__filename);
|
|
|
15
15
|
export { createRedisStore } from "./redis-store.js";
|
|
16
16
|
export { createMongoDbStore } from "./mongodb-store.js";
|
|
17
17
|
|
|
18
|
+
let activeMappings = [];
|
|
19
|
+
let lastMappingTime = 0;
|
|
20
|
+
let isCompilingMapping = false;
|
|
21
|
+
const MAPPING_ROTATION_INTERVAL = 60000; // 60 seconds
|
|
22
|
+
|
|
23
|
+
function generateSessionMapping() {
|
|
24
|
+
const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
|
|
25
|
+
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
headers: {
|
|
29
|
+
'x-device-fingerprint': randomHeader(),
|
|
30
|
+
'x-behavior-metrics': randomHeader(),
|
|
31
|
+
},
|
|
32
|
+
globals: {
|
|
33
|
+
'ClientLibrary': `ClientLib_${randomStr(6)}`,
|
|
34
|
+
'getDeviceFingerprint': `getFP_${randomStr(6)}`,
|
|
35
|
+
'getClientBehaviorMetrics': `getMetrics_${randomStr(6)}`,
|
|
36
|
+
},
|
|
37
|
+
keys: {
|
|
38
|
+
'ua': randomStr(4),
|
|
39
|
+
'hw': randomStr(4),
|
|
40
|
+
'geo': randomStr(4),
|
|
41
|
+
'scr': randomStr(4),
|
|
42
|
+
'os': randomStr(4),
|
|
43
|
+
'gpu': randomStr(4),
|
|
44
|
+
'cvs': randomStr(4),
|
|
45
|
+
'cdp': randomStr(4),
|
|
46
|
+
'bot': randomStr(4),
|
|
47
|
+
'wasm': randomStr(4),
|
|
48
|
+
},
|
|
49
|
+
wasmConstants: {
|
|
50
|
+
seed: crypto.randomBytes(4).readInt32LE(0),
|
|
51
|
+
multiplier: crypto.randomBytes(4).readInt32LE(0) | 1,
|
|
52
|
+
adder: crypto.randomBytes(4).readInt32LE(0)
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function compilePolymorphicJs(mapping) {
|
|
58
|
+
const clientScriptPath = join(__dirname, 'fingerprint.client.js');
|
|
59
|
+
let jsCode = '';
|
|
60
|
+
try {
|
|
61
|
+
jsCode = readFileSync(clientScriptPath, 'utf-8');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('[Fingerprint] Could not read fingerprint.client.js for dynamic obfuscation. Fallback to obfuscated build.');
|
|
64
|
+
try {
|
|
65
|
+
return readFileSync(join(__dirname, 'fingerprint.client.obfuscated.js'), 'utf-8');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
jsCode = jsCode.replace(/X-Device-Fingerprint/g, mapping.headers['x-device-fingerprint']);
|
|
72
|
+
jsCode = jsCode.replace(/X-Behavior-Metrics/g, mapping.headers['x-behavior-metrics']);
|
|
73
|
+
jsCode = jsCode.replace(/ClientLibrary/g, mapping.globals['ClientLibrary']);
|
|
74
|
+
jsCode = jsCode.replace(/getDeviceFingerprint/g, mapping.globals['getDeviceFingerprint']);
|
|
75
|
+
jsCode = jsCode.replace(/getClientBehaviorMetrics/g, mapping.globals['getClientBehaviorMetrics']);
|
|
76
|
+
|
|
77
|
+
for (const [origKey, randKey] of Object.entries(mapping.keys)) {
|
|
78
|
+
const regex1 = new RegExp(`add\\(["']${origKey}["']`, 'g');
|
|
79
|
+
jsCode = jsCode.replace(regex1, `add("${randKey}"`);
|
|
80
|
+
|
|
81
|
+
const regex2 = new RegExp(`addRaw\\(["']${origKey}["']`, 'g');
|
|
82
|
+
jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const obfuscationResult = JavaScriptObfuscator.obfuscate(jsCode, {
|
|
86
|
+
compact: true,
|
|
87
|
+
controlFlowFlattening: true,
|
|
88
|
+
deadCodeInjection: true,
|
|
89
|
+
stringArray: true,
|
|
90
|
+
stringArrayRotate: true,
|
|
91
|
+
stringArrayShuffle: true,
|
|
92
|
+
seed: Math.abs(mapping.wasmConstants.seed),
|
|
93
|
+
selfDefending: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return obfuscationResult.getObfuscatedCode();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function ensureLatestMapping() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
if ((now - lastMappingTime > MAPPING_ROTATION_INTERVAL || activeMappings.length === 0) && !isCompilingMapping) {
|
|
102
|
+
isCompilingMapping = true;
|
|
103
|
+
try {
|
|
104
|
+
const mapping = generateSessionMapping();
|
|
105
|
+
const polymorphicJs = await compilePolymorphicJs(mapping);
|
|
106
|
+
const polymorphicWasm = DynamicWasmGenerator.generate(mapping.wasmConstants);
|
|
107
|
+
|
|
108
|
+
mapping.jsBuffer = Buffer.from(polymorphicJs, 'utf8');
|
|
109
|
+
mapping.wasmBuffer = polymorphicWasm;
|
|
110
|
+
mapping.timestamp = now;
|
|
111
|
+
|
|
112
|
+
activeMappings.unshift(mapping);
|
|
113
|
+
if (activeMappings.length > 5) {
|
|
114
|
+
activeMappings.pop();
|
|
115
|
+
}
|
|
116
|
+
lastMappingTime = now;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await store.set('active-polymorphic-mappings', activeMappings.map(m => ({
|
|
120
|
+
headers: m.headers,
|
|
121
|
+
keys: m.keys
|
|
122
|
+
})));
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Ignore
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
isCompilingMapping = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getActiveMappingForRequest(headers) {
|
|
133
|
+
if (!headers) return null;
|
|
134
|
+
for (const mapping of activeMappings) {
|
|
135
|
+
const headerName = mapping.headers['x-device-fingerprint'].toLowerCase();
|
|
136
|
+
if (headers[headerName]) {
|
|
137
|
+
return mapping;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function decodePolymorphicFingerprint(fpString, mapping) {
|
|
144
|
+
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
145
|
+
const reverseKeys = {};
|
|
146
|
+
for (const [orig, rand] of Object.entries(mapping.keys)) {
|
|
147
|
+
reverseKeys[rand] = orig;
|
|
148
|
+
}
|
|
149
|
+
const parts = fpString.split('|');
|
|
150
|
+
const mappedParts = parts.map(part => {
|
|
151
|
+
const pair = part.split(':');
|
|
152
|
+
if (pair.length === 2) {
|
|
153
|
+
const origKey = reverseKeys[pair[0]] || pair[0];
|
|
154
|
+
return `${origKey}:${pair[1]}`;
|
|
155
|
+
}
|
|
156
|
+
return part;
|
|
157
|
+
});
|
|
158
|
+
return mappedParts.join('|');
|
|
159
|
+
}
|
|
18
160
|
|
|
19
161
|
const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
20
162
|
const base64UrlDecode = (str) => {
|
|
@@ -948,6 +1090,32 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
|
|
|
948
1090
|
}
|
|
949
1091
|
return finalHash === parseInt(solution, 10);
|
|
950
1092
|
};
|
|
1093
|
+
|
|
1094
|
+
export function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
1095
|
+
const combined = new Uint8Array(queries.length * 1024);
|
|
1096
|
+
for (let i = 0; i < queries.length; i++) {
|
|
1097
|
+
const idx = queries[i];
|
|
1098
|
+
const block = generateBlock(seed, idx);
|
|
1099
|
+
combined.set(block, i * 1024);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
|
|
1103
|
+
const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
|
|
1104
|
+
|
|
1105
|
+
const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
|
|
1106
|
+
return hash === solution;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
1110
|
+
const block = new Uint8Array(blockSize);
|
|
1111
|
+
let h = cyrb53(seed + ":" + blockIndex);
|
|
1112
|
+
for (let i = 0; i < blockSize; i++) {
|
|
1113
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
1114
|
+
block[i] = h & 0xff;
|
|
1115
|
+
}
|
|
1116
|
+
return block;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
951
1119
|
export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
|
|
952
1120
|
// Input validation: ensure the ticket is a non-empty string with the correct format.
|
|
953
1121
|
if (typeof ticket !== 'string' || ticket.length === 0) return false;
|
|
@@ -1087,6 +1255,66 @@ function getHeaderAnomalies(context) {
|
|
|
1087
1255
|
};
|
|
1088
1256
|
}
|
|
1089
1257
|
|
|
1258
|
+
export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
|
|
1259
|
+
const sizeMb = securityConfig?.pospace?.sizeMb || 100;
|
|
1260
|
+
const numQueries = securityConfig?.pospace?.numQueries || 10;
|
|
1261
|
+
|
|
1262
|
+
const queries = [];
|
|
1263
|
+
const maxBlocks = sizeMb * 1024;
|
|
1264
|
+
while (queries.length < numQueries) {
|
|
1265
|
+
const idx = Math.floor(Math.random() * maxBlocks);
|
|
1266
|
+
if (!queries.includes(idx)) {
|
|
1267
|
+
queries.push(idx);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return {
|
|
1272
|
+
type: "pospace",
|
|
1273
|
+
nonce: nonce,
|
|
1274
|
+
sizeMb,
|
|
1275
|
+
queries,
|
|
1276
|
+
path: originalUrl
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1281
|
+
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1282
|
+
const solverCode = getPowSolverCode();
|
|
1283
|
+
|
|
1284
|
+
const challengeScript = `
|
|
1285
|
+
async function solve() {
|
|
1286
|
+
const nonce = ${JSON.stringify(nonce)};
|
|
1287
|
+
const path = ${JSON.stringify(path)};
|
|
1288
|
+
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1289
|
+
const queries = ${JSON.stringify(queries)};
|
|
1290
|
+
const sizeMb = ${sizeMb};
|
|
1291
|
+
|
|
1292
|
+
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1293
|
+
await new Promise(r => setTimeout(r, 10));
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
1297
|
+
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
1298
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
1299
|
+
|
|
1300
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
1301
|
+
} catch(e) {
|
|
1302
|
+
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
solve();
|
|
1306
|
+
`;
|
|
1307
|
+
|
|
1308
|
+
return `<html><head><title>Security Check</title></head>
|
|
1309
|
+
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
1310
|
+
<h1>Security Check (Level 2)</h1>
|
|
1311
|
+
<p>We are verifying your storage allocation. This may take a few seconds on first load.</p>
|
|
1312
|
+
<div id="loader" style="margin:20px;">⚙️ Initializing storage space...</div>
|
|
1313
|
+
<script>${solverCode}</script>
|
|
1314
|
+
<script>${challengeScript}</script>
|
|
1315
|
+
</body></html>`;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1090
1318
|
/**
|
|
1091
1319
|
* Checks for submitted honeypot fields to detect bots.
|
|
1092
1320
|
* @param {object} context - The request context.
|
|
@@ -1469,11 +1697,27 @@ function getCrossLayerInconsistency(context) {
|
|
|
1469
1697
|
const clientScreenHash = clientFpMap.get('scr');
|
|
1470
1698
|
const viewportWidth = context.headers['sec-ch-viewport-width'];
|
|
1471
1699
|
if (clientScreenHash && viewportWidth) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1700
|
+
const viewportWidthInt = parseInt(viewportWidth, 10);
|
|
1701
|
+
let matchedScreenWidth = null;
|
|
1702
|
+
const commonWidths = [320, 360, 375, 390, 412, 414, 768, 1024, 1280, 1366, 1440, 1536, 1600, 1920, 2560, 3840];
|
|
1703
|
+
const commonHeights = [480, 568, 640, 667, 736, 800, 812, 844, 896, 900, 1024, 1080, 1200, 1440, 1600, 2160];
|
|
1704
|
+
const commonDepths = [24, 30, 32];
|
|
1705
|
+
|
|
1706
|
+
for (const w of commonWidths) {
|
|
1707
|
+
for (const h of commonHeights) {
|
|
1708
|
+
for (const d of commonDepths) {
|
|
1709
|
+
const candidate = `${w}x${h}_${d}`;
|
|
1710
|
+
if (clientScreenHash === String(cyrb53(candidate))) {
|
|
1711
|
+
matchedScreenWidth = w;
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (matchedScreenWidth !== null) break;
|
|
1716
|
+
}
|
|
1717
|
+
if (matchedScreenWidth !== null) break;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (matchedScreenWidth !== null && viewportWidthInt > matchedScreenWidth) {
|
|
1477
1721
|
score += 20;
|
|
1478
1722
|
}
|
|
1479
1723
|
}
|
|
@@ -1484,10 +1728,17 @@ function getCrossLayerInconsistency(context) {
|
|
|
1484
1728
|
const clientGpuHash = clientFpMap.get('gpu');
|
|
1485
1729
|
const ja3 = getTlsFingerprint(context)?.ja3;
|
|
1486
1730
|
if (clientGpuHash && ja3) {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1731
|
+
let expectedBrowsers = tlsFingerprintDb[ja3];
|
|
1732
|
+
if (expectedBrowsers) {
|
|
1733
|
+
if (!Array.isArray(expectedBrowsers)) {
|
|
1734
|
+
expectedBrowsers = [expectedBrowsers];
|
|
1735
|
+
}
|
|
1736
|
+
const nonBrowserLibraries = ['Python', 'Go', 'Java', 'curl'];
|
|
1737
|
+
const isLibrary = expectedBrowsers.some(lib => nonBrowserLibraries.includes(lib));
|
|
1738
|
+
if (isLibrary) {
|
|
1739
|
+
score += 30;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1491
1742
|
}
|
|
1492
1743
|
|
|
1493
1744
|
return { crossLayerInconsistencyScore: Math.min(100, score) };
|
|
@@ -2345,11 +2596,20 @@ export const configureStore = (externalStore) => {
|
|
|
2345
2596
|
async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
2346
2597
|
const existingDeviceId = context.cookies?.device_id;
|
|
2347
2598
|
const currentDeviceHash = getCompositeDeviceHash(context); // Use the composite hash for consistency checks
|
|
2348
|
-
|
|
2599
|
+
const tlsSessionId = getTlsSessionId(context);
|
|
2600
|
+
let deviceId = existingDeviceId;
|
|
2349
2601
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
2350
2602
|
let deviceData = null;
|
|
2351
2603
|
let newCookie = null;
|
|
2352
|
-
|
|
2604
|
+
|
|
2605
|
+
if (!deviceId && tlsSessionId) {
|
|
2606
|
+
const resumedDeviceId = await store.get(`tls-session:${tlsSessionId}`);
|
|
2607
|
+
if (resumedDeviceId) {
|
|
2608
|
+
deviceId = resumedDeviceId;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
if (deviceId) {
|
|
2353
2613
|
deviceData = await store.get(`device:${deviceId}`);
|
|
2354
2614
|
}
|
|
2355
2615
|
|
|
@@ -2392,6 +2652,9 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
2392
2652
|
// The write will happen in getSuspicionVector after all modifications.
|
|
2393
2653
|
}
|
|
2394
2654
|
|
|
2655
|
+
if (deviceId && tlsSessionId) {
|
|
2656
|
+
await store.set(`tls-session:${tlsSessionId}`, deviceId, 3600); // Bind TLS session for 1 hour
|
|
2657
|
+
}
|
|
2395
2658
|
return { deviceId, deviceData, consistencyScore, newCookie };
|
|
2396
2659
|
}
|
|
2397
2660
|
|
|
@@ -3213,7 +3476,7 @@ export class FingerprintEngine {
|
|
|
3213
3476
|
async processRequest(requestContext) {
|
|
3214
3477
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
3215
3478
|
|
|
3216
|
-
const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3479
|
+
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
3217
3480
|
const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
|
|
3218
3481
|
|
|
3219
3482
|
this._log('Processing request', { clientIp, path, isStatic });
|
|
@@ -3293,6 +3556,13 @@ export class FingerprintEngine {
|
|
|
3293
3556
|
decision.action = 'next';
|
|
3294
3557
|
delete decision.status;
|
|
3295
3558
|
delete decision.body;
|
|
3559
|
+
|
|
3560
|
+
if (requestContext._newCookies) {
|
|
3561
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3562
|
+
if (deviceCookie) {
|
|
3563
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3296
3566
|
}
|
|
3297
3567
|
return decision;
|
|
3298
3568
|
}
|
|
@@ -3332,8 +3602,8 @@ export class FingerprintEngine {
|
|
|
3332
3602
|
// --- NOUVELLE LOGIQUE DE PRIORITÉ ---
|
|
3333
3603
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
3334
3604
|
// avant même de recalculer le score de suspicion.
|
|
3335
|
-
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
3336
|
-
if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
3605
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id, pow_solution_space } = query;
|
|
3606
|
+
if (pow_nonce && (pow_solution || pow_solution_cpu || pow_solution_space)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
3337
3607
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
3338
3608
|
|
|
3339
3609
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
@@ -3411,13 +3681,13 @@ export class FingerprintEngine {
|
|
|
3411
3681
|
} else {
|
|
3412
3682
|
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
3413
3683
|
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
3414
|
-
this._log('Challenge context found, verifying solution', {
|
|
3684
|
+
this._log('Challenge context found, verifying solution', {optimalTtl, finalTtl});
|
|
3415
3685
|
|
|
3416
3686
|
if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
|
|
3417
3687
|
const cpuSolution = pow_solution_cpu || pow_solution;
|
|
3418
3688
|
ticket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext, deviceId, currentDeviceHash);
|
|
3419
3689
|
isValid = ticket !== null;
|
|
3420
|
-
this._log('CPU target challenge verification', {
|
|
3690
|
+
this._log('CPU target challenge verification', {isValid});
|
|
3421
3691
|
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
3422
3692
|
const cpuTicket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext, deviceId, currentDeviceHash);
|
|
3423
3693
|
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
|
|
@@ -3428,6 +3698,18 @@ export class FingerprintEngine {
|
|
|
3428
3698
|
memValid: isMemValid,
|
|
3429
3699
|
isValid
|
|
3430
3700
|
});
|
|
3701
|
+
} else if (pow_type === "pospace" && pow_solution_space) {
|
|
3702
|
+
const isSpaceValid = verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
|
|
3703
|
+
isValid = isSpaceValid;
|
|
3704
|
+
if (isValid) {
|
|
3705
|
+
const ttl = finalTtl || 3600000;
|
|
3706
|
+
ticket = generateStatelessTicket({
|
|
3707
|
+
expiry: Date.now() + ttl,
|
|
3708
|
+
originalIp: clientIp,
|
|
3709
|
+
deviceId,
|
|
3710
|
+
deviceHash
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3431
3713
|
}
|
|
3432
3714
|
}
|
|
3433
3715
|
} else {
|
|
@@ -3464,6 +3746,7 @@ export class FingerprintEngine {
|
|
|
3464
3746
|
finalSearchParams.delete('pow_solution_cpu');
|
|
3465
3747
|
finalSearchParams.delete('pow_solution_mem');
|
|
3466
3748
|
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
3749
|
+
finalSearchParams.delete('pow_solution_space');
|
|
3467
3750
|
// NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
|
|
3468
3751
|
finalSearchParams.delete('pow_solution_population');
|
|
3469
3752
|
finalSearchParams.delete('pow_solution_work_result');
|
|
@@ -3501,6 +3784,12 @@ export class FingerprintEngine {
|
|
|
3501
3784
|
decision.action = 'next';
|
|
3502
3785
|
delete decision.status;
|
|
3503
3786
|
delete decision.body;
|
|
3787
|
+
if (requestContext._newCookies) {
|
|
3788
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3789
|
+
if (deviceCookie) {
|
|
3790
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3504
3793
|
}
|
|
3505
3794
|
return decision;
|
|
3506
3795
|
}
|
|
@@ -3593,6 +3882,12 @@ export class FingerprintEngine {
|
|
|
3593
3882
|
decision.action = 'next';
|
|
3594
3883
|
delete decision.status;
|
|
3595
3884
|
delete decision.body;
|
|
3885
|
+
if (requestContext._newCookies) {
|
|
3886
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3887
|
+
if (deviceCookie) {
|
|
3888
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3596
3891
|
}
|
|
3597
3892
|
return decision;
|
|
3598
3893
|
}
|
|
@@ -3640,6 +3935,12 @@ export class FingerprintEngine {
|
|
|
3640
3935
|
decision.action = 'next';
|
|
3641
3936
|
delete decision.status;
|
|
3642
3937
|
delete decision.body;
|
|
3938
|
+
if (requestContext._newCookies) {
|
|
3939
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3940
|
+
if (deviceCookie) {
|
|
3941
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3643
3944
|
}
|
|
3644
3945
|
return decision;
|
|
3645
3946
|
}
|
|
@@ -3664,6 +3965,12 @@ export class FingerprintEngine {
|
|
|
3664
3965
|
decision.action = 'next';
|
|
3665
3966
|
delete decision.status;
|
|
3666
3967
|
delete decision.body;
|
|
3968
|
+
if (requestContext._newCookies) {
|
|
3969
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
3970
|
+
if (deviceCookie) {
|
|
3971
|
+
decision.newCookieForResponse = deviceCookie;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3667
3974
|
}
|
|
3668
3975
|
return decision;
|
|
3669
3976
|
}
|
|
@@ -3698,6 +4005,12 @@ export class FingerprintEngine {
|
|
|
3698
4005
|
decision.action = 'next';
|
|
3699
4006
|
delete decision.status;
|
|
3700
4007
|
delete decision.body;
|
|
4008
|
+
if (requestContext._newCookies) {
|
|
4009
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
4010
|
+
if (deviceCookie) {
|
|
4011
|
+
decision.newCookieForResponse = deviceCookie;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
3701
4014
|
}
|
|
3702
4015
|
return decision;
|
|
3703
4016
|
}
|
|
@@ -3707,6 +4020,7 @@ export class FingerprintEngine {
|
|
|
3707
4020
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
3708
4021
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
3709
4022
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4023
|
+
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3710
4024
|
|
|
3711
4025
|
// Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
|
|
3712
4026
|
// Cela rend l'automatisation plus difficile pour un attaquant.
|
|
@@ -3749,7 +4063,6 @@ export class FingerprintEngine {
|
|
|
3749
4063
|
}
|
|
3750
4064
|
|
|
3751
4065
|
if (isSuspicious && usefulWorkDispatched) {
|
|
3752
|
-
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3753
4066
|
if (isApi) {
|
|
3754
4067
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
3755
4068
|
} else {
|
|
@@ -3767,6 +4080,33 @@ export class FingerprintEngine {
|
|
|
3767
4080
|
delete decision.status;
|
|
3768
4081
|
return decision;
|
|
3769
4082
|
}
|
|
4083
|
+
if (this.securityConfig.enableProofOfSpace) {
|
|
4084
|
+
const spaceChallenge = generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4085
|
+
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
4086
|
+
await store.set(`secret:${nonce}`, {
|
|
4087
|
+
clientSecret,
|
|
4088
|
+
suspicionScore: finalScore,
|
|
4089
|
+
queries: spaceChallenge.queries,
|
|
4090
|
+
sizeMb: spaceChallenge.sizeMb,
|
|
4091
|
+
originalPath: path,
|
|
4092
|
+
}, this.securityConfig.challengeTtl || 300);
|
|
4093
|
+
|
|
4094
|
+
if (isApi) {
|
|
4095
|
+
decision.body = {
|
|
4096
|
+
challenge: {
|
|
4097
|
+
type: 'pospace',
|
|
4098
|
+
nonce: nonce,
|
|
4099
|
+
clientSecret,
|
|
4100
|
+
queries: spaceChallenge.queries,
|
|
4101
|
+
sizeMb: spaceChallenge.sizeMb,
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
} else {
|
|
4105
|
+
const page = generateSpaceChallengePage(spaceChallenge, clientSecret, this.securityConfig);
|
|
4106
|
+
decision.body = page;
|
|
4107
|
+
}
|
|
4108
|
+
return decision;
|
|
4109
|
+
}
|
|
3770
4110
|
// Generate some trap URLs to embed in the challenge page.
|
|
3771
4111
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
3772
4112
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
|
|
@@ -3825,9 +4165,6 @@ export class FingerprintEngine {
|
|
|
3825
4165
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
3826
4166
|
}
|
|
3827
4167
|
|
|
3828
|
-
// Check if the request is an API request to return a JSON challenge
|
|
3829
|
-
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
3830
|
-
|
|
3831
4168
|
if (isApi) {
|
|
3832
4169
|
// For API clients, send a JSON response with challenge details.
|
|
3833
4170
|
const challengePayload = {
|
|
@@ -3861,8 +4198,14 @@ export class FingerprintEngine {
|
|
|
3861
4198
|
if (logger) {
|
|
3862
4199
|
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
3863
4200
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
4201
|
+
const response = { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
|
|
4202
|
+
if (requestContext._newCookies) {
|
|
4203
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
4204
|
+
if (deviceCookie) {
|
|
4205
|
+
response.newCookieForResponse = deviceCookie;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
return response;
|
|
3866
4209
|
}
|
|
3867
4210
|
|
|
3868
4211
|
/**
|
|
@@ -4489,6 +4832,32 @@ export const default_whitelist = () => [
|
|
|
4489
4832
|
];
|
|
4490
4833
|
|
|
4491
4834
|
|
|
4835
|
+
/**
|
|
4836
|
+
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
4837
|
+
* Prioritizes proxy-provided headers and falls back to Node's native socket session.
|
|
4838
|
+
* @private
|
|
4839
|
+
* @param {object} context - The request context.
|
|
4840
|
+
* @returns {string|null}
|
|
4841
|
+
*/
|
|
4842
|
+
function getTlsSessionId(context) {
|
|
4843
|
+
if (!context) return null;
|
|
4844
|
+
const fromHeader = context.headers ? (context.headers['x-tls-session-id'] || context.headers['x-ssl-session-id']) : null;
|
|
4845
|
+
if (fromHeader) return fromHeader;
|
|
4846
|
+
|
|
4847
|
+
const socket = context.rawReq?.socket;
|
|
4848
|
+
if (socket) {
|
|
4849
|
+
if (socket.sessionId) {
|
|
4850
|
+
return socket.sessionId.toString('hex');
|
|
4851
|
+
}
|
|
4852
|
+
if (typeof socket.getSession === 'function') {
|
|
4853
|
+
const session = socket.getSession();
|
|
4854
|
+
if (session) {
|
|
4855
|
+
return crypto.createHash('sha256').update(session).digest('hex');
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
return null;
|
|
4860
|
+
}
|
|
4492
4861
|
|
|
4493
4862
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
4494
4863
|
export const powMiddleware = (securityConfig) => {
|
|
@@ -4521,6 +4890,24 @@ export const powMiddleware = (securityConfig) => {
|
|
|
4521
4890
|
}
|
|
4522
4891
|
|
|
4523
4892
|
return async (req, res, next) => {
|
|
4893
|
+
if (!req.headers_translated) {
|
|
4894
|
+
req.headers_translated = true;
|
|
4895
|
+
const matchedMapping = getActiveMappingForRequest(req.headers);
|
|
4896
|
+
if (matchedMapping) {
|
|
4897
|
+
const devFpHeader = matchedMapping.headers['x-device-fingerprint'].toLowerCase();
|
|
4898
|
+
const behaviorHeader = matchedMapping.headers['x-behavior-metrics'].toLowerCase();
|
|
4899
|
+
|
|
4900
|
+
if (req.headers[devFpHeader]) {
|
|
4901
|
+
req.headers['x-device-fingerprint'] = req.headers[devFpHeader];
|
|
4902
|
+
}
|
|
4903
|
+
if (req.headers[behaviorHeader]) {
|
|
4904
|
+
req.headers['x-behavior-metrics'] = req.headers[behaviorHeader];
|
|
4905
|
+
}
|
|
4906
|
+
if (req.headers['x-device-fingerprint']) {
|
|
4907
|
+
req.headers['x-device-fingerprint'] = decodePolymorphicFingerprint(req.headers['x-device-fingerprint'], matchedMapping);
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4524
4911
|
if (securityConfig?.wasm) {
|
|
4525
4912
|
const wasmConfig = securityConfig.wasm;
|
|
4526
4913
|
let jsPath = '/fp.js';
|
|
@@ -4542,45 +4929,40 @@ export const powMiddleware = (securityConfig) => {
|
|
|
4542
4929
|
wasmFile = wasmConfig.wasmFile ? resolve(wasmConfig.wasmFile) : resolve(__dirname, '..', '..', 'public', 'fp.wasm');
|
|
4543
4930
|
}
|
|
4544
4931
|
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
} catch (e) {
|
|
4562
|
-
// Fallback
|
|
4932
|
+
if (req.path === jsPath) {
|
|
4933
|
+
try {
|
|
4934
|
+
if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
|
|
4935
|
+
await ensureLatestMapping();
|
|
4936
|
+
const latest = activeMappings[0];
|
|
4937
|
+
if (latest && latest.jsBuffer) {
|
|
4938
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
4939
|
+
return res.send(latest.jsBuffer);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
if (jsFile && existsSync(jsFile)) {
|
|
4943
|
+
const fileContent = readFileSync(jsFile);
|
|
4944
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
4945
|
+
return res.send(fileContent);
|
|
4946
|
+
}
|
|
4947
|
+
} catch (e) {}
|
|
4563
4948
|
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
} catch (e) {
|
|
4581
|
-
// Fallback
|
|
4949
|
+
if (req.path === wasmPath) {
|
|
4950
|
+
try {
|
|
4951
|
+
if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
|
|
4952
|
+
await ensureLatestMapping();
|
|
4953
|
+
const latest = activeMappings[0];
|
|
4954
|
+
if (latest && latest.wasmBuffer) {
|
|
4955
|
+
res.setHeader('Content-Type', 'application/wasm');
|
|
4956
|
+
return res.send(latest.wasmBuffer);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
if (wasmFile && existsSync(wasmFile)) {
|
|
4960
|
+
const fileContent = readFileSync(wasmFile);
|
|
4961
|
+
res.setHeader('Content-Type', 'application/wasm');
|
|
4962
|
+
return res.send(fileContent);
|
|
4963
|
+
}
|
|
4964
|
+
} catch (e) {}
|
|
4582
4965
|
}
|
|
4583
|
-
}
|
|
4584
4966
|
}
|
|
4585
4967
|
|
|
4586
4968
|
const requestContext = {
|
|
@@ -4657,6 +5039,7 @@ export const __internal = {
|
|
|
4657
5039
|
getDeviceHash,
|
|
4658
5040
|
getCompositeDeviceHash,
|
|
4659
5041
|
getSuspicionVector,
|
|
5042
|
+
getTlsSessionId,
|
|
4660
5043
|
cyrb53, // Export for testing
|
|
4661
5044
|
FingerprintBuilder, // Export for testing
|
|
4662
5045
|
calculateTarget,
|