@anonympins/fingerprint 0.4.4 → 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.
@@ -4,6 +4,7 @@ import * as dns from "node:dns/promises";
4
4
  import {getProblemManager, problemManager} from "./problem-manager.js";
5
5
  import {Optimization} from "./library.js";
6
6
  import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
7
+ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
7
8
  import {readFileSync, existsSync} from "node:fs";
8
9
  import {fileURLToPath} from "node:url";
9
10
  import {dirname, join, resolve} from "node:path";
@@ -14,6 +15,198 @@ const __dirname = dirname(__filename);
14
15
  export { createRedisStore } from "./redis-store.js";
15
16
  export { createMongoDbStore } from "./mongodb-store.js";
16
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
+ }
160
+
161
+ const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
162
+ const base64UrlDecode = (str) => {
163
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
164
+ while (base64.length % 4) {
165
+ base64 += '=';
166
+ }
167
+ return Buffer.from(base64, 'base64');
168
+ };
169
+
170
+ export function generateStatelessTicket(payload) {
171
+ const secret = getPowSecret();
172
+ const key = crypto.createHash('sha256').update(secret).digest();
173
+ const iv = crypto.randomBytes(16);
174
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
175
+ let encrypted = cipher.update(JSON.stringify(payload));
176
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
177
+
178
+ const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
179
+ return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
180
+ }
181
+
182
+ export function parseStatelessTicket(ticket) {
183
+ try {
184
+ const parts = ticket.split('.');
185
+ if (parts.length !== 3) return null;
186
+
187
+ const iv = base64UrlDecode(parts[0]);
188
+ const encrypted = base64UrlDecode(parts[1]);
189
+ const signature = base64UrlDecode(parts[2]);
190
+
191
+ if (iv.length !== 16) return null;
192
+
193
+ const secret = getPowSecret();
194
+ const key = crypto.createHash('sha256').update(secret).digest();
195
+
196
+ const expectedSignature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
197
+ if (signature.length !== expectedSignature.length || !crypto.timingSafeEqual(signature, expectedSignature)) {
198
+ return null;
199
+ }
200
+
201
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
202
+ let decrypted = decipher.update(encrypted);
203
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
204
+
205
+ return JSON.parse(decrypted.toString('utf8'));
206
+ } catch (e) {
207
+ return null;
208
+ }
209
+ }
17
210
 
18
211
  /**
19
212
  * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
@@ -493,6 +686,7 @@ function getCompositeDeviceHash(context) {
493
686
  "ch_model": "sec-ch-ua-model",
494
687
  "ch_arch": "sec-ch-ua-arch",
495
688
  "ch_bitness": "sec-ch-ua-bitness",
689
+ "ch_full_version_list": "sec-ch-ua-full-version-list",
496
690
  "upgrade_req": "upgrade-insecure-requests",
497
691
  "accept_lang": "accept-language",
498
692
  "accept_enc": "accept-encoding",
@@ -835,18 +1029,16 @@ export const verifyPoWAndGenerateTicket = async (
835
1029
  return null;
836
1030
  }
837
1031
 
838
- // 2. Generate an opaque ticket ID and store session metadata securely on the server
839
- const ticketId = crypto.randomUUID();
1032
+ // 2. Generate a signed and encrypted stateless ticket
840
1033
  const expiry = Date.now() + 3600000; // 1 hour
841
-
842
- await store.set(`ticket:${ticketId}`, {
1034
+ const payload = {
843
1035
  expiry,
844
1036
  originalIp: ip,
845
1037
  deviceId,
846
1038
  deviceHash
847
- }, 3600); // 1 hour TTL
1039
+ };
848
1040
 
849
- return ticketId;
1041
+ return generateStatelessTicket(payload);
850
1042
  };
851
1043
 
852
1044
 
@@ -898,11 +1090,56 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
898
1090
  }
899
1091
  return finalHash === parseInt(solution, 10);
900
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
+
901
1119
  export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
902
1120
  // Input validation: ensure the ticket is a non-empty string with the correct format.
903
1121
  if (typeof ticket !== 'string' || ticket.length === 0) return false;
904
1122
 
905
- // 1. Resolve opaque ticket session from server-side store
1123
+ // 1. Resolve stateless ticket first (zero database I/O cost)
1124
+ const statelessData = parseStatelessTicket(ticket);
1125
+ if (statelessData) {
1126
+ const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
1127
+
1128
+ if (!expiry || Date.now() > expiry) {
1129
+ return false;
1130
+ }
1131
+
1132
+ if (ip === originalIp) return true;
1133
+ const currentSubnet = getIpSubnet(ip);
1134
+ const originalSubnet = getIpSubnet(originalIp);
1135
+ if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
1136
+
1137
+ if (!allowCrossNetworkRoaming) return false;
1138
+
1139
+ return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
1140
+ }
1141
+
1142
+ // 2. Resolve opaque ticket session from server-side store
906
1143
  const ticketData = await store.get(`ticket:${ticket}`);
907
1144
  if (ticketData) {
908
1145
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = ticketData;
@@ -922,7 +1159,7 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
922
1159
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
923
1160
  }
924
1161
 
925
- // 2. Legacy fallback verification (backward compatibility for old client tokens)
1162
+ // 3. Legacy fallback verification (backward compatibility for old client tokens)
926
1163
  let expiry, originalIp, sig;
927
1164
  if (ticket.includes('|')) {
928
1165
  const parts = ticket.split('|');
@@ -1018,6 +1255,66 @@ function getHeaderAnomalies(context) {
1018
1255
  };
1019
1256
  }
1020
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
+
1021
1318
  /**
1022
1319
  * Checks for submitted honeypot fields to detect bots.
1023
1320
  * @param {object} context - The request context.
@@ -1400,11 +1697,27 @@ function getCrossLayerInconsistency(context) {
1400
1697
  const clientScreenHash = clientFpMap.get('scr');
1401
1698
  const viewportWidth = context.headers['sec-ch-viewport-width'];
1402
1699
  if (clientScreenHash && viewportWidth) {
1403
- const clientWidth = clientFpMap.get('scr')?.split('x')[0];
1404
- // Ce n'est pas une comparaison directe, mais un bot pourrait oublier de forger les CH.
1405
- // Si le client FP a une largeur et que le CH en a une autre, c'est suspect.
1406
- // Cette vérification est basique et pourrait être affinée.
1407
- 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) {
1408
1721
  score += 20;
1409
1722
  }
1410
1723
  }
@@ -1415,10 +1728,17 @@ function getCrossLayerInconsistency(context) {
1415
1728
  const clientGpuHash = clientFpMap.get('gpu');
1416
1729
  const ja3 = getTlsFingerprint(context)?.ja3;
1417
1730
  if (clientGpuHash && ja3) {
1418
- // Une vraie implémentation nécessiterait une base de données mappant les GPU connus
1419
- // à des signatures JA3 typiques. Pour l'exemple, on simule une pénalité si les deux
1420
- // sont présents mais que le score de cohérence global est déjà faible.
1421
- // (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
+ }
1422
1742
  }
1423
1743
 
1424
1744
  return { crossLayerInconsistencyScore: Math.min(100, score) };
@@ -1647,6 +1967,48 @@ function getClientHintsInconsistencyScore(context) {
1647
1967
  return { clientHintsInconsistencyScore: 0 };
1648
1968
  }
1649
1969
 
1970
+ const fullVersionList = context.headers['sec-ch-ua-full-version-list'];
1971
+ if (fullVersionList) {
1972
+ let chFullVersion = null;
1973
+ let chFullBrowser = null;
1974
+ const matches = [...fullVersionList.matchAll(/"([^"]+)";v="([^"]+)"/g)];
1975
+ for (const match of matches) {
1976
+ const brand = match[1];
1977
+ const version = match[2];
1978
+ if (brand === 'Google Chrome' || brand === 'Chromium' || brand === 'Microsoft Edge') {
1979
+ chFullVersion = version;
1980
+ chFullBrowser = brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
1981
+ if (brand === 'Google Chrome' || brand === 'Microsoft Edge') {
1982
+ break;
1983
+ }
1984
+ }
1985
+ }
1986
+ if (chFullVersion && chFullBrowser) {
1987
+ let uaFullVersion = null;
1988
+ const uaFullMatch = ua.match(/(Chrome|Edg)\/([\d\.]+)/);
1989
+ if (uaFullMatch) {
1990
+ const uaBrowserMapped = uaFullMatch[1] === 'Edg' ? 'Edge' : 'Chrome';
1991
+ uaFullVersion = uaFullMatch[2];
1992
+ if (uaBrowserMapped === chFullBrowser && uaFullVersion !== chFullVersion) {
1993
+ const parts1 = uaFullVersion.split('.').map(Number);
1994
+ const parts2 = chFullVersion.split('.').map(Number);
1995
+ let diffIndex = -1;
1996
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1997
+ if ((parts1[i] || 0) !== (parts2[i] || 0)) {
1998
+ diffIndex = i;
1999
+ break;
2000
+ }
2001
+ }
2002
+ const baseScores = [95, 90, 85, 80];
2003
+ const baseScore = baseScores[diffIndex] || 80;
2004
+ const delta = Math.abs((parts1[diffIndex] || 0) - (parts2[diffIndex] || 0));
2005
+ const finalFullScore = Math.min(100, baseScore + Math.min(5, delta * 5));
2006
+ return { clientHintsInconsistencyScore: finalFullScore };
2007
+ }
2008
+ }
2009
+ }
2010
+ }
2011
+
1650
2012
  // 1. Extract browser and version from User-Agent
1651
2013
  let uaVersion = null;
1652
2014
  let uaBrowser = null;
@@ -1681,10 +2043,19 @@ function getClientHintsInconsistencyScore(context) {
1681
2043
 
1682
2044
  const versionDifference = Math.abs(parseInt(uaVersion, 10) - parseInt(chVersion, 10));
1683
2045
 
1684
- if (versionDifference > 5) return { clientHintsInconsistencyScore: 80 };
1685
- if (versionDifference > 1) return { clientHintsInconsistencyScore: 40 };
2046
+ let clientHintsInconsistencyScore = 0;
2047
+ if (versionDifference > 0) {
2048
+ if (versionDifference <= 2) {
2049
+ clientHintsInconsistencyScore = versionDifference * 20;
2050
+ } else if (versionDifference <= 7) {
2051
+ clientHintsInconsistencyScore = 40 + (versionDifference - 2) * 8;
2052
+ } else {
2053
+ clientHintsInconsistencyScore = Math.min(100, 80 + (versionDifference - 7) * 3.33);
2054
+ }
2055
+ clientHintsInconsistencyScore = Math.round(clientHintsInconsistencyScore);
2056
+ }
1686
2057
 
1687
- return { clientHintsInconsistencyScore: 0 };
2058
+ return { clientHintsInconsistencyScore };
1688
2059
  }
1689
2060
 
1690
2061
  /**
@@ -2225,11 +2596,20 @@ export const configureStore = (externalStore) => {
2225
2596
  async function resolveRequestIdentity(context, securityConfig = {}) {
2226
2597
  const existingDeviceId = context.cookies?.device_id;
2227
2598
  const currentDeviceHash = getCompositeDeviceHash(context); // Use the composite hash for consistency checks
2228
- let deviceId = existingDeviceId;
2599
+ const tlsSessionId = getTlsSessionId(context);
2600
+ let deviceId = existingDeviceId;
2229
2601
  let consistencyScore = 1.0; // 1.0 = perfectly consistent
2230
2602
  let deviceData = null;
2231
2603
  let newCookie = null;
2232
- 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) {
2233
2613
  deviceData = await store.get(`device:${deviceId}`);
2234
2614
  }
2235
2615
 
@@ -2272,6 +2652,9 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
2272
2652
  // The write will happen in getSuspicionVector after all modifications.
2273
2653
  }
2274
2654
 
2655
+ if (deviceId && tlsSessionId) {
2656
+ await store.set(`tls-session:${tlsSessionId}`, deviceId, 3600); // Bind TLS session for 1 hour
2657
+ }
2275
2658
  return { deviceId, deviceData, consistencyScore, newCookie };
2276
2659
  }
2277
2660
 
@@ -2416,6 +2799,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
2416
2799
  const stableFpHash = cyrb53(stableFp).toString();
2417
2800
  const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
2418
2801
 
2802
+ const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2803
+
2419
2804
  // Save the updated device state to the store
2420
2805
  // Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
2421
2806
  await store.set(`device:${deviceId}`, deviceData);
@@ -2426,7 +2811,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2426
2811
  deviceData.ips = new Set(deviceData.ips);
2427
2812
  }
2428
2813
  // Le vecteur de suspicion est maintenant complet.
2429
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore };
2814
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
2430
2815
  };
2431
2816
 
2432
2817
  // A residential user can change networks (home, 4G, public wifi).
@@ -2722,19 +3107,15 @@ export async function verifyCpuTargetPoWAndGenerateTicket(
2722
3107
  if (isValid) {
2723
3108
  console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
2724
3109
  });
2725
- // Generate an opaque ticket ID and store session metadata securely on the server
2726
- const ticketId = crypto.randomUUID();
2727
3110
  const ttl = ticketTtl || 3600000; // Calculates expiration from TTL
2728
3111
  const expiry = Date.now() + ttl;
2729
3112
 
2730
- await store.set(`ticket:${ticketId}`, {
3113
+ return generateStatelessTicket({
2731
3114
  expiry,
2732
3115
  originalIp: clientIp,
2733
3116
  deviceId,
2734
3117
  deviceHash
2735
3118
  }, Math.ceil(ttl / 1000));
2736
-
2737
- return ticketId;
2738
3119
  }
2739
3120
 
2740
3121
  return null;
@@ -2836,7 +3217,8 @@ export class FingerprintEngine {
2836
3217
  (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
2837
3218
  (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
2838
3219
  (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
2839
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0);
3220
+ (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
3221
+ (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0);
2840
3222
 
2841
3223
  return Math.min(100, score);
2842
3224
  }
@@ -3094,7 +3476,7 @@ export class FingerprintEngine {
3094
3476
  async processRequest(requestContext) {
3095
3477
  sanitizeProxyHeaders(requestContext, this.securityConfig);
3096
3478
 
3097
- const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3479
+ const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3098
3480
  const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
3099
3481
 
3100
3482
  this._log('Processing request', { clientIp, path, isStatic });
@@ -3174,6 +3556,13 @@ export class FingerprintEngine {
3174
3556
  decision.action = 'next';
3175
3557
  delete decision.status;
3176
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
+ }
3177
3566
  }
3178
3567
  return decision;
3179
3568
  }
@@ -3213,8 +3602,8 @@ export class FingerprintEngine {
3213
3602
  // --- NOUVELLE LOGIQUE DE PRIORITÉ ---
3214
3603
  // Si une solution de challenge est soumise, on la traite en priorité absolue,
3215
3604
  // avant même de recalculer le score de suspicion.
3216
- const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
3217
- 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
3218
3607
  this._log('Challenge solution submitted', { pow_type, pow_nonce });
3219
3608
 
3220
3609
  // On doit calculer le score de suspicion *avant* de valider le ticket,
@@ -3292,13 +3681,13 @@ export class FingerprintEngine {
3292
3681
  } else {
3293
3682
  optimalTtl = determineOptimalTicketTtl(preliminaryScore);
3294
3683
  finalTtl = isProbationary ? probationaryTtl : optimalTtl;
3295
- this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
3684
+ this._log('Challenge context found, verifying solution', {optimalTtl, finalTtl});
3296
3685
 
3297
3686
  if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
3298
3687
  const cpuSolution = pow_solution_cpu || pow_solution;
3299
3688
  ticket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext, deviceId, currentDeviceHash);
3300
3689
  isValid = ticket !== null;
3301
- this._log('CPU target challenge verification', { isValid });
3690
+ this._log('CPU target challenge verification', {isValid});
3302
3691
  } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
3303
3692
  const cpuTicket = await verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext, deviceId, currentDeviceHash);
3304
3693
  const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
@@ -3309,6 +3698,18 @@ export class FingerprintEngine {
3309
3698
  memValid: isMemValid,
3310
3699
  isValid
3311
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
+ }
3312
3713
  }
3313
3714
  }
3314
3715
  } else {
@@ -3345,6 +3746,7 @@ export class FingerprintEngine {
3345
3746
  finalSearchParams.delete('pow_solution_cpu');
3346
3747
  finalSearchParams.delete('pow_solution_mem');
3347
3748
  finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
3749
+ finalSearchParams.delete('pow_solution_space');
3348
3750
  // NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
3349
3751
  finalSearchParams.delete('pow_solution_population');
3350
3752
  finalSearchParams.delete('pow_solution_work_result');
@@ -3382,6 +3784,12 @@ export class FingerprintEngine {
3382
3784
  decision.action = 'next';
3383
3785
  delete decision.status;
3384
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
+ }
3385
3793
  }
3386
3794
  return decision;
3387
3795
  }
@@ -3474,6 +3882,12 @@ export class FingerprintEngine {
3474
3882
  decision.action = 'next';
3475
3883
  delete decision.status;
3476
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
+ }
3477
3891
  }
3478
3892
  return decision;
3479
3893
  }
@@ -3521,6 +3935,12 @@ export class FingerprintEngine {
3521
3935
  decision.action = 'next';
3522
3936
  delete decision.status;
3523
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
+ }
3524
3944
  }
3525
3945
  return decision;
3526
3946
  }
@@ -3545,6 +3965,12 @@ export class FingerprintEngine {
3545
3965
  decision.action = 'next';
3546
3966
  delete decision.status;
3547
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
+ }
3548
3974
  }
3549
3975
  return decision;
3550
3976
  }
@@ -3579,6 +4005,12 @@ export class FingerprintEngine {
3579
4005
  decision.action = 'next';
3580
4006
  delete decision.status;
3581
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
+ }
3582
4014
  }
3583
4015
  return decision;
3584
4016
  }
@@ -3588,6 +4020,7 @@ export class FingerprintEngine {
3588
4020
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
3589
4021
  const nonce = crypto.randomBytes(16).toString("hex");
3590
4022
  const clientSecret = crypto.randomBytes(16).toString("hex");
4023
+ const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
3591
4024
 
3592
4025
  // Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
3593
4026
  // Cela rend l'automatisation plus difficile pour un attaquant.
@@ -3630,7 +4063,6 @@ export class FingerprintEngine {
3630
4063
  }
3631
4064
 
3632
4065
  if (isSuspicious && usefulWorkDispatched) {
3633
- const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
3634
4066
  if (isApi) {
3635
4067
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
3636
4068
  } else {
@@ -3648,6 +4080,33 @@ export class FingerprintEngine {
3648
4080
  delete decision.status;
3649
4081
  return decision;
3650
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
+ }
3651
4110
  // Generate some trap URLs to embed in the challenge page.
3652
4111
  // These links are visually hidden but present in the DOM to trap bots.
3653
4112
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
@@ -3706,9 +4165,6 @@ export class FingerprintEngine {
3706
4165
  logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
3707
4166
  }
3708
4167
 
3709
- // Check if the request is an API request to return a JSON challenge
3710
- const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
3711
-
3712
4168
  if (isApi) {
3713
4169
  // For API clients, send a JSON response with challenge details.
3714
4170
  const challengePayload = {
@@ -3742,8 +4198,14 @@ export class FingerprintEngine {
3742
4198
  if (logger) {
3743
4199
  logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
3744
4200
  }
3745
-
3746
- 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;
3747
4209
  }
3748
4210
 
3749
4211
  /**
@@ -3982,6 +4444,185 @@ function determineOptimalTicketTtl(suspicionScore) {
3982
4444
  return ttl;
3983
4445
  }
3984
4446
 
4447
+
4448
+ /**
4449
+ * Parse une trame TCP SYN brute (IPv4 ou IPv6).
4450
+ * @private
4451
+ * @param {Buffer|Uint8Array} binary - Le paquet binaire.
4452
+ * @returns {object|null}
4453
+ */
4454
+ function parseTcpSyn(binary) {
4455
+ if (!binary || binary.length < 40) return null;
4456
+ let ttl = 64;
4457
+ let tcpOffset = 20;
4458
+ const version = binary[0] >> 4;
4459
+
4460
+ if (version === 4) {
4461
+ ttl = binary[8];
4462
+ const ihl = binary[0] & 0x0f;
4463
+ tcpOffset = ihl * 4;
4464
+ } else if (version === 6) {
4465
+ ttl = binary[7]; // Hop Limit
4466
+ tcpOffset = 40;
4467
+ } else {
4468
+ tcpOffset = 0;
4469
+ ttl = 64;
4470
+ }
4471
+
4472
+ if (binary.length < tcpOffset + 20) return null;
4473
+
4474
+ const windowSize = (binary[tcpOffset + 14] << 8) | binary[tcpOffset + 15];
4475
+ const dataOffset = (binary[tcpOffset + 12] >> 4) * 4;
4476
+ const optionsEnd = tcpOffset + dataOffset;
4477
+
4478
+ let mss = null;
4479
+ let ws = null;
4480
+ let sack = false;
4481
+
4482
+ let i = tcpOffset + 20;
4483
+ while (i < optionsEnd && i < binary.length) {
4484
+ const optType = binary[i];
4485
+ if (optType === 0) break;
4486
+ if (optType === 1) {
4487
+ i++;
4488
+ continue;
4489
+ }
4490
+ if (i + 1 >= binary.length) break;
4491
+ const optLen = binary[i + 1];
4492
+ if (optLen < 2 || i + optLen > binary.length) break;
4493
+
4494
+ if (optType === 2 && optLen === 4) {
4495
+ mss = (binary[i + 2] << 8) | binary[i + 3];
4496
+ } else if (optType === 3 && optLen === 3) {
4497
+ ws = binary[i + 2];
4498
+ } else if (optType === 4 && optLen === 2) {
4499
+ sack = true;
4500
+ }
4501
+ i += optLen;
4502
+ }
4503
+
4504
+ return { ttl, windowSize, mss, ws, sack };
4505
+ }
4506
+
4507
+ /**
4508
+ * Classifie l'OS à partir du fingerprint de la pile TCP/IP.
4509
+ * @private
4510
+ * @param {object|null} fingerprint
4511
+ * @returns {string}
4512
+ */
4513
+ function classifyTcpOs(fingerprint) {
4514
+ if (!fingerprint) return 'unknown';
4515
+ const ttl = fingerprint.ttl ?? 64;
4516
+ const windowSize = fingerprint.windowSize ?? 0;
4517
+ const ws = fingerprint.ws ?? null;
4518
+
4519
+ if (ttl > 64 && ttl <= 128) {
4520
+ return 'Windows';
4521
+ }
4522
+ if (ttl > 32 && ttl <= 64) {
4523
+ if (windowSize === 29200 || windowSize === 14600 || windowSize === 5840) {
4524
+ return 'Linux';
4525
+ }
4526
+ return 'Linux';
4527
+ }
4528
+ if (ttl <= 64) {
4529
+ if (windowSize === 65535 && (ws === 6 || ws === 8 || ws === 5)) {
4530
+ return 'macOS/iOS';
4531
+ }
4532
+ }
4533
+ if (ttl > 64) return 'Windows';
4534
+ if (ttl > 0) return 'Linux';
4535
+ return 'unknown';
4536
+ }
4537
+
4538
+ /**
4539
+ * Détecte les anomalies de pile réseau par rapport au User-Agent.
4540
+ * @private
4541
+ * @param {object} context - Le contexte de la requête.
4542
+ * @returns {{tcpAnomalyScore: number}}
4543
+ */
4544
+ function getTcpAnomalyScore(context) {
4545
+ let fp = null;
4546
+ const rawTcpBinary = context.headers?.['x-raw-tcp-binary'] || context.rawTcpBinary || null;
4547
+ if (rawTcpBinary) {
4548
+ const binary = Buffer.isBuffer(rawTcpBinary) ? rawTcpBinary : (typeof rawTcpBinary === 'string' ? Buffer.from(rawTcpBinary, 'hex') : rawTcpBinary);
4549
+ fp = parseTcpSyn(binary);
4550
+ }
4551
+
4552
+ if (!fp) {
4553
+ const tcpHeader = context.headers?.['x-tcp-fingerprint'] || context.tcpFingerprint || null;
4554
+ if (tcpHeader && typeof tcpHeader === 'string') {
4555
+ const parts = tcpHeader.split(':');
4556
+ if (parts.length >= 2) {
4557
+ fp = {
4558
+ ttl: parseInt(parts[0], 10),
4559
+ windowSize: parseInt(parts[1], 10),
4560
+ mss: parts[2] ? parseInt(parts[2], 10) : null,
4561
+ ws: parts[3] ? parseInt(parts[3], 10) : null,
4562
+ sack: parts[4] === '1' || parts[4] === 'true'
4563
+ };
4564
+ }
4565
+ }
4566
+ }
4567
+
4568
+ if (!fp) {
4569
+ return { tcpAnomalyScore: 0.0 };
4570
+ }
4571
+
4572
+ const tcpOs = classifyTcpOs(fp);
4573
+ const ua = context.headers?.['user-agent'] || '';
4574
+ const uaParts = parseUserAgent(ua);
4575
+ const uaOs = uaParts.os;
4576
+
4577
+ if (!uaOs || tcpOs === 'unknown') {
4578
+ return { tcpAnomalyScore: 0.0 };
4579
+ }
4580
+
4581
+ let mappedOs = null;
4582
+ if (uaOs.startsWith('Windows')) mappedOs = 'Windows';
4583
+ else if (uaOs.startsWith('Mac') || uaOs.startsWith('macOS')) mappedOs = 'macOS';
4584
+ else if (uaOs.startsWith('iOS')) mappedOs = 'iOS';
4585
+ else if (uaOs.startsWith('Linux')) mappedOs = 'Linux';
4586
+
4587
+ if (!mappedOs) {
4588
+ return { tcpAnomalyScore: 0.0 };
4589
+ }
4590
+
4591
+ const OS_EXPECTED_TCP = {
4592
+ 'Windows': { ttl: 128, windowSize: 64240, ws: 8, mss: 1460, sack: true },
4593
+ 'Linux': { ttl: 64, windowSize: 29200, ws: 7, mss: 1460, sack: true },
4594
+ 'macOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true },
4595
+ 'iOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true }
4596
+ };
4597
+
4598
+ const expected = OS_EXPECTED_TCP[mappedOs];
4599
+ const ttlDiff = Math.abs(fp.ttl - expected.ttl) / expected.ttl;
4600
+ const winDiff = Math.abs(fp.windowSize - expected.windowSize) / expected.windowSize;
4601
+ const wsDiff = expected.ws !== null && fp.ws !== null ? Math.abs(fp.ws - expected.ws) / expected.ws : 0.0;
4602
+ const mssDiff = expected.mss !== null && fp.mss !== null ? Math.abs(fp.mss - expected.mss) / expected.mss : 0.0;
4603
+ const sackDiff = (fp.sack ?? true) === (expected.sack ?? true) ? 0.0 : 1.0;
4604
+
4605
+ const deviation = (
4606
+ Math.min(1.0, ttlDiff) * 0.50 +
4607
+ Math.min(1.0, winDiff) * 0.25 +
4608
+ Math.min(1.0, wsDiff) * 0.15 +
4609
+ Math.min(1.0, mssDiff) * 0.05 +
4610
+ sackDiff * 0.05
4611
+ );
4612
+
4613
+ let tcpAnomalyScore = 0.0;
4614
+ if (tcpOs !== mappedOs && tcpOs !== 'unknown') {
4615
+ const baseAnomaly = mappedOs === 'Windows' ? 80.0 :
4616
+ (mappedOs === 'macOS' || mappedOs === 'iOS' ? 85.0 : 75.0);
4617
+ tcpAnomalyScore = baseAnomaly + (deviation - 0.4) * 10.0;
4618
+ } else {
4619
+ tcpAnomalyScore = deviation * 40.0;
4620
+ }
4621
+
4622
+ tcpAnomalyScore = Math.max(0.0, Math.min(100.0, Math.round(tcpAnomalyScore * 10) / 10));
4623
+ return { tcpAnomalyScore };
4624
+ }
4625
+
3985
4626
  /**
3986
4627
  * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
3987
4628
  * @private
@@ -4191,6 +4832,32 @@ export const default_whitelist = () => [
4191
4832
  ];
4192
4833
 
4193
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
+ }
4194
4861
 
4195
4862
  // --- Proof-of-Work Middleware (The Tollbooth) ---
4196
4863
  export const powMiddleware = (securityConfig) => {
@@ -4223,6 +4890,24 @@ export const powMiddleware = (securityConfig) => {
4223
4890
  }
4224
4891
 
4225
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
+ }
4226
4911
  if (securityConfig?.wasm) {
4227
4912
  const wasmConfig = securityConfig.wasm;
4228
4913
  let jsPath = '/fp.js';
@@ -4244,24 +4929,40 @@ export const powMiddleware = (securityConfig) => {
4244
4929
  wasmFile = wasmConfig.wasmFile ? resolve(wasmConfig.wasmFile) : resolve(__dirname, '..', '..', 'public', 'fp.wasm');
4245
4930
  }
4246
4931
 
4247
- if (jsFile && req.path === jsPath) {
4248
- try {
4249
- const fileContent = readFileSync(jsFile);
4250
- res.setHeader('Content-Type', 'application/javascript');
4251
- return res.send(fileContent);
4252
- } catch (e) {
4253
- // 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) {}
4254
4948
  }
4255
- }
4256
- if (wasmFile && req.path === wasmPath) {
4257
- try {
4258
- const fileContent = readFileSync(wasmFile);
4259
- res.setHeader('Content-Type', 'application/wasm');
4260
- return res.send(fileContent);
4261
- } catch (e) {
4262
- // 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) {}
4263
4965
  }
4264
- }
4265
4966
  }
4266
4967
 
4267
4968
  const requestContext = {
@@ -4338,6 +5039,7 @@ export const __internal = {
4338
5039
  getDeviceHash,
4339
5040
  getCompositeDeviceHash,
4340
5041
  getSuspicionVector,
5042
+ getTlsSessionId,
4341
5043
  cyrb53, // Export for testing
4342
5044
  FingerprintBuilder, // Export for testing
4343
5045
  calculateTarget,
@@ -4352,6 +5054,8 @@ export const __internal = {
4352
5054
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
4353
5055
  sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
4354
5056
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
5057
+ generateStatelessTicket,
5058
+ parseStatelessTicket,
4355
5059
  parseJa3,
4356
5060
  getBotnetClusterScore, // NOUVEAU: Expose pour les tests
4357
5061
  generateCpuTargetChallengePage,
@@ -4364,6 +5068,9 @@ export const __internal = {
4364
5068
  getIpReputationScore, // Expose for testing
4365
5069
  updateIpReputationScore, // Expose for testing
4366
5070
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5071
+ parseTcpSyn, // Expose for testing
5072
+ classifyTcpOs, // Expose for testing
5073
+ getTcpAnomalyScore // Expose for testing
4367
5074
  };
4368
5075
 
4369
5076
  // --- THRESHOLD AUTO-TUNING SECTION ---