@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.
@@ -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
- const clientWidth = clientFpMap.get('scr')?.split('x')[0];
1473
- // Ce n'est pas une comparaison directe, mais un bot pourrait oublier de forger les CH.
1474
- // Si le client FP a une largeur et que le CH en a une autre, c'est suspect.
1475
- // Cette vérification est basique et pourrait être affinée.
1476
- if (clientWidth && clientWidth !== viewportWidth) {
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
- // Une vraie implémentation nécessiterait une base de données mappant les GPU connus
1488
- // à des signatures JA3 typiques. Pour l'exemple, on simule une pénalité si les deux
1489
- // sont présents mais que le score de cohérence global est déjà faible.
1490
- // (Cette logique est déjà en partie couverte par le `consistencyScore`).
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
- let deviceId = existingDeviceId;
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
- if (deviceId) {
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', { optimalTtl, finalTtl });
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', { isValid });
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
- return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
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
- if (jsFile && req.path === jsPath) {
4546
- try {
4547
- if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
4548
- console.log('[Fingerprint] Generating dynamic polymorphic WASM module...');
4549
- // Génère des constantes aléatoires uniques pour cette session / requête
4550
- const seed = crypto.randomBytes(4).readInt32LE(0);
4551
- const multiplier = crypto.randomBytes(4).readInt32LE(0) | 1; // Doit être impair pour un LCG optimal
4552
- const adder = crypto.randomBytes(4).readInt32LE(0);
4553
-
4554
- const wasmBuffer = DynamicWasmGenerator.generate({ seed, multiplier, adder });
4555
- res.setHeader('Content-Type', 'application/wasm');
4556
- return res.send(wasmBuffer);
4557
- }
4558
- const fileContent = readFileSync(wasmFile);
4559
- res.setHeader('Content-Type', 'application/wasm');
4560
- return res.send(fileContent);
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
- if (wasmFile && req.path === wasmPath) {
4566
- try {
4567
- if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
4568
- // Génère des constantes aléatoires uniques pour cette session / requête
4569
- const seed = crypto.randomBytes(4).readInt32LE(0);
4570
- const multiplier = crypto.randomBytes(4).readInt32LE(0) | 1; // Doit être impair pour un LCG optimal
4571
- const adder = crypto.randomBytes(4).readInt32LE(0);
4572
-
4573
- const wasmBuffer = DynamicWasmGenerator.generate({ seed, multiplier, adder });
4574
- res.setHeader('Content-Type', 'application/wasm');
4575
- return res.send(wasmBuffer);
4576
- }
4577
- const fileContent = readFileSync(wasmFile);
4578
- res.setHeader('Content-Type', 'application/wasm');
4579
- return res.send(fileContent);
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,