@anonympins/fingerprint 0.4.6 → 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);
@@ -140,23 +142,7 @@ function getActiveMappingForRequest(headers) {
140
142
  return null;
141
143
  }
142
144
 
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
- }
145
+
160
146
 
161
147
  const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
162
148
  const base64UrlDecode = (str) => {
@@ -168,6 +154,31 @@ const base64UrlDecode = (str) => {
168
154
  };
169
155
 
170
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
+
171
182
  const secret = getPowSecret();
172
183
  const key = crypto.createHash('sha256').update(secret).digest();
173
184
  const iv = crypto.randomBytes(16);
@@ -181,6 +192,40 @@ export function generateStatelessTicket(payload) {
181
192
 
182
193
  export function parseStatelessTicket(ticket) {
183
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
+
184
229
  const parts = ticket.split('.');
185
230
  if (parts.length !== 3) return null;
186
231
 
@@ -239,26 +284,6 @@ async function checkChallengeRateLimit(clientIp) {
239
284
  return true;
240
285
  }
241
286
 
242
- /**
243
- * @private
244
- * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
245
- * @param {object} target - The target object.
246
- * @param {object} source - The source object.
247
- * @returns {object} The merged object.
248
- */
249
- function deepMerge(target, source) {
250
- const output = { ...target };
251
- if (target && typeof target === 'object' && source && typeof source === 'object') {
252
- Object.keys(source).forEach(key => {
253
- if (source[key] && typeof source[key] === 'object' && key in target) {
254
- output[key] = deepMerge(target[key], source[key]);
255
- } else {
256
- output[key] = source[key];
257
- }
258
- });
259
- }
260
- return output;
261
- }
262
287
 
263
288
  const securityProfiles = {
264
289
  /**
@@ -593,44 +618,6 @@ function getTlsFingerprint(context) {
593
618
  return { ja3, ja4 };
594
619
  }
595
620
 
596
- /**
597
- * Analyses a raw JA3 string.
598
- * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
599
- * @param {string} ja3String
600
- * @returns {object|null}
601
- */
602
- export function parseJa3(ja3String) {
603
- if (!ja3String || typeof ja3String !== 'string') {
604
- return null;
605
- }
606
- const parts = ja3String.split(',');
607
- if (parts.length !== 5) {
608
- return null;
609
- }
610
- return {
611
- tlsVersion: parseInt(parts[0], 10),
612
- ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
613
- extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
614
- curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
615
- points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
616
- };
617
- }
618
-
619
- /**
620
- * Creates a stable hash based on device characteristics, independent of the IP.
621
- * This is our "level 2 fingerprint".
622
- * @param {object} context - The request context.
623
- * @returns {string} A hash representing the device.
624
- */
625
- function getHeaderSignature(context) {
626
- if (!context.rawHeaders) return '';
627
- const headerKeys = [];
628
- for (let i = 0; i < context.rawHeaders.length; i += 2) {
629
- headerKeys.push(context.rawHeaders[i]);
630
- }
631
- return cyrb53(headerKeys.sort().join(','));
632
- }
633
-
634
621
  /**
635
622
  * Returns the client-side fingerprint if available, otherwise computes a server-side hash.
636
623
  * This aligns with the test's expectation for prioritization.
@@ -766,79 +753,6 @@ function hasGrease(values) {
766
753
  return values.some(val => GREASE_VALUES.includes(val));
767
754
  }
768
755
 
769
- // Fonctions utilitaires
770
- function parseUserAgent(ua) {
771
- // Parser basique du User-Agent
772
- const result = {};
773
-
774
- // Détection du navigateur
775
- if (ua.includes('Chrome') && !ua.includes('Edg')) {
776
- result.browser = 'Chrome';
777
- const match = ua.match(/Chrome\/(\d+)/);
778
- if (match) result.browser += `/${match[1]}`;
779
- } else if (ua.includes('Firefox')) {
780
- result.browser = 'Firefox';
781
- const match = ua.match(/Firefox\/(\d+)/);
782
- if (match) result.browser += `/${match[1]}`;
783
- } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
784
- result.browser = 'Safari';
785
- const match = ua.match(/Version\/(\d+)/);
786
- if (match) result.browser += `/${match[1]}`;
787
- } else if (ua.includes('Edg')) {
788
- result.browser = 'Edge';
789
- const match = ua.match(/Edg\/(\d+)/);
790
- if (match) result.browser += `/${match[1]}`;
791
- }
792
-
793
- // Détection de l'OS
794
- if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
795
- else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
796
- else if (ua.includes('Mac OS X')) result.os = 'macOS';
797
- else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
798
- else if (ua.includes('Android')) result.os = 'Android';
799
- else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
800
-
801
- // Détection du type d'appareil
802
- if (ua.includes('Mobile')) result.device = 'mobile';
803
- else if (ua.includes('Tablet')) result.device = 'tablet';
804
- else result.device = 'desktop';
805
-
806
- return result;
807
- }
808
-
809
- function normalizeReferer(referer) {
810
- try {
811
- const url = new URL(referer);
812
- return `${url.protocol}//${url.hostname}`;
813
- } catch {
814
- return referer;
815
- }
816
- }
817
-
818
- function isPrivateIp(ip) {
819
- // Vérifier si l'IP est privée
820
- const parts = ip.split('.');
821
- if (parts.length !== 4) return false;
822
- const first = parseInt(parts[0]);
823
- return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
824
- }
825
-
826
- function hashNetwork(ip, prefix = 24) {
827
- // Hash du réseau (masque /24 ou /16)
828
- const parts = ip.split('.');
829
- if (parts.length !== 4) return null;
830
- const maskBytes = prefix / 8;
831
- const network = parts.slice(0, maskBytes).join('.');
832
- // Hash simple
833
- let hash = 0;
834
- for (let i = 0; i < network.length; i++) {
835
- const char = network.charCodeAt(i);
836
- hash = ((hash << 5) - hash) + char;
837
- hash = hash & hash;
838
- }
839
- return hash.toString(16);
840
- }
841
-
842
756
  /**
843
757
  * Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
844
758
  * @param {string} nonce - Unique nonce for the challenge.
@@ -1091,19 +1005,30 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
1091
1005
  return finalHash === parseInt(solution, 10);
1092
1006
  };
1093
1007
 
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;
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;
1107
1032
  }
1108
1033
 
1109
1034
  function generateBlock(seed, blockIndex, blockSize = 1024) {
@@ -1116,26 +1041,31 @@ function generateBlock(seed, blockIndex, blockSize = 1024) {
1116
1041
  return block;
1117
1042
  }
1118
1043
 
1119
- export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false) => {
1044
+ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '', allowCrossNetworkRoaming = false, zkpProof = '') => {
1120
1045
  // Input validation: ensure the ticket is a non-empty string with the correct format.
1121
- if (typeof ticket !== 'string' || ticket.length === 0) return false;
1122
-
1046
+ if (typeof ticket !== 'string' || ticket.length === 0) return false;
1123
1047
  // 1. Resolve stateless ticket first (zero database I/O cost)
1124
1048
  const statelessData = parseStatelessTicket(ticket);
1125
1049
  if (statelessData) {
1126
1050
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
1127
-
1128
1051
  if (!expiry || Date.now() > expiry) {
1129
1052
  return false;
1130
1053
  }
1131
-
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
+ }
1132
1064
  if (ip === originalIp) return true;
1133
1065
  const currentSubnet = getIpSubnet(ip);
1134
1066
  const originalSubnet = getIpSubnet(originalIp);
1135
1067
  if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
1136
-
1137
1068
  if (!allowCrossNetworkRoaming) return false;
1138
-
1139
1069
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
1140
1070
  }
1141
1071
 
@@ -1148,6 +1078,16 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
1148
1078
  await store.delete(`ticket:${ticket}`);
1149
1079
  return false;
1150
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
+ }
1151
1091
 
1152
1092
  if (ip === originalIp) return true;
1153
1093
  const currentSubnet = getIpSubnet(ip);
@@ -1255,7 +1195,7 @@ function getHeaderAnomalies(context) {
1255
1195
  };
1256
1196
  }
1257
1197
 
1258
- export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
1198
+ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, originalUrl, securityConfig) {
1259
1199
  const sizeMb = securityConfig?.pospace?.sizeMb || 100;
1260
1200
  const numQueries = securityConfig?.pospace?.numQueries || 10;
1261
1201
 
@@ -1268,13 +1208,27 @@ export function generateSpaceChallenge(clientIp, nonce, suspicionFactor, origina
1268
1208
  }
1269
1209
  }
1270
1210
 
1271
- return {
1211
+ const challenge = {
1272
1212
  type: "pospace",
1273
1213
  nonce: nonce,
1274
1214
  sizeMb,
1275
1215
  queries,
1276
1216
  path: originalUrl
1277
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;
1278
1232
  }
1279
1233
 
1280
1234
  function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
@@ -1418,6 +1372,20 @@ const injectionPatterns = {
1418
1372
  traversal: /(\.\.\/|\.\.\\)/,
1419
1373
  // Remote Command Execution (RCE)
1420
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
1421
1389
  };
1422
1390
 
1423
1391
  /**
@@ -3147,12 +3115,33 @@ function parseGraphQLQuery(body) {
3147
3115
  export class FingerprintEngine {
3148
3116
  constructor(securityConfig) {
3149
3117
  const isProduction = process.env.NODE_ENV === 'production';
3150
- 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;
3151
3140
  this.isProduction = isProduction;
3152
3141
  this._allowlist = this._buildAllowlist();
3153
- this._validateConfig(securityConfig); // Validate the configuration
3154
- this.verbose = securityConfig.verbose || false;
3155
- this.dryRun = securityConfig.dryRun || false;
3142
+ this._validateConfig(finalConfig); // Validate the configuration
3143
+ this.verbose = finalConfig.verbose || false;
3144
+ this.dryRun = finalConfig.dryRun || false;
3156
3145
  }
3157
3146
 
3158
3147
  /**
@@ -3173,7 +3162,8 @@ export class FingerprintEngine {
3173
3162
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
3174
3163
  'trustedProxies',
3175
3164
  'wasm',
3176
- 'similarityThreshold'
3165
+ 'similarityThreshold',
3166
+ 'ed25519_private_key', 'ed25519_public_key'
3177
3167
  ]);
3178
3168
 
3179
3169
  // 1. Check for essential keys
@@ -3477,7 +3467,17 @@ export class FingerprintEngine {
3477
3467
  sanitizeProxyHeaders(requestContext, this.securityConfig);
3478
3468
 
3479
3469
  const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
3480
- const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
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;
3481
3481
 
3482
3482
  this._log('Processing request', { clientIp, path, isStatic });
3483
3483
 
@@ -3699,7 +3699,7 @@ export class FingerprintEngine {
3699
3699
  isValid
3700
3700
  });
3701
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);
3702
+ const isSpaceValid = await verifySpacePoW(pow_nonce, pow_solution_space, challengeContext.queries, pow_nonce + ":" + challengeContext.clientSecret, challengeContext.clientSecret);
3703
3703
  isValid = isSpaceValid;
3704
3704
  if (isValid) {
3705
3705
  const ttl = finalTtl || 3600000;
@@ -3707,7 +3707,7 @@ export class FingerprintEngine {
3707
3707
  expiry: Date.now() + ttl,
3708
3708
  originalIp: clientIp,
3709
3709
  deviceId,
3710
- deviceHash
3710
+ deviceHash: currentDeviceHash
3711
3711
  });
3712
3712
  }
3713
3713
  }
@@ -3980,7 +3980,8 @@ export class FingerprintEngine {
3980
3980
  // 1. La requête est suspecte ET il n'y a pas de ticket valide.
3981
3981
  // OU
3982
3982
  // 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
3983
- 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);
3984
3985
  const mustReChallenge = isSuspiciousHigh && hasValidTicket;
3985
3986
 
3986
3987
  if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
@@ -4081,7 +4082,7 @@ export class FingerprintEngine {
4081
4082
  return decision;
4082
4083
  }
4083
4084
  if (this.securityConfig.enableProofOfSpace) {
4084
- const spaceChallenge = generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4085
+ const spaceChallenge = await generateSpaceChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4085
4086
  const clientSecret = crypto.randomBytes(16).toString("hex");
4086
4087
  await store.set(`secret:${nonce}`, {
4087
4088
  clientSecret,
@@ -4262,6 +4263,117 @@ const staticExtensions = new RegExp(
4262
4263
  );
4263
4264
  const isStaticResource = (path) => staticExtensions.test(path);
4264
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
+
4265
4377
 
4266
4378
  /** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
4267
4379
  let optimizedTtlCache = new Map();
@@ -4630,7 +4742,7 @@ function getTcpAnomalyScore(context) {
4630
4742
  * @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
4631
4743
  * @returns {boolean} - True si un pattern malveillant est détecté.
4632
4744
  */
4633
- function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
4745
+ function isMalicious(str, typesToDetect = Object.keys(injectionPatterns).filter(k => k !== 'openRedirect')) {
4634
4746
  if (typeof str !== 'string') return false;
4635
4747
 
4636
4748
  for (const type of typesToDetect) {
@@ -5040,6 +5152,7 @@ export const __internal = {
5040
5152
  getCompositeDeviceHash,
5041
5153
  getSuspicionVector,
5042
5154
  getTlsSessionId,
5155
+ pruneTrafficData,
5043
5156
  cyrb53, // Export for testing
5044
5157
  FingerprintBuilder, // Export for testing
5045
5158
  calculateTarget,
@@ -5068,9 +5181,14 @@ export const __internal = {
5068
5181
  getIpReputationScore, // Expose for testing
5069
5182
  updateIpReputationScore, // Expose for testing
5070
5183
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
5184
+ verifyZkpProof,
5185
+ modPow,
5071
5186
  parseTcpSyn, // Expose for testing
5072
5187
  classifyTcpOs, // Expose for testing
5073
- getTcpAnomalyScore // Expose for testing
5188
+ getTcpAnomalyScore, // Expose for testing,
5189
+ registerCooperativeNode,
5190
+ findPeerInSubnet,
5191
+ handleCooperativeRequest
5074
5192
  };
5075
5193
 
5076
5194
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -5132,13 +5250,55 @@ export function sanitizeTrafficData(trafficData) {
5132
5250
 
5133
5251
  return [...suspiciousLogs, ...selectedPassed];
5134
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
+ }
5135
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
+ }
5136
5293
  /**
5137
5294
  * Executes a threshold optimization pass using collected traffic data.
5138
5295
  * @private
5139
5296
  */
5140
- function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath) {
5141
- 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);
5142
5302
 
5143
5303
  const highConfidenceLogs = sanitizedData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
5144
5304
  const highConfidenceRatio = sanitizedData.length > 0 ? highConfidenceLogs / sanitizedData.length : 0;
@@ -5155,12 +5315,6 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5155
5315
  }
5156
5316
  return;
5157
5317
  }
5158
-
5159
- if (trafficData.length > maxDataPoints) {
5160
- console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
5161
- trafficData.splice(0, trafficData.length - maxDataPoints);
5162
- }
5163
-
5164
5318
  console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${sanitizedData.length} points de données assainis.`);
5165
5319
 
5166
5320
  const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData: sanitizedData });
@@ -5295,6 +5449,18 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
5295
5449
  console.error(`[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : ${error.message}`);
5296
5450
  }
5297
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
+ }
5298
5464
  }
5299
5465
 
5300
5466
  /**
@@ -5321,6 +5487,9 @@ export function startThresholdAutoTuning(options) {
5321
5487
  minDataPoints = 200,
5322
5488
  maxDataPoints = 10000, // Limite par défaut à 10 000 entrées
5323
5489
  savePath, // NOUVEAU: Chemin de sauvegarde optionnel
5490
+ maxAgeMs,
5491
+ clearAfterTuning = false,
5492
+ onCleanup,
5324
5493
  } = options;
5325
5494
 
5326
5495
  if (!securityConfig || !trafficData) {
@@ -5330,7 +5499,7 @@ export function startThresholdAutoTuning(options) {
5330
5499
  console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
5331
5500
 
5332
5501
  autoTuningJobId = setInterval(() => {
5333
- runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath);
5502
+ runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints, savePath, { maxAgeMs, clearAfterTuning, onCleanup });
5334
5503
  }, interval);
5335
5504
  }
5336
5505