@anonympins/fingerprint 0.5.2 → 0.5.4

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +1 -1
  3. package/src/js/fingerprint.client.js +1271 -1191
  4. package/src/js/fingerprint.js +202 -108
  5. package/src/js/fingerprint.utils.js +213 -213
  6. package/src/js/library.js +24 -8
  7. package/src/js/mongodb-store.js +165 -79
  8. package/src/js/pow.solver.inline.js +500 -14
  9. package/src/js/pow.solver.js +323 -14
  10. package/src/js/problem-manager.js +1 -1
  11. package/src/js/redis-store.js +114 -42
  12. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  13. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  14. package/src/js/tests/fingerprint.client.test.js +128 -128
  15. package/src/js/tests/fingerprint.test.js +32 -2
  16. package/src/js/tests/ip-reputation.test.js +1 -1
  17. package/src/js/tests/pow.solver.test.js +5 -2
  18. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  19. package/src/php/Challenge/ChallengeUtils.php +21 -8
  20. package/src/php/FingerprintClient.php +22 -5
  21. package/src/php/FingerprintEngine.php +232 -56
  22. package/src/php/Optimization/Optimization.php +23 -16
  23. package/src/php/Optimization/OptimizationOperators.php +27 -0
  24. package/src/php/ProblemManager.php +1 -1
  25. package/src/php/Store/MongoDbStore.php +184 -104
  26. package/src/php/Store/RedisStore.php +91 -11
  27. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  28. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  29. package/src/php/Tests/IpReputationTest.php +1 -1
  30. package/src/php/Tests/PowTest.php +1 -1
  31. package/src/php/Tests/RequestUtilsTest.php +29 -0
  32. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  33. package/src/php/Utils/BigInt.php +202 -202
  34. package/src/php/Utils/RequestUtils.php +74 -21
  35. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -1,214 +1,214 @@
1
- import {cyrb53} from "./fingerprint.builder.js";
2
-
3
- export function verifyZkpProof(yStr, tStr, sStr) {
4
- try {
5
- const y = BigInt('0x' + yStr);
6
- const t = BigInt('0x' + tStr);
7
- const s = BigInt('0x' + sStr);
8
-
9
- const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
10
- const ZKP_G = 2n;
11
-
12
- const cStr = ZKP_G.toString() + y.toString() + t.toString();
13
- const hashHex = crypto.createHash('sha256').update(cStr).digest('hex');
14
- const c = BigInt('0x' + hashHex) % ZKP_P;
15
-
16
- return modPow(ZKP_G, s, ZKP_P) === (t * modPow(y, c, ZKP_P)) % ZKP_P;
17
- } catch (e) {
18
- return false;
19
- }
20
- }
21
-
22
- export function safeJsonStringify(val) {
23
- return JSON.stringify(val)
24
- .replace(/</g, '\\u003c')
25
- .replace(/>/g, '\\u003e')
26
- .replace(/\u2028/g, '\\u2028')
27
- .replace(/\u2029/g, '\\u2029');
28
- }
29
-
30
- export function sanitizeRedirectPath(p) {
31
- if (typeof p !== 'string') return '/';
32
- let sanitized = p.replace(/[^a-zA-Z0-9\/.\-_~%?&=:@+,;]/g, '');
33
-
34
- // Prevent protocol-relative redirects (e.g. //evil.com)
35
- if (sanitized.startsWith('//')) {
36
- sanitized = '/' + sanitized.replace(/^\/+/g, '');
37
- }
38
- // Prevent absolute redirects (e.g. http://evil.com)
39
- if (/^https?:\/\//i.test(sanitized)) {
40
- try {
41
- const parsed = new URL(sanitized);
42
- sanitized = parsed.pathname + parsed.search + parsed.hash;
43
- } catch (e) {
44
- sanitized = '/';
45
- }
46
- }
47
- if (!sanitized.startsWith('/')) {
48
- sanitized = '/' + sanitized;
49
- }
50
- return sanitized.replace(/^\/+/g, '/');
51
- }
52
- export function decodePolymorphicFingerprint(fpString, mapping) {
53
- if (!fpString || !mapping || !mapping.keys) return fpString;
54
- const reverseKeys = {};
55
- for (const [orig, rand] of Object.entries(mapping.keys)) {
56
- reverseKeys[rand] = orig;
57
- }
58
- const parts = fpString.split('|');
59
- const mappedParts = parts.map(part => {
60
- const pair = part.split(':');
61
- if (pair.length === 2) {
62
- const origKey = reverseKeys[pair[0]] || pair[0];
63
- return `${origKey}:${pair[1]}`;
64
- }
65
- return part;
66
- });
67
- return mappedParts.join('|');
68
- }
69
-
70
-
71
- /**
72
- * @private
73
- * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
74
- * @param {object} target - The target object.
75
- * @param {object} source - The source object.
76
- * @returns {object} The merged object.
77
- */
78
- export function deepMerge(target, source) {
79
- const output = { ...target };
80
- if (target && typeof target === 'object' && source && typeof source === 'object') {
81
- Object.keys(source).forEach(key => {
82
- if (source[key] && typeof source[key] === 'object' && key in target) {
83
- output[key] = deepMerge(target[key], source[key]);
84
- } else {
85
- output[key] = source[key];
86
- }
87
- });
88
- }
89
- return output;
90
- }
91
-
92
- /**
93
- * Creates a stable hash based on device characteristics, independent of the IP.
94
- * This is our "level 2 fingerprint".
95
- * @param {object} context - The request context.
96
- * @returns {string} A hash representing the device.
97
- */
98
- export function getHeaderSignature(context) {
99
- if (!context.rawHeaders) return '';
100
- const headerKeys = [];
101
- for (let i = 0; i < context.rawHeaders.length; i += 2) {
102
- headerKeys.push(context.rawHeaders[i]);
103
- }
104
- return cyrb53(headerKeys.sort().join(','));
105
- }
106
-
107
- /**
108
- * Analyses a raw JA3 string.
109
- * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
110
- * @param {string} ja3String
111
- * @returns {object|null}
112
- */
113
- export function parseJa3(ja3String) {
114
- if (!ja3String || typeof ja3String !== 'string') {
115
- return null;
116
- }
117
- const parts = ja3String.split(',');
118
- if (parts.length !== 5) {
119
- return null;
120
- }
121
- return {
122
- tlsVersion: parseInt(parts[0], 10),
123
- ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
124
- extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
125
- curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
126
- points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
127
- };
128
- }
129
-
130
- export function modPow(base, exponent, modulus) {
131
- if (modulus === 1n) return 0n;
132
- let result = 1n;
133
- base = base % modulus;
134
- while (exponent > 0n) {
135
- if (exponent % 2n === 1n) {
136
- result = (result * base) % modulus;
137
- }
138
- exponent = exponent >> 1n;
139
- base = (base * base) % modulus;
140
- }
141
- return result;
142
- }
143
-
144
-
145
- export function hashNetwork(ip, prefix = 24) {
146
- // Hash du réseau (masque /24 ou /16)
147
- const parts = ip.split('.');
148
- if (parts.length !== 4) return null;
149
- const maskBytes = prefix / 8;
150
- const network = parts.slice(0, maskBytes).join('.');
151
- // Hash simple
152
- let hash = 0;
153
- for (let i = 0; i < network.length; i++) {
154
- const char = network.charCodeAt(i);
155
- hash = ((hash << 5) - hash) + char;
156
- hash = hash & hash;
157
- }
158
- return hash.toString(16);
159
- }
160
- export function normalizeReferer(referer) {
161
- try {
162
- const url = new URL(referer);
163
- return `${url.protocol}//${url.hostname}`;
164
- } catch {
165
- return referer;
166
- }
167
- }
168
-
169
- export function isPrivateIp(ip) {
170
- // Vérifier si l'IP est privée
171
- const parts = ip.split('.');
172
- if (parts.length !== 4) return false;
173
- const first = parseInt(parts[0]);
174
- return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
175
- }
176
- // Fonctions utilitaires
177
- export function parseUserAgent(ua) {
178
- // Parser basique du User-Agent
179
- const result = {};
180
-
181
- // Détection du navigateur
182
- if (ua.includes('Chrome') && !ua.includes('Edg')) {
183
- result.browser = 'Chrome';
184
- const match = ua.match(/Chrome\/(\d+)/);
185
- if (match) result.browser += `/${match[1]}`;
186
- } else if (ua.includes('Firefox')) {
187
- result.browser = 'Firefox';
188
- const match = ua.match(/Firefox\/(\d+)/);
189
- if (match) result.browser += `/${match[1]}`;
190
- } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
191
- result.browser = 'Safari';
192
- const match = ua.match(/Version\/(\d+)/);
193
- if (match) result.browser += `/${match[1]}`;
194
- } else if (ua.includes('Edg')) {
195
- result.browser = 'Edge';
196
- const match = ua.match(/Edg\/(\d+)/);
197
- if (match) result.browser += `/${match[1]}`;
198
- }
199
-
200
- // Détection de l'OS
201
- if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
202
- else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
203
- else if (ua.includes('Mac OS X')) result.os = 'macOS';
204
- else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
205
- else if (ua.includes('Android')) result.os = 'Android';
206
- else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
207
-
208
- // Détection du type d'appareil
209
- if (ua.includes('Mobile')) result.device = 'mobile';
210
- else if (ua.includes('Tablet')) result.device = 'tablet';
211
- else result.device = 'desktop';
212
-
213
- return result;
1
+ import {cyrb53} from "./fingerprint.builder.js";
2
+
3
+ export function verifyZkpProof(yStr, tStr, sStr) {
4
+ try {
5
+ const y = BigInt('0x' + yStr);
6
+ const t = BigInt('0x' + tStr);
7
+ const s = BigInt('0x' + sStr);
8
+
9
+ const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
10
+ const ZKP_G = 2n;
11
+
12
+ const cStr = ZKP_G.toString() + y.toString() + t.toString();
13
+ const hashHex = crypto.createHash('sha256').update(cStr).digest('hex');
14
+ const c = BigInt('0x' + hashHex) % ZKP_P;
15
+
16
+ return modPow(ZKP_G, s, ZKP_P) === (t * modPow(y, c, ZKP_P)) % ZKP_P;
17
+ } catch (e) {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export function safeJsonStringify(val) {
23
+ return JSON.stringify(val)
24
+ .replace(/</g, '\\u003c')
25
+ .replace(/>/g, '\\u003e')
26
+ .replace(/\u2028/g, '\\u2028')
27
+ .replace(/\u2029/g, '\\u2029');
28
+ }
29
+
30
+ export function sanitizeRedirectPath(p) {
31
+ if (typeof p !== 'string') return '/';
32
+ let sanitized = p.replace(/[^a-zA-Z0-9\/.\-_~%?&=:@+,;]/g, '');
33
+
34
+ // Prevent protocol-relative redirects (e.g. //evil.com)
35
+ if (sanitized.startsWith('//')) {
36
+ sanitized = '/' + sanitized.replace(/^\/+/g, '');
37
+ }
38
+ // Prevent absolute redirects (e.g. http://evil.com)
39
+ if (/^https?:\/\//i.test(sanitized)) {
40
+ try {
41
+ const parsed = new URL(sanitized);
42
+ sanitized = parsed.pathname + parsed.search + parsed.hash;
43
+ } catch (e) {
44
+ sanitized = '/';
45
+ }
46
+ }
47
+ if (!sanitized.startsWith('/')) {
48
+ sanitized = '/' + sanitized;
49
+ }
50
+ return sanitized.replace(/^\/+/g, '/');
51
+ }
52
+ export function decodePolymorphicFingerprint(fpString, mapping) {
53
+ if (!fpString || !mapping || !mapping.keys) return fpString;
54
+ const reverseKeys = {};
55
+ for (const [orig, rand] of Object.entries(mapping.keys)) {
56
+ reverseKeys[rand] = orig;
57
+ }
58
+ const parts = fpString.split('|');
59
+ const mappedParts = parts.map(part => {
60
+ const pair = part.split(':');
61
+ if (pair.length === 2) {
62
+ const origKey = reverseKeys[pair[0]] || pair[0];
63
+ return `${origKey}:${pair[1]}`;
64
+ }
65
+ return part;
66
+ });
67
+ return mappedParts.join('|');
68
+ }
69
+
70
+
71
+ /**
72
+ * @private
73
+ * Deep merges two objects. The `source` object's properties overwrite the `target`'s.
74
+ * @param {object} target - The target object.
75
+ * @param {object} source - The source object.
76
+ * @returns {object} The merged object.
77
+ */
78
+ export function deepMerge(target, source) {
79
+ const output = { ...target };
80
+ if (target && typeof target === 'object' && source && typeof source === 'object') {
81
+ Object.keys(source).forEach(key => {
82
+ if (source[key] && typeof source[key] === 'object' && key in target) {
83
+ output[key] = deepMerge(target[key], source[key]);
84
+ } else {
85
+ output[key] = source[key];
86
+ }
87
+ });
88
+ }
89
+ return output;
90
+ }
91
+
92
+ /**
93
+ * Creates a stable hash based on device characteristics, independent of the IP.
94
+ * This is our "level 2 fingerprint".
95
+ * @param {object} context - The request context.
96
+ * @returns {string} A hash representing the device.
97
+ */
98
+ export function getHeaderSignature(context) {
99
+ if (!context.rawHeaders) return '';
100
+ const headerKeys = [];
101
+ for (let i = 0; i < context.rawHeaders.length; i += 2) {
102
+ headerKeys.push(context.rawHeaders[i]);
103
+ }
104
+ return cyrb53(headerKeys.sort().join(','));
105
+ }
106
+
107
+ /**
108
+ * Analyses a raw JA3 string.
109
+ * Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
110
+ * @param {string} ja3String
111
+ * @returns {object|null}
112
+ */
113
+ export function parseJa3(ja3String) {
114
+ if (!ja3String || typeof ja3String !== 'string') {
115
+ return null;
116
+ }
117
+ const parts = ja3String.split(',');
118
+ if (parts.length !== 5) {
119
+ return null;
120
+ }
121
+ return {
122
+ tlsVersion: parseInt(parts[0], 10),
123
+ ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
124
+ extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
125
+ curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
126
+ points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
127
+ };
128
+ }
129
+
130
+ export function modPow(base, exponent, modulus) {
131
+ if (modulus === 1n) return 0n;
132
+ let result = 1n;
133
+ base = base % modulus;
134
+ while (exponent > 0n) {
135
+ if (exponent % 2n === 1n) {
136
+ result = (result * base) % modulus;
137
+ }
138
+ exponent = exponent >> 1n;
139
+ base = (base * base) % modulus;
140
+ }
141
+ return result;
142
+ }
143
+
144
+
145
+ export function hashNetwork(ip, prefix = 24) {
146
+ // Hash du réseau (masque /24 ou /16)
147
+ const parts = ip.split('.');
148
+ if (parts.length !== 4) return null;
149
+ const maskBytes = prefix / 8;
150
+ const network = parts.slice(0, maskBytes).join('.');
151
+ // Hash simple
152
+ let hash = 0;
153
+ for (let i = 0; i < network.length; i++) {
154
+ const char = network.charCodeAt(i);
155
+ hash = ((hash << 5) - hash) + char;
156
+ hash = hash & hash;
157
+ }
158
+ return hash.toString(16);
159
+ }
160
+ export function normalizeReferer(referer) {
161
+ try {
162
+ const url = new URL(referer);
163
+ return `${url.protocol}//${url.hostname}`;
164
+ } catch {
165
+ return referer;
166
+ }
167
+ }
168
+
169
+ export function isPrivateIp(ip) {
170
+ // Vérifier si l'IP est privée
171
+ const parts = ip.split('.');
172
+ if (parts.length !== 4) return false;
173
+ const first = parseInt(parts[0]);
174
+ return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
175
+ }
176
+ // Fonctions utilitaires
177
+ export function parseUserAgent(ua) {
178
+ // Parser basique du User-Agent
179
+ const result = {};
180
+
181
+ // Détection du navigateur
182
+ if (ua.includes('Chrome') && !ua.includes('Edg')) {
183
+ result.browser = 'Chrome';
184
+ const match = ua.match(/Chrome\/(\d+)/);
185
+ if (match) result.browser += `/${match[1]}`;
186
+ } else if (ua.includes('Firefox')) {
187
+ result.browser = 'Firefox';
188
+ const match = ua.match(/Firefox\/(\d+)/);
189
+ if (match) result.browser += `/${match[1]}`;
190
+ } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
191
+ result.browser = 'Safari';
192
+ const match = ua.match(/Version\/(\d+)/);
193
+ if (match) result.browser += `/${match[1]}`;
194
+ } else if (ua.includes('Edg')) {
195
+ result.browser = 'Edge';
196
+ const match = ua.match(/Edg\/(\d+)/);
197
+ if (match) result.browser += `/${match[1]}`;
198
+ }
199
+
200
+ // Détection de l'OS
201
+ if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
202
+ else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
203
+ else if (ua.includes('Mac OS X')) result.os = 'macOS';
204
+ else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
205
+ else if (ua.includes('Android')) result.os = 'Android';
206
+ else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
207
+
208
+ // Détection du type d'appareil
209
+ if (ua.includes('Mobile')) result.device = 'mobile';
210
+ else if (ua.includes('Tablet')) result.device = 'tablet';
211
+ else result.device = 'desktop';
212
+
213
+ return result;
214
214
  }
package/src/js/library.js CHANGED
@@ -1329,16 +1329,32 @@ Optimization.Operators.benfordTest = (numbers) => {
1329
1329
  // Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
1330
1330
  return 0;
1331
1331
  }
1332
- const leadingDigits = numbers.map(n => String(n).trim().charAt(0))
1333
- .filter(d => d >= '1' && d <= '9'); // On ne garde que les chiffres de 1 à 9.
1332
+ const counts = Array(10).fill(0);
1333
+ let validCount = 0;
1334
1334
 
1335
- if (leadingDigits.length < 10) {
1336
- return 0; // Pas assez de données pour un test fiable
1335
+ for (let i = 0; i < numbers.length; i++) {
1336
+ let n = numbers[i];
1337
+ if (typeof n === 'string') {
1338
+ n = parseFloat(n);
1339
+ }
1340
+ if (typeof n !== 'number' || isNaN(n) || !isFinite(n)) {
1341
+ continue;
1342
+ }
1343
+ const val = Math.abs(n);
1344
+ if (val === 0) {
1345
+ continue;
1346
+ }
1347
+ const log = Math.log10(val);
1348
+ const factor = Math.pow(10, Math.floor(log));
1349
+ let digit = Math.floor(val / factor);
1350
+ if (digit >= 1 && digit <= 9) {
1351
+ counts[digit]++;
1352
+ validCount++;
1353
+ }
1337
1354
  }
1338
1355
 
1339
- const counts = Array(10).fill(0);
1340
- for (let i = 0; i < leadingDigits.length; i++) {
1341
- counts[parseInt(leadingDigits[i], 10)]++;
1356
+ if (validCount < 10) {
1357
+ return 0; // Pas assez de données pour un test fiable
1342
1358
  }
1343
1359
 
1344
1360
  // Distribution attendue selon la loi de Benford pour le premier chiffre
@@ -1349,7 +1365,7 @@ Optimization.Operators.benfordTest = (numbers) => {
1349
1365
 
1350
1366
  let totalDeviation = 0;
1351
1367
  for (let i = 1; i <= 9; i++) {
1352
- const observedFrequency = (counts[i] / leadingDigits.length) * 100;
1368
+ const observedFrequency = (counts[i] / validCount) * 100;
1353
1369
  const expectedFrequency = benfordDistribution[i];
1354
1370
  totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
1355
1371
  }