@anonympins/fingerprint 0.4.5 → 0.5.0

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.
@@ -8,6 +8,8 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
8
8
  import {readFileSync, existsSync} from "node:fs";
9
9
  import {fileURLToPath} from "node:url";
10
10
  import {dirname, join, resolve} from "node:path";
11
+ import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "./fingerprint.utils.js";
12
+
11
13
 
12
14
  const __filename = fileURLToPath(import.meta.url);
13
15
  const __dirname = dirname(__filename);
@@ -15,6 +17,132 @@ const __dirname = dirname(__filename);
15
17
  export { createRedisStore } from "./redis-store.js";
16
18
  export { createMongoDbStore } from "./mongodb-store.js";
17
19
 
20
+ let activeMappings = [];
21
+ let lastMappingTime = 0;
22
+ let isCompilingMapping = false;
23
+ const MAPPING_ROTATION_INTERVAL = 60000; // 60 seconds
24
+
25
+ function generateSessionMapping() {
26
+ const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
27
+ const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
28
+
29
+ return {
30
+ headers: {
31
+ 'x-device-fingerprint': randomHeader(),
32
+ 'x-behavior-metrics': randomHeader(),
33
+ },
34
+ globals: {
35
+ 'ClientLibrary': `ClientLib_${randomStr(6)}`,
36
+ 'getDeviceFingerprint': `getFP_${randomStr(6)}`,
37
+ 'getClientBehaviorMetrics': `getMetrics_${randomStr(6)}`,
38
+ },
39
+ keys: {
40
+ 'ua': randomStr(4),
41
+ 'hw': randomStr(4),
42
+ 'geo': randomStr(4),
43
+ 'scr': randomStr(4),
44
+ 'os': randomStr(4),
45
+ 'gpu': randomStr(4),
46
+ 'cvs': randomStr(4),
47
+ 'cdp': randomStr(4),
48
+ 'bot': randomStr(4),
49
+ 'wasm': randomStr(4),
50
+ },
51
+ wasmConstants: {
52
+ seed: crypto.randomBytes(4).readInt32LE(0),
53
+ multiplier: crypto.randomBytes(4).readInt32LE(0) | 1,
54
+ adder: crypto.randomBytes(4).readInt32LE(0)
55
+ }
56
+ };
57
+ }
58
+
59
+ async function compilePolymorphicJs(mapping) {
60
+ const clientScriptPath = join(__dirname, 'fingerprint.client.js');
61
+ let jsCode = '';
62
+ try {
63
+ jsCode = readFileSync(clientScriptPath, 'utf-8');
64
+ } catch (e) {
65
+ console.error('[Fingerprint] Could not read fingerprint.client.js for dynamic obfuscation. Fallback to obfuscated build.');
66
+ try {
67
+ return readFileSync(join(__dirname, 'fingerprint.client.obfuscated.js'), 'utf-8');
68
+ } catch (err) {
69
+ return '';
70
+ }
71
+ }
72
+
73
+ jsCode = jsCode.replace(/X-Device-Fingerprint/g, mapping.headers['x-device-fingerprint']);
74
+ jsCode = jsCode.replace(/X-Behavior-Metrics/g, mapping.headers['x-behavior-metrics']);
75
+ jsCode = jsCode.replace(/ClientLibrary/g, mapping.globals['ClientLibrary']);
76
+ jsCode = jsCode.replace(/getDeviceFingerprint/g, mapping.globals['getDeviceFingerprint']);
77
+ jsCode = jsCode.replace(/getClientBehaviorMetrics/g, mapping.globals['getClientBehaviorMetrics']);
78
+
79
+ for (const [origKey, randKey] of Object.entries(mapping.keys)) {
80
+ const regex1 = new RegExp(`add\\(["']${origKey}["']`, 'g');
81
+ jsCode = jsCode.replace(regex1, `add("${randKey}"`);
82
+
83
+ const regex2 = new RegExp(`addRaw\\(["']${origKey}["']`, 'g');
84
+ jsCode = jsCode.replace(regex2, `addRaw("${randKey}"`);
85
+ }
86
+
87
+ const obfuscationResult = JavaScriptObfuscator.obfuscate(jsCode, {
88
+ compact: true,
89
+ controlFlowFlattening: true,
90
+ deadCodeInjection: true,
91
+ stringArray: true,
92
+ stringArrayRotate: true,
93
+ stringArrayShuffle: true,
94
+ seed: Math.abs(mapping.wasmConstants.seed),
95
+ selfDefending: true,
96
+ });
97
+
98
+ return obfuscationResult.getObfuscatedCode();
99
+ }
100
+
101
+ async function ensureLatestMapping() {
102
+ const now = Date.now();
103
+ if ((now - lastMappingTime > MAPPING_ROTATION_INTERVAL || activeMappings.length === 0) && !isCompilingMapping) {
104
+ isCompilingMapping = true;
105
+ try {
106
+ const mapping = generateSessionMapping();
107
+ const polymorphicJs = await compilePolymorphicJs(mapping);
108
+ const polymorphicWasm = DynamicWasmGenerator.generate(mapping.wasmConstants);
109
+
110
+ mapping.jsBuffer = Buffer.from(polymorphicJs, 'utf8');
111
+ mapping.wasmBuffer = polymorphicWasm;
112
+ mapping.timestamp = now;
113
+
114
+ activeMappings.unshift(mapping);
115
+ if (activeMappings.length > 5) {
116
+ activeMappings.pop();
117
+ }
118
+ lastMappingTime = now;
119
+
120
+ try {
121
+ await store.set('active-polymorphic-mappings', activeMappings.map(m => ({
122
+ headers: m.headers,
123
+ keys: m.keys
124
+ })));
125
+ } catch (e) {
126
+ // Ignore
127
+ }
128
+ } finally {
129
+ isCompilingMapping = false;
130
+ }
131
+ }
132
+ }
133
+
134
+ function getActiveMappingForRequest(headers) {
135
+ if (!headers) return null;
136
+ for (const mapping of activeMappings) {
137
+ const headerName = mapping.headers['x-device-fingerprint'].toLowerCase();
138
+ if (headers[headerName]) {
139
+ return mapping;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+
18
146
 
19
147
  const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
20
148
  const base64UrlDecode = (str) => {
@@ -26,6 +154,31 @@ const base64UrlDecode = (str) => {
26
154
  };
27
155
 
28
156
  export function generateStatelessTicket(payload) {
157
+ let ed25519Key = process.env.ED25519_PRIVATE_KEY;
158
+ if (ed25519Key) {
159
+ try {
160
+ ed25519Key = ed25519Key.replace(/\\n/g, '\n');
161
+ const serialized = JSON.stringify(payload);
162
+ let signature;
163
+ try {
164
+ signature = crypto.sign(undefined, Buffer.from(serialized), {
165
+ key: ed25519Key,
166
+ format: 'pem',
167
+ type: 'pkcs8'
168
+ });
169
+ } catch (signErr) {
170
+ signature = crypto.sign(null, Buffer.from(serialized), {
171
+ key: ed25519Key,
172
+ format: 'pem',
173
+ type: 'pkcs8'
174
+ });
175
+ }
176
+ return `ed25519.${base64UrlEncode(Buffer.from(serialized))}.${base64UrlEncode(signature)}`;
177
+ } catch (e) {
178
+ console.error('[Fingerprint] Ed25519 signing failed, falling back to symmetric:', e.message);
179
+ }
180
+ }
181
+
29
182
  const secret = getPowSecret();
30
183
  const key = crypto.createHash('sha256').update(secret).digest();
31
184
  const iv = crypto.randomBytes(16);
@@ -39,6 +192,40 @@ export function generateStatelessTicket(payload) {
39
192
 
40
193
  export function parseStatelessTicket(ticket) {
41
194
  try {
195
+ if (ticket.startsWith('ed25519.')) {
196
+ const parts = ticket.split('.');
197
+ if (parts.length !== 3) return null;
198
+ const payloadBuffer = base64UrlDecode(parts[1]);
199
+ const signatureBuffer = base64UrlDecode(parts[2]);
200
+ let publicKey = process.env.ED25519_PUBLIC_KEY;
201
+ if (!publicKey) {
202
+ console.error('[Fingerprint] ED25519_PUBLIC_KEY is not defined in environment.');
203
+ return null;
204
+ }
205
+ publicKey = publicKey.replace(/\\n/g, '\n');
206
+
207
+ let isVerified = false;
208
+ try {
209
+ isVerified = crypto.verify(undefined, payloadBuffer, {
210
+ key: publicKey,
211
+ format: 'pem',
212
+ type: 'spki'
213
+ }, signatureBuffer);
214
+ } catch (verifyErr) {
215
+ try {
216
+ isVerified = crypto.verify(null, payloadBuffer, {
217
+ key: publicKey,
218
+ format: 'pem',
219
+ type: 'spki'
220
+ }, signatureBuffer);
221
+ } catch (verifyErr2) {
222
+ isVerified = false;
223
+ }
224
+ }
225
+ if (!isVerified) return null;
226
+ return JSON.parse(payloadBuffer.toString('utf8'));
227
+ }
228
+
42
229
  const parts = ticket.split('.');
43
230
  if (parts.length !== 3) return null;
44
231
 
@@ -97,26 +284,6 @@ async function checkChallengeRateLimit(clientIp) {
97
284
  return true;
98
285
  }
99
286
 
100
- /**
101
- * @private
102
- * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
103
- * @param {object} target - The target object.
104
- * @param {object} source - The source object.
105
- * @returns {object} The merged object.
106
- */
107
- function deepMerge(target, source) {
108
- const output = { ...target };
109
- if (target && typeof target === 'object' && source && typeof source === 'object') {
110
- Object.keys(source).forEach(key => {
111
- if (source[key] && typeof source[key] === 'object' && key in target) {
112
- output[key] = deepMerge(target[key], source[key]);
113
- } else {
114
- output[key] = source[key];
115
- }
116
- });
117
- }
118
- return output;
119
- }
120
287
 
121
288
  const securityProfiles = {
122
289
  /**
@@ -451,44 +618,6 @@ function getTlsFingerprint(context) {
451
618
  return { ja3, ja4 };
452
619
  }
453
620
 
454
- /**
455
- * Analyses a raw JA3 string.
456
- * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
457
- * @param {string} ja3String
458
- * @returns {object|null}
459
- */
460
- export function parseJa3(ja3String) {
461
- if (!ja3String || typeof ja3String !== 'string') {
462
- return null;
463
- }
464
- const parts = ja3String.split(',');
465
- if (parts.length !== 5) {
466
- return null;
467
- }
468
- return {
469
- tlsVersion: parseInt(parts[0], 10),
470
- ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
471
- extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
472
- curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
473
- points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
474
- };
475
- }
476
-
477
- /**
478
- * Creates a stable hash based on device characteristics, independent of the IP.
479
- * This is our "level 2 fingerprint".
480
- * @param {object} context - The request context.
481
- * @returns {string} A hash representing the device.
482
- */
483
- function getHeaderSignature(context) {
484
- if (!context.rawHeaders) return '';
485
- const headerKeys = [];
486
- for (let i = 0; i < context.rawHeaders.length; i += 2) {
487
- headerKeys.push(context.rawHeaders[i]);
488
- }
489
- return cyrb53(headerKeys.sort().join(','));
490
- }
491
-
492
621
  /**
493
622
  * Returns the client-side fingerprint if available, otherwise computes a server-side hash.
494
623
  * This aligns with the test's expectation for prioritization.
@@ -624,79 +753,6 @@ function hasGrease(values) {
624
753
  return values.some(val => GREASE_VALUES.includes(val));
625
754
  }
626
755
 
627
- // Fonctions utilitaires
628
- function parseUserAgent(ua) {
629
- // Parser basique du User-Agent
630
- const result = {};
631
-
632
- // Détection du navigateur
633
- if (ua.includes('Chrome') && !ua.includes('Edg')) {
634
- result.browser = 'Chrome';
635
- const match = ua.match(/Chrome\/(\d+)/);
636
- if (match) result.browser += `/${match[1]}`;
637
- } else if (ua.includes('Firefox')) {
638
- result.browser = 'Firefox';
639
- const match = ua.match(/Firefox\/(\d+)/);
640
- if (match) result.browser += `/${match[1]}`;
641
- } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
642
- result.browser = 'Safari';
643
- const match = ua.match(/Version\/(\d+)/);
644
- if (match) result.browser += `/${match[1]}`;
645
- } else if (ua.includes('Edg')) {
646
- result.browser = 'Edge';
647
- const match = ua.match(/Edg\/(\d+)/);
648
- if (match) result.browser += `/${match[1]}`;
649
- }
650
-
651
- // Détection de l'OS
652
- if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
653
- else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
654
- else if (ua.includes('Mac OS X')) result.os = 'macOS';
655
- else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
656
- else if (ua.includes('Android')) result.os = 'Android';
657
- else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
658
-
659
- // Détection du type d'appareil
660
- if (ua.includes('Mobile')) result.device = 'mobile';
661
- else if (ua.includes('Tablet')) result.device = 'tablet';
662
- else result.device = 'desktop';
663
-
664
- return result;
665
- }
666
-
667
- function normalizeReferer(referer) {
668
- try {
669
- const url = new URL(referer);
670
- return `${url.protocol}//${url.hostname}`;
671
- } catch {
672
- return referer;
673
- }
674
- }
675
-
676
- function isPrivateIp(ip) {
677
- // Vérifier si l'IP est privée
678
- const parts = ip.split('.');
679
- if (parts.length !== 4) return false;
680
- const first = parseInt(parts[0]);
681
- return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
682
- }
683
-
684
- function hashNetwork(ip, prefix = 24) {
685
- // Hash du réseau (masque /24 ou /16)
686
- const parts = ip.split('.');
687
- if (parts.length !== 4) return null;
688
- const maskBytes = prefix / 8;
689
- const network = parts.slice(0, maskBytes).join('.');
690
- // Hash simple
691
- let hash = 0;
692
- for (let i = 0; i < network.length; i++) {
693
- const char = network.charCodeAt(i);
694
- hash = ((hash << 5) - hash) + char;
695
- hash = hash & hash;
696
- }
697
- return hash.toString(16);
698
- }
699
-
700
756
  /**
701
757
  * Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
702
758
  * @param {string} nonce - Unique nonce for the challenge.
@@ -948,26 +1004,68 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
948
1004
  }
949
1005
  return finalHash === parseInt(solution, 10);
950
1006
  };
951
- export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
952
- // Input validation: ensure the ticket is a non-empty string with the correct format.
953
- if (typeof ticket !== 'string' || ticket.length === 0) return false;
954
1007
 
1008
+ export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
1009
+ let combined = [];
1010
+ for (let i = 0; i < queries.length; i++) {
1011
+ const idx = queries[i];
1012
+ const block = generateBlock(seed, idx);
1013
+ combined.push(...block);
1014
+ }
1015
+
1016
+ const assoc = await store.get(`coop-assoc:${nonce}`);
1017
+ if (assoc) {
1018
+ const peerSeed = assoc.peerSeed;
1019
+ const peerBlockIdx = assoc.peerBlockIdx;
1020
+ if (peerSeed !== undefined && peerBlockIdx !== undefined) {
1021
+ const peerBlock = generateBlock(peerSeed, peerBlockIdx);
1022
+ combined.push(...peerBlock);
1023
+ }
1024
+ await store.delete(`coop-assoc:${nonce}`);
1025
+ }
1026
+
1027
+ const nonceBytes = Buffer.from(nonce + ":" + clientSecret, "utf8");
1028
+ const finalBlock = Buffer.concat([Buffer.from(combined), nonceBytes]);
1029
+
1030
+ const hash = crypto.createHash("sha256").update(finalBlock).digest("hex");
1031
+ return hash === solution;
1032
+ }
1033
+
1034
+ function generateBlock(seed, blockIndex, blockSize = 1024) {
1035
+ const block = new Uint8Array(blockSize);
1036
+ let h = cyrb53(seed + ":" + blockIndex);
1037
+ for (let i = 0; i < blockSize; i++) {
1038
+ h = Math.imul(h ^ i, 1597334677);
1039
+ block[i] = h & 0xff;
1040
+ }
1041
+ return block;
1042
+ }
1043
+
1044
+ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
1045
+ // Input validation: ensure the ticket is a non-empty string with the correct format.
1046
+ if (typeof ticket !== 'string' || ticket.length === 0) return false;
955
1047
  // 1. Resolve stateless ticket first (zero database I/O cost)
956
1048
  const statelessData = parseStatelessTicket(ticket);
957
1049
  if (statelessData) {
958
1050
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
959
-
960
1051
  if (!expiry || Date.now() > expiry) {
961
1052
  return false;
962
1053
  }
963
-
1054
+ if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1055
+ const expectedY = storedDeviceHash.split(':')[1];
1056
+ if (zkpProof) {
1057
+ const [y, t, s] = zkpProof.split(':');
1058
+ if (y === expectedY && verifyZkpProof(y, t, s)) {
1059
+ return true;
1060
+ }
1061
+ }
1062
+ return false;
1063
+ }
964
1064
  if (ip === originalIp) return true;
965
1065
  const currentSubnet = getIpSubnet(ip);
966
1066
  const originalSubnet = getIpSubnet(originalIp);
967
1067
  if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
968
-
969
1068
  if (!allowCrossNetworkRoaming) return false;
970
-
971
1069
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
972
1070
  }
973
1071
 
@@ -980,6 +1078,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
980
1078
  await store.delete(`ticket:${ticket}`);
981
1079
  return false;
982
1080
  }
1081
+ if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
1082
+ const expectedY = storedDeviceHash.split(':')[1];
1083
+ if (zkpProof) {
1084
+ const [y, t, s] = zkpProof.split(':');
1085
+ if (y === expectedY && verifyZkpProof(y, t, s)) {
1086
+ return true;
1087
+ }
1088
+ }
1089
+ return false;
1090
+ }
983
1091
 
984
1092
  if (ip === originalIp) return true;
985
1093
  const currentSubnet = getIpSubnet(ip);
@@ -1087,6 +1195,80 @@ function getHeaderAnomalies(context) {
1087
1195
  };
1088
1196
  }
1089
1197
 
1198
+ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
1199
+ const sizeMb = securityConfig?.pospace?.sizeMb || 100;
1200
+ const numQueries = securityConfig?.pospace?.numQueries || 10;
1201
+
1202
+ const queries = [];
1203
+ const maxBlocks = sizeMb * 1024;
1204
+ while (queries.length < numQueries) {
1205
+ const idx = Math.floor(Math.random() * maxBlocks);
1206
+ if (!queries.includes(idx)) {
1207
+ queries.push(idx);
1208
+ }
1209
+ }
1210
+
1211
+ const challenge = {
1212
+ type: "pospace",
1213
+ nonce: nonce,
1214
+ sizeMb,
1215
+ queries,
1216
+ path: originalUrl
1217
+ };
1218
+
1219
+ const peer = await findPeerInSubnet(clientIp, nonce);
1220
+ if (peer) {
1221
+ challenge.peerId = peer.nodeId;
1222
+ challenge.peerBlockIdx = Math.floor(Math.random() * maxBlocks);
1223
+
1224
+ await store.set(`coop-assoc:${nonce}`, {
1225
+ peerNodeId: peer.nodeId,
1226
+ peerSeed: peer.seed,
1227
+ peerBlockIdx: challenge.peerBlockIdx
1228
+ }, 120);
1229
+ }
1230
+
1231
+ return challenge;
1232
+ }
1233
+
1234
+ function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
1235
+ const { nonce, sizeMb, queries, path } = challengeDetails;
1236
+ const solverCode = getPowSolverCode();
1237
+
1238
+ const challengeScript = `
1239
+ async function solve() {
1240
+ const nonce = ${JSON.stringify(nonce)};
1241
+ const path = ${JSON.stringify(path)};
1242
+ const clientSecret = ${JSON.stringify(clientSecret)};
1243
+ const queries = ${JSON.stringify(queries)};
1244
+ const sizeMb = ${sizeMb};
1245
+
1246
+ document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1247
+ await new Promise(r => setTimeout(r, 10));
1248
+
1249
+ try {
1250
+ await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
1251
+ document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
1252
+ const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
1253
+
1254
+ window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
1255
+ } catch(e) {
1256
+ document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
1257
+ }
1258
+ }
1259
+ solve();
1260
+ `;
1261
+
1262
+ return `<html><head><title>Security Check</title></head>
1263
+ <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1264
+ <h1>Security Check (Level 2)</h1>
1265
+ <p>We are verifying your storage allocation. This may take a few seconds on first load.</p>
1266
+ <div id="loader" style="margin:20px;">⚙️ Initializing storage space...</div>
1267
+ <script>${solverCode}</script>
1268
+ <script>${challengeScript}</script>
1269
+ </body></html>`;
1270
+ }
1271
+
1090
1272
  /**
1091
1273
  * Checks for submitted honeypot fields to detect bots.
1092
1274
  * @param {object} context - The request context.
@@ -1190,6 +1372,20 @@ const injectionPatterns = {
1190
1372
  traversal: /(\.\.\/|\.\.\\)/,
1191
1373
  // Remote Command Execution (RCE)
1192
1374
  rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
1375
+ // Server-Side Request Forgery (SSRF) - Detects local/private IPs and hosts
1376
+ ssrf: /((?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+\b|169\.254\.169\.254\b|10\.\d+\.\d+\.\d+\b|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+\b|192\.168\.\d+\.\d+\b|localhost\b|0\.0\.0\.0\b|\[[0:]+1\](?=\W|$)))/i,
1377
+ // Carriage Return Line Feed (CRLF) Injection / HTTP Response Splitting
1378
+ crlf: /[\r\n]|%0[ad]/i,
1379
+ // Cross-Site Scripting (XSS) - Fast native regex fallback
1380
+ xss: /(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i,
1381
+ // Open Redirect - Basic detection of external protocol/URLs
1382
+ openRedirect: /^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i,
1383
+ // Local/Remote File Inclusion (LFI/RFI)
1384
+ lfi: /(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i,
1385
+ // Shellshock (CVE-2014-6271)
1386
+ shellshock: /\(\)\s*\{\s*:\s*;\s*\}\s*/i,
1387
+ // NoSQL Injection (MongoDB query operators)
1388
+ nosql: /\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i
1193
1389
  };
1194
1390
 
1195
1391
  /**
@@ -1469,11 +1665,27 @@ function getCrossLayerInconsistency(context) {
1469
1665
  const clientScreenHash = clientFpMap.get('scr');
1470
1666
  const viewportWidth = context.headers['sec-ch-viewport-width'];
1471
1667
  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) {
1668
+ const viewportWidthInt = parseInt(viewportWidth, 10);
1669
+ let matchedScreenWidth = null;
1670
+ const commonWidths = [320, 360, 375, 390, 412, 414, 768, 1024, 1280, 1366, 1440, 1536, 1600, 1920, 2560, 3840];
1671
+ const commonHeights = [480, 568, 640, 667, 736, 800, 812, 844, 896, 900, 1024, 1080, 1200, 1440, 1600, 2160];
1672
+ const commonDepths = [24, 30, 32];
1673
+
1674
+ for (const w of commonWidths) {
1675
+ for (const h of commonHeights) {
1676
+ for (const d of commonDepths) {
1677
+ const candidate = `${w}x${h}_${d}`;
1678
+ if (clientScreenHash === String(cyrb53(candidate))) {
1679
+ matchedScreenWidth = w;
1680
+ break;
1681
+ }
1682
+ }
1683
+ if (matchedScreenWidth !== null) break;
1684
+ }
1685
+ if (matchedScreenWidth !== null) break;
1686
+ }
1687
+
1688
+ if (matchedScreenWidth !== null && viewportWidthInt > matchedScreenWidth) {
1477
1689
  score += 20;
1478
1690
  }
1479
1691
  }
@@ -1484,10 +1696,17 @@ function getCrossLayerInconsistency(context) {
1484
1696
  const clientGpuHash = clientFpMap.get('gpu');
1485
1697
  const ja3 = getTlsFingerprint(context)?.ja3;
1486
1698
  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`).
1699
+ let expectedBrowsers = tlsFingerprintDb[ja3];
1700
+ if (expectedBrowsers) {
1701
+ if (!Array.isArray(expectedBrowsers)) {
1702
+ expectedBrowsers = [expectedBrowsers];
1703
+ }
1704
+ const nonBrowserLibraries = ['Python', 'Go', 'Java', 'curl'];
1705
+ const isLibrary = expectedBrowsers.some(lib => nonBrowserLibraries.includes(lib));
1706
+ if (isLibrary) {
1707
+ score += 30;
1708
+ }
1709
+ }
1491
1710
  }
1492
1711
 
1493
1712
  return { crossLayerInconsistencyScore: Math.min(100, score) };
@@ -2345,11 +2564,20 @@ export const configureStore = (externalStore) => {
2345
2564
  async function resolveRequestIdentity(context, securityConfig = {}) {
2346
2565
  const existingDeviceId = context.cookies?.device_id;
2347
2566
  const currentDeviceHash = getCompositeDeviceHash(context); // Use the composite hash for consistency checks
2348
- let deviceId = existingDeviceId;
2567
+ const tlsSessionId = getTlsSessionId(context);
2568
+ let deviceId = existingDeviceId;
2349
2569
  let consistencyScore = 1.0; // 1.0 = perfectly consistent
2350
2570
  let deviceData = null;
2351
2571
  let newCookie = null;
2352
- if (deviceId) {
2572
+
2573
+ if (!deviceId && tlsSessionId) {
2574
+ const resumedDeviceId = await store.get(`tls-session:${tlsSessionId}`);
2575
+ if (resumedDeviceId) {
2576
+ deviceId = resumedDeviceId;
2577
+ }
2578
+ }
2579
+
2580
+ if (deviceId) {
2353
2581
  deviceData = await store.get(`device:${deviceId}`);
2354
2582
  }
2355
2583
 
@@ -2392,6 +2620,9 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
2392
2620
  // The write will happen in getSuspicionVector after all modifications.
2393
2621
  }
2394
2622
 
2623
+ if (deviceId && tlsSessionId) {
2624
+ await store.set(`tls-session:${tlsSessionId}`, deviceId, 3600); // Bind TLS session for 1 hour
2625
+ }
2395
2626
  return { deviceId, deviceData, consistencyScore, newCookie };
2396
2627
  }
2397
2628
 
@@ -2884,12 +3115,33 @@ function parseGraphQLQuery(body) {
2884
3115
  export class FingerprintEngine {
2885
3116
  constructor(securityConfig) {
2886
3117
  const isProduction = process.env.NODE_ENV === 'production';
2887
- this.securityConfig = securityConfig;
3118
+
3119
+ // Dynamically bind Ed25519 keys if passed via config
3120
+ if (securityConfig && securityConfig.ed25519_private_key) {
3121
+ process.env.ED25519_PRIVATE_KEY = securityConfig.ed25519_private_key;
3122
+ }
3123
+ if (securityConfig && securityConfig.ed25519_public_key) {
3124
+ process.env.ED25519_PUBLIC_KEY = securityConfig.ed25519_public_key;
3125
+ }
3126
+
3127
+ let finalConfig = securityConfig;
3128
+ if (securityConfig && securityConfig.autotuning && securityConfig.autotuning.savePath) {
3129
+ const sPath = securityConfig.autotuning.savePath;
3130
+ if (existsSync(sPath)) {
3131
+ try {
3132
+ const savedConfig = JSON.parse(readFileSync(sPath, 'utf-8'));
3133
+ finalConfig = deepMerge(securityConfig, savedConfig);
3134
+ } catch (e) {
3135
+ console.warn(`[Fingerprint] Failed to auto-load optimized config from ${sPath}:`, e.message);
3136
+ }
3137
+ }
3138
+ }
3139
+ this.securityConfig = finalConfig;
2888
3140
  this.isProduction = isProduction;
2889
3141
  this._allowlist = this._buildAllowlist();
2890
- this._validateConfig(securityConfig); // Validate the configuration
2891
- this.verbose = securityConfig.verbose || false;
2892
- this.dryRun = securityConfig.dryRun || false;
3142
+ this._validateConfig(finalConfig); // Validate the configuration
3143
+ this.verbose = finalConfig.verbose || false;
3144
+ this.dryRun = finalConfig.dryRun || false;
2893
3145
  }
2894
3146
 
2895
3147
  /**
@@ -2910,7 +3162,8 @@ export class FingerprintEngine {
2910
3162
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
2911
3163
  'trustedProxies',
2912
3164
  'wasm',
2913
- 'similarityThreshold'
3165
+ 'similarityThreshold',
3166
+ 'ed25519_private_key', 'ed25519_public_key'
2914
3167
  ]);
2915
3168
 
2916
3169
  // 1. Check for essential keys
@@ -3213,8 +3466,18 @@ export class FingerprintEngine {
3213
3466
  async processRequest(requestContext) {
3214
3467
  sanitizeProxyHeaders(requestContext, this.securityConfig);
3215
3468
 
3216
- const { clientIp = "unknown", path, cookies, query, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3217
- const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
3469
+ const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3470
+
3471
+ if (query.coop_op) {
3472
+ const result = await handleCooperativeRequest(query, clientIp);
3473
+ return {
3474
+ action: 'challenge',
3475
+ status: 200,
3476
+ body: result
3477
+ };
3478
+ }
3479
+
3480
+ const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
3218
3481
 
3219
3482
  this._log('Processing request', { clientIp, path, isStatic });
3220
3483
 
@@ -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 = await 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: currentDeviceHash
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
  }
@@ -3673,7 +3980,8 @@ export class FingerprintEngine {
3673
3980
  // 1. La requête est suspecte ET il n'y a pas de ticket valide.
3674
3981
  // OU
3675
3982
  // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
3676
- const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming);
3983
+ const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
3984
+ const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
3677
3985
  const mustReChallenge = isSuspiciousHigh && hasValidTicket;
3678
3986
 
3679
3987
  if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
@@ -3698,6 +4006,12 @@ export class FingerprintEngine {
3698
4006
  decision.action = 'next';
3699
4007
  delete decision.status;
3700
4008
  delete decision.body;
4009
+ if (requestContext._newCookies) {
4010
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
4011
+ if (deviceCookie) {
4012
+ decision.newCookieForResponse = deviceCookie;
4013
+ }
4014
+ }
3701
4015
  }
3702
4016
  return decision;
3703
4017
  }
@@ -3707,6 +4021,7 @@ export class FingerprintEngine {
3707
4021
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
3708
4022
  const nonce = crypto.randomBytes(16).toString("hex");
3709
4023
  const clientSecret = crypto.randomBytes(16).toString("hex");
4024
+ const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
3710
4025
 
3711
4026
  // Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
3712
4027
  // Cela rend l'automatisation plus difficile pour un attaquant.
@@ -3749,7 +4064,6 @@ export class FingerprintEngine {
3749
4064
  }
3750
4065
 
3751
4066
  if (isSuspicious && usefulWorkDispatched) {
3752
- const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
3753
4067
  if (isApi) {
3754
4068
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
3755
4069
  } else {
@@ -3767,6 +4081,33 @@ export class FingerprintEngine {
3767
4081
  delete decision.status;
3768
4082
  return decision;
3769
4083
  }
4084
+ if (this.securityConfig.enableProofOfSpace) {
4085
+ const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4086
+ const clientSecret = crypto.randomBytes(16).toString("hex");
4087
+ await store.set(`secret:${nonce}`, {
4088
+ clientSecret,
4089
+ suspicionScore: finalScore,
4090
+ queries: spaceChallenge.queries,
4091
+ sizeMb: spaceChallenge.sizeMb,
4092
+ originalPath: path,
4093
+ }, this.securityConfig.challengeTtl || 300);
4094
+
4095
+ if (isApi) {
4096
+ decision.body = {
4097
+ challenge: {
4098
+ type: 'pospace',
4099
+ nonce: nonce,
4100
+ clientSecret,
4101
+ queries: spaceChallenge.queries,
4102
+ sizeMb: spaceChallenge.sizeMb,
4103
+ }
4104
+ };
4105
+ } else {
4106
+ const page = generateSpaceChallengePage(spaceChallenge, clientSecret, this.securityConfig);
4107
+ decision.body = page;
4108
+ }
4109
+ return decision;
4110
+ }
3770
4111
  // Generate some trap URLs to embed in the challenge page.
3771
4112
  // These links are visually hidden but present in the DOM to trap bots.
3772
4113
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce)); // Génère les URL
@@ -3825,9 +4166,6 @@ export class FingerprintEngine {
3825
4166
  logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
3826
4167
  }
3827
4168
 
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
4169
  if (isApi) {
3832
4170
  // For API clients, send a JSON response with challenge details.
3833
4171
  const challengePayload = {
@@ -3861,8 +4199,14 @@ export class FingerprintEngine {
3861
4199
  if (logger) {
3862
4200
  logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
3863
4201
  }
3864
-
3865
- return { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
4202
+ const response = { action: 'next', score: finalScore, vector: suspicionVector, intendedAction: 'next' };
4203
+ if (requestContext._newCookies) {
4204
+ const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
4205
+ if (deviceCookie) {
4206
+ response.newCookieForResponse = deviceCookie;
4207
+ }
4208
+ }
4209
+ return response;
3866
4210
  }
3867
4211
 
3868
4212
  /**
@@ -3919,6 +4263,117 @@ const staticExtensions = new RegExp(
3919
4263
  );
3920
4264
  const isStaticResource = (path) => staticExtensions.test(path);
3921
4265
 
4266
+ export async function registerCooperativeNode(clientIp, nodeId, seed) {
4267
+ const subnet = getIpSubnet(clientIp);
4268
+ if (!subnet) return;
4269
+
4270
+ const key = `coop-pospace:subnet:${subnet}`;
4271
+ const nodes = (await store.get(key)) || {};
4272
+ const now = Math.floor(Date.now() / 1000);
4273
+
4274
+ // Clean up expired nodes (older than 120 seconds)
4275
+ const cleanedNodes = {};
4276
+ for (const [id, node] of Object.entries(nodes)) {
4277
+ if (now - node.timestamp < 120) {
4278
+ cleanedNodes[id] = node;
4279
+ }
4280
+ }
4281
+
4282
+ cleanedNodes[nodeId] = {
4283
+ nodeId,
4284
+ seed,
4285
+ timestamp: now
4286
+ };
4287
+
4288
+ await store.set(key, cleanedNodes, 120);
4289
+ }
4290
+
4291
+ export async function findPeerInSubnet(clientIp, excludeNodeId) {
4292
+ const subnet = getIpSubnet(clientIp);
4293
+ if (!subnet) return null;
4294
+
4295
+ const key = `coop-pospace:subnet:${subnet}`;
4296
+ const nodes = (await store.get(key)) || {};
4297
+ const now = Math.floor(Date.now() / 1000);
4298
+
4299
+ const activePeers = [];
4300
+ for (const [id, node] of Object.entries(nodes)) {
4301
+ if (id !== excludeNodeId && now - node.timestamp < 120) {
4302
+ activePeers.push(node);
4303
+ }
4304
+ }
4305
+
4306
+ if (activePeers.length === 0) return null;
4307
+
4308
+ // Select a random peer
4309
+ const randomIndex = Math.floor(Math.random() * activePeers.length);
4310
+ return activePeers[randomIndex];
4311
+ }
4312
+
4313
+ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1') {
4314
+ const op = params.coop_op;
4315
+ if (!op) return null;
4316
+
4317
+ const nodeId = params.node_id || '';
4318
+ if (!nodeId) {
4319
+ return { error: 'Missing node_id' };
4320
+ }
4321
+
4322
+ switch (op) {
4323
+ case 'register':
4324
+ const seed = params.seed || '';
4325
+ await registerCooperativeNode(clientIp, nodeId, seed);
4326
+ return { status: 'registered' };
4327
+
4328
+ case 'request_peer_block':
4329
+ const peerId = params.peer_id || '';
4330
+ const blockIdx = parseInt(params.block_idx || '0', 10);
4331
+ const requestId = params.req_id || '';
4332
+ if (!peerId || !requestId) {
4333
+ return { error: 'Invalid parameters' };
4334
+ }
4335
+
4336
+ const queueKey = `coop-mailbox:queue:${peerId}`;
4337
+ const requests = (await store.get(queueKey)) || [];
4338
+ requests.push({
4339
+ req_id: requestId,
4340
+ requester_id: nodeId,
4341
+ block_idx: blockIdx
4342
+ });
4343
+ await store.set(queueKey, requests, 30);
4344
+ return { status: 'queued' };
4345
+
4346
+ case 'poll_requests':
4347
+ const pollQueueKey = `coop-mailbox:queue:${nodeId}`;
4348
+ const polledRequests = (await store.get(pollQueueKey)) || [];
4349
+ await store.delete(pollQueueKey);
4350
+ return { requests: polledRequests };
4351
+
4352
+ case 'respond_block':
4353
+ const requesterId = params.requester_id || '';
4354
+ const respondRequestId = params.req_id || '';
4355
+ const blockData = params.block_data || '';
4356
+ if (!requesterId || !respondRequestId) {
4357
+ return { error: 'Invalid parameters' };
4358
+ }
4359
+
4360
+ const responseKey = `coop-mailbox:res:${requesterId}:${respondRequestId}`;
4361
+ await store.set(responseKey, { block_data: blockData }, 30);
4362
+ return { status: 'delivered' };
4363
+
4364
+ case 'poll_response':
4365
+ const pollResponseRequestId = params.req_id || '';
4366
+ const pollResponseKey = `coop-mailbox:res:${nodeId}:${pollResponseRequestId}`;
4367
+ const data = await store.get(pollResponseKey);
4368
+ if (data) {
4369
+ await store.delete(pollResponseKey);
4370
+ return { status: 'ready', block_data: data.block_data };
4371
+ }
4372
+ return { status: 'pending' };
4373
+ }
4374
+ return null;
4375
+ }
4376
+
3922
4377
 
3923
4378
  /** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
3924
4379
  let optimizedTtlCache = new Map();
@@ -4287,7 +4742,7 @@ function getTcpAnomalyScore(context) {
4287
4742
  * @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
4288
4743
  * @returns {boolean} - True si un pattern malveillant est détecté.
4289
4744
  */
4290
- function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
4745
+ function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
4291
4746
  if (typeof str !== 'string') return false;
4292
4747
 
4293
4748
  for (const type of typesToDetect) {
@@ -4489,6 +4944,32 @@ export const default_whitelist = () => [
4489
4944
  ];
4490
4945
 
4491
4946
 
4947
+ /**
4948
+ * Extracts the TLS Session ID or ticket hash from the request context.
4949
+ * Prioritizes proxy-provided headers and falls back to Node's native socket session.
4950
+ * @private
4951
+ * @param {object} context - The request context.
4952
+ * @returns {string|null}
4953
+ */
4954
+ function getTlsSessionId(context) {
4955
+ if (!context) return null;
4956
+ const fromHeader = context.headers ? (context.headers['x-tls-session-id'] || context.headers['x-ssl-session-id']) : null;
4957
+ if (fromHeader) return fromHeader;
4958
+
4959
+ const socket = context.rawReq?.socket;
4960
+ if (socket) {
4961
+ if (socket.sessionId) {
4962
+ return socket.sessionId.toString('hex');
4963
+ }
4964
+ if (typeof socket.getSession === 'function') {
4965
+ const session = socket.getSession();
4966
+ if (session) {
4967
+ return crypto.createHash('sha256').update(session).digest('hex');
4968
+ }
4969
+ }
4970
+ }
4971
+ return null;
4972
+ }
4492
4973
 
4493
4974
  // --- Proof-of-Work Middleware (The Tollbooth) ---
4494
4975
  export const powMiddleware = (securityConfig) => {
@@ -4521,6 +5002,24 @@ export const powMiddleware = (securityConfig) => {
4521
5002
  }
4522
5003
 
4523
5004
  return async (req, res, next) => {
5005
+ if (!req.headers_translated) {
5006
+ req.headers_translated = true;
5007
+ const matchedMapping = getActiveMappingForRequest(req.headers);
5008
+ if (matchedMapping) {
5009
+ const devFpHeader = matchedMapping.headers['x-device-fingerprint'].toLowerCase();
5010
+ const behaviorHeader = matchedMapping.headers['x-behavior-metrics'].toLowerCase();
5011
+
5012
+ if (req.headers[devFpHeader]) {
5013
+ req.headers['x-device-fingerprint'] = req.headers[devFpHeader];
5014
+ }
5015
+ if (req.headers[behaviorHeader]) {
5016
+ req.headers['x-behavior-metrics'] = req.headers[behaviorHeader];
5017
+ }
5018
+ if (req.headers['x-device-fingerprint']) {
5019
+ req.headers['x-device-fingerprint'] = decodePolymorphicFingerprint(req.headers['x-device-fingerprint'], matchedMapping);
5020
+ }
5021
+ }
5022
+ }
4524
5023
  if (securityConfig?.wasm) {
4525
5024
  const wasmConfig = securityConfig.wasm;
4526
5025
  let jsPath = '/fp.js';
@@ -4542,45 +5041,40 @@ export const powMiddleware = (securityConfig) => {
4542
5041
  wasmFile = wasmConfig.wasmFile ? resolve(wasmConfig.wasmFile) : resolve(__dirname, '..', '..', 'public', 'fp.wasm');
4543
5042
  }
4544
5043
 
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
5044
+ if (req.path === jsPath) {
5045
+ try {
5046
+ if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
5047
+ await ensureLatestMapping();
5048
+ const latest = activeMappings[0];
5049
+ if (latest && latest.jsBuffer) {
5050
+ res.setHeader('Content-Type', 'application/javascript');
5051
+ return res.send(latest.jsBuffer);
5052
+ }
5053
+ }
5054
+ if (jsFile && existsSync(jsFile)) {
5055
+ const fileContent = readFileSync(jsFile);
5056
+ res.setHeader('Content-Type', 'application/javascript');
5057
+ return res.send(fileContent);
5058
+ }
5059
+ } catch (e) {}
4563
5060
  }
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
5061
+ if (req.path === wasmPath) {
5062
+ try {
5063
+ if (wasmConfig === 'dynamic' || wasmConfig.dynamic || wasmConfig.polymorphic) {
5064
+ await ensureLatestMapping();
5065
+ const latest = activeMappings[0];
5066
+ if (latest && latest.wasmBuffer) {
5067
+ res.setHeader('Content-Type', 'application/wasm');
5068
+ return res.send(latest.wasmBuffer);
5069
+ }
5070
+ }
5071
+ if (wasmFile && existsSync(wasmFile)) {
5072
+ const fileContent = readFileSync(wasmFile);
5073
+ res.setHeader('Content-Type', 'application/wasm');
5074
+ return res.send(fileContent);
5075
+ }
5076
+ } catch (e) {}
4582
5077
  }
4583
- }
4584
5078
  }
4585
5079
 
4586
5080
  const requestContext = {
@@ -4657,6 +5151,8 @@ export const __internal = {
4657
5151
  getDeviceHash,
4658
5152
  getCompositeDeviceHash,
4659
5153
  getSuspicionVector,
5154
+ getTlsSessionId,
5155
+ pruneTrafficData,
4660
5156
  cyrb53, // Export for testing
4661
5157
  FingerprintBuilder, // Export for testing
4662
5158
  calculateTarget,
@@ -4685,9 +5181,14 @@ export const __internal = {
4685
5181
  getIpReputationScore, // Expose for testing
4686
5182
  updateIpReputationScore, // Expose for testing
4687
5183
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5184
+ verifyZkpProof,
5185
+ modPow,
4688
5186
  parseTcpSyn, // Expose for testing
4689
5187
  classifyTcpOs, // Expose for testing
4690
- getTcpAnomalyScore // Expose for testing
5188
+ getTcpAnomalyScore, // Expose for testing,
5189
+ registerCooperativeNode,
5190
+ findPeerInSubnet,
5191
+ handleCooperativeRequest
4691
5192
  };
4692
5193
 
4693
5194
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -4749,13 +5250,55 @@ export function sanitizeTrafficData(trafficData) {
4749
5250
 
4750
5251
  return [...suspiciousLogs, ...selectedPassed];
4751
5252
  }
5253
+ /**
5254
+ * Assainit et limite la taille/ancienneté des données de trafic pour éviter les fuites de mémoire.
5255
+ * @private
5256
+ */
5257
+ function pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup) {
5258
+ if (!Array.isArray(trafficData)) return;
5259
+ const now = Date.now();
5260
+ const removed = [];
5261
+
5262
+ // 1. Politique temporelle d'expiration
5263
+ if (maxAgeMs && maxAgeMs > 0) {
5264
+ const threshold = now - maxAgeMs;
5265
+ let i = 0;
5266
+ while (i < trafficData.length) {
5267
+ const log = trafficData[i];
5268
+ const logTs = log.timestamp || log.requestTimestamp || now;
5269
+ if (logTs < threshold) {
5270
+ removed.push(trafficData.splice(i, 1)[0]);
5271
+ } else {
5272
+ i++;
5273
+ }
5274
+ }
5275
+ }
4752
5276
 
5277
+ // 2. Politique de taille maximale (conserver les plus récents)
5278
+ if (maxDataPoints && maxDataPoints > 0 && trafficData.length > maxDataPoints) {
5279
+ const overflowCount = trafficData.length - maxDataPoints;
5280
+ const spliced = trafficData.splice(0, overflowCount);
5281
+ removed.push(...spliced);
5282
+ }
5283
+
5284
+ // 3. Callback de nettoyage
5285
+ if (onCleanup && typeof onCleanup === 'function' && removed.length > 0) {
5286
+ try {
5287
+ onCleanup(removed);
5288
+ } catch (e) {
5289
+ console.error('[AutoTuning] Error in onCleanup callback:', e);
5290
+ }
5291
+ }
5292
+ }
4753
5293
  /**
4754
5294
  * Executes a threshold optimization pass using collected traffic data.
4755
5295
  * @private
4756
5296
  */
4757
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
4758
- const sanitizedData = sanitizeTrafficData(trafficData);
5297
+ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, tuningOptions = {}) {
5298
+ const { maxAgeMs, clearAfterTuning = false, onCleanup } = tuningOptions;
5299
+
5300
+ pruneTrafficData(trafficData, maxDataPoints, maxAgeMs, onCleanup);
5301
+ const sanitizedData = sanitizeTrafficData(trafficData);
4759
5302
 
4760
5303
  const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
4761
5304
  const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
@@ -4772,12 +5315,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
4772
5315
  }
4773
5316
  return;
4774
5317
  }
4775
-
4776
- if (trafficData.length > maxDataPoints) {
4777
- console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
4778
- trafficData.splice(0, trafficData.length - maxDataPoints);
4779
- }
4780
-
4781
5318
  console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
4782
5319
 
4783
5320
  const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
@@ -4912,6 +5449,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
4912
5449
  console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
4913
5450
  }
4914
5451
  }
5452
+
5453
+ if (clearAfterTuning) {
5454
+ const cleared = trafficData.splice(0, trafficData.length);
5455
+ if (onCleanup && typeof onCleanup === 'function' && cleared.length > 0) {
5456
+ try {
5457
+ onCleanup(cleared);
5458
+ } catch (e) {
5459
+ console.error('[AutoTuning] Error in onCleanup callback after clearing:', e);
5460
+ }
5461
+ }
5462
+ console.log(`[AutoTuning] Explicitly cleared ${cleared.length} processed traffic data points.`);
5463
+ }
4915
5464
  }
4916
5465
 
4917
5466
  /**
@@ -4938,6 +5487,9 @@ export function startThresholdAutoTuning(options) {
4938
5487
  minDataPoints = 200,
4939
5488
  maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
4940
5489
  savePath, // NOUVEAU: Chemin de sauvegarde optionnel
5490
+ maxAgeMs,
5491
+ clearAfterTuning = false,
5492
+ onCleanup,
4941
5493
  } = options;
4942
5494
 
4943
5495
  if (!securityConfig || !trafficData) {
@@ -4947,7 +5499,7 @@ export function startThresholdAutoTuning(options) {
4947
5499
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
4948
5500
 
4949
5501
  autoTuningJobId = setInterval(() => {
4950
- runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
5502
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
4951
5503
  }, interval);
4952
5504
  }
4953
5505