@anonympins/fingerprint 0.4.4 → 0.4.5

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";
@@ -15,6 +16,56 @@ export { createRedisStore } from "./redis-store.js";
15
16
  export { createMongoDbStore } from "./mongodb-store.js";
16
17
 
17
18
 
19
+ const base64UrlEncode = (buf) => buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
20
+ const base64UrlDecode = (str) => {
21
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
22
+ while (base64.length % 4) {
23
+ base64 += '=';
24
+ }
25
+ return Buffer.from(base64, 'base64');
26
+ };
27
+
28
+ export function generateStatelessTicket(payload) {
29
+ const secret = getPowSecret();
30
+ const key = crypto.createHash('sha256').update(secret).digest();
31
+ const iv = crypto.randomBytes(16);
32
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
33
+ let encrypted = cipher.update(JSON.stringify(payload));
34
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
35
+
36
+ const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
37
+ return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
38
+ }
39
+
40
+ export function parseStatelessTicket(ticket) {
41
+ try {
42
+ const parts = ticket.split('.');
43
+ if (parts.length !== 3) return null;
44
+
45
+ const iv = base64UrlDecode(parts[0]);
46
+ const encrypted = base64UrlDecode(parts[1]);
47
+ const signature = base64UrlDecode(parts[2]);
48
+
49
+ if (iv.length !== 16) return null;
50
+
51
+ const secret = getPowSecret();
52
+ const key = crypto.createHash('sha256').update(secret).digest();
53
+
54
+ const expectedSignature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
55
+ if (signature.length !== expectedSignature.length || !crypto.timingSafeEqual(signature, expectedSignature)) {
56
+ return null;
57
+ }
58
+
59
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
60
+ let decrypted = decipher.update(encrypted);
61
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
62
+
63
+ return JSON.parse(decrypted.toString('utf8'));
64
+ } catch (e) {
65
+ return null;
66
+ }
67
+ }
68
+
18
69
  /**
19
70
  * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
20
71
  * @param {string} clientIp - L'adresse IP du client.
@@ -493,6 +544,7 @@ function getCompositeDeviceHash(context) {
493
544
  "ch_model": "sec-ch-ua-model",
494
545
  "ch_arch": "sec-ch-ua-arch",
495
546
  "ch_bitness": "sec-ch-ua-bitness",
547
+ "ch_full_version_list": "sec-ch-ua-full-version-list",
496
548
  "upgrade_req": "upgrade-insecure-requests",
497
549
  "accept_lang": "accept-language",
498
550
  "accept_enc": "accept-encoding",
@@ -835,18 +887,16 @@ export const verifyPoWAndGenerateTicket = async (
835
887
  return null;
836
888
  }
837
889
 
838
- // 2. Generate an opaque ticket ID and store session metadata securely on the server
839
- const ticketId = crypto.randomUUID();
890
+ // 2. Generate a signed and encrypted stateless ticket
840
891
  const expiry = Date.now() + 3600000; // 1 hour
841
-
842
- await store.set(`ticket:${ticketId}`, {
892
+ const payload = {
843
893
  expiry,
844
894
  originalIp: ip,
845
895
  deviceId,
846
896
  deviceHash
847
- }, 3600); // 1 hour TTL
897
+ };
848
898
 
849
- return ticketId;
899
+ return generateStatelessTicket(payload);
850
900
  };
851
901
 
852
902
 
@@ -902,7 +952,26 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
902
952
  // Input validation: ensure the ticket is a non-empty string with the correct format.
903
953
  if (typeof ticket !== 'string' || ticket.length === 0) return false;
904
954
 
905
- // 1. Resolve opaque ticket session from server-side store
955
+ // 1. Resolve stateless ticket first (zero database I/O cost)
956
+ const statelessData = parseStatelessTicket(ticket);
957
+ if (statelessData) {
958
+ const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = statelessData;
959
+
960
+ if (!expiry || Date.now() > expiry) {
961
+ return false;
962
+ }
963
+
964
+ if (ip === originalIp) return true;
965
+ const currentSubnet = getIpSubnet(ip);
966
+ const originalSubnet = getIpSubnet(originalIp);
967
+ if (currentSubnet && originalSubnet && currentSubnet === originalSubnet) return true;
968
+
969
+ if (!allowCrossNetworkRoaming) return false;
970
+
971
+ return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
972
+ }
973
+
974
+ // 2. Resolve opaque ticket session from server-side store
906
975
  const ticketData = await store.get(`ticket:${ticket}`);
907
976
  if (ticketData) {
908
977
  const { expiry, originalIp, deviceId: storedDeviceId, deviceHash: storedDeviceHash } = ticketData;
@@ -922,7 +991,7 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
922
991
  return !!(deviceId && deviceId === storedDeviceId && deviceHash && deviceHash === storedDeviceHash);
923
992
  }
924
993
 
925
- // 2. Legacy fallback verification (backward compatibility for old client tokens)
994
+ // 3. Legacy fallback verification (backward compatibility for old client tokens)
926
995
  let expiry, originalIp, sig;
927
996
  if (ticket.includes('|')) {
928
997
  const parts = ticket.split('|');
@@ -1647,6 +1716,48 @@ function getClientHintsInconsistencyScore(context) {
1647
1716
  return { clientHintsInconsistencyScore: 0 };
1648
1717
  }
1649
1718
 
1719
+ const fullVersionList = context.headers['sec-ch-ua-full-version-list'];
1720
+ if (fullVersionList) {
1721
+ let chFullVersion = null;
1722
+ let chFullBrowser = null;
1723
+ const matches = [...fullVersionList.matchAll(/"([^"]+)";v="([^"]+)"/g)];
1724
+ for (const match of matches) {
1725
+ const brand = match[1];
1726
+ const version = match[2];
1727
+ if (brand === 'Google Chrome' || brand === 'Chromium' || brand === 'Microsoft Edge') {
1728
+ chFullVersion = version;
1729
+ chFullBrowser = brand === 'Microsoft Edge' ? 'Edge' : 'Chrome';
1730
+ if (brand === 'Google Chrome' || brand === 'Microsoft Edge') {
1731
+ break;
1732
+ }
1733
+ }
1734
+ }
1735
+ if (chFullVersion && chFullBrowser) {
1736
+ let uaFullVersion = null;
1737
+ const uaFullMatch = ua.match(/(Chrome|Edg)\/([\d\.]+)/);
1738
+ if (uaFullMatch) {
1739
+ const uaBrowserMapped = uaFullMatch[1] === 'Edg' ? 'Edge' : 'Chrome';
1740
+ uaFullVersion = uaFullMatch[2];
1741
+ if (uaBrowserMapped === chFullBrowser && uaFullVersion !== chFullVersion) {
1742
+ const parts1 = uaFullVersion.split('.').map(Number);
1743
+ const parts2 = chFullVersion.split('.').map(Number);
1744
+ let diffIndex = -1;
1745
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1746
+ if ((parts1[i] || 0) !== (parts2[i] || 0)) {
1747
+ diffIndex = i;
1748
+ break;
1749
+ }
1750
+ }
1751
+ const baseScores = [95, 90, 85, 80];
1752
+ const baseScore = baseScores[diffIndex] || 80;
1753
+ const delta = Math.abs((parts1[diffIndex] || 0) - (parts2[diffIndex] || 0));
1754
+ const finalFullScore = Math.min(100, baseScore + Math.min(5, delta * 5));
1755
+ return { clientHintsInconsistencyScore: finalFullScore };
1756
+ }
1757
+ }
1758
+ }
1759
+ }
1760
+
1650
1761
  // 1. Extract browser and version from User-Agent
1651
1762
  let uaVersion = null;
1652
1763
  let uaBrowser = null;
@@ -1681,10 +1792,19 @@ function getClientHintsInconsistencyScore(context) {
1681
1792
 
1682
1793
  const versionDifference = Math.abs(parseInt(uaVersion, 10) - parseInt(chVersion, 10));
1683
1794
 
1684
- if (versionDifference > 5) return { clientHintsInconsistencyScore: 80 };
1685
- if (versionDifference > 1) return { clientHintsInconsistencyScore: 40 };
1795
+ let clientHintsInconsistencyScore = 0;
1796
+ if (versionDifference > 0) {
1797
+ if (versionDifference <= 2) {
1798
+ clientHintsInconsistencyScore = versionDifference * 20;
1799
+ } else if (versionDifference <= 7) {
1800
+ clientHintsInconsistencyScore = 40 + (versionDifference - 2) * 8;
1801
+ } else {
1802
+ clientHintsInconsistencyScore = Math.min(100, 80 + (versionDifference - 7) * 3.33);
1803
+ }
1804
+ clientHintsInconsistencyScore = Math.round(clientHintsInconsistencyScore);
1805
+ }
1686
1806
 
1687
- return { clientHintsInconsistencyScore: 0 };
1807
+ return { clientHintsInconsistencyScore };
1688
1808
  }
1689
1809
 
1690
1810
  /**
@@ -2416,6 +2536,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
2416
2536
  const stableFpHash = cyrb53(stableFp).toString();
2417
2537
  const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
2418
2538
 
2539
+ const { tcpAnomalyScore } = getTcpAnomalyScore(context);
2540
+
2419
2541
  // Save the updated device state to the store
2420
2542
  // 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
2543
  await store.set(`device:${deviceId}`, deviceData);
@@ -2426,7 +2548,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
2426
2548
  deviceData.ips = new Set(deviceData.ips);
2427
2549
  }
2428
2550
  // Le vecteur de suspicion est maintenant complet.
2429
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore };
2551
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
2430
2552
  };
2431
2553
 
2432
2554
  // A residential user can change networks (home, 4G, public wifi).
@@ -2722,19 +2844,15 @@ export async function verifyCpuTargetPoWAndGenerateTicket(
2722
2844
  if (isValid) {
2723
2845
  console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
2724
2846
  });
2725
- // Generate an opaque ticket ID and store session metadata securely on the server
2726
- const ticketId = crypto.randomUUID();
2727
2847
  const ttl = ticketTtl || 3600000; // Calculates expiration from TTL
2728
2848
  const expiry = Date.now() + ttl;
2729
2849
 
2730
- await store.set(`ticket:${ticketId}`, {
2850
+ return generateStatelessTicket({
2731
2851
  expiry,
2732
2852
  originalIp: clientIp,
2733
2853
  deviceId,
2734
2854
  deviceHash
2735
2855
  }, Math.ceil(ttl / 1000));
2736
-
2737
- return ticketId;
2738
2856
  }
2739
2857
 
2740
2858
  return null;
@@ -2836,7 +2954,8 @@ export class FingerprintEngine {
2836
2954
  (suspicionVector.clickVarianceScore || 0) * (weights.clickVarianceScore || 0) +
2837
2955
  (suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
2838
2956
  (suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
2839
- (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0);
2957
+ (suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
2958
+ (suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0);
2840
2959
 
2841
2960
  return Math.min(100, score);
2842
2961
  }
@@ -3982,6 +4101,185 @@ function determineOptimalTicketTtl(suspicionScore) {
3982
4101
  return ttl;
3983
4102
  }
3984
4103
 
4104
+
4105
+ /**
4106
+ * Parse une trame TCP SYN brute (IPv4 ou IPv6).
4107
+ * @private
4108
+ * @param {Buffer|Uint8Array} binary - Le paquet binaire.
4109
+ * @returns {object|null}
4110
+ */
4111
+ function parseTcpSyn(binary) {
4112
+ if (!binary || binary.length < 40) return null;
4113
+ let ttl = 64;
4114
+ let tcpOffset = 20;
4115
+ const version = binary[0] >> 4;
4116
+
4117
+ if (version === 4) {
4118
+ ttl = binary[8];
4119
+ const ihl = binary[0] & 0x0f;
4120
+ tcpOffset = ihl * 4;
4121
+ } else if (version === 6) {
4122
+ ttl = binary[7]; // Hop Limit
4123
+ tcpOffset = 40;
4124
+ } else {
4125
+ tcpOffset = 0;
4126
+ ttl = 64;
4127
+ }
4128
+
4129
+ if (binary.length < tcpOffset + 20) return null;
4130
+
4131
+ const windowSize = (binary[tcpOffset + 14] << 8) | binary[tcpOffset + 15];
4132
+ const dataOffset = (binary[tcpOffset + 12] >> 4) * 4;
4133
+ const optionsEnd = tcpOffset + dataOffset;
4134
+
4135
+ let mss = null;
4136
+ let ws = null;
4137
+ let sack = false;
4138
+
4139
+ let i = tcpOffset + 20;
4140
+ while (i < optionsEnd && i < binary.length) {
4141
+ const optType = binary[i];
4142
+ if (optType === 0) break;
4143
+ if (optType === 1) {
4144
+ i++;
4145
+ continue;
4146
+ }
4147
+ if (i + 1 >= binary.length) break;
4148
+ const optLen = binary[i + 1];
4149
+ if (optLen < 2 || i + optLen > binary.length) break;
4150
+
4151
+ if (optType === 2 && optLen === 4) {
4152
+ mss = (binary[i + 2] << 8) | binary[i + 3];
4153
+ } else if (optType === 3 && optLen === 3) {
4154
+ ws = binary[i + 2];
4155
+ } else if (optType === 4 && optLen === 2) {
4156
+ sack = true;
4157
+ }
4158
+ i += optLen;
4159
+ }
4160
+
4161
+ return { ttl, windowSize, mss, ws, sack };
4162
+ }
4163
+
4164
+ /**
4165
+ * Classifie l'OS à partir du fingerprint de la pile TCP/IP.
4166
+ * @private
4167
+ * @param {object|null} fingerprint
4168
+ * @returns {string}
4169
+ */
4170
+ function classifyTcpOs(fingerprint) {
4171
+ if (!fingerprint) return 'unknown';
4172
+ const ttl = fingerprint.ttl ?? 64;
4173
+ const windowSize = fingerprint.windowSize ?? 0;
4174
+ const ws = fingerprint.ws ?? null;
4175
+
4176
+ if (ttl > 64 && ttl <= 128) {
4177
+ return 'Windows';
4178
+ }
4179
+ if (ttl > 32 && ttl <= 64) {
4180
+ if (windowSize === 29200 || windowSize === 14600 || windowSize === 5840) {
4181
+ return 'Linux';
4182
+ }
4183
+ return 'Linux';
4184
+ }
4185
+ if (ttl <= 64) {
4186
+ if (windowSize === 65535 && (ws === 6 || ws === 8 || ws === 5)) {
4187
+ return 'macOS/iOS';
4188
+ }
4189
+ }
4190
+ if (ttl > 64) return 'Windows';
4191
+ if (ttl > 0) return 'Linux';
4192
+ return 'unknown';
4193
+ }
4194
+
4195
+ /**
4196
+ * Détecte les anomalies de pile réseau par rapport au User-Agent.
4197
+ * @private
4198
+ * @param {object} context - Le contexte de la requête.
4199
+ * @returns {{tcpAnomalyScore: number}}
4200
+ */
4201
+ function getTcpAnomalyScore(context) {
4202
+ let fp = null;
4203
+ const rawTcpBinary = context.headers?.['x-raw-tcp-binary'] || context.rawTcpBinary || null;
4204
+ if (rawTcpBinary) {
4205
+ const binary = Buffer.isBuffer(rawTcpBinary) ? rawTcpBinary : (typeof rawTcpBinary === 'string' ? Buffer.from(rawTcpBinary, 'hex') : rawTcpBinary);
4206
+ fp = parseTcpSyn(binary);
4207
+ }
4208
+
4209
+ if (!fp) {
4210
+ const tcpHeader = context.headers?.['x-tcp-fingerprint'] || context.tcpFingerprint || null;
4211
+ if (tcpHeader && typeof tcpHeader === 'string') {
4212
+ const parts = tcpHeader.split(':');
4213
+ if (parts.length >= 2) {
4214
+ fp = {
4215
+ ttl: parseInt(parts[0], 10),
4216
+ windowSize: parseInt(parts[1], 10),
4217
+ mss: parts[2] ? parseInt(parts[2], 10) : null,
4218
+ ws: parts[3] ? parseInt(parts[3], 10) : null,
4219
+ sack: parts[4] === '1' || parts[4] === 'true'
4220
+ };
4221
+ }
4222
+ }
4223
+ }
4224
+
4225
+ if (!fp) {
4226
+ return { tcpAnomalyScore: 0.0 };
4227
+ }
4228
+
4229
+ const tcpOs = classifyTcpOs(fp);
4230
+ const ua = context.headers?.['user-agent'] || '';
4231
+ const uaParts = parseUserAgent(ua);
4232
+ const uaOs = uaParts.os;
4233
+
4234
+ if (!uaOs || tcpOs === 'unknown') {
4235
+ return { tcpAnomalyScore: 0.0 };
4236
+ }
4237
+
4238
+ let mappedOs = null;
4239
+ if (uaOs.startsWith('Windows')) mappedOs = 'Windows';
4240
+ else if (uaOs.startsWith('Mac') || uaOs.startsWith('macOS')) mappedOs = 'macOS';
4241
+ else if (uaOs.startsWith('iOS')) mappedOs = 'iOS';
4242
+ else if (uaOs.startsWith('Linux')) mappedOs = 'Linux';
4243
+
4244
+ if (!mappedOs) {
4245
+ return { tcpAnomalyScore: 0.0 };
4246
+ }
4247
+
4248
+ const OS_EXPECTED_TCP = {
4249
+ 'Windows': { ttl: 128, windowSize: 64240, ws: 8, mss: 1460, sack: true },
4250
+ 'Linux': { ttl: 64, windowSize: 29200, ws: 7, mss: 1460, sack: true },
4251
+ 'macOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true },
4252
+ 'iOS': { ttl: 64, windowSize: 65535, ws: 6, mss: 1460, sack: true }
4253
+ };
4254
+
4255
+ const expected = OS_EXPECTED_TCP[mappedOs];
4256
+ const ttlDiff = Math.abs(fp.ttl - expected.ttl) / expected.ttl;
4257
+ const winDiff = Math.abs(fp.windowSize - expected.windowSize) / expected.windowSize;
4258
+ const wsDiff = expected.ws !== null && fp.ws !== null ? Math.abs(fp.ws - expected.ws) / expected.ws : 0.0;
4259
+ const mssDiff = expected.mss !== null && fp.mss !== null ? Math.abs(fp.mss - expected.mss) / expected.mss : 0.0;
4260
+ const sackDiff = (fp.sack ?? true) === (expected.sack ?? true) ? 0.0 : 1.0;
4261
+
4262
+ const deviation = (
4263
+ Math.min(1.0, ttlDiff) * 0.50 +
4264
+ Math.min(1.0, winDiff) * 0.25 +
4265
+ Math.min(1.0, wsDiff) * 0.15 +
4266
+ Math.min(1.0, mssDiff) * 0.05 +
4267
+ sackDiff * 0.05
4268
+ );
4269
+
4270
+ let tcpAnomalyScore = 0.0;
4271
+ if (tcpOs !== mappedOs && tcpOs !== 'unknown') {
4272
+ const baseAnomaly = mappedOs === 'Windows' ? 80.0 :
4273
+ (mappedOs === 'macOS' || mappedOs === 'iOS' ? 85.0 : 75.0);
4274
+ tcpAnomalyScore = baseAnomaly + (deviation - 0.4) * 10.0;
4275
+ } else {
4276
+ tcpAnomalyScore = deviation * 40.0;
4277
+ }
4278
+
4279
+ tcpAnomalyScore = Math.max(0.0, Math.min(100.0, Math.round(tcpAnomalyScore * 10) / 10));
4280
+ return { tcpAnomalyScore };
4281
+ }
4282
+
3985
4283
  /**
3986
4284
  * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
3987
4285
  * @private
@@ -4246,8 +4544,19 @@ export const powMiddleware = (securityConfig) => {
4246
4544
 
4247
4545
  if (jsFile && req.path === jsPath) {
4248
4546
  try {
4249
- const fileContent = readFileSync(jsFile);
4250
- res.setHeader('Content-Type', 'application/javascript');
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');
4251
4560
  return res.send(fileContent);
4252
4561
  } catch (e) {
4253
4562
  // Fallback
@@ -4255,6 +4564,16 @@ export const powMiddleware = (securityConfig) => {
4255
4564
  }
4256
4565
  if (wasmFile && req.path === wasmPath) {
4257
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
+ }
4258
4577
  const fileContent = readFileSync(wasmFile);
4259
4578
  res.setHeader('Content-Type', 'application/wasm');
4260
4579
  return res.send(fileContent);
@@ -4352,6 +4671,8 @@ export const __internal = {
4352
4671
  getTlsFingerprint, // NOUVEAU: Expose pour les tests
4353
4672
  sanitizeTrafficData, // NOUVEAU: Expose pour l'auto-tuner/tests
4354
4673
  getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
4674
+ generateStatelessTicket,
4675
+ parseStatelessTicket,
4355
4676
  parseJa3,
4356
4677
  getBotnetClusterScore, // NOUVEAU: Expose pour les tests
4357
4678
  generateCpuTargetChallengePage,
@@ -4364,6 +4685,9 @@ export const __internal = {
4364
4685
  getIpReputationScore, // Expose for testing
4365
4686
  updateIpReputationScore, // Expose for testing
4366
4687
  setLastBestSolution: (val) => { lastBestSolution = val; }, // Expose to test auto-tuning metrics
4688
+ parseTcpSyn, // Expose for testing
4689
+ classifyTcpOs, // Expose for testing
4690
+ getTcpAnomalyScore // Expose for testing
4367
4691
  };
4368
4692
 
4369
4693
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DynamicWasmGenerator } from '../dynamic-wasm.js';
3
+
4
+ // Helper function to decode ULEB128 (for verification)
5
+ function decodeULEB128(bytes) {
6
+ let result = 0;
7
+ let shift = 0;
8
+ for (const byte of bytes) {
9
+ result |= (byte & 0x7f) << shift;
10
+ if (!(byte & 0x80)) {
11
+ break;
12
+ }
13
+ shift += 7;
14
+ }
15
+ return result;
16
+ }
17
+
18
+ describe('DynamicWasmGenerator', () => {
19
+ it('should generate a valid WASM module buffer', () => {
20
+ const constants = { seed: 123, multiplier: 33, adder: 7 };
21
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
22
+
23
+ // Basic check: ensure it's a Buffer and starts with WASM magic number
24
+ expect(wasmBuffer).toBeInstanceOf(Buffer);
25
+ expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
26
+ expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
27
+
28
+ // Attempt to compile and instantiate the module to ensure validity
29
+ let instance;
30
+ try {
31
+ const module = new WebAssembly.Module(wasmBuffer);
32
+ instance = new WebAssembly.Instance(module, {});
33
+ } catch (e) {
34
+ // If compilation/instantiation fails, it's an invalid WASM module
35
+ expect.fail(`Generated WASM module is invalid: ${e.message}`);
36
+ }
37
+
38
+ // Verify the exported hash function exists
39
+ expect(instance.exports.hash).toBeInstanceOf(Function);
40
+ expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
41
+ });
42
+
43
+ it('should produce a consistent hash result with the JS fallback', () => {
44
+ const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
45
+ const testString = "hello world";
46
+
47
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
48
+ const module = new WebAssembly.Module(wasmBuffer);
49
+ const instance = new WebAssembly.Instance(module, {});
50
+ const exports = instance.exports;
51
+ const memory = exports.memory;
52
+
53
+ // Write string to WASM memory
54
+ const encoder = new TextEncoder();
55
+ const bytes = encoder.encode(testString);
56
+ const view = new Uint8Array(memory.buffer);
57
+ view.set(bytes, 0); // Assuming hash function expects string at address 0
58
+
59
+ // Calculate hash using WASM
60
+ const wasmHash = exports.hash(0, bytes.length);
61
+
62
+ // Calculate hash using JS fallback
63
+ const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
64
+
65
+ // The WASM and JS implementations should yield the same result
66
+ expect(wasmHash).toBe(jsHash);
67
+ });
68
+
69
+ it('should generate different WASM modules for different constants', () => {
70
+ const constants1 = { seed: 1, multiplier: 2, adder: 3 };
71
+ const constants2 = { seed: 4, multiplier: 5, adder: 6 };
72
+
73
+ const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
74
+ const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
75
+
76
+ expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
77
+ });
78
+ });