@anonympins/fingerprint 0.6.7 → 0.7.1
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 +33 -0
- package/README.md +3 -7
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +9 -11
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +236 -152
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +517 -101
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +355 -384
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +206 -62
- package/src/js/pow.solver.js +235 -101
- package/src/js/pow.worker.js +48 -26
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +101 -40
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +204 -32
- package/src/php/Config/SecurityProfiles.php +21 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +24 -25
- package/src/php/FingerprintEngine.php +1836 -1880
- package/src/php/Ja3AnomalyDetector.php +258 -227
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +28 -27
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -693
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +236 -19
- package/src/php/Utils/TLSClientHelloParser.php +369 -212
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -2,16 +2,16 @@ import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
|
|
|
2
2
|
import {solveChallenge} from './pow.solver.js';
|
|
3
3
|
|
|
4
4
|
function secureRandom() {
|
|
5
|
-
if (typeof
|
|
5
|
+
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
|
|
6
6
|
const array = new Uint32Array(1);
|
|
7
|
-
|
|
8
|
-
return array[0] /
|
|
7
|
+
globalThis.crypto.getRandomValues(array);
|
|
8
|
+
return array[0] / 0xffffffff;
|
|
9
9
|
}
|
|
10
10
|
return Math.random();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
// Variable
|
|
14
|
-
//
|
|
13
|
+
// Variable storing the active hash function.
|
|
14
|
+
// Defaults to the JavaScript implementation.
|
|
15
15
|
let activeCyrb53 = jsCyrb53;
|
|
16
16
|
let derivedKey = null;
|
|
17
17
|
|
|
@@ -37,7 +37,13 @@ async function negotiateSessionKey() {
|
|
|
37
37
|
body: JSON.stringify({ fp: ClientLibrary.getDeviceFingerprint() })
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
if (!response || !response.headers) return;
|
|
41
|
+
let serverPubKeyHex = null;
|
|
42
|
+
if (typeof response.headers.get === 'function') {
|
|
43
|
+
serverPubKeyHex = response.headers.get('x-server-ephemeral-key');
|
|
44
|
+
} else if (typeof response.headers === 'object') {
|
|
45
|
+
serverPubKeyHex = response.headers['x-server-ephemeral-key'] || response.headers['X-Server-Ephemeral-Key'];
|
|
46
|
+
}
|
|
41
47
|
if (serverPubKeyHex) {
|
|
42
48
|
const serverPubKeyBuffer = new Uint8Array(
|
|
43
49
|
serverPubKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
|
|
@@ -144,7 +150,7 @@ function cacheWasm(url, data) {
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
const ClientLibrary = {
|
|
147
|
-
// Cache
|
|
153
|
+
// Cache to avoid recalculating hardware constants
|
|
148
154
|
_cachedBuilder: null,
|
|
149
155
|
/**
|
|
150
156
|
* @private
|
|
@@ -159,16 +165,48 @@ const ClientLibrary = {
|
|
|
159
165
|
},
|
|
160
166
|
|
|
161
167
|
/**
|
|
162
|
-
*
|
|
168
|
+
* Checks if native JavaScript prototypes have been tampered with or hooked (Frida, Puppeteer Stealth).
|
|
169
|
+
*/
|
|
170
|
+
detectTamperedPrototypes() {
|
|
171
|
+
const checkNative = (obj, method) => {
|
|
172
|
+
try {
|
|
173
|
+
if (!obj) return false;
|
|
174
|
+
const fn = obj[method];
|
|
175
|
+
if (!fn) return false;
|
|
176
|
+
const str = Function.prototype.toString.call(fn);
|
|
177
|
+
if (!str.includes('[native code]')) return true; // Standard JS hook
|
|
178
|
+
const desc = Object.getOwnPropertyDescriptor(obj, method);
|
|
179
|
+
if (desc && (!desc.writable && !desc.configurable && desc.value)) return false;
|
|
180
|
+
return false;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const win = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
187
|
+
if (!win) return false;
|
|
188
|
+
|
|
189
|
+
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : win.HTMLCanvasElement;
|
|
190
|
+
const canvas2d = typeof CanvasRenderingContext2D !== 'undefined' ? CanvasRenderingContext2D : win.CanvasRenderingContext2D;
|
|
191
|
+
const webgl = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext : win.WebGLRenderingContext;
|
|
192
|
+
|
|
193
|
+
return checkNative(htmlCanvas?.prototype, 'toDataURL') ||
|
|
194
|
+
checkNative(canvas2d?.prototype, 'getImageData') ||
|
|
195
|
+
checkNative(webgl?.prototype, 'getParameter') ||
|
|
196
|
+
checkNative(win, 'fetch');
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Internal wrapper for the active hash function.
|
|
163
201
|
* @private
|
|
164
202
|
*/
|
|
165
203
|
_hasher: (str, seed) => activeCyrb53(str, seed),
|
|
166
204
|
|
|
167
205
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* @param {string} fingerprint -
|
|
171
|
-
* @returns {Promise<string>}
|
|
206
|
+
* Generates a Schnorr Zero-Knowledge Proof (ZKP) of the device fingerprint.
|
|
207
|
+
* Ensures compatibility across JS, PHP, and Java verification engines.
|
|
208
|
+
* @param {string} fingerprint - Device fingerprint string.
|
|
209
|
+
* @returns {Promise<string>} Proof in hex format "y:t:s".
|
|
172
210
|
*/
|
|
173
211
|
async generateZkpProof(fingerprint) {
|
|
174
212
|
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
@@ -215,7 +253,7 @@ const ClientLibrary = {
|
|
|
215
253
|
},
|
|
216
254
|
|
|
217
255
|
/**
|
|
218
|
-
*
|
|
256
|
+
* Generates the current device fingerprint.
|
|
219
257
|
*/
|
|
220
258
|
getDeviceFingerprint() {
|
|
221
259
|
if (typeof window === "undefined") {
|
|
@@ -229,19 +267,19 @@ const ClientLibrary = {
|
|
|
229
267
|
|
|
230
268
|
this._cachedBuilder = new FingerprintBuilder();
|
|
231
269
|
|
|
232
|
-
// 1. Hardware (
|
|
270
|
+
// 1. Hardware (Very stable): Cores, RAM, GPU, Touch
|
|
233
271
|
this._cachedBuilder.add(
|
|
234
|
-
"hw",
|
|
272
|
+
"hw",
|
|
235
273
|
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
236
274
|
);
|
|
237
275
|
|
|
238
|
-
// 2. Geo/Locale (Stable
|
|
276
|
+
// 2. Geo/Locale (Stable unless traveling/VPN): Timezone, Language
|
|
239
277
|
this._cachedBuilder.add(
|
|
240
278
|
"geo",
|
|
241
279
|
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
242
280
|
);
|
|
243
281
|
|
|
244
|
-
// 3. Screen (Stable
|
|
282
|
+
// 3. Screen (Stable unless monitor/zoom changes): Dimensions, ColorDepth
|
|
245
283
|
this._cachedBuilder.add(
|
|
246
284
|
"scr",
|
|
247
285
|
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
@@ -288,8 +326,8 @@ const ClientLibrary = {
|
|
|
288
326
|
} catch (e) {
|
|
289
327
|
}
|
|
290
328
|
|
|
291
|
-
// 7.
|
|
292
|
-
//
|
|
329
|
+
// 7. Detection of Chrome DevTools Protocol (CDP) artifacts
|
|
330
|
+
// Injected by automation tools
|
|
293
331
|
const cdpFootprints = [
|
|
294
332
|
'cdc_adoQpoasnfa76pfcZLmcfl_Array',
|
|
295
333
|
'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
|
|
@@ -302,15 +340,18 @@ const ClientLibrary = {
|
|
|
302
340
|
this._cachedBuilder.add("cdp", "true");
|
|
303
341
|
}
|
|
304
342
|
|
|
305
|
-
//
|
|
343
|
+
// 8. Bot Detection (Hidden indicators)
|
|
306
344
|
if (nav.webdriver) this._cachedBuilder.add("bot", "true");
|
|
345
|
+
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
346
|
+
this._cachedBuilder.add("tampered", "true");
|
|
347
|
+
}
|
|
307
348
|
}
|
|
308
349
|
|
|
309
350
|
return this._cachedBuilder.toString();
|
|
310
351
|
},
|
|
311
352
|
|
|
312
353
|
/**
|
|
313
|
-
*
|
|
354
|
+
* Generates a request signature including context.
|
|
314
355
|
* @param {object} payload
|
|
315
356
|
*/
|
|
316
357
|
/**
|
|
@@ -328,10 +369,10 @@ const ClientLibrary = {
|
|
|
328
369
|
},
|
|
329
370
|
|
|
330
371
|
/**
|
|
331
|
-
*
|
|
332
|
-
* @param {object} payload -
|
|
333
|
-
* @param {string} secret -
|
|
334
|
-
* @returns {Promise<string>}
|
|
372
|
+
* Generates an HMAC-SHA256 signature using the Web Crypto API.
|
|
373
|
+
* @param {object} payload - Data payload to sign.
|
|
374
|
+
* @param {string} secret - Shared secret key.
|
|
375
|
+
* @returns {Promise<string>} Hexadecimal signature.
|
|
335
376
|
*/
|
|
336
377
|
async generateClientSideSignature(payload, secret) {
|
|
337
378
|
const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
|
|
@@ -350,28 +391,28 @@ const ClientLibrary = {
|
|
|
350
391
|
* Resets the cached fingerprint builder. Used for testing purposes.
|
|
351
392
|
*/
|
|
352
393
|
_resetCache() {
|
|
353
|
-
//
|
|
394
|
+
// Reset hasher to default JS implementation.
|
|
354
395
|
activeCyrb53 = jsCyrb53;
|
|
355
396
|
this._cachedBuilder = null;
|
|
356
397
|
},
|
|
357
398
|
|
|
358
399
|
/**
|
|
359
|
-
*
|
|
400
|
+
* Injects invisible phantom interactive elements to trap bots (focus/hover).
|
|
360
401
|
*/
|
|
361
402
|
injectPhantomTraps() {
|
|
362
403
|
if (typeof document === 'undefined') return;
|
|
363
404
|
|
|
364
|
-
//
|
|
405
|
+
// Create a phantom interactive element
|
|
365
406
|
const phantom = document.createElement('a');
|
|
366
407
|
phantom.href = '#';
|
|
367
|
-
//
|
|
408
|
+
// Random deceptive name to attract automated link/form parsers
|
|
368
409
|
const phantomNames = ['sys-session-recovery', 'auth-token-refresh', 'debug-console-login', 'admin-portal-access', 'security-bypass-bypass', 'recovery-key-session', 'api-key-test', 'client-secrets-access'];
|
|
369
410
|
phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
|
|
370
411
|
phantom.className = genRandStr(8);
|
|
371
|
-
phantom.tabIndex = 0; //
|
|
372
|
-
phantom.setAttribute('aria-hidden', 'true'); //
|
|
412
|
+
phantom.tabIndex = 0; // In natural tab flow
|
|
413
|
+
phantom.setAttribute('aria-hidden', 'true'); // Hidden from legitimate screen readers
|
|
373
414
|
|
|
374
|
-
//
|
|
415
|
+
// Invisible yet interactive style (1px x 1px, nearly transparent)
|
|
375
416
|
phantom.style.position = 'fixed';
|
|
376
417
|
phantom.style.top = '1px';
|
|
377
418
|
phantom.style.left = '1px';
|
|
@@ -389,7 +430,7 @@ const ClientLibrary = {
|
|
|
389
430
|
phantom.addEventListener('focus', triggerTrap, { passive: true });
|
|
390
431
|
phantom.addEventListener('mouseover', triggerTrap, { passive: true });
|
|
391
432
|
|
|
392
|
-
//
|
|
433
|
+
// Polymorphic DOM insertion of the phantom trap
|
|
393
434
|
const nestingOptions = [
|
|
394
435
|
() => document.body.appendChild(phantom),
|
|
395
436
|
() => {
|
|
@@ -407,7 +448,31 @@ const ClientLibrary = {
|
|
|
407
448
|
},
|
|
408
449
|
|
|
409
450
|
/**
|
|
410
|
-
*
|
|
451
|
+
* Measures gyroscope/accelerometer noise to detect phone farm racks.
|
|
452
|
+
*/
|
|
453
|
+
startMotionTracker() {
|
|
454
|
+
if (this._motionTrackerAttached || typeof window === 'undefined') return;
|
|
455
|
+
this._motionTrackerAttached = true;
|
|
456
|
+
const motionSamples = [];
|
|
457
|
+
const handleMotion = (e) => {
|
|
458
|
+
const acc = e.accelerationIncludingGravity || e.acceleration;
|
|
459
|
+
if (!acc) return;
|
|
460
|
+
const mag = Math.sqrt((acc.x || 0)**2 + (acc.y || 0)**2 + (acc.z || 0)**2);
|
|
461
|
+
motionSamples.push(mag);
|
|
462
|
+
if (motionSamples.length > 20) motionSamples.shift();
|
|
463
|
+
if (motionSamples.length >= 10) {
|
|
464
|
+
const avg = motionSamples.reduce((a, b) => a + b, 0) / motionSamples.length;
|
|
465
|
+
const variance = motionSamples.reduce((a, b) => a + (b - avg)**2, 0) / motionSamples.length;
|
|
466
|
+
metrics.motionVariance = Math.round(variance * 10000) / 10000;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
window.addEventListener('devicemotion', handleMotion, { passive: true });
|
|
471
|
+
} catch (e) {}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Starts tracking touch events on mobile/tablet devices.
|
|
411
476
|
*/
|
|
412
477
|
startTouchEventTracker() {
|
|
413
478
|
if (this._touchTrackerAttached) return;
|
|
@@ -441,7 +506,7 @@ const ClientLibrary = {
|
|
|
441
506
|
},
|
|
442
507
|
|
|
443
508
|
/**
|
|
444
|
-
*
|
|
509
|
+
* Starts tracking display regularity (V-Sync/rAF) to detect software framebuffers lacking V-Sync.
|
|
445
510
|
*/
|
|
446
511
|
startRenderingTracker() {
|
|
447
512
|
if (this._renderingTrackerAttached) return;
|
|
@@ -455,8 +520,9 @@ const ClientLibrary = {
|
|
|
455
520
|
|
|
456
521
|
const checkOffscreenAnom = () => {
|
|
457
522
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
523
|
+
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : window.HTMLCanvasElement;
|
|
524
|
+
if ('OffscreenCanvas' in window && htmlCanvas?.prototype?.transferControlToOffscreen) {
|
|
525
|
+
const nativeToString = Function.prototype.toString.call(htmlCanvas.prototype.transferControlToOffscreen);
|
|
460
526
|
return !nativeToString.includes('[native code]');
|
|
461
527
|
}
|
|
462
528
|
} catch (e) {}
|
|
@@ -487,7 +553,7 @@ const ClientLibrary = {
|
|
|
487
553
|
},
|
|
488
554
|
|
|
489
555
|
/**
|
|
490
|
-
*
|
|
556
|
+
* Initializes persistent Proof-of-Space allocation in local IndexedDB.
|
|
491
557
|
*/
|
|
492
558
|
async initializeSpace(seed, sizeMb) {
|
|
493
559
|
return new Promise((resolve, reject) => {
|
|
@@ -529,7 +595,7 @@ const ClientLibrary = {
|
|
|
529
595
|
},
|
|
530
596
|
|
|
531
597
|
/**
|
|
532
|
-
*
|
|
598
|
+
* Reads a specific block from local IndexedDB formatted as hex.
|
|
533
599
|
*/
|
|
534
600
|
async readSpaceBlock(blockIdx) {
|
|
535
601
|
return new Promise((resolve, reject) => {
|
|
@@ -555,7 +621,7 @@ const ClientLibrary = {
|
|
|
555
621
|
},
|
|
556
622
|
|
|
557
623
|
/**
|
|
558
|
-
*
|
|
624
|
+
* Solves Proof-of-Space by optionally combining a peer's block.
|
|
559
625
|
*/
|
|
560
626
|
async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
|
|
561
627
|
const blocks = [];
|
|
@@ -572,8 +638,8 @@ const ClientLibrary = {
|
|
|
572
638
|
},
|
|
573
639
|
|
|
574
640
|
/**
|
|
575
|
-
*
|
|
576
|
-
*
|
|
641
|
+
* Starts tracking mouse movements to evaluate entropy.
|
|
642
|
+
* Call once per page lifecycle.
|
|
577
643
|
*/
|
|
578
644
|
startMouseEntropyTracker() {
|
|
579
645
|
// Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
|
|
@@ -581,9 +647,9 @@ const ClientLibrary = {
|
|
|
581
647
|
this._mouseTrackerAttached = true;
|
|
582
648
|
|
|
583
649
|
document.addEventListener('mousemove', (e) => {
|
|
584
|
-
//
|
|
650
|
+
// Capture trajectory points {x, y, t}
|
|
585
651
|
if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
|
|
586
|
-
//
|
|
652
|
+
// Cap history size to prevent unbounded memory growth
|
|
587
653
|
mouseMovementsHistory.shift();
|
|
588
654
|
}
|
|
589
655
|
mouseMovementsHistory.push({
|
|
@@ -595,8 +661,8 @@ const ClientLibrary = {
|
|
|
595
661
|
},
|
|
596
662
|
|
|
597
663
|
/**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
664
|
+
* Starts tracking keystroke dynamics to evaluate dwell and flight times.
|
|
665
|
+
* Call once per page lifecycle.
|
|
600
666
|
*/
|
|
601
667
|
startKeystrokeDynamicsTracker() {
|
|
602
668
|
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
@@ -694,18 +760,18 @@ const ClientLibrary = {
|
|
|
694
760
|
}, { passive: true });
|
|
695
761
|
},
|
|
696
762
|
/**
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* @param {string[]} honeypotFieldNames -
|
|
763
|
+
* Initializes or resets client-side honeypots for immediate detection.
|
|
764
|
+
* Previous listeners are removed before attaching new ones.
|
|
765
|
+
* @param {string[]} honeypotFieldNames - Hidden form field names.
|
|
700
766
|
*/
|
|
701
767
|
initializeHoneypots(honeypotFieldNames) {
|
|
702
|
-
// 1.
|
|
768
|
+
// 1. Clean up existing listeners
|
|
703
769
|
activeHoneypotListeners.forEach((listener, field) => {
|
|
704
770
|
field.removeEventListener('input', listener);
|
|
705
771
|
});
|
|
706
772
|
activeHoneypotListeners.clear();
|
|
707
773
|
|
|
708
|
-
// 2.
|
|
774
|
+
// 2. Attach new listeners to existing DOM elements
|
|
709
775
|
honeypotFieldNames.forEach(fieldName => {
|
|
710
776
|
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
711
777
|
if (field) {
|
|
@@ -714,11 +780,11 @@ const ClientLibrary = {
|
|
|
714
780
|
field.removeEventListener('input', listener);
|
|
715
781
|
};
|
|
716
782
|
field.addEventListener('input', listener);
|
|
717
|
-
activeHoneypotListeners.set(field, listener);
|
|
783
|
+
activeHoneypotListeners.set(field, listener);
|
|
718
784
|
}
|
|
719
785
|
});
|
|
720
786
|
|
|
721
|
-
// 3.
|
|
787
|
+
// 3. Generate trap inputs inside a closed Shadow DOM
|
|
722
788
|
if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
|
|
723
789
|
const host = document.createElement('div');
|
|
724
790
|
host.setAttribute('aria-hidden', 'true');
|
|
@@ -729,7 +795,7 @@ const ClientLibrary = {
|
|
|
729
795
|
|
|
730
796
|
const shadow = host.attachShadow({ mode: 'closed' });
|
|
731
797
|
|
|
732
|
-
//
|
|
798
|
+
// Randomize classes, CSS variables, and layout tags
|
|
733
799
|
const wrapperClass = genRandStr(10);
|
|
734
800
|
const posStateVar = `--${genRandStr(8)}`;
|
|
735
801
|
const offValVar = `--${genRandStr(8)}`;
|
|
@@ -778,7 +844,7 @@ const ClientLibrary = {
|
|
|
778
844
|
input.addEventListener('change', trigger, { passive: true });
|
|
779
845
|
input.addEventListener('focus', trigger, { passive: true });
|
|
780
846
|
|
|
781
|
-
//
|
|
847
|
+
// Polymorphic nesting of label and input
|
|
782
848
|
const nestingType = Math.floor(secureRandom() * 3);
|
|
783
849
|
if (nestingType === 1) {
|
|
784
850
|
label.appendChild(input);
|
|
@@ -800,21 +866,22 @@ const ClientLibrary = {
|
|
|
800
866
|
}
|
|
801
867
|
},
|
|
802
868
|
/**
|
|
803
|
-
*
|
|
804
|
-
*
|
|
869
|
+
* Silently requests FIDO2 hardware attestation (WebAuthn)
|
|
870
|
+
* to anchor physical device identity via TPM / Secure Enclave.
|
|
805
871
|
* @returns {Promise<object|null>}
|
|
806
872
|
*/
|
|
807
873
|
async getWebAuthnAnchor() {
|
|
808
|
-
|
|
874
|
+
const win = typeof window !== 'undefined' ? window : null;
|
|
875
|
+
if (!win || !win.PublicKeyCredential) return null;
|
|
809
876
|
try {
|
|
810
|
-
const isPlatformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
877
|
+
const isPlatformAvailable = await win.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
811
878
|
if (!isPlatformAvailable) return null;
|
|
812
879
|
|
|
813
880
|
const storedCredIdBase64 = localStorage.getItem('fp_webauthn_cred_id');
|
|
814
881
|
const challenge = new Uint8Array([109, 101, 116, 114, 105, 99, 115, 95, 97, 110, 99, 104, 111, 114, 95, 115, 101, 99, 117, 114, 101]); // Static stable challenge
|
|
815
882
|
|
|
816
883
|
if (storedCredIdBase64) {
|
|
817
|
-
//
|
|
884
|
+
// Silent assertion attempt (Authentication)
|
|
818
885
|
const credentialId = Uint8Array.from(atob(storedCredIdBase64), c => c.charCodeAt(0));
|
|
819
886
|
const assertion = await navigator.credentials.get({
|
|
820
887
|
publicKey: {
|
|
@@ -837,7 +904,7 @@ const ClientLibrary = {
|
|
|
837
904
|
};
|
|
838
905
|
}
|
|
839
906
|
} else {
|
|
840
|
-
//
|
|
907
|
+
// Silent creation attempt (Registration)
|
|
841
908
|
const options = {
|
|
842
909
|
publicKey: {
|
|
843
910
|
challenge,
|
|
@@ -875,34 +942,38 @@ const ClientLibrary = {
|
|
|
875
942
|
}
|
|
876
943
|
}
|
|
877
944
|
} catch (e) {
|
|
878
|
-
//
|
|
945
|
+
// Bypass silently when unsupported to remain non-intrusive
|
|
879
946
|
console.log('[WebAuthn-Anchor] Silent attestation bypassed:', e.message);
|
|
880
947
|
}
|
|
881
948
|
return null;
|
|
882
949
|
},
|
|
883
950
|
|
|
884
951
|
/**
|
|
885
|
-
*
|
|
886
|
-
*
|
|
952
|
+
* Retrieves collected client behavioral metrics.
|
|
953
|
+
* Call before submitting sensitive requests.
|
|
887
954
|
* @returns {ClientBehaviorMetrics}
|
|
888
955
|
*/
|
|
889
956
|
getClientBehaviorMetrics() {
|
|
890
957
|
// Add history length as a behavioral signal.
|
|
891
958
|
metrics.historyLength = window.history.length;
|
|
892
959
|
|
|
893
|
-
//
|
|
960
|
+
// Attach collection timestamp for replay detection
|
|
894
961
|
metrics.clicksHistory = clicksHistory;
|
|
895
962
|
metrics.clientTimestamp = Date.now();
|
|
896
963
|
|
|
897
964
|
metrics.touchMovementsHistory = touchMovementsHistory;
|
|
898
|
-
|
|
965
|
+
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
966
|
+
metrics.prototypeTampered = true;
|
|
967
|
+
}
|
|
968
|
+
metrics.motionVariance = metrics.motionVariance ?? -1;
|
|
969
|
+
// Include mouse trajectory history for server-side evaluation
|
|
899
970
|
metrics.mouseMovementsHistory = mouseMovementsHistory;
|
|
900
971
|
|
|
901
972
|
// NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
|
|
902
973
|
metrics.keystrokeDwellTimes = keystrokeDwellTimes;
|
|
903
974
|
metrics.keystrokeFlightTimes = keystrokeFlightTimes;
|
|
904
975
|
|
|
905
|
-
//
|
|
976
|
+
// Calculate mean keystroke latency
|
|
906
977
|
if (keystrokeLatencies.length > 0) {
|
|
907
978
|
const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
|
|
908
979
|
metrics.keystrokeLatency = sum / keystrokeLatencies.length;
|
|
@@ -913,7 +984,7 @@ const ClientLibrary = {
|
|
|
913
984
|
},
|
|
914
985
|
|
|
915
986
|
/**
|
|
916
|
-
*
|
|
987
|
+
* Enriches a fetch request with fingerprinting and behavioral headers.
|
|
917
988
|
* @param {RequestInfo} resource
|
|
918
989
|
* @param {RequestInit} [options]
|
|
919
990
|
* @returns {Promise<Response>}
|
|
@@ -933,18 +1004,16 @@ const ClientLibrary = {
|
|
|
933
1004
|
return fetch(resource, options);
|
|
934
1005
|
},
|
|
935
1006
|
|
|
936
|
-
// ---
|
|
937
|
-
|
|
1007
|
+
// --- Fetch Interception Chain ---
|
|
938
1008
|
_isFetchPatched: false,
|
|
939
1009
|
_interceptorChain: [],
|
|
940
|
-
//
|
|
941
|
-
// pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
|
|
1010
|
+
// Store original fetch bound to window to prevent illegal invocation errors
|
|
942
1011
|
_originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
|
|
943
1012
|
|
|
944
1013
|
/**
|
|
945
1014
|
* Adds an interceptor function to the `fetch` chain.
|
|
946
|
-
*
|
|
947
|
-
*
|
|
1015
|
+
* Each interceptor receives `resource`, `options`, and a `next` function.
|
|
1016
|
+
* Must call `next(resource, options)` to continue the chain.
|
|
948
1017
|
* @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
|
|
949
1018
|
*/
|
|
950
1019
|
addFetchInterceptor(interceptor) {
|
|
@@ -959,14 +1028,11 @@ const ClientLibrary = {
|
|
|
959
1028
|
|
|
960
1029
|
this._isFetchPatched = true;
|
|
961
1030
|
window.fetch = (resource, options) => {
|
|
962
|
-
// Le "dispatcher" qui exécute la chaîne.
|
|
963
1031
|
const dispatch = (index, res, opts) => {
|
|
964
1032
|
if (index >= this._interceptorChain.length) {
|
|
965
|
-
// Fin de la chaîne, on appelle le fetch original.
|
|
966
1033
|
return this._originalFetch(res, opts);
|
|
967
1034
|
}
|
|
968
1035
|
const nextInterceptor = this._interceptorChain[index];
|
|
969
|
-
// Appelle l'intercepteur actuel en lui passant la fonction pour appeler le suivant.
|
|
970
1036
|
return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
|
|
971
1037
|
};
|
|
972
1038
|
return dispatch(0, resource, options || {});
|
|
@@ -974,20 +1040,19 @@ const ClientLibrary = {
|
|
|
974
1040
|
},
|
|
975
1041
|
|
|
976
1042
|
/**
|
|
977
|
-
*
|
|
1043
|
+
* Invoked when a honeypot trigger occurs.
|
|
978
1044
|
* @private
|
|
979
1045
|
*/
|
|
980
1046
|
onHoneypotTrigger() {
|
|
981
1047
|
metrics.honeypotInteraction = true;
|
|
982
|
-
//
|
|
1048
|
+
// Dispatch event allowing host applications to react
|
|
983
1049
|
this._dispatchEvent('honeypotTriggered');
|
|
984
1050
|
},
|
|
985
1051
|
|
|
986
1052
|
/**
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
* @param {string[]} [targetDomains] -
|
|
990
|
-
* Si non fourni, protège les requêtes de même origine.
|
|
1053
|
+
* Initializes the fingerprinting fetch interceptor.
|
|
1054
|
+
* Appends to interceptor chain without clobbering existing handlers.
|
|
1055
|
+
* @param {string[]} [targetDomains] - Domains to protect. Defaults to same-origin.
|
|
991
1056
|
*/
|
|
992
1057
|
initializeFetch(targetDomains = []) {
|
|
993
1058
|
const fingerprintInterceptor = async (resource, options, next) => {
|
|
@@ -996,14 +1061,11 @@ const ClientLibrary = {
|
|
|
996
1061
|
|
|
997
1062
|
try {
|
|
998
1063
|
const url = new URL(requestUrl, window.location.origin);
|
|
999
|
-
//
|
|
1000
|
-
// OU si le domaine de la requête est dans la liste fournie.
|
|
1064
|
+
// Protect if targetDomains is empty and same-origin, or host matches targetDomains
|
|
1001
1065
|
shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
|
|
1002
1066
|
(targetDomains.length > 0 && targetDomains.includes(url.hostname));
|
|
1003
1067
|
} catch (e) {
|
|
1004
|
-
//
|
|
1005
|
-
// Ce bloc est une sécurité pour les cas où l'URL serait malformée.
|
|
1006
|
-
// On protège par défaut si aucune liste de domaines n'est spécifiée.
|
|
1068
|
+
// Fallback for malformed URLs
|
|
1007
1069
|
shouldProtect = targetDomains.length === 0;
|
|
1008
1070
|
}
|
|
1009
1071
|
|
|
@@ -1017,12 +1079,12 @@ const ClientLibrary = {
|
|
|
1017
1079
|
options.headers = headers;
|
|
1018
1080
|
}
|
|
1019
1081
|
|
|
1020
|
-
//
|
|
1082
|
+
// Forward to next interceptor in chain
|
|
1021
1083
|
return next(resource, options);
|
|
1022
1084
|
};
|
|
1023
1085
|
|
|
1024
1086
|
this.addFetchInterceptor(fingerprintInterceptor);
|
|
1025
|
-
},
|
|
1087
|
+
},
|
|
1026
1088
|
|
|
1027
1089
|
/**
|
|
1028
1090
|
* Injects visually hidden "honeypot" links into the DOM to trap bots.
|
|
@@ -1043,7 +1105,7 @@ const ClientLibrary = {
|
|
|
1043
1105
|
|
|
1044
1106
|
const shadow = host.attachShadow({ mode: 'closed' });
|
|
1045
1107
|
|
|
1046
|
-
//
|
|
1108
|
+
// Randomize classes, CSS variables, and layout tags
|
|
1047
1109
|
const wrapperClass = genRandStr(10);
|
|
1048
1110
|
const layoutPosVar = `--${genRandStr(8)}`;
|
|
1049
1111
|
const offsetValVar = `--${genRandStr(8)}`;
|
|
@@ -1089,7 +1151,7 @@ const ClientLibrary = {
|
|
|
1089
1151
|
link.className = linkColorClass;
|
|
1090
1152
|
link.id = genRandStr(8);
|
|
1091
1153
|
|
|
1092
|
-
//
|
|
1154
|
+
// Polymorphic link content structure
|
|
1093
1155
|
const contentNestingType = Math.floor(secureRandom() * 3);
|
|
1094
1156
|
if (contentNestingType === 1) {
|
|
1095
1157
|
const span = document.createElement('span');
|
|
@@ -1112,7 +1174,7 @@ const ClientLibrary = {
|
|
|
1112
1174
|
link.addEventListener('focus', trigger, { passive: true });
|
|
1113
1175
|
link.addEventListener('mouseover', trigger, { passive: true });
|
|
1114
1176
|
|
|
1115
|
-
//
|
|
1177
|
+
// Polymorphic link nesting
|
|
1116
1178
|
if (secureRandom() > 0.5) {
|
|
1117
1179
|
const itemContainer = document.createElement('span');
|
|
1118
1180
|
itemContainer.className = genRandStr(5);
|
|
@@ -1127,73 +1189,72 @@ const ClientLibrary = {
|
|
|
1127
1189
|
document.body.appendChild(host);
|
|
1128
1190
|
},
|
|
1129
1191
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
* @param {Response} response -
|
|
1132
|
-
* @param {RequestInfo} resource -
|
|
1133
|
-
* @param {RequestInit} options -
|
|
1134
|
-
* @returns {Promise<Response>} -
|
|
1192
|
+
* Intercepts a JSON challenge response, solves it, and retries the request.
|
|
1193
|
+
* @param {Response} response - Initial response (status 404/challenge).
|
|
1194
|
+
* @param {RequestInfo} resource - Original request resource.
|
|
1195
|
+
* @param {RequestInit} options - Original request options.
|
|
1196
|
+
* @returns {Promise<Response>} - Retried response.
|
|
1135
1197
|
* @private
|
|
1136
1198
|
*/
|
|
1137
1199
|
async solveChallengeAndRetry(response, resource, options) {
|
|
1138
|
-
if (response.status !== 404 || !response.headers
|
|
1200
|
+
if (!response || response.status !== 404 || !response.headers?.get?.('content-type')?.includes('application/json') || response.bodyUsed) {
|
|
1139
1201
|
return response;
|
|
1140
1202
|
}
|
|
1141
1203
|
|
|
1142
1204
|
try {
|
|
1143
1205
|
const challengeData = await response.json();
|
|
1144
1206
|
if (!challengeData.challenge || !challengeData.challenge.type) {
|
|
1145
|
-
return response; //
|
|
1207
|
+
return response; // Not a valid JSON challenge
|
|
1146
1208
|
}
|
|
1147
1209
|
|
|
1148
1210
|
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
1149
1211
|
this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
|
|
1150
1212
|
|
|
1151
|
-
//
|
|
1213
|
+
// The device fingerprint solving the challenge is crucial
|
|
1152
1214
|
const solverFp = this.getDeviceFingerprint();
|
|
1153
1215
|
const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
|
|
1154
1216
|
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
1155
1217
|
|
|
1156
1218
|
this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
|
|
1157
|
-
//
|
|
1219
|
+
// Append solution parameters to retry request URL
|
|
1158
1220
|
const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
|
|
1159
|
-
// La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
|
|
1160
1221
|
solutionWrapper.applyToUrl(url);
|
|
1161
1222
|
|
|
1162
|
-
//
|
|
1223
|
+
// Attach solver fingerprint to retry request
|
|
1163
1224
|
url.searchParams.set('pow_fp', solverFp);
|
|
1164
1225
|
|
|
1165
|
-
//
|
|
1166
|
-
// ce qui garantit que le fetch original est appelé avec le bon contexte.
|
|
1167
|
-
// Cela évite de réintroduire l'erreur "Illegal invocation".
|
|
1226
|
+
// Use fetch to retry with appropriate execution context
|
|
1168
1227
|
return window.fetch(url.toString(), options);
|
|
1169
1228
|
} catch (e) {
|
|
1170
1229
|
console.error('[Fingerprint] Failed to solve or retry challenge:', e);
|
|
1171
1230
|
return response; // Retourne la réponse 429 originale en cas d'échec
|
|
1172
1231
|
}
|
|
1173
|
-
},
|
|
1232
|
+
},
|
|
1174
1233
|
|
|
1175
1234
|
/**
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
* @param {ClientConfig} [config={}] -
|
|
1235
|
+
* Initializes all client-side protections in one call.
|
|
1236
|
+
* Also loads WASM module if `wasmPath` is provided.
|
|
1237
|
+
* Recommended initialization approach.
|
|
1238
|
+
* @param {ClientConfig} [config={}] - Client configuration options.
|
|
1180
1239
|
*/
|
|
1181
1240
|
initializeClient(config = {}) {
|
|
1182
1241
|
const {
|
|
1183
1242
|
mouse = true,
|
|
1184
1243
|
keystrokes = true,
|
|
1185
1244
|
clicks = true, // Add new option
|
|
1186
|
-
touches = true, //
|
|
1245
|
+
touches = true, // Touch event tracking
|
|
1246
|
+
motion = true,
|
|
1187
1247
|
rendering = true,
|
|
1188
|
-
|
|
1248
|
+
phantomTraps = true,
|
|
1189
1249
|
honeypots = [],
|
|
1190
|
-
trapUrls = [], //
|
|
1191
|
-
wasmPath,
|
|
1250
|
+
trapUrls = [], // Trap URLs
|
|
1251
|
+
wasmPath,
|
|
1252
|
+
workerPath, // NOUVEAU
|
|
1192
1253
|
fetch: fetchConfig = {}
|
|
1193
1254
|
} = config;
|
|
1194
1255
|
|
|
1195
1256
|
negotiateSessionKey();
|
|
1196
|
-
//
|
|
1257
|
+
// Load WASM module if path provided
|
|
1197
1258
|
if (wasmPath) {
|
|
1198
1259
|
this.initializeWasm(wasmPath);
|
|
1199
1260
|
}
|
|
@@ -1201,6 +1262,9 @@ const ClientLibrary = {
|
|
|
1201
1262
|
if (mouse) {
|
|
1202
1263
|
this.startMouseEntropyTracker();
|
|
1203
1264
|
}
|
|
1265
|
+
if (workerPath) {
|
|
1266
|
+
this.workerPath = workerPath;
|
|
1267
|
+
}
|
|
1204
1268
|
if (keystrokes) {
|
|
1205
1269
|
this.startKeystrokeDynamicsTracker();
|
|
1206
1270
|
}
|
|
@@ -1209,6 +1273,12 @@ const ClientLibrary = {
|
|
|
1209
1273
|
}
|
|
1210
1274
|
if (touches) {
|
|
1211
1275
|
this.startTouchEventTracker();
|
|
1276
|
+
}
|
|
1277
|
+
if (motion) {
|
|
1278
|
+
this.startMotionTracker();
|
|
1279
|
+
}
|
|
1280
|
+
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
1281
|
+
metrics.prototypeTampered = true;
|
|
1212
1282
|
}
|
|
1213
1283
|
if (rendering) {
|
|
1214
1284
|
this.startRenderingTracker();
|
|
@@ -1220,19 +1290,19 @@ const ClientLibrary = {
|
|
|
1220
1290
|
this.initializeHoneypots(honeypots);
|
|
1221
1291
|
}
|
|
1222
1292
|
|
|
1223
|
-
//
|
|
1293
|
+
// Dynamically inject trap links on startup
|
|
1224
1294
|
if (trapUrls.length > 0) {
|
|
1225
1295
|
this.injectTrapLinks(trapUrls);
|
|
1226
1296
|
}
|
|
1227
|
-
//
|
|
1297
|
+
// Enable fetch interception if configured
|
|
1228
1298
|
if (config.fetch) {
|
|
1229
1299
|
this.initializeFetch(fetchConfig.targetDomains);
|
|
1230
1300
|
|
|
1231
|
-
//
|
|
1301
|
+
// Add challenge resolution interceptor
|
|
1232
1302
|
if (fetchConfig.handleChallenges !== false) {
|
|
1233
1303
|
this.addFetchInterceptor(async (resource, options, next) => {
|
|
1234
1304
|
const originalResponse = await next(resource, options);
|
|
1235
|
-
//
|
|
1305
|
+
// Clone response to prevent draining the body stream for original caller
|
|
1236
1306
|
return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
|
|
1237
1307
|
});
|
|
1238
1308
|
}
|
|
@@ -1240,18 +1310,30 @@ const ClientLibrary = {
|
|
|
1240
1310
|
},
|
|
1241
1311
|
|
|
1242
1312
|
/**
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
* @param {string} wasmPath -
|
|
1313
|
+
* Attempts to load and initialize WebAssembly module for accelerated hashing.
|
|
1314
|
+
* Falls back to JavaScript implementation on error.
|
|
1315
|
+
* @param {string} wasmPath - Path to WASM loader script or binary.
|
|
1246
1316
|
*/
|
|
1247
1317
|
async initializeWasm(wasmPath) {
|
|
1248
1318
|
try {
|
|
1249
1319
|
// Direct standalone polymorphic WebAssembly loading
|
|
1250
1320
|
if (wasmPath.endsWith('.wasm')) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1321
|
+
let instance;
|
|
1322
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
1323
|
+
try {
|
|
1324
|
+
const response = await fetch(wasmPath);
|
|
1325
|
+
const result = await WebAssembly.instantiateStreaming(response, {});
|
|
1326
|
+
instance = result.instance;
|
|
1327
|
+
} catch (streamingError) {
|
|
1328
|
+
console.warn('[Fingerprint] WebAssembly.instantiateStreaming failed, falling back to compile:', streamingError);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!instance) {
|
|
1332
|
+
const response = await fetch(wasmPath);
|
|
1333
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1334
|
+
const module = await WebAssembly.compile(arrayBuffer);
|
|
1335
|
+
instance = await WebAssembly.instantiate(module, {});
|
|
1336
|
+
}
|
|
1255
1337
|
const exports = instance.exports;
|
|
1256
1338
|
const memory = exports.memory;
|
|
1257
1339
|
activeCyrb53 = (str) => {
|
|
@@ -1267,7 +1349,7 @@ const ClientLibrary = {
|
|
|
1267
1349
|
}
|
|
1268
1350
|
return;
|
|
1269
1351
|
}
|
|
1270
|
-
// 1.
|
|
1352
|
+
// 1. Inject WASM loader script
|
|
1271
1353
|
const script = document.createElement('script');
|
|
1272
1354
|
script.src = wasmPath;
|
|
1273
1355
|
await new Promise((resolve, reject) => {
|
|
@@ -1276,12 +1358,12 @@ const ClientLibrary = {
|
|
|
1276
1358
|
document.head.appendChild(script);
|
|
1277
1359
|
});
|
|
1278
1360
|
|
|
1279
|
-
// 2.
|
|
1361
|
+
// 2. Await global module factory
|
|
1280
1362
|
if (typeof window.createFingerprintModule !== 'function') {
|
|
1281
1363
|
throw new Error('WASM loader script did not expose createFingerprintModule.');
|
|
1282
1364
|
}
|
|
1283
1365
|
|
|
1284
|
-
// 3.
|
|
1366
|
+
// 3. Initialize module
|
|
1285
1367
|
const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
|
|
1286
1368
|
const wasmModule = await window.createFingerprintModule({
|
|
1287
1369
|
instantiateWasm: (imports, successCallback) => {
|
|
@@ -1342,13 +1424,13 @@ const ClientLibrary = {
|
|
|
1342
1424
|
window.wasmModule = wasmModule;
|
|
1343
1425
|
ClientLibrary.wasmModule = wasmModule;
|
|
1344
1426
|
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1427
|
+
// Static memory pool: 4KB pre-allocated buffer
|
|
1428
|
+
// Avoids heap malloc/free churn on rapid mouse and keyboard tracking
|
|
1347
1429
|
const staticBufferSize = 4096;
|
|
1348
1430
|
const staticBufferPtr = wasmModule._malloc(staticBufferSize);
|
|
1349
1431
|
const encoder = new TextEncoder();
|
|
1350
1432
|
|
|
1351
|
-
// 4.
|
|
1433
|
+
// 4. Swap hash function with WASM implementation
|
|
1352
1434
|
activeCyrb53 = (str) => {
|
|
1353
1435
|
const bytes = encoder.encode(str);
|
|
1354
1436
|
if (bytes.length < staticBufferSize) {
|
|
@@ -1356,12 +1438,12 @@ const ClientLibrary = {
|
|
|
1356
1438
|
wasmModule.HEAPU8[staticBufferPtr + bytes.length] = 0; // null-terminator
|
|
1357
1439
|
return wasmModule._hash_string(staticBufferPtr);
|
|
1358
1440
|
}
|
|
1359
|
-
// Fallback
|
|
1441
|
+
// Fallback if string exceeds static buffer size
|
|
1360
1442
|
return wasmModule._hash_string(str);
|
|
1361
1443
|
};
|
|
1362
1444
|
|
|
1363
1445
|
console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
|
|
1364
|
-
//
|
|
1446
|
+
// Tag fingerprint with wasm marker
|
|
1365
1447
|
if (this._cachedBuilder) {
|
|
1366
1448
|
this._cachedBuilder.addRaw('wasm', 'true');
|
|
1367
1449
|
}
|
|
@@ -1373,19 +1455,19 @@ const ClientLibrary = {
|
|
|
1373
1455
|
|
|
1374
1456
|
/**
|
|
1375
1457
|
* @typedef {object} ClientBehaviorMetrics
|
|
1376
|
-
* @property {number} mouseEntropy -
|
|
1377
|
-
* @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory -
|
|
1378
|
-
* @property {number} keystrokeLatency -
|
|
1379
|
-
* @property {boolean} honeypotInteraction -
|
|
1380
|
-
* @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory -
|
|
1381
|
-
* @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory -
|
|
1382
|
-
* @property {number} historyLength -
|
|
1383
|
-
* @property {number} clientTimestamp -
|
|
1384
|
-
* @property {string[]} [trapUrls] -
|
|
1458
|
+
* @property {number} mouseEntropy - Mouse entropy estimate.
|
|
1459
|
+
* @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Mouse movement coordinates.
|
|
1460
|
+
* @property {number} keystrokeLatency - Average latency between keystrokes.
|
|
1461
|
+
* @property {boolean} honeypotInteraction - True if honeypot was triggered.
|
|
1462
|
+
* @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Click interaction history.
|
|
1463
|
+
* @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Touch movement trajectory.
|
|
1464
|
+
* @property {number} historyLength - Browser window.history.length.
|
|
1465
|
+
* @property {number} clientTimestamp - Metrics collection timestamp.
|
|
1466
|
+
* @property {string[]} [trapUrls] - Dynamically injected trap URLs.
|
|
1385
1467
|
*/
|
|
1386
1468
|
/** @type {ClientBehaviorMetrics} */
|
|
1387
1469
|
const metrics = {
|
|
1388
|
-
mouseEntropy: 0, //
|
|
1470
|
+
mouseEntropy: 0, // Retained for compatibility; analysis primarily evaluates trajectory history
|
|
1389
1471
|
mouseMovementsHistory: [],
|
|
1390
1472
|
touchMovementsHistory: [],
|
|
1391
1473
|
clicksHistory: [],
|
|
@@ -1397,18 +1479,18 @@ const metrics = {
|
|
|
1397
1479
|
};
|
|
1398
1480
|
|
|
1399
1481
|
let lastMousePos = { x: 0, y: 0 };
|
|
1400
|
-
let mouseMovementsHistory = []; //
|
|
1401
|
-
let touchMovementsHistory = []; //
|
|
1482
|
+
let mouseMovementsHistory = []; // Mouse movement trajectory
|
|
1483
|
+
let touchMovementsHistory = []; // Touch movement trajectory
|
|
1402
1484
|
const TOUCH_HISTORY_MAX = 100;
|
|
1403
|
-
const MOUSE_HISTORY_MAX = 100;
|
|
1485
|
+
const MOUSE_HISTORY_MAX = 100;
|
|
1404
1486
|
let clicksHistory = [];
|
|
1405
1487
|
const CLICKS_HISTORY_MAX = 50;
|
|
1406
|
-
let activeHoneypotListeners = new Map();
|
|
1488
|
+
let activeHoneypotListeners = new Map();
|
|
1407
1489
|
let keystrokeTimestamps = [];
|
|
1408
|
-
let keystrokeLatencies = [];
|
|
1490
|
+
let keystrokeLatencies = [];
|
|
1409
1491
|
let keystrokeDwellTimes = [];
|
|
1410
1492
|
let keystrokeFlightTimes = [];
|
|
1411
|
-
const KEYSTROKE_HISTORY_MAX = 20; //
|
|
1493
|
+
const KEYSTROKE_HISTORY_MAX = 20; // Keep the last 20 keystroke events
|
|
1412
1494
|
|
|
1413
1495
|
|
|
1414
1496
|
|
|
@@ -1422,6 +1504,8 @@ export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamic
|
|
|
1422
1504
|
export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
|
|
1423
1505
|
export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
|
|
1424
1506
|
export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
|
|
1507
|
+
export const startMotionTracker = ClientLibrary.startMotionTracker.bind(ClientLibrary);
|
|
1508
|
+
export const detectTamperedPrototypes = ClientLibrary.detectTamperedPrototypes.bind(ClientLibrary);
|
|
1425
1509
|
export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
|
|
1426
1510
|
export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
|
|
1427
1511
|
export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
|