@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.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +158 -0
- package/src/js/fingerprint.client.js +21 -0
- package/src/js/fingerprint.js +344 -20
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +20 -1
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/Challenge/ChallengeUtils.php +77 -12
- package/src/php/Config/SecurityProfiles.php +10 -0
- package/src/php/FingerprintEngine.php +4 -0
- package/src/php/Tests/FingerprintEngineTest.php +27 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Utils/RequestUtils.php +274 -5
package/src/js/fingerprint.js
CHANGED
|
@@ -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
|
|
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
|
-
}
|
|
897
|
+
};
|
|
848
898
|
|
|
849
|
-
return
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4250
|
-
|
|
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
|
+
});
|