@anonympins/fingerprint 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/fingerprint.client.js +1784 -1772
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +198 -28
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +19 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +382 -86
- package/src/php/Config/SecurityProfiles.php +75 -62
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2120 -1837
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +2 -2
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +327 -14
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
|
@@ -1,1773 +1,1785 @@
|
|
|
1
|
-
import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
|
|
2
|
-
import {solveChallenge} from './pow.solver.js';
|
|
3
|
-
|
|
4
|
-
function secureRandom() {
|
|
5
|
-
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
|
|
6
|
-
const array = new Uint32Array(1);
|
|
7
|
-
globalThis.crypto.getRandomValues(array);
|
|
8
|
-
return array[0] / 0xffffffff;
|
|
9
|
-
}
|
|
10
|
-
return Math.random();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Variable storing the active hash function.
|
|
14
|
-
// Defaults to the JavaScript implementation.
|
|
15
|
-
let activeCyrb53 = jsCyrb53;
|
|
16
|
-
let derivedKey = null;
|
|
17
|
-
|
|
18
|
-
async function negotiateSessionKey() {
|
|
19
|
-
if (typeof window === 'undefined' || !window.crypto || !window.crypto.subtle) return;
|
|
20
|
-
try {
|
|
21
|
-
const keyPair = await window.crypto.subtle.generateKey(
|
|
22
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
23
|
-
false,
|
|
24
|
-
["deriveKey"]
|
|
25
|
-
);
|
|
26
|
-
const clientPubKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
27
|
-
const clientPubKeyHex = Array.from(new Uint8Array(clientPubKeyBuffer))
|
|
28
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
29
|
-
.join('');
|
|
30
|
-
|
|
31
|
-
const response = await fetch(window.location.pathname + '?fp_handshake=1', {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type': 'application/json',
|
|
35
|
-
'x-client-ephemeral-key': clientPubKeyHex
|
|
36
|
-
},
|
|
37
|
-
body: JSON.stringify({ fp: ClientLibrary.getDeviceFingerprint() })
|
|
38
|
-
});
|
|
39
|
-
|
|
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
|
-
}
|
|
47
|
-
if (serverPubKeyHex) {
|
|
48
|
-
const serverPubKeyBuffer = new Uint8Array(
|
|
49
|
-
serverPubKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
|
|
50
|
-
);
|
|
51
|
-
const serverPubKey = await window.crypto.subtle.importKey(
|
|
52
|
-
"raw",
|
|
53
|
-
serverPubKeyBuffer,
|
|
54
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
55
|
-
true,
|
|
56
|
-
[]
|
|
57
|
-
);
|
|
58
|
-
derivedKey = await window.crypto.subtle.deriveKey(
|
|
59
|
-
{ name: "ECDH", public: serverPubKey },
|
|
60
|
-
keyPair.privateKey,
|
|
61
|
-
{ name: "HMAC", hash: "SHA-256", length: 256 },
|
|
62
|
-
true,
|
|
63
|
-
["sign"]
|
|
64
|
-
);
|
|
65
|
-
console.log('[Fingerprint] Cryptographic session negotiated successfully.');
|
|
66
|
-
}
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.warn('[Fingerprint] Cryptographic session negotiation failed:', e);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function signMetrics(metricsObj) {
|
|
73
|
-
if (!derivedKey) return metricsObj;
|
|
74
|
-
try {
|
|
75
|
-
const copy = JSON.parse(JSON.stringify(metricsObj));
|
|
76
|
-
delete copy.signature;
|
|
77
|
-
const dataToSign = new TextEncoder().encode(JSON.stringify(copy));
|
|
78
|
-
const signatureBuffer = await window.crypto.subtle.sign(
|
|
79
|
-
"HMAC",
|
|
80
|
-
derivedKey,
|
|
81
|
-
dataToSign
|
|
82
|
-
);
|
|
83
|
-
copy.signature = Array.from(new Uint8Array(signatureBuffer))
|
|
84
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
85
|
-
.join('');
|
|
86
|
-
return copy;
|
|
87
|
-
} catch (e) {
|
|
88
|
-
return metricsObj;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const DB_NAME = 'wasm-cache-db';
|
|
93
|
-
const DB_VERSION = 1;
|
|
94
|
-
const STORE_NAME = 'wasm-modules';
|
|
95
|
-
|
|
96
|
-
const genRandStr = (len = 8) => {
|
|
97
|
-
return Array.from({ length: len }, () => String.fromCharCode(97 + Math.floor(secureRandom() * 26))).join('');
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
function getCachedWasm(url) {
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
if (typeof indexedDB === 'undefined') return resolve(null);
|
|
103
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
104
|
-
request.onupgradeneeded = (e) => {
|
|
105
|
-
const db = e.target.result;
|
|
106
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
107
|
-
db.createObjectStore(STORE_NAME);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
request.onsuccess = (e) => {
|
|
111
|
-
const db = e.target.result;
|
|
112
|
-
try {
|
|
113
|
-
const transaction = db.transaction(STORE_NAME, 'readonly');
|
|
114
|
-
const store = transaction.objectStore(STORE_NAME);
|
|
115
|
-
const getReq = store.get(url);
|
|
116
|
-
getReq.onsuccess = () => resolve(getReq.result);
|
|
117
|
-
getReq.onerror = () => resolve(null);
|
|
118
|
-
} catch (err) {
|
|
119
|
-
resolve(null);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
request.onerror = () => resolve(null);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function cacheWasm(url, data) {
|
|
127
|
-
return new Promise((resolve) => {
|
|
128
|
-
if (typeof indexedDB === 'undefined') return resolve(false);
|
|
129
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
130
|
-
request.onupgradeneeded = (e) => {
|
|
131
|
-
const db = e.target.result;
|
|
132
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
133
|
-
db.createObjectStore(STORE_NAME);
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
request.onsuccess = (e) => {
|
|
137
|
-
const db = e.target.result;
|
|
138
|
-
try {
|
|
139
|
-
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
140
|
-
const store = transaction.objectStore(STORE_NAME);
|
|
141
|
-
store.put(data, url);
|
|
142
|
-
transaction.oncomplete = () => resolve(true);
|
|
143
|
-
transaction.onerror = () => resolve(false);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
resolve(false);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
request.onerror = () => resolve(false);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const ClientLibrary = {
|
|
153
|
-
// Cache to avoid recalculating hardware constants
|
|
154
|
-
_cachedBuilder: null,
|
|
155
|
-
/**
|
|
156
|
-
* @private
|
|
157
|
-
* Dispatches a custom event from the window object.
|
|
158
|
-
* @param {string} eventName - The name of the event.
|
|
159
|
-
* @param {object} [detail={}] - The data to include in the event's detail property.
|
|
160
|
-
*/
|
|
161
|
-
_dispatchEvent(eventName, detail = {}) {
|
|
162
|
-
if (typeof window === 'undefined') return;
|
|
163
|
-
const CustomEventCtor = window.CustomEvent || (typeof CustomEvent !== 'undefined' ? CustomEvent : null);
|
|
164
|
-
if (!CustomEventCtor) return;
|
|
165
|
-
try {
|
|
166
|
-
const event = new CustomEventCtor(`fingerprint:${eventName}`, { detail });
|
|
167
|
-
window.dispatchEvent(event);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Checks if native JavaScript prototypes have been tampered with or hooked (Frida, Puppeteer Stealth).
|
|
174
|
-
*/
|
|
175
|
-
detectTamperedPrototypes() {
|
|
176
|
-
const checkNative = (obj, method) => {
|
|
177
|
-
try {
|
|
178
|
-
if (!obj) return false;
|
|
179
|
-
const fn = obj[method];
|
|
180
|
-
if (!fn) return false;
|
|
181
|
-
const str = Function.prototype.toString.call(fn);
|
|
182
|
-
if (!str.includes('[native code]')) return true; // Standard JS hook
|
|
183
|
-
const desc = Object.getOwnPropertyDescriptor(obj, method);
|
|
184
|
-
if (desc && (!desc.writable && !desc.configurable && desc.value)) return false;
|
|
185
|
-
return false;
|
|
186
|
-
} catch (e) {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const win = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
192
|
-
if (!win) return false;
|
|
193
|
-
|
|
194
|
-
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : win.HTMLCanvasElement;
|
|
195
|
-
const canvas2d = typeof CanvasRenderingContext2D !== 'undefined' ? CanvasRenderingContext2D : win.CanvasRenderingContext2D;
|
|
196
|
-
const webgl = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext : win.WebGLRenderingContext;
|
|
197
|
-
|
|
198
|
-
const isFetchTampered = () => {
|
|
199
|
-
if (ClientLibrary._isFetchPatched) {
|
|
200
|
-
if (ClientLibrary._originalFetch) {
|
|
201
|
-
return !Function.prototype.toString.call(ClientLibrary._originalFetch).includes('[native code]');
|
|
202
|
-
}
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
return checkNative(win, 'fetch');
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
return checkNative(htmlCanvas?.prototype, 'toDataURL') ||
|
|
209
|
-
checkNative(canvas2d?.prototype, 'getImageData') ||
|
|
210
|
-
checkNative(webgl?.prototype, 'getParameter') ||
|
|
211
|
-
isFetchTampered();
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Internal wrapper for the active hash function.
|
|
216
|
-
* @private
|
|
217
|
-
*/
|
|
218
|
-
_hasher: (str, seed) => activeCyrb53(str, seed),
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Generates a Schnorr Zero-Knowledge Proof (ZKP) of the device fingerprint.
|
|
222
|
-
* Ensures compatibility across JS, PHP, and Java verification engines.
|
|
223
|
-
* @param {string} fingerprint - Device fingerprint string.
|
|
224
|
-
* @returns {Promise<string>} Proof in hex format "y:t:s".
|
|
225
|
-
*/
|
|
226
|
-
async generateZkpProof(fingerprint) {
|
|
227
|
-
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
228
|
-
const ZKP_G = 2n;
|
|
229
|
-
const cryptoObj = window.crypto || window.msCrypto;
|
|
230
|
-
|
|
231
|
-
const sha256Hex = async (str) => {
|
|
232
|
-
const encoder = new TextEncoder();
|
|
233
|
-
const data = encoder.encode(str);
|
|
234
|
-
const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
|
|
235
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
236
|
-
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const modPow = (base, exponent, modulus) => {
|
|
240
|
-
if (modulus === 1n) return 0n;
|
|
241
|
-
let result = 1n;
|
|
242
|
-
base = base % modulus;
|
|
243
|
-
while (exponent > 0n) {
|
|
244
|
-
if (exponent % 2n === 1n) {
|
|
245
|
-
result = (result * base) % modulus;
|
|
246
|
-
}
|
|
247
|
-
exponent = exponent >> 1n;
|
|
248
|
-
base = (base * base) % modulus;
|
|
249
|
-
}
|
|
250
|
-
return result;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const xHex = await sha256Hex(fingerprint);
|
|
254
|
-
const x = BigInt('0x' + xHex) % ZKP_P;
|
|
255
|
-
const y = modPow(ZKP_G, x, ZKP_P);
|
|
256
|
-
const randomBytes = new Uint8Array(32);
|
|
257
|
-
cryptoObj.getRandomValues(randomBytes);
|
|
258
|
-
let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
259
|
-
let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
|
|
260
|
-
if (v === 0n) v = 1n;
|
|
261
|
-
const t = modPow(ZKP_G, v, ZKP_P);
|
|
262
|
-
const cStr = ZKP_G.toString() + y.toString() + t.toString();
|
|
263
|
-
const cHex = await sha256Hex(cStr);
|
|
264
|
-
const c = BigInt('0x' + cHex) % ZKP_P;
|
|
265
|
-
const s = (v + c * x) % (ZKP_P - 1n);
|
|
266
|
-
|
|
267
|
-
return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
|
|
268
|
-
},
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Generates the current device fingerprint.
|
|
272
|
-
*/
|
|
273
|
-
getDeviceFingerprint() {
|
|
274
|
-
if (typeof window === "undefined") {
|
|
275
|
-
console.error("getDeviceFingerprint can only be called on the client-side.");
|
|
276
|
-
return "";
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (!this._cachedBuilder) {
|
|
280
|
-
const nav = window.navigator;
|
|
281
|
-
const screen = window.screen;
|
|
282
|
-
|
|
283
|
-
this._cachedBuilder = new FingerprintBuilder();
|
|
284
|
-
|
|
285
|
-
// 1. Hardware (Very stable): Cores, RAM, GPU, Touch
|
|
286
|
-
this._cachedBuilder.add(
|
|
287
|
-
"hw",
|
|
288
|
-
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// 2. Geo/Locale (Stable unless traveling/VPN): Timezone, Language
|
|
292
|
-
this._cachedBuilder.add(
|
|
293
|
-
"geo",
|
|
294
|
-
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
// 3. Screen (Stable unless monitor/zoom changes): Dimensions, ColorDepth
|
|
298
|
-
this._cachedBuilder.add(
|
|
299
|
-
"scr",
|
|
300
|
-
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// 4. Platform (Stable) : OS, Engine
|
|
304
|
-
this._cachedBuilder.add("os", nav.platform);
|
|
305
|
-
|
|
306
|
-
// 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
|
|
307
|
-
try {
|
|
308
|
-
const canvas = document.createElement("canvas");
|
|
309
|
-
const gl =
|
|
310
|
-
canvas.getContext("webgl2") ||
|
|
311
|
-
canvas.getContext("webgl") ||
|
|
312
|
-
canvas.getContext("experimental-webgl");
|
|
313
|
-
if (gl) {
|
|
314
|
-
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
315
|
-
if (debugInfo) {
|
|
316
|
-
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
317
|
-
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
318
|
-
this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
} catch (e) {
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 6. Canvas Fingerprinting (Rendering quirks)
|
|
325
|
-
try {
|
|
326
|
-
const canvas = document.createElement("canvas");
|
|
327
|
-
const ctx = canvas.getContext("2d");
|
|
328
|
-
if (ctx) {
|
|
329
|
-
canvas.width = 200;
|
|
330
|
-
canvas.height = 50;
|
|
331
|
-
ctx.textBaseline = "alphabetic";
|
|
332
|
-
ctx.font = "14px 'Arial'";
|
|
333
|
-
ctx.fillStyle = "#f60";
|
|
334
|
-
ctx.fillRect(125, 1, 62, 20);
|
|
335
|
-
ctx.fillStyle = "#069";
|
|
336
|
-
ctx.fillText("fingerprint", 2, 15);
|
|
337
|
-
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
338
|
-
ctx.fillText("fingerprint", 4, 17);
|
|
339
|
-
this._cachedBuilder.add("cvs", canvas.toDataURL());
|
|
340
|
-
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 7. Detection of Chrome DevTools Protocol (CDP) artifacts
|
|
345
|
-
// Injected by automation tools
|
|
346
|
-
const cdpFootprints = [
|
|
347
|
-
'cdc_adoQpoasnfa76pfcZLmcfl_Array',
|
|
348
|
-
'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
|
|
349
|
-
'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
|
|
350
|
-
'$cdc_asdjflasutopfhvcZLmcfl_',
|
|
351
|
-
'_selenium',
|
|
352
|
-
'_driver'
|
|
353
|
-
];
|
|
354
|
-
if (cdpFootprints.some(fp => window[fp])) {
|
|
355
|
-
this._cachedBuilder.add("cdp", "true");
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 8. Bot Detection (Hidden indicators)
|
|
359
|
-
if (nav.webdriver) this._cachedBuilder.add("bot", "true");
|
|
360
|
-
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
361
|
-
this._cachedBuilder.add("tampered", "true");
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return this._cachedBuilder.toString();
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Generates a request signature including context.
|
|
370
|
-
* @param {object} payload
|
|
371
|
-
*/
|
|
372
|
-
/**
|
|
373
|
-
* Génère une signature de requête incluant le contexte.
|
|
374
|
-
* @param {object} payload
|
|
375
|
-
*/
|
|
376
|
-
generateRequestSignature(payload = {}) {
|
|
377
|
-
const deviceFp = this.getDeviceFingerprint();
|
|
378
|
-
const sortedPayload = Object.keys(payload)
|
|
379
|
-
.sort()
|
|
380
|
-
.map((k) => `${k}=${payload[k]}`)
|
|
381
|
-
.join("&");
|
|
382
|
-
const payloadHash = this._hasher(sortedPayload);
|
|
383
|
-
return `${deviceFp}|req:${payloadHash}`;
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Generates an HMAC-SHA256 signature using the Web Crypto API.
|
|
388
|
-
* @param {object} payload - Data payload to sign.
|
|
389
|
-
* @param {string} secret - Shared secret key.
|
|
390
|
-
* @returns {Promise<string>} Hexadecimal signature.
|
|
391
|
-
*/
|
|
392
|
-
async generateClientSideSignature(payload, secret) {
|
|
393
|
-
const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
|
|
394
|
-
const encoder = new TextEncoder();
|
|
395
|
-
const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
396
|
-
name: "HMAC",
|
|
397
|
-
hash: "SHA-256"
|
|
398
|
-
}, false, ["sign"]);
|
|
399
|
-
const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
|
|
400
|
-
const hashArray = Array.from(new Uint8Array(signatureBuffer));
|
|
401
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* @internal
|
|
406
|
-
* Resets the cached fingerprint builder. Used for testing purposes.
|
|
407
|
-
*/
|
|
408
|
-
_resetCache() {
|
|
409
|
-
// Reset hasher to default JS implementation.
|
|
410
|
-
activeCyrb53 = jsCyrb53;
|
|
411
|
-
this._cachedBuilder = null;
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Injects invisible phantom interactive elements and forms within a closed Shadow DOM container to trap bots (focus/hover/click).
|
|
416
|
-
*/
|
|
417
|
-
injectPhantomTraps() {
|
|
418
|
-
if (typeof document === 'undefined' || !document.body) return;
|
|
419
|
-
|
|
420
|
-
const host = document.createElement('div');
|
|
421
|
-
host.setAttribute('aria-hidden', 'true');
|
|
422
|
-
host.style.position = 'absolute';
|
|
423
|
-
host.style.left = '-9999px';
|
|
424
|
-
host.style.top = '-9999px';
|
|
425
|
-
host.style.width = '0';
|
|
426
|
-
host.style.height = '0';
|
|
427
|
-
host.style.overflow = 'hidden';
|
|
428
|
-
|
|
429
|
-
const shadow = typeof host.attachShadow === 'function'
|
|
430
|
-
? host.attachShadow({ mode: 'closed' })
|
|
431
|
-
: host;
|
|
432
|
-
|
|
433
|
-
const style = document.createElement('style');
|
|
434
|
-
style.textContent = `
|
|
435
|
-
:host {
|
|
436
|
-
position: absolute;
|
|
437
|
-
left: -9999px;
|
|
438
|
-
top: -9999px;
|
|
439
|
-
width: 0;
|
|
440
|
-
height: 0;
|
|
441
|
-
overflow: hidden;
|
|
442
|
-
}
|
|
443
|
-
a, input, button, form {
|
|
444
|
-
opacity: 0.001;
|
|
445
|
-
pointer-events: auto;
|
|
446
|
-
}
|
|
447
|
-
`;
|
|
448
|
-
shadow.appendChild(style);
|
|
449
|
-
|
|
450
|
-
const triggerTrap = () => {
|
|
451
|
-
this.onHoneypotTrigger();
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
// 1. Phantom interactive link
|
|
455
|
-
const phantom = document.createElement('a');
|
|
456
|
-
phantom.href = '#';
|
|
457
|
-
// Random deceptive name to attract automated link/form parsers
|
|
458
|
-
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'];
|
|
459
|
-
phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
|
|
460
|
-
phantom.className = genRandStr(8);
|
|
461
|
-
phantom.tabIndex = 0; // In natural tab flow for crawler parsers
|
|
462
|
-
phantom.addEventListener('focus', triggerTrap, { passive: true });
|
|
463
|
-
phantom.addEventListener('mouseover', triggerTrap, { passive: true });
|
|
464
|
-
phantom.addEventListener('click', (e) => {
|
|
465
|
-
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
|
466
|
-
triggerTrap();
|
|
467
|
-
}, { passive: false });
|
|
468
|
-
|
|
469
|
-
// 2. Deceptive hidden form with bait input
|
|
470
|
-
const phantomForm = document.createElement('form');
|
|
471
|
-
phantomForm.action = '#';
|
|
472
|
-
phantomForm.tabIndex = -1;
|
|
473
|
-
const inputNames = ['session_token', 'admin_pwd', 'secret_key', 'auth_bypass'];
|
|
474
|
-
const phantomInput = document.createElement('input');
|
|
475
|
-
phantomInput.type = 'text';
|
|
476
|
-
phantomInput.name = inputNames[Math.floor(secureRandom() * inputNames.length)] + '_' + genRandStr(4);
|
|
477
|
-
phantomInput.tabIndex = 0;
|
|
478
|
-
phantomInput.autocomplete = 'off';
|
|
479
|
-
phantomInput.addEventListener('focus', triggerTrap, { passive: true });
|
|
480
|
-
phantomInput.addEventListener('input', triggerTrap, { passive: true });
|
|
481
|
-
phantomInput.addEventListener('click', triggerTrap, { passive: true });
|
|
482
|
-
phantomInput.addEventListener('change', triggerTrap, { passive: true });
|
|
483
|
-
|
|
484
|
-
phantomForm.appendChild(phantomInput);
|
|
485
|
-
shadow.appendChild(phantom);
|
|
486
|
-
shadow.appendChild(phantomForm);
|
|
487
|
-
|
|
488
|
-
document.body.appendChild(host);
|
|
489
|
-
},
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Measures gyroscope/accelerometer noise to detect phone farm racks.
|
|
493
|
-
*/
|
|
494
|
-
startMotionTracker() {
|
|
495
|
-
if (this._motionTrackerAttached || typeof window === 'undefined') return;
|
|
496
|
-
this._motionTrackerAttached = true;
|
|
497
|
-
const motionSamples = [];
|
|
498
|
-
const handleMotion = (e) => {
|
|
499
|
-
const acc = e.accelerationIncludingGravity || e.acceleration;
|
|
500
|
-
if (!acc) return;
|
|
501
|
-
const mag = Math.sqrt((acc.x || 0)**2 + (acc.y || 0)**2 + (acc.z || 0)**2);
|
|
502
|
-
motionSamples.push(mag);
|
|
503
|
-
if (motionSamples.length > 20) motionSamples.shift();
|
|
504
|
-
if (motionSamples.length >= 10) {
|
|
505
|
-
const avg = motionSamples.reduce((a, b) => a + b, 0) / motionSamples.length;
|
|
506
|
-
const variance = motionSamples.reduce((a, b) => a + (b - avg)**2, 0) / motionSamples.length;
|
|
507
|
-
metrics.motionVariance = Math.round(variance * 10000) / 10000;
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
try {
|
|
511
|
-
window.addEventListener('devicemotion', handleMotion, { passive: true });
|
|
512
|
-
} catch (e) {}
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Starts tracking touch events on mobile/tablet devices.
|
|
517
|
-
*/
|
|
518
|
-
startTouchEventTracker() {
|
|
519
|
-
if (this._touchTrackerAttached) return;
|
|
520
|
-
this._touchTrackerAttached = true;
|
|
521
|
-
|
|
522
|
-
const handleTouch = (e) => {
|
|
523
|
-
if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
|
|
524
|
-
touchMovementsHistory.shift();
|
|
525
|
-
}
|
|
526
|
-
const touch = e.touches[0] || e.changedTouches[0];
|
|
527
|
-
if (!touch) return;
|
|
528
|
-
|
|
529
|
-
const radiusX = touch.radiusX || 0;
|
|
530
|
-
const radiusY = touch.radiusY || 0;
|
|
531
|
-
const radius = (radiusX + radiusY) / 2;
|
|
532
|
-
const force = touch.force || touch.webkitForce || 0;
|
|
533
|
-
|
|
534
|
-
touchMovementsHistory.push({
|
|
535
|
-
x: touch.clientX,
|
|
536
|
-
y: touch.clientY,
|
|
537
|
-
t: performance.now(),
|
|
538
|
-
p: force,
|
|
539
|
-
r: radius,
|
|
540
|
-
num: e.touches.length
|
|
541
|
-
});
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
document.addEventListener('touchstart', handleTouch, { passive: true });
|
|
545
|
-
document.addEventListener('touchmove', handleTouch, { passive: true });
|
|
546
|
-
document.addEventListener('touchend', handleTouch, { passive: true });
|
|
547
|
-
},
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Starts tracking display regularity (V-Sync/rAF) to detect software framebuffers lacking V-Sync.
|
|
551
|
-
*/
|
|
552
|
-
startRenderingTracker() {
|
|
553
|
-
if (this._renderingTrackerAttached) return;
|
|
554
|
-
this._renderingTrackerAttached = true;
|
|
555
|
-
|
|
556
|
-
if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
|
|
557
|
-
|
|
558
|
-
const rAfTimestamps = [];
|
|
559
|
-
let lastTime = performance.now();
|
|
560
|
-
const maxSamples = 15;
|
|
561
|
-
|
|
562
|
-
const checkOffscreenAnom = () => {
|
|
563
|
-
try {
|
|
564
|
-
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : window.HTMLCanvasElement;
|
|
565
|
-
if ('OffscreenCanvas' in window && htmlCanvas?.prototype?.transferControlToOffscreen) {
|
|
566
|
-
const nativeToString = Function.prototype.toString.call(htmlCanvas.prototype.transferControlToOffscreen);
|
|
567
|
-
return !nativeToString.includes('[native code]');
|
|
568
|
-
}
|
|
569
|
-
} catch (e) {}
|
|
570
|
-
return false;
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
const loop = (time) => {
|
|
574
|
-
const delta = time - lastTime;
|
|
575
|
-
lastTime = time;
|
|
576
|
-
if (rAfTimestamps.length < maxSamples) {
|
|
577
|
-
if (rAfTimestamps.length > 0) { // Skip first delta
|
|
578
|
-
rAfTimestamps.push(delta);
|
|
579
|
-
}
|
|
580
|
-
window.requestAnimationFrame(loop);
|
|
581
|
-
} else {
|
|
582
|
-
const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
|
|
583
|
-
const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
|
|
584
|
-
const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
|
|
585
|
-
|
|
586
|
-
metrics.rendering = {
|
|
587
|
-
fps: Math.round((1000 / avg) * 100) / 100,
|
|
588
|
-
jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
|
|
589
|
-
offscreenAnom: checkOffscreenAnom()
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
window.requestAnimationFrame(loop);
|
|
594
|
-
},
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Initializes persistent Proof-of-Space allocation in local IndexedDB.
|
|
598
|
-
*/
|
|
599
|
-
async initializeSpace(seed, sizeMb) {
|
|
600
|
-
return new Promise((resolve, reject) => {
|
|
601
|
-
const request = indexedDB.open('pospace-db', 1);
|
|
602
|
-
request.onupgradeneeded = (e) => {
|
|
603
|
-
const db = e.target.result;
|
|
604
|
-
if (!db.objectStoreNames.contains('blocks')) {
|
|
605
|
-
db.createObjectStore('blocks');
|
|
606
|
-
}
|
|
607
|
-
};
|
|
608
|
-
request.onsuccess = async (e) => {
|
|
609
|
-
const db = e.target.result;
|
|
610
|
-
const tx = db.transaction('blocks', 'readwrite');
|
|
611
|
-
const store = tx.objectStore('blocks');
|
|
612
|
-
|
|
613
|
-
const maxBlocks = sizeMb * 1024;
|
|
614
|
-
const countReq = store.count();
|
|
615
|
-
countReq.onsuccess = async () => {
|
|
616
|
-
if (countReq.result < maxBlocks) {
|
|
617
|
-
for (let i = 0; i < maxBlocks; i++) {
|
|
618
|
-
const block = new Uint8Array(1024);
|
|
619
|
-
let h = 5381;
|
|
620
|
-
for (let j = 0; j < seed.length; j++) {
|
|
621
|
-
h = (h << 5) + h + seed.charCodeAt(j);
|
|
622
|
-
}
|
|
623
|
-
h = (h << 5) + h + i;
|
|
624
|
-
for (let k = 0; k < 1024; k++) {
|
|
625
|
-
h = Math.imul(h ^ k, 1597334677);
|
|
626
|
-
block[k] = h & 0xff;
|
|
627
|
-
}
|
|
628
|
-
store.put(block, i);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
resolve();
|
|
632
|
-
};
|
|
633
|
-
};
|
|
634
|
-
request.onerror = () => reject(new Error("Failed to open pospace database"));
|
|
635
|
-
});
|
|
636
|
-
},
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Reads a specific block from local IndexedDB formatted as hex.
|
|
640
|
-
*/
|
|
641
|
-
async readSpaceBlock(blockIdx) {
|
|
642
|
-
return new Promise((resolve, reject) => {
|
|
643
|
-
const request = indexedDB.open('pospace-db', 1);
|
|
644
|
-
request.onsuccess = (e) => {
|
|
645
|
-
const db = e.target.result;
|
|
646
|
-
const tx = db.transaction('blocks', 'readonly');
|
|
647
|
-
const store = tx.objectStore('blocks');
|
|
648
|
-
const getReq = store.get(blockIdx);
|
|
649
|
-
getReq.onsuccess = () => {
|
|
650
|
-
const block = getReq.result;
|
|
651
|
-
if (block) {
|
|
652
|
-
const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
653
|
-
resolve(hex);
|
|
654
|
-
} else {
|
|
655
|
-
reject(new Error("Block not found"));
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
getReq.onerror = () => reject(getReq.error);
|
|
659
|
-
};
|
|
660
|
-
request.onerror = () => reject(new Error("Failed to open pospace database"));
|
|
661
|
-
});
|
|
662
|
-
},
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Solves Proof-of-Space by optionally combining a peer's block.
|
|
666
|
-
*/
|
|
667
|
-
async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
|
|
668
|
-
const blocks = [];
|
|
669
|
-
for (const idx of queries) {
|
|
670
|
-
const blockHex = await this.readSpaceBlock(idx);
|
|
671
|
-
blocks.push(blockHex);
|
|
672
|
-
}
|
|
673
|
-
let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
|
|
674
|
-
const encoder = new TextEncoder();
|
|
675
|
-
const data = encoder.encode(finalPayload);
|
|
676
|
-
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
|
|
677
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
678
|
-
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
679
|
-
},
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Starts tracking mouse movements to evaluate entropy.
|
|
683
|
-
* Call once per page lifecycle.
|
|
684
|
-
*/
|
|
685
|
-
startMouseEntropyTracker() {
|
|
686
|
-
// Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
|
|
687
|
-
if (this._mouseTrackerAttached) return;
|
|
688
|
-
this._mouseTrackerAttached = true;
|
|
689
|
-
|
|
690
|
-
document.addEventListener('mousemove', (e) => {
|
|
691
|
-
// Capture trajectory points {x, y, t}
|
|
692
|
-
if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
|
|
693
|
-
// Cap history size to prevent unbounded memory growth
|
|
694
|
-
mouseMovementsHistory.shift();
|
|
695
|
-
}
|
|
696
|
-
mouseMovementsHistory.push({
|
|
697
|
-
x: e.clientX,
|
|
698
|
-
y: e.clientY,
|
|
699
|
-
t: performance.now()
|
|
700
|
-
});
|
|
701
|
-
}, {passive: true});
|
|
702
|
-
},
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Starts tracking keystroke dynamics to evaluate dwell and flight times.
|
|
706
|
-
* Call once per page lifecycle.
|
|
707
|
-
*/
|
|
708
|
-
startKeystrokeDynamicsTracker() {
|
|
709
|
-
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
710
|
-
if (this._keystrokeTrackerAttached) return;
|
|
711
|
-
this._keystrokeTrackerAttached = true;
|
|
712
|
-
|
|
713
|
-
const activeKeys = new Map();
|
|
714
|
-
let lastKeyDownTime = 0;
|
|
715
|
-
let lastKeyName = '';
|
|
716
|
-
|
|
717
|
-
document.addEventListener('keydown', (e) => {
|
|
718
|
-
const now = performance.now();
|
|
719
|
-
const key = e.key;
|
|
720
|
-
const code = e.code;
|
|
721
|
-
if (!key && !code) return;
|
|
722
|
-
|
|
723
|
-
const keyIdentifier = code || key;
|
|
724
|
-
|
|
725
|
-
// Prevent key repeat triggering multiple events
|
|
726
|
-
if (activeKeys.has(keyIdentifier)) return;
|
|
727
|
-
activeKeys.set(keyIdentifier, now);
|
|
728
|
-
|
|
729
|
-
if (keystrokeTimestamps.length > 0) {
|
|
730
|
-
const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
|
|
731
|
-
const latency = now - lastTimestamp;
|
|
732
|
-
if (latency > 10 && latency < 2000) {
|
|
733
|
-
if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
|
|
734
|
-
keystrokeLatencies.shift();
|
|
735
|
-
}
|
|
736
|
-
keystrokeLatencies.push(latency);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
keystrokeTimestamps.push(now);
|
|
740
|
-
|
|
741
|
-
// Flight Time (KeyDown to KeyDown)
|
|
742
|
-
if (lastKeyDownTime > 0) {
|
|
743
|
-
const flightTime = now - lastKeyDownTime;
|
|
744
|
-
if (flightTime > 10 && flightTime < 2000) {
|
|
745
|
-
if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
746
|
-
keystrokeFlightTimes.shift();
|
|
747
|
-
}
|
|
748
|
-
const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
|
|
749
|
-
keystrokeFlightTimes.push({ digraph, time: flightTime });
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
lastKeyDownTime = now;
|
|
753
|
-
lastKeyName = keyIdentifier;
|
|
754
|
-
}, {passive: true});
|
|
755
|
-
|
|
756
|
-
document.addEventListener('keyup', (e) => {
|
|
757
|
-
const now = performance.now();
|
|
758
|
-
const key = e.key;
|
|
759
|
-
const code = e.code;
|
|
760
|
-
if (!key && !code) return;
|
|
761
|
-
|
|
762
|
-
const keyIdentifier = code || key;
|
|
763
|
-
|
|
764
|
-
if (activeKeys.has(keyIdentifier)) {
|
|
765
|
-
const pressTime = activeKeys.get(keyIdentifier);
|
|
766
|
-
const dwellTime = now - pressTime;
|
|
767
|
-
activeKeys.delete(keyIdentifier);
|
|
768
|
-
|
|
769
|
-
if (dwellTime > 5 && dwellTime < 1000) {
|
|
770
|
-
if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
771
|
-
keystrokeDwellTimes.shift();
|
|
772
|
-
}
|
|
773
|
-
keystrokeDwellTimes.push(dwellTime);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}, {passive: true});
|
|
777
|
-
},
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Starts tracking click events to analyze position variance.
|
|
781
|
-
* @private
|
|
782
|
-
*/
|
|
783
|
-
startClickTracker() {
|
|
784
|
-
if (this._clickTrackerAttached) return;
|
|
785
|
-
this._clickTrackerAttached = true;
|
|
786
|
-
|
|
787
|
-
document.addEventListener('click', (e) => {
|
|
788
|
-
if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
|
|
789
|
-
clicksHistory.shift();
|
|
790
|
-
}
|
|
791
|
-
// Generate a simple identifier for the target element
|
|
792
|
-
const target = e.target;
|
|
793
|
-
const targetId = target.id || target.name || target.tagName;
|
|
794
|
-
|
|
795
|
-
clicksHistory.push({
|
|
796
|
-
x: e.clientX,
|
|
797
|
-
y: e.clientY,
|
|
798
|
-
t: performance.now(),
|
|
799
|
-
targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
|
|
800
|
-
});
|
|
801
|
-
}, { passive: true });
|
|
802
|
-
},
|
|
803
|
-
/**
|
|
804
|
-
* Initializes or resets client-side honeypots for immediate detection.
|
|
805
|
-
* Previous listeners are removed before attaching new ones.
|
|
806
|
-
* @param {string[]} honeypotFieldNames - Hidden form field names.
|
|
807
|
-
*/
|
|
808
|
-
initializeHoneypots(honeypotFieldNames) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
host
|
|
835
|
-
host.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
input
|
|
879
|
-
input.
|
|
880
|
-
input.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (metrics.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return
|
|
1108
|
-
},
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
*
|
|
1112
|
-
* @
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
this.
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
.${
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
if (
|
|
1510
|
-
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
this.
|
|
1517
|
-
}
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (
|
|
1528
|
-
this.
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
/**
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
export const
|
|
1750
|
-
export const
|
|
1751
|
-
export const
|
|
1752
|
-
export const
|
|
1753
|
-
export const
|
|
1754
|
-
export const
|
|
1755
|
-
export const
|
|
1756
|
-
export const
|
|
1757
|
-
export const
|
|
1758
|
-
export const
|
|
1759
|
-
export const
|
|
1760
|
-
export const
|
|
1761
|
-
export const
|
|
1762
|
-
export const
|
|
1763
|
-
export const
|
|
1764
|
-
export const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
export
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1
|
+
import {cyrb53 as jsCyrb53, FingerprintBuilder} from './fingerprint.builder.js';
|
|
2
|
+
import {solveChallenge} from './pow.solver.js';
|
|
3
|
+
|
|
4
|
+
function secureRandom() {
|
|
5
|
+
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
|
|
6
|
+
const array = new Uint32Array(1);
|
|
7
|
+
globalThis.crypto.getRandomValues(array);
|
|
8
|
+
return array[0] / 0xffffffff;
|
|
9
|
+
}
|
|
10
|
+
return Math.random();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Variable storing the active hash function.
|
|
14
|
+
// Defaults to the JavaScript implementation.
|
|
15
|
+
let activeCyrb53 = jsCyrb53;
|
|
16
|
+
let derivedKey = null;
|
|
17
|
+
|
|
18
|
+
async function negotiateSessionKey() {
|
|
19
|
+
if (typeof window === 'undefined' || !window.crypto || !window.crypto.subtle) return;
|
|
20
|
+
try {
|
|
21
|
+
const keyPair = await window.crypto.subtle.generateKey(
|
|
22
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
23
|
+
false,
|
|
24
|
+
["deriveKey"]
|
|
25
|
+
);
|
|
26
|
+
const clientPubKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
27
|
+
const clientPubKeyHex = Array.from(new Uint8Array(clientPubKeyBuffer))
|
|
28
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
29
|
+
.join('');
|
|
30
|
+
|
|
31
|
+
const response = await fetch(window.location.pathname + '?fp_handshake=1', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'x-client-ephemeral-key': clientPubKeyHex
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({ fp: ClientLibrary.getDeviceFingerprint() })
|
|
38
|
+
});
|
|
39
|
+
|
|
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
|
+
}
|
|
47
|
+
if (serverPubKeyHex) {
|
|
48
|
+
const serverPubKeyBuffer = new Uint8Array(
|
|
49
|
+
serverPubKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
|
|
50
|
+
);
|
|
51
|
+
const serverPubKey = await window.crypto.subtle.importKey(
|
|
52
|
+
"raw",
|
|
53
|
+
serverPubKeyBuffer,
|
|
54
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
55
|
+
true,
|
|
56
|
+
[]
|
|
57
|
+
);
|
|
58
|
+
derivedKey = await window.crypto.subtle.deriveKey(
|
|
59
|
+
{ name: "ECDH", public: serverPubKey },
|
|
60
|
+
keyPair.privateKey,
|
|
61
|
+
{ name: "HMAC", hash: "SHA-256", length: 256 },
|
|
62
|
+
true,
|
|
63
|
+
["sign"]
|
|
64
|
+
);
|
|
65
|
+
console.log('[Fingerprint] Cryptographic session negotiated successfully.');
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.warn('[Fingerprint] Cryptographic session negotiation failed:', e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function signMetrics(metricsObj) {
|
|
73
|
+
if (!derivedKey) return metricsObj;
|
|
74
|
+
try {
|
|
75
|
+
const copy = JSON.parse(JSON.stringify(metricsObj));
|
|
76
|
+
delete copy.signature;
|
|
77
|
+
const dataToSign = new TextEncoder().encode(JSON.stringify(copy));
|
|
78
|
+
const signatureBuffer = await window.crypto.subtle.sign(
|
|
79
|
+
"HMAC",
|
|
80
|
+
derivedKey,
|
|
81
|
+
dataToSign
|
|
82
|
+
);
|
|
83
|
+
copy.signature = Array.from(new Uint8Array(signatureBuffer))
|
|
84
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
85
|
+
.join('');
|
|
86
|
+
return copy;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return metricsObj;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DB_NAME = 'wasm-cache-db';
|
|
93
|
+
const DB_VERSION = 1;
|
|
94
|
+
const STORE_NAME = 'wasm-modules';
|
|
95
|
+
|
|
96
|
+
const genRandStr = (len = 8) => {
|
|
97
|
+
return Array.from({ length: len }, () => String.fromCharCode(97 + Math.floor(secureRandom() * 26))).join('');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function getCachedWasm(url) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
if (typeof indexedDB === 'undefined') return resolve(null);
|
|
103
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
104
|
+
request.onupgradeneeded = (e) => {
|
|
105
|
+
const db = e.target.result;
|
|
106
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
107
|
+
db.createObjectStore(STORE_NAME);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
request.onsuccess = (e) => {
|
|
111
|
+
const db = e.target.result;
|
|
112
|
+
try {
|
|
113
|
+
const transaction = db.transaction(STORE_NAME, 'readonly');
|
|
114
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
115
|
+
const getReq = store.get(url);
|
|
116
|
+
getReq.onsuccess = () => resolve(getReq.result);
|
|
117
|
+
getReq.onerror = () => resolve(null);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
resolve(null);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
request.onerror = () => resolve(null);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cacheWasm(url, data) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
if (typeof indexedDB === 'undefined') return resolve(false);
|
|
129
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
130
|
+
request.onupgradeneeded = (e) => {
|
|
131
|
+
const db = e.target.result;
|
|
132
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
133
|
+
db.createObjectStore(STORE_NAME);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
request.onsuccess = (e) => {
|
|
137
|
+
const db = e.target.result;
|
|
138
|
+
try {
|
|
139
|
+
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
140
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
141
|
+
store.put(data, url);
|
|
142
|
+
transaction.oncomplete = () => resolve(true);
|
|
143
|
+
transaction.onerror = () => resolve(false);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
resolve(false);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
request.onerror = () => resolve(false);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ClientLibrary = {
|
|
153
|
+
// Cache to avoid recalculating hardware constants
|
|
154
|
+
_cachedBuilder: null,
|
|
155
|
+
/**
|
|
156
|
+
* @private
|
|
157
|
+
* Dispatches a custom event from the window object.
|
|
158
|
+
* @param {string} eventName - The name of the event.
|
|
159
|
+
* @param {object} [detail={}] - The data to include in the event's detail property.
|
|
160
|
+
*/
|
|
161
|
+
_dispatchEvent(eventName, detail = {}) {
|
|
162
|
+
if (typeof window === 'undefined') return;
|
|
163
|
+
const CustomEventCtor = window.CustomEvent || (typeof CustomEvent !== 'undefined' ? CustomEvent : null);
|
|
164
|
+
if (!CustomEventCtor) return;
|
|
165
|
+
try {
|
|
166
|
+
const event = new CustomEventCtor(`fingerprint:${eventName}`, { detail });
|
|
167
|
+
window.dispatchEvent(event);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Checks if native JavaScript prototypes have been tampered with or hooked (Frida, Puppeteer Stealth).
|
|
174
|
+
*/
|
|
175
|
+
detectTamperedPrototypes() {
|
|
176
|
+
const checkNative = (obj, method) => {
|
|
177
|
+
try {
|
|
178
|
+
if (!obj) return false;
|
|
179
|
+
const fn = obj[method];
|
|
180
|
+
if (!fn) return false;
|
|
181
|
+
const str = Function.prototype.toString.call(fn);
|
|
182
|
+
if (!str.includes('[native code]')) return true; // Standard JS hook
|
|
183
|
+
const desc = Object.getOwnPropertyDescriptor(obj, method);
|
|
184
|
+
if (desc && (!desc.writable && !desc.configurable && desc.value)) return false;
|
|
185
|
+
return false;
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const win = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
192
|
+
if (!win) return false;
|
|
193
|
+
|
|
194
|
+
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : win.HTMLCanvasElement;
|
|
195
|
+
const canvas2d = typeof CanvasRenderingContext2D !== 'undefined' ? CanvasRenderingContext2D : win.CanvasRenderingContext2D;
|
|
196
|
+
const webgl = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext : win.WebGLRenderingContext;
|
|
197
|
+
|
|
198
|
+
const isFetchTampered = () => {
|
|
199
|
+
if (ClientLibrary._isFetchPatched) {
|
|
200
|
+
if (ClientLibrary._originalFetch) {
|
|
201
|
+
return !Function.prototype.toString.call(ClientLibrary._originalFetch).includes('[native code]');
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return checkNative(win, 'fetch');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return checkNative(htmlCanvas?.prototype, 'toDataURL') ||
|
|
209
|
+
checkNative(canvas2d?.prototype, 'getImageData') ||
|
|
210
|
+
checkNative(webgl?.prototype, 'getParameter') ||
|
|
211
|
+
isFetchTampered();
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Internal wrapper for the active hash function.
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
_hasher: (str, seed) => activeCyrb53(str, seed),
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generates a Schnorr Zero-Knowledge Proof (ZKP) of the device fingerprint.
|
|
222
|
+
* Ensures compatibility across JS, PHP, and Java verification engines.
|
|
223
|
+
* @param {string} fingerprint - Device fingerprint string.
|
|
224
|
+
* @returns {Promise<string>} Proof in hex format "y:t:s".
|
|
225
|
+
*/
|
|
226
|
+
async generateZkpProof(fingerprint) {
|
|
227
|
+
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
228
|
+
const ZKP_G = 2n;
|
|
229
|
+
const cryptoObj = window.crypto || window.msCrypto;
|
|
230
|
+
|
|
231
|
+
const sha256Hex = async (str) => {
|
|
232
|
+
const encoder = new TextEncoder();
|
|
233
|
+
const data = encoder.encode(str);
|
|
234
|
+
const hashBuffer = await cryptoObj.subtle.digest('SHA-256', data);
|
|
235
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
236
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const modPow = (base, exponent, modulus) => {
|
|
240
|
+
if (modulus === 1n) return 0n;
|
|
241
|
+
let result = 1n;
|
|
242
|
+
base = base % modulus;
|
|
243
|
+
while (exponent > 0n) {
|
|
244
|
+
if (exponent % 2n === 1n) {
|
|
245
|
+
result = (result * base) % modulus;
|
|
246
|
+
}
|
|
247
|
+
exponent = exponent >> 1n;
|
|
248
|
+
base = (base * base) % modulus;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const xHex = await sha256Hex(fingerprint);
|
|
254
|
+
const x = BigInt('0x' + xHex) % ZKP_P;
|
|
255
|
+
const y = modPow(ZKP_G, x, ZKP_P);
|
|
256
|
+
const randomBytes = new Uint8Array(32);
|
|
257
|
+
cryptoObj.getRandomValues(randomBytes);
|
|
258
|
+
let vHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
259
|
+
let v = BigInt('0x' + vHex) % (ZKP_P - 1n);
|
|
260
|
+
if (v === 0n) v = 1n;
|
|
261
|
+
const t = modPow(ZKP_G, v, ZKP_P);
|
|
262
|
+
const cStr = ZKP_G.toString() + y.toString() + t.toString();
|
|
263
|
+
const cHex = await sha256Hex(cStr);
|
|
264
|
+
const c = BigInt('0x' + cHex) % ZKP_P;
|
|
265
|
+
const s = (v + c * x) % (ZKP_P - 1n);
|
|
266
|
+
|
|
267
|
+
return `${y.toString(16)}:${t.toString(16)}:${s.toString(16)}`;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generates the current device fingerprint.
|
|
272
|
+
*/
|
|
273
|
+
getDeviceFingerprint() {
|
|
274
|
+
if (typeof window === "undefined") {
|
|
275
|
+
console.error("getDeviceFingerprint can only be called on the client-side.");
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!this._cachedBuilder) {
|
|
280
|
+
const nav = window.navigator;
|
|
281
|
+
const screen = window.screen;
|
|
282
|
+
|
|
283
|
+
this._cachedBuilder = new FingerprintBuilder();
|
|
284
|
+
|
|
285
|
+
// 1. Hardware (Very stable): Cores, RAM, GPU, Touch
|
|
286
|
+
this._cachedBuilder.add(
|
|
287
|
+
"hw",
|
|
288
|
+
`${nav.hardwareConcurrency}_${nav.deviceMemory}_${nav.maxTouchPoints}`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// 2. Geo/Locale (Stable unless traveling/VPN): Timezone, Language
|
|
292
|
+
this._cachedBuilder.add(
|
|
293
|
+
"geo",
|
|
294
|
+
`${Intl.DateTimeFormat().resolvedOptions().timeZone}_${nav.language}_${new Date().getTimezoneOffset()}`,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// 3. Screen (Stable unless monitor/zoom changes): Dimensions, ColorDepth
|
|
298
|
+
this._cachedBuilder.add(
|
|
299
|
+
"scr",
|
|
300
|
+
`${screen.width}x${screen.height}_${screen.colorDepth}`,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// 4. Platform (Stable) : OS, Engine
|
|
304
|
+
this._cachedBuilder.add("os", nav.platform);
|
|
305
|
+
|
|
306
|
+
// 5. Graphics (WebGL Vendor/Renderer) - Invariant matériel fort
|
|
307
|
+
try {
|
|
308
|
+
const canvas = document.createElement("canvas");
|
|
309
|
+
const gl =
|
|
310
|
+
canvas.getContext("webgl2") ||
|
|
311
|
+
canvas.getContext("webgl") ||
|
|
312
|
+
canvas.getContext("experimental-webgl");
|
|
313
|
+
if (gl) {
|
|
314
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
315
|
+
if (debugInfo) {
|
|
316
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
317
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
318
|
+
this._cachedBuilder.add("gpu", `${vendor}_${renderer}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 6. Canvas Fingerprinting (Rendering quirks)
|
|
325
|
+
try {
|
|
326
|
+
const canvas = document.createElement("canvas");
|
|
327
|
+
const ctx = canvas.getContext("2d");
|
|
328
|
+
if (ctx) {
|
|
329
|
+
canvas.width = 200;
|
|
330
|
+
canvas.height = 50;
|
|
331
|
+
ctx.textBaseline = "alphabetic";
|
|
332
|
+
ctx.font = "14px 'Arial'";
|
|
333
|
+
ctx.fillStyle = "#f60";
|
|
334
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
335
|
+
ctx.fillStyle = "#069";
|
|
336
|
+
ctx.fillText("fingerprint", 2, 15);
|
|
337
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
338
|
+
ctx.fillText("fingerprint", 4, 17);
|
|
339
|
+
this._cachedBuilder.add("cvs", canvas.toDataURL());
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 7. Detection of Chrome DevTools Protocol (CDP) artifacts
|
|
345
|
+
// Injected by automation tools
|
|
346
|
+
const cdpFootprints = [
|
|
347
|
+
'cdc_adoQpoasnfa76pfcZLmcfl_Array',
|
|
348
|
+
'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
|
|
349
|
+
'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
|
|
350
|
+
'$cdc_asdjflasutopfhvcZLmcfl_',
|
|
351
|
+
'_selenium',
|
|
352
|
+
'_driver'
|
|
353
|
+
];
|
|
354
|
+
if (cdpFootprints.some(fp => window[fp])) {
|
|
355
|
+
this._cachedBuilder.add("cdp", "true");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 8. Bot Detection (Hidden indicators)
|
|
359
|
+
if (nav.webdriver) this._cachedBuilder.add("bot", "true");
|
|
360
|
+
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
361
|
+
this._cachedBuilder.add("tampered", "true");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return this._cachedBuilder.toString();
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Generates a request signature including context.
|
|
370
|
+
* @param {object} payload
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* Génère une signature de requête incluant le contexte.
|
|
374
|
+
* @param {object} payload
|
|
375
|
+
*/
|
|
376
|
+
generateRequestSignature(payload = {}) {
|
|
377
|
+
const deviceFp = this.getDeviceFingerprint();
|
|
378
|
+
const sortedPayload = Object.keys(payload)
|
|
379
|
+
.sort()
|
|
380
|
+
.map((k) => `${k}=${payload[k]}`)
|
|
381
|
+
.join("&");
|
|
382
|
+
const payloadHash = this._hasher(sortedPayload);
|
|
383
|
+
return `${deviceFp}|req:${payloadHash}`;
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Generates an HMAC-SHA256 signature using the Web Crypto API.
|
|
388
|
+
* @param {object} payload - Data payload to sign.
|
|
389
|
+
* @param {string} secret - Shared secret key.
|
|
390
|
+
* @returns {Promise<string>} Hexadecimal signature.
|
|
391
|
+
*/
|
|
392
|
+
async generateClientSideSignature(payload, secret) {
|
|
393
|
+
const sortedPayload = Object.keys(payload).sort().map((k) => `${k}=${payload[k]}`).join("&");
|
|
394
|
+
const encoder = new TextEncoder();
|
|
395
|
+
const key = await window.crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
396
|
+
name: "HMAC",
|
|
397
|
+
hash: "SHA-256"
|
|
398
|
+
}, false, ["sign"]);
|
|
399
|
+
const signatureBuffer = await window.crypto.subtle.sign("HMAC", key, encoder.encode(sortedPayload));
|
|
400
|
+
const hashArray = Array.from(new Uint8Array(signatureBuffer));
|
|
401
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @internal
|
|
406
|
+
* Resets the cached fingerprint builder. Used for testing purposes.
|
|
407
|
+
*/
|
|
408
|
+
_resetCache() {
|
|
409
|
+
// Reset hasher to default JS implementation.
|
|
410
|
+
activeCyrb53 = jsCyrb53;
|
|
411
|
+
this._cachedBuilder = null;
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Injects invisible phantom interactive elements and forms within a closed Shadow DOM container to trap bots (focus/hover/click).
|
|
416
|
+
*/
|
|
417
|
+
injectPhantomTraps() {
|
|
418
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
419
|
+
|
|
420
|
+
const host = document.createElement('div');
|
|
421
|
+
host.setAttribute('aria-hidden', 'true');
|
|
422
|
+
host.style.position = 'absolute';
|
|
423
|
+
host.style.left = '-9999px';
|
|
424
|
+
host.style.top = '-9999px';
|
|
425
|
+
host.style.width = '0';
|
|
426
|
+
host.style.height = '0';
|
|
427
|
+
host.style.overflow = 'hidden';
|
|
428
|
+
|
|
429
|
+
const shadow = typeof host.attachShadow === 'function'
|
|
430
|
+
? host.attachShadow({ mode: 'closed' })
|
|
431
|
+
: host;
|
|
432
|
+
|
|
433
|
+
const style = document.createElement('style');
|
|
434
|
+
style.textContent = `
|
|
435
|
+
:host {
|
|
436
|
+
position: absolute;
|
|
437
|
+
left: -9999px;
|
|
438
|
+
top: -9999px;
|
|
439
|
+
width: 0;
|
|
440
|
+
height: 0;
|
|
441
|
+
overflow: hidden;
|
|
442
|
+
}
|
|
443
|
+
a, input, button, form {
|
|
444
|
+
opacity: 0.001;
|
|
445
|
+
pointer-events: auto;
|
|
446
|
+
}
|
|
447
|
+
`;
|
|
448
|
+
shadow.appendChild(style);
|
|
449
|
+
|
|
450
|
+
const triggerTrap = () => {
|
|
451
|
+
this.onHoneypotTrigger();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// 1. Phantom interactive link
|
|
455
|
+
const phantom = document.createElement('a');
|
|
456
|
+
phantom.href = '#';
|
|
457
|
+
// Random deceptive name to attract automated link/form parsers
|
|
458
|
+
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'];
|
|
459
|
+
phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
|
|
460
|
+
phantom.className = genRandStr(8);
|
|
461
|
+
phantom.tabIndex = 0; // In natural tab flow for crawler parsers
|
|
462
|
+
phantom.addEventListener('focus', triggerTrap, { passive: true });
|
|
463
|
+
phantom.addEventListener('mouseover', triggerTrap, { passive: true });
|
|
464
|
+
phantom.addEventListener('click', (e) => {
|
|
465
|
+
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
|
466
|
+
triggerTrap();
|
|
467
|
+
}, { passive: false });
|
|
468
|
+
|
|
469
|
+
// 2. Deceptive hidden form with bait input
|
|
470
|
+
const phantomForm = document.createElement('form');
|
|
471
|
+
phantomForm.action = '#';
|
|
472
|
+
phantomForm.tabIndex = -1;
|
|
473
|
+
const inputNames = ['session_token', 'admin_pwd', 'secret_key', 'auth_bypass'];
|
|
474
|
+
const phantomInput = document.createElement('input');
|
|
475
|
+
phantomInput.type = 'text';
|
|
476
|
+
phantomInput.name = inputNames[Math.floor(secureRandom() * inputNames.length)] + '_' + genRandStr(4);
|
|
477
|
+
phantomInput.tabIndex = 0;
|
|
478
|
+
phantomInput.autocomplete = 'off';
|
|
479
|
+
phantomInput.addEventListener('focus', triggerTrap, { passive: true });
|
|
480
|
+
phantomInput.addEventListener('input', triggerTrap, { passive: true });
|
|
481
|
+
phantomInput.addEventListener('click', triggerTrap, { passive: true });
|
|
482
|
+
phantomInput.addEventListener('change', triggerTrap, { passive: true });
|
|
483
|
+
|
|
484
|
+
phantomForm.appendChild(phantomInput);
|
|
485
|
+
shadow.appendChild(phantom);
|
|
486
|
+
shadow.appendChild(phantomForm);
|
|
487
|
+
|
|
488
|
+
document.body.appendChild(host);
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Measures gyroscope/accelerometer noise to detect phone farm racks.
|
|
493
|
+
*/
|
|
494
|
+
startMotionTracker() {
|
|
495
|
+
if (this._motionTrackerAttached || typeof window === 'undefined') return;
|
|
496
|
+
this._motionTrackerAttached = true;
|
|
497
|
+
const motionSamples = [];
|
|
498
|
+
const handleMotion = (e) => {
|
|
499
|
+
const acc = e.accelerationIncludingGravity || e.acceleration;
|
|
500
|
+
if (!acc) return;
|
|
501
|
+
const mag = Math.sqrt((acc.x || 0)**2 + (acc.y || 0)**2 + (acc.z || 0)**2);
|
|
502
|
+
motionSamples.push(mag);
|
|
503
|
+
if (motionSamples.length > 20) motionSamples.shift();
|
|
504
|
+
if (motionSamples.length >= 10) {
|
|
505
|
+
const avg = motionSamples.reduce((a, b) => a + b, 0) / motionSamples.length;
|
|
506
|
+
const variance = motionSamples.reduce((a, b) => a + (b - avg)**2, 0) / motionSamples.length;
|
|
507
|
+
metrics.motionVariance = Math.round(variance * 10000) / 10000;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
try {
|
|
511
|
+
window.addEventListener('devicemotion', handleMotion, { passive: true });
|
|
512
|
+
} catch (e) {}
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Starts tracking touch events on mobile/tablet devices.
|
|
517
|
+
*/
|
|
518
|
+
startTouchEventTracker() {
|
|
519
|
+
if (this._touchTrackerAttached) return;
|
|
520
|
+
this._touchTrackerAttached = true;
|
|
521
|
+
|
|
522
|
+
const handleTouch = (e) => {
|
|
523
|
+
if (touchMovementsHistory.length >= TOUCH_HISTORY_MAX) {
|
|
524
|
+
touchMovementsHistory.shift();
|
|
525
|
+
}
|
|
526
|
+
const touch = e.touches[0] || e.changedTouches[0];
|
|
527
|
+
if (!touch) return;
|
|
528
|
+
|
|
529
|
+
const radiusX = touch.radiusX || 0;
|
|
530
|
+
const radiusY = touch.radiusY || 0;
|
|
531
|
+
const radius = (radiusX + radiusY) / 2;
|
|
532
|
+
const force = touch.force || touch.webkitForce || 0;
|
|
533
|
+
|
|
534
|
+
touchMovementsHistory.push({
|
|
535
|
+
x: touch.clientX,
|
|
536
|
+
y: touch.clientY,
|
|
537
|
+
t: performance.now(),
|
|
538
|
+
p: force,
|
|
539
|
+
r: radius,
|
|
540
|
+
num: e.touches.length
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
document.addEventListener('touchstart', handleTouch, { passive: true });
|
|
545
|
+
document.addEventListener('touchmove', handleTouch, { passive: true });
|
|
546
|
+
document.addEventListener('touchend', handleTouch, { passive: true });
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Starts tracking display regularity (V-Sync/rAF) to detect software framebuffers lacking V-Sync.
|
|
551
|
+
*/
|
|
552
|
+
startRenderingTracker() {
|
|
553
|
+
if (this._renderingTrackerAttached) return;
|
|
554
|
+
this._renderingTrackerAttached = true;
|
|
555
|
+
|
|
556
|
+
if (typeof window === 'undefined' || !window.requestAnimationFrame) return;
|
|
557
|
+
|
|
558
|
+
const rAfTimestamps = [];
|
|
559
|
+
let lastTime = performance.now();
|
|
560
|
+
const maxSamples = 15;
|
|
561
|
+
|
|
562
|
+
const checkOffscreenAnom = () => {
|
|
563
|
+
try {
|
|
564
|
+
const htmlCanvas = typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement : window.HTMLCanvasElement;
|
|
565
|
+
if ('OffscreenCanvas' in window && htmlCanvas?.prototype?.transferControlToOffscreen) {
|
|
566
|
+
const nativeToString = Function.prototype.toString.call(htmlCanvas.prototype.transferControlToOffscreen);
|
|
567
|
+
return !nativeToString.includes('[native code]');
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {}
|
|
570
|
+
return false;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const loop = (time) => {
|
|
574
|
+
const delta = time - lastTime;
|
|
575
|
+
lastTime = time;
|
|
576
|
+
if (rAfTimestamps.length < maxSamples) {
|
|
577
|
+
if (rAfTimestamps.length > 0) { // Skip first delta
|
|
578
|
+
rAfTimestamps.push(delta);
|
|
579
|
+
}
|
|
580
|
+
window.requestAnimationFrame(loop);
|
|
581
|
+
} else {
|
|
582
|
+
const avg = rAfTimestamps.reduce((a, b) => a + b, 0) / rAfTimestamps.length;
|
|
583
|
+
const sqDiffs = rAfTimestamps.map(v => Math.pow(v - avg, 2));
|
|
584
|
+
const avgSqDiff = sqDiffs.reduce((a, b) => a + b, 0) / sqDiffs.length;
|
|
585
|
+
|
|
586
|
+
metrics.rendering = {
|
|
587
|
+
fps: Math.round((1000 / avg) * 100) / 100,
|
|
588
|
+
jitter: Math.round(Math.sqrt(avgSqDiff) * 100) / 100,
|
|
589
|
+
offscreenAnom: checkOffscreenAnom()
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
window.requestAnimationFrame(loop);
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Initializes persistent Proof-of-Space allocation in local IndexedDB.
|
|
598
|
+
*/
|
|
599
|
+
async initializeSpace(seed, sizeMb) {
|
|
600
|
+
return new Promise((resolve, reject) => {
|
|
601
|
+
const request = indexedDB.open('pospace-db', 1);
|
|
602
|
+
request.onupgradeneeded = (e) => {
|
|
603
|
+
const db = e.target.result;
|
|
604
|
+
if (!db.objectStoreNames.contains('blocks')) {
|
|
605
|
+
db.createObjectStore('blocks');
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
request.onsuccess = async (e) => {
|
|
609
|
+
const db = e.target.result;
|
|
610
|
+
const tx = db.transaction('blocks', 'readwrite');
|
|
611
|
+
const store = tx.objectStore('blocks');
|
|
612
|
+
|
|
613
|
+
const maxBlocks = sizeMb * 1024;
|
|
614
|
+
const countReq = store.count();
|
|
615
|
+
countReq.onsuccess = async () => {
|
|
616
|
+
if (countReq.result < maxBlocks) {
|
|
617
|
+
for (let i = 0; i < maxBlocks; i++) {
|
|
618
|
+
const block = new Uint8Array(1024);
|
|
619
|
+
let h = 5381;
|
|
620
|
+
for (let j = 0; j < seed.length; j++) {
|
|
621
|
+
h = (h << 5) + h + seed.charCodeAt(j);
|
|
622
|
+
}
|
|
623
|
+
h = (h << 5) + h + i;
|
|
624
|
+
for (let k = 0; k < 1024; k++) {
|
|
625
|
+
h = Math.imul(h ^ k, 1597334677);
|
|
626
|
+
block[k] = h & 0xff;
|
|
627
|
+
}
|
|
628
|
+
store.put(block, i);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
resolve();
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
request.onerror = () => reject(new Error("Failed to open pospace database"));
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Reads a specific block from local IndexedDB formatted as hex.
|
|
640
|
+
*/
|
|
641
|
+
async readSpaceBlock(blockIdx) {
|
|
642
|
+
return new Promise((resolve, reject) => {
|
|
643
|
+
const request = indexedDB.open('pospace-db', 1);
|
|
644
|
+
request.onsuccess = (e) => {
|
|
645
|
+
const db = e.target.result;
|
|
646
|
+
const tx = db.transaction('blocks', 'readonly');
|
|
647
|
+
const store = tx.objectStore('blocks');
|
|
648
|
+
const getReq = store.get(blockIdx);
|
|
649
|
+
getReq.onsuccess = () => {
|
|
650
|
+
const block = getReq.result;
|
|
651
|
+
if (block) {
|
|
652
|
+
const hex = Array.from(block).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
653
|
+
resolve(hex);
|
|
654
|
+
} else {
|
|
655
|
+
reject(new Error("Block not found"));
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
getReq.onerror = () => reject(getReq.error);
|
|
659
|
+
};
|
|
660
|
+
request.onerror = () => reject(new Error("Failed to open pospace database"));
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Solves Proof-of-Space by optionally combining a peer's block.
|
|
666
|
+
*/
|
|
667
|
+
async solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
|
|
668
|
+
const blocks = [];
|
|
669
|
+
for (const idx of queries) {
|
|
670
|
+
const blockHex = await this.readSpaceBlock(idx);
|
|
671
|
+
blocks.push(blockHex);
|
|
672
|
+
}
|
|
673
|
+
let finalPayload = blocks.join('') + peerBlock + nonce + ":" + clientSecret;
|
|
674
|
+
const encoder = new TextEncoder();
|
|
675
|
+
const data = encoder.encode(finalPayload);
|
|
676
|
+
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
|
|
677
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
678
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Starts tracking mouse movements to evaluate entropy.
|
|
683
|
+
* Call once per page lifecycle.
|
|
684
|
+
*/
|
|
685
|
+
startMouseEntropyTracker() {
|
|
686
|
+
// Utiliser un drapeau pour éviter d'attacher l'écouteur plusieurs fois
|
|
687
|
+
if (this._mouseTrackerAttached) return;
|
|
688
|
+
this._mouseTrackerAttached = true;
|
|
689
|
+
|
|
690
|
+
document.addEventListener('mousemove', (e) => {
|
|
691
|
+
// Capture trajectory points {x, y, t}
|
|
692
|
+
if (mouseMovementsHistory.length >= MOUSE_HISTORY_MAX) {
|
|
693
|
+
// Cap history size to prevent unbounded memory growth
|
|
694
|
+
mouseMovementsHistory.shift();
|
|
695
|
+
}
|
|
696
|
+
mouseMovementsHistory.push({
|
|
697
|
+
x: e.clientX,
|
|
698
|
+
y: e.clientY,
|
|
699
|
+
t: performance.now()
|
|
700
|
+
});
|
|
701
|
+
}, {passive: true});
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Starts tracking keystroke dynamics to evaluate dwell and flight times.
|
|
706
|
+
* Call once per page lifecycle.
|
|
707
|
+
*/
|
|
708
|
+
startKeystrokeDynamicsTracker() {
|
|
709
|
+
// S'assurer de ne pas attacher l'écouteur plusieurs fois
|
|
710
|
+
if (this._keystrokeTrackerAttached) return;
|
|
711
|
+
this._keystrokeTrackerAttached = true;
|
|
712
|
+
|
|
713
|
+
const activeKeys = new Map();
|
|
714
|
+
let lastKeyDownTime = 0;
|
|
715
|
+
let lastKeyName = '';
|
|
716
|
+
|
|
717
|
+
document.addEventListener('keydown', (e) => {
|
|
718
|
+
const now = performance.now();
|
|
719
|
+
const key = e.key;
|
|
720
|
+
const code = e.code;
|
|
721
|
+
if (!key && !code) return;
|
|
722
|
+
|
|
723
|
+
const keyIdentifier = code || key;
|
|
724
|
+
|
|
725
|
+
// Prevent key repeat triggering multiple events
|
|
726
|
+
if (activeKeys.has(keyIdentifier)) return;
|
|
727
|
+
activeKeys.set(keyIdentifier, now);
|
|
728
|
+
|
|
729
|
+
if (keystrokeTimestamps.length > 0) {
|
|
730
|
+
const lastTimestamp = keystrokeTimestamps[keystrokeTimestamps.length - 1];
|
|
731
|
+
const latency = now - lastTimestamp;
|
|
732
|
+
if (latency > 10 && latency < 2000) {
|
|
733
|
+
if (keystrokeLatencies.length >= KEYSTROKE_HISTORY_MAX) {
|
|
734
|
+
keystrokeLatencies.shift();
|
|
735
|
+
}
|
|
736
|
+
keystrokeLatencies.push(latency);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
keystrokeTimestamps.push(now);
|
|
740
|
+
|
|
741
|
+
// Flight Time (KeyDown to KeyDown)
|
|
742
|
+
if (lastKeyDownTime > 0) {
|
|
743
|
+
const flightTime = now - lastKeyDownTime;
|
|
744
|
+
if (flightTime > 10 && flightTime < 2000) {
|
|
745
|
+
if (keystrokeFlightTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
746
|
+
keystrokeFlightTimes.shift();
|
|
747
|
+
}
|
|
748
|
+
const digraph = lastKeyName ? this._hasher(lastKeyName + "_" + keyIdentifier).toString() : "unknown";
|
|
749
|
+
keystrokeFlightTimes.push({ digraph, time: flightTime });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
lastKeyDownTime = now;
|
|
753
|
+
lastKeyName = keyIdentifier;
|
|
754
|
+
}, {passive: true});
|
|
755
|
+
|
|
756
|
+
document.addEventListener('keyup', (e) => {
|
|
757
|
+
const now = performance.now();
|
|
758
|
+
const key = e.key;
|
|
759
|
+
const code = e.code;
|
|
760
|
+
if (!key && !code) return;
|
|
761
|
+
|
|
762
|
+
const keyIdentifier = code || key;
|
|
763
|
+
|
|
764
|
+
if (activeKeys.has(keyIdentifier)) {
|
|
765
|
+
const pressTime = activeKeys.get(keyIdentifier);
|
|
766
|
+
const dwellTime = now - pressTime;
|
|
767
|
+
activeKeys.delete(keyIdentifier);
|
|
768
|
+
|
|
769
|
+
if (dwellTime > 5 && dwellTime < 1000) {
|
|
770
|
+
if (keystrokeDwellTimes.length >= KEYSTROKE_HISTORY_MAX) {
|
|
771
|
+
keystrokeDwellTimes.shift();
|
|
772
|
+
}
|
|
773
|
+
keystrokeDwellTimes.push(dwellTime);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}, {passive: true});
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Starts tracking click events to analyze position variance.
|
|
781
|
+
* @private
|
|
782
|
+
*/
|
|
783
|
+
startClickTracker() {
|
|
784
|
+
if (this._clickTrackerAttached) return;
|
|
785
|
+
this._clickTrackerAttached = true;
|
|
786
|
+
|
|
787
|
+
document.addEventListener('click', (e) => {
|
|
788
|
+
if (clicksHistory.length >= CLICKS_HISTORY_MAX) {
|
|
789
|
+
clicksHistory.shift();
|
|
790
|
+
}
|
|
791
|
+
// Generate a simple identifier for the target element
|
|
792
|
+
const target = e.target;
|
|
793
|
+
const targetId = target.id || target.name || target.tagName;
|
|
794
|
+
|
|
795
|
+
clicksHistory.push({
|
|
796
|
+
x: e.clientX,
|
|
797
|
+
y: e.clientY,
|
|
798
|
+
t: performance.now(),
|
|
799
|
+
targetId: this._hasher(targetId) // Hash the ID to keep it short and consistent
|
|
800
|
+
});
|
|
801
|
+
}, { passive: true });
|
|
802
|
+
},
|
|
803
|
+
/**
|
|
804
|
+
* Initializes or resets client-side honeypots for immediate detection.
|
|
805
|
+
* Previous listeners are removed before attaching new ones.
|
|
806
|
+
* @param {string[]} honeypotFieldNames - Hidden form field names.
|
|
807
|
+
*/
|
|
808
|
+
initializeHoneypots(honeypotFieldNames) {
|
|
809
|
+
const fieldNames = Array.isArray(honeypotFieldNames)
|
|
810
|
+
? honeypotFieldNames
|
|
811
|
+
: (honeypotFieldNames ? [honeypotFieldNames] : []);
|
|
812
|
+
|
|
813
|
+
// 1. Clean up existing listeners
|
|
814
|
+
activeHoneypotListeners.forEach((listener, field) => {
|
|
815
|
+
field.removeEventListener('input', listener);
|
|
816
|
+
});
|
|
817
|
+
activeHoneypotListeners.clear();
|
|
818
|
+
|
|
819
|
+
// 2. Attach new listeners to existing DOM elements
|
|
820
|
+
fieldNames.forEach(fieldName => {
|
|
821
|
+
const field = document.querySelector(`[name="${fieldName}"]`);
|
|
822
|
+
if (field) {
|
|
823
|
+
const listener = () => {
|
|
824
|
+
this.onHoneypotTrigger();
|
|
825
|
+
field.removeEventListener('input', listener);
|
|
826
|
+
};
|
|
827
|
+
field.addEventListener('input', listener);
|
|
828
|
+
activeHoneypotListeners.set(field, listener);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
// 3. Generate trap inputs inside a closed Shadow DOM
|
|
833
|
+
if (typeof document !== 'undefined' && fieldNames.length > 0 && document.body) {
|
|
834
|
+
const host = document.createElement('div');
|
|
835
|
+
host.setAttribute('aria-hidden', 'true');
|
|
836
|
+
host.style.position = 'absolute';
|
|
837
|
+
host.style.width = '0';
|
|
838
|
+
host.style.height = '0';
|
|
839
|
+
host.style.overflow = 'hidden';
|
|
840
|
+
|
|
841
|
+
const shadow = typeof host.attachShadow === 'function'
|
|
842
|
+
? host.attachShadow({ mode: 'closed' })
|
|
843
|
+
: host;
|
|
844
|
+
|
|
845
|
+
// Randomize classes, CSS variables, and layout tags
|
|
846
|
+
const wrapperClass = genRandStr(10);
|
|
847
|
+
const posStateVar = `--${genRandStr(8)}`;
|
|
848
|
+
const offValVar = `--${genRandStr(8)}`;
|
|
849
|
+
const visStateVar = `--${genRandStr(8)}`;
|
|
850
|
+
const scaleValVar = `--${genRandStr(8)}`;
|
|
851
|
+
|
|
852
|
+
const style = document.createElement('style');
|
|
853
|
+
style.textContent = `
|
|
854
|
+
:host {
|
|
855
|
+
${posStateVar}: absolute;
|
|
856
|
+
${offValVar}: -9999px;
|
|
857
|
+
${visStateVar}: hidden;
|
|
858
|
+
${scaleValVar}: 0;
|
|
859
|
+
}
|
|
860
|
+
.${wrapperClass} {
|
|
861
|
+
position: var(${posStateVar});
|
|
862
|
+
left: var(${offValVar});
|
|
863
|
+
top: var(${offValVar});
|
|
864
|
+
visibility: var(${visStateVar});
|
|
865
|
+
transform: scale(var(${scaleValVar}));
|
|
866
|
+
}
|
|
867
|
+
`;
|
|
868
|
+
shadow.appendChild(style);
|
|
869
|
+
|
|
870
|
+
const wrapperTags = ['div', 'section', 'p', 'span', 'form', 'main'];
|
|
871
|
+
const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
|
|
872
|
+
const wrapper = document.createElement(selectedWrapperTag);
|
|
873
|
+
wrapper.className = wrapperClass;
|
|
874
|
+
|
|
875
|
+
fieldNames.forEach(fieldName => {
|
|
876
|
+
const label = document.createElement('label');
|
|
877
|
+
label.textContent = fieldName;
|
|
878
|
+
const input = document.createElement('input');
|
|
879
|
+
input.type = 'text';
|
|
880
|
+
input.name = fieldName;
|
|
881
|
+
input.tabIndex = -1;
|
|
882
|
+
input.className = genRandStr(6);
|
|
883
|
+
input.id = genRandStr(8);
|
|
884
|
+
input.autocomplete = 'off';
|
|
885
|
+
|
|
886
|
+
const trigger = () => {
|
|
887
|
+
this.onHoneypotTrigger();
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
input.addEventListener('input', trigger, { passive: true });
|
|
891
|
+
input.addEventListener('change', trigger, { passive: true });
|
|
892
|
+
input.addEventListener('focus', trigger, { passive: true });
|
|
893
|
+
input.addEventListener('click', trigger, { passive: true });
|
|
894
|
+
|
|
895
|
+
// Polymorphic nesting of label and input
|
|
896
|
+
const nestingType = Math.floor(secureRandom() * 3);
|
|
897
|
+
if (nestingType === 1) {
|
|
898
|
+
label.appendChild(input);
|
|
899
|
+
wrapper.appendChild(label);
|
|
900
|
+
} else if (nestingType === 2) {
|
|
901
|
+
const innerContainer = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
|
|
902
|
+
innerContainer.className = genRandStr(5);
|
|
903
|
+
innerContainer.appendChild(label);
|
|
904
|
+
innerContainer.appendChild(input);
|
|
905
|
+
wrapper.appendChild(innerContainer);
|
|
906
|
+
} else {
|
|
907
|
+
wrapper.appendChild(label);
|
|
908
|
+
wrapper.appendChild(input);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
shadow.appendChild(wrapper);
|
|
913
|
+
document.body.appendChild(host);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
/**
|
|
917
|
+
* Silently requests FIDO2 hardware attestation (WebAuthn)
|
|
918
|
+
* to anchor physical device identity via TPM / Secure Enclave.
|
|
919
|
+
* @returns {Promise<object|null>}
|
|
920
|
+
*/
|
|
921
|
+
async getWebAuthnAnchor() {
|
|
922
|
+
const win = typeof window !== 'undefined' ? window : null;
|
|
923
|
+
if (!win || !win.PublicKeyCredential) return null;
|
|
924
|
+
try {
|
|
925
|
+
const isPlatformAvailable = await win.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
926
|
+
if (!isPlatformAvailable) return null;
|
|
927
|
+
|
|
928
|
+
const storedCredIdBase64 = localStorage.getItem('fp_webauthn_cred_id');
|
|
929
|
+
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
|
|
930
|
+
|
|
931
|
+
if (storedCredIdBase64) {
|
|
932
|
+
// Silent assertion attempt (Authentication)
|
|
933
|
+
const credentialId = Uint8Array.from(atob(storedCredIdBase64), c => c.charCodeAt(0));
|
|
934
|
+
const assertion = await navigator.credentials.get({
|
|
935
|
+
publicKey: {
|
|
936
|
+
challenge,
|
|
937
|
+
allowCredentials: [{
|
|
938
|
+
id: credentialId,
|
|
939
|
+
type: 'public-key'
|
|
940
|
+
}],
|
|
941
|
+
userVerification: 'discouraged',
|
|
942
|
+
timeout: 1000
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
if (assertion) {
|
|
946
|
+
return {
|
|
947
|
+
type: 'assertion',
|
|
948
|
+
credentialId: storedCredIdBase64,
|
|
949
|
+
signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
|
|
950
|
+
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
|
|
951
|
+
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON)))
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
// Silent creation attempt (Registration)
|
|
956
|
+
const options = {
|
|
957
|
+
publicKey: {
|
|
958
|
+
challenge,
|
|
959
|
+
rp: { name: window.location.hostname, id: window.location.hostname },
|
|
960
|
+
user: {
|
|
961
|
+
id: new Uint8Array([102, 112, 105, 100]),
|
|
962
|
+
name: 'silent-device-anchor',
|
|
963
|
+
displayName: 'Hardware Device Anchor'
|
|
964
|
+
},
|
|
965
|
+
pubKeyCredParams: [
|
|
966
|
+
{ type: 'public-key', alg: -7 }, // ES256 (P-256) - Secure Enclave / TPM / Android Keystore
|
|
967
|
+
{ type: 'public-key', alg: -257 } // RS256 - Windows Hello TPM
|
|
968
|
+
],
|
|
969
|
+
authenticatorSelection: {
|
|
970
|
+
authenticatorAttachment: 'platform',
|
|
971
|
+
userVerification: 'discouraged',
|
|
972
|
+
residentKey: 'preferred'
|
|
973
|
+
},
|
|
974
|
+
timeout: 1500,
|
|
975
|
+
attestation: 'indirect'
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
const credential = await navigator.credentials.create(options);
|
|
979
|
+
if (credential) {
|
|
980
|
+
const credIdBase64 = btoa(String.fromCharCode(...new Uint8Array(credential.rawId)));
|
|
981
|
+
localStorage.setItem('fp_webauthn_cred_id', credIdBase64);
|
|
982
|
+
const publicKeyDer = credential.response.getPublicKey ? credential.response.getPublicKey() : null;
|
|
983
|
+
return {
|
|
984
|
+
type: 'registration',
|
|
985
|
+
credentialId: credIdBase64,
|
|
986
|
+
publicKey: publicKeyDer ? btoa(String.fromCharCode(...new Uint8Array(publicKeyDer))) : null,
|
|
987
|
+
attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.getAttestationObject()))),
|
|
988
|
+
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} catch (e) {
|
|
993
|
+
// Bypass silently when unsupported to remain non-intrusive
|
|
994
|
+
console.log('[WebAuthn-Anchor] Silent attestation bypassed:', e.message);
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Évalue le comportement côté client en local sans transmettre de données brutes.
|
|
1001
|
+
* Retourne 1 si le comportement est humain/légitime, 0 en cas de suspicion/bot.
|
|
1002
|
+
* @returns {number} 1 ou 0
|
|
1003
|
+
*/
|
|
1004
|
+
evaluateBehavior() {
|
|
1005
|
+
let score = 0;
|
|
1006
|
+
if (metrics.honeypotInteraction) return 100; // Score maximal immédiat pour interaction avec un pot de miel
|
|
1007
|
+
if (ClientLibrary.detectTamperedPrototypes()) score += 80;
|
|
1008
|
+
|
|
1009
|
+
// Vérification du rendu graphique
|
|
1010
|
+
if (metrics.rendering) {
|
|
1011
|
+
if (metrics.rendering.offscreenAnom) return 100; // Score maximal pour anomalie de rendu
|
|
1012
|
+
const fps = parseFloat(metrics.rendering.fps || 0);
|
|
1013
|
+
const jitter = parseFloat(metrics.rendering.jitter || 0);
|
|
1014
|
+
if (fps > 250 || (fps > 0 && fps < 15)) score += 50;
|
|
1015
|
+
if (jitter > 6.0) score += Math.min(80, (jitter - 6.0) * 10);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Mouvements de souris robotiques
|
|
1019
|
+
if (mouseMovementsHistory.length >= 3) {
|
|
1020
|
+
let totalDist = 0;
|
|
1021
|
+
let pauses = 0;
|
|
1022
|
+
const segments = [];
|
|
1023
|
+
for (let i = 1; i < mouseMovementsHistory.length; i++) {
|
|
1024
|
+
const p1 = mouseMovementsHistory[i - 1];
|
|
1025
|
+
const p2 = mouseMovementsHistory[i];
|
|
1026
|
+
const dx = p2.x - p1.x;
|
|
1027
|
+
const dy = p2.y - p1.y;
|
|
1028
|
+
const dt = p2.t - p1.t;
|
|
1029
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1030
|
+
if (dt > 0) segments.push(dist / dt);
|
|
1031
|
+
totalDist += dist;
|
|
1032
|
+
if (dt > 100 && dist < 5) pauses++;
|
|
1033
|
+
}
|
|
1034
|
+
if (segments.length >= 2) {
|
|
1035
|
+
const totalTime = mouseMovementsHistory[mouseMovementsHistory.length - 1].t - mouseMovementsHistory[0].t;
|
|
1036
|
+
const avgSpeed = totalTime > 0 ? totalDist / totalTime : 0;
|
|
1037
|
+
const pStart = mouseMovementsHistory[0];
|
|
1038
|
+
const pEnd = mouseMovementsHistory[mouseMovementsHistory.length - 1];
|
|
1039
|
+
const straightDist = Math.sqrt(Math.pow(pEnd.x - pStart.x, 2) + Math.pow(pEnd.y - pStart.y, 2));
|
|
1040
|
+
const straightness = totalDist > 0 ? straightDist / totalDist : 1;
|
|
1041
|
+
if (avgSpeed > 3.0) score += 25;
|
|
1042
|
+
if (straightness > 0.95) score += 30;
|
|
1043
|
+
if (pauses === 0 && segments.length > 20) score += 15;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Analyse des événements tactiles
|
|
1048
|
+
if (touchMovementsHistory.length >= 3) {
|
|
1049
|
+
let totalDist = 0;
|
|
1050
|
+
let totalPressure = 0;
|
|
1051
|
+
let totalRadius = 0;
|
|
1052
|
+
for (const pt of touchMovementsHistory) {
|
|
1053
|
+
totalPressure += pt.p || 0;
|
|
1054
|
+
totalRadius += pt.r || 0;
|
|
1055
|
+
}
|
|
1056
|
+
const avgPressure = totalPressure / touchMovementsHistory.length;
|
|
1057
|
+
const avgRadius = totalRadius / touchMovementsHistory.length;
|
|
1058
|
+
let sqDiffPressure = 0;
|
|
1059
|
+
let sqDiffRadius = 0;
|
|
1060
|
+
for (const pt of touchMovementsHistory) {
|
|
1061
|
+
sqDiffPressure += Math.pow((pt.p || 0) - avgPressure, 2);
|
|
1062
|
+
sqDiffRadius += Math.pow((pt.r || 0) - avgRadius, 2);
|
|
1063
|
+
}
|
|
1064
|
+
if (avgPressure > 0 && (sqDiffPressure / touchMovementsHistory.length) === 0) {
|
|
1065
|
+
score += 30;
|
|
1066
|
+
}
|
|
1067
|
+
if (avgRadius > 0 && (sqDiffRadius / touchMovementsHistory.length) === 0) {
|
|
1068
|
+
score += 30;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Détection de rack / ferme de téléphones mobiles immobiles
|
|
1073
|
+
if (typeof window !== 'undefined' && (window.navigator?.userAgent || '').includes('Mobile')) {
|
|
1074
|
+
// Un appareil mobile parfaitement immobile est très suspect (probable ferme de téléphones).
|
|
1075
|
+
// On ne déclenche que s'il n'y a pas d'événements tactiles, car un utilisateur peut le tenir immobile en lisant.
|
|
1076
|
+
if (metrics.motionVariance === 0 && touchMovementsHistory.length === 0) return 90;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Variance des clics ultra-précise (bot de clic)
|
|
1080
|
+
if (clicksHistory.length >= 3) {
|
|
1081
|
+
const targets = {};
|
|
1082
|
+
for (const c of clicksHistory) {
|
|
1083
|
+
if (!c.targetId) continue;
|
|
1084
|
+
if (!targets[c.targetId]) targets[c.targetId] = [];
|
|
1085
|
+
targets[c.targetId].push(c);
|
|
1086
|
+
}
|
|
1087
|
+
for (const id in targets) {
|
|
1088
|
+
const clks = targets[id];
|
|
1089
|
+
if (clks.length >= 3) {
|
|
1090
|
+
const mx = clks.reduce((s, c) => s + c.x, 0) / clks.length;
|
|
1091
|
+
const my = clks.reduce((s, c) => s + c.y, 0) / clks.length;
|
|
1092
|
+
const v = clks.reduce((s, c) => s + Math.pow(c.x - mx, 2) + Math.pow(c.y - my, 2), 0) / clks.length;
|
|
1093
|
+
// Une variance extrêmement faible indique un script qui clique sur les mêmes coordonnées.
|
|
1094
|
+
if (v < 1.0) return 100;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Dynamique des touches de clavier robotique
|
|
1100
|
+
if (keystrokeDwellTimes.length >= 5) {
|
|
1101
|
+
const mean = keystrokeDwellTimes.reduce((a, b) => a + b, 0) / keystrokeDwellTimes.length;
|
|
1102
|
+
const variance = keystrokeDwellTimes.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / keystrokeDwellTimes.length;
|
|
1103
|
+
// Une frappe robotique a une variance très faible ou des pressions de touche extrêmement courtes.
|
|
1104
|
+
if (Math.sqrt(variance) < 2.0 || mean < 15.0) return 95;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return Math.min(100, Math.round(score));
|
|
1108
|
+
},
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Récupère le résultat comportemental sous forme binaire (1 = valide/humain, 0 = suspect).
|
|
1112
|
+
* @returns {number} 1 ou 0
|
|
1113
|
+
*/
|
|
1114
|
+
getClientBehaviorMetrics() {
|
|
1115
|
+
return this.evaluateBehavior();
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Enriches a fetch request with fingerprinting and behavioral headers.
|
|
1120
|
+
* @param {RequestInfo} resource
|
|
1121
|
+
* @param {RequestInit} [options]
|
|
1122
|
+
* @returns {Promise<Response>}
|
|
1123
|
+
*/
|
|
1124
|
+
async protectedFetch(resource, options = {}) {
|
|
1125
|
+
const fp = this.getDeviceFingerprint();
|
|
1126
|
+
const zkpProof = await this.generateZkpProof(fp); // Generate ZKP proof
|
|
1127
|
+
const behavior = this.getClientBehaviorMetrics();
|
|
1128
|
+
|
|
1129
|
+
const headers = new Headers(options.headers || {});
|
|
1130
|
+
headers.set('X-ZKP-Proof', zkpProof); // Add ZKP proof to headers
|
|
1131
|
+
headers.set('X-Device-Fingerprint', fp);
|
|
1132
|
+
headers.set('X-Behavior-Metrics', String(behavior));
|
|
1133
|
+
|
|
1134
|
+
options.headers = headers;
|
|
1135
|
+
return fetch(resource, options);
|
|
1136
|
+
},
|
|
1137
|
+
|
|
1138
|
+
// --- Fetch Interception Chain ---
|
|
1139
|
+
_isFetchPatched: false,
|
|
1140
|
+
_interceptorChain: [],
|
|
1141
|
+
// Store original fetch bound to window to prevent illegal invocation errors
|
|
1142
|
+
_targetDomains: [],
|
|
1143
|
+
_swRegistration: null,
|
|
1144
|
+
_originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Adds an interceptor function to the `fetch` chain.
|
|
1148
|
+
* Each interceptor receives `resource`, `options`, and a `next` function.
|
|
1149
|
+
* Must call `next(resource, options)` to continue the chain.
|
|
1150
|
+
* @param {function(RequestInfo, RequestInit, function): Promise<Response>} interceptor
|
|
1151
|
+
*/
|
|
1152
|
+
addFetchInterceptor(interceptor) {
|
|
1153
|
+
if (!this._isFetchPatched) {
|
|
1154
|
+
this.patchGlobalFetch();
|
|
1155
|
+
}
|
|
1156
|
+
this._interceptorChain.push(interceptor);
|
|
1157
|
+
},
|
|
1158
|
+
|
|
1159
|
+
patchGlobalFetch() {
|
|
1160
|
+
if (this._isFetchPatched || !this._originalFetch) return;
|
|
1161
|
+
|
|
1162
|
+
this._isFetchPatched = true;
|
|
1163
|
+
window.fetch = (resource, options) => {
|
|
1164
|
+
const dispatch = (index, res, opts) => {
|
|
1165
|
+
if (index >= this._interceptorChain.length) {
|
|
1166
|
+
return this._originalFetch(res, opts);
|
|
1167
|
+
}
|
|
1168
|
+
const nextInterceptor = this._interceptorChain[index];
|
|
1169
|
+
return nextInterceptor(res, opts, (nextRes, nextOpts) => dispatch(index + 1, nextRes, nextOpts));
|
|
1170
|
+
};
|
|
1171
|
+
return dispatch(0, resource, options || {});
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Invoked when a honeypot trigger occurs.
|
|
1177
|
+
* @private
|
|
1178
|
+
*/
|
|
1179
|
+
onHoneypotTrigger() {
|
|
1180
|
+
metrics.honeypotInteraction = true;
|
|
1181
|
+
if (this.workerPath) {
|
|
1182
|
+
this.syncServiceWorkerCredentials();
|
|
1183
|
+
}
|
|
1184
|
+
// Dispatch event allowing host applications to react
|
|
1185
|
+
this._dispatchEvent('honeypotTriggered');
|
|
1186
|
+
},
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Registers and manages the Service Worker for transparent HTTPS request decoration.
|
|
1190
|
+
* @param {string} workerPath - Path to the worker script.
|
|
1191
|
+
* @param {string[]} [targetDomains=[]] - Domains to intercept.
|
|
1192
|
+
* @param {string} [scope] - Service Worker registration scope.
|
|
1193
|
+
* @returns {Promise<ServiceWorkerRegistration|null>}
|
|
1194
|
+
*/
|
|
1195
|
+
async initServiceWorker(workerPath, targetDomains = [], scope = undefined) {
|
|
1196
|
+
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
|
|
1197
|
+
console.warn('[Fingerprint] Service Worker is not supported in this browser.');
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
const registerOptions = scope ? { scope } : {};
|
|
1203
|
+
const registration = await navigator.serviceWorker.register(workerPath, registerOptions);
|
|
1204
|
+
this._swRegistration = registration;
|
|
1205
|
+
|
|
1206
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
1207
|
+
this.syncServiceWorkerCredentials(targetDomains);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
await navigator.serviceWorker.ready;
|
|
1211
|
+
this.syncServiceWorkerCredentials(targetDomains);
|
|
1212
|
+
|
|
1213
|
+
if (typeof window !== 'undefined') {
|
|
1214
|
+
window.addEventListener('focus', () => this.syncServiceWorkerCredentials(targetDomains), { passive: true });
|
|
1215
|
+
document.addEventListener('visibilitychange', () => {
|
|
1216
|
+
if (document.visibilityState === 'visible') {
|
|
1217
|
+
this.syncServiceWorkerCredentials(targetDomains);
|
|
1218
|
+
}
|
|
1219
|
+
}, { passive: true });
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
console.log('[Fingerprint] Network Service Worker active and synchronized.');
|
|
1223
|
+
return registration;
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
console.warn('[Fingerprint] Failed to register Service Worker:', error);
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Synchronizes device fingerprint and behavior metrics with the active Service Worker.
|
|
1232
|
+
* @param {string[]} [targetDomains=[]]
|
|
1233
|
+
*/
|
|
1234
|
+
syncServiceWorkerCredentials(targetDomains = []) {
|
|
1235
|
+
if (typeof navigator === 'undefined' || !navigator.serviceWorker) return;
|
|
1236
|
+
const domains = (Array.isArray(targetDomains) && targetDomains.length > 0)
|
|
1237
|
+
? targetDomains
|
|
1238
|
+
: (this._targetDomains || []);
|
|
1239
|
+
const payload = {
|
|
1240
|
+
type: 'UPDATE_CREDENTIALS',
|
|
1241
|
+
fp: this.getDeviceFingerprint(),
|
|
1242
|
+
behavior: this.getClientBehaviorMetrics(),
|
|
1243
|
+
targetDomains: domains
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
if (navigator.serviceWorker.controller) {
|
|
1247
|
+
navigator.serviceWorker.controller.postMessage(payload);
|
|
1248
|
+
} else if (this._swRegistration && this._swRegistration.active) {
|
|
1249
|
+
this._swRegistration.active.postMessage(payload);
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Initializes the fingerprinting fetch interceptor.
|
|
1255
|
+
* Appends to interceptor chain without clobbering existing handlers.
|
|
1256
|
+
* @param {string[]} [targetDomains] - Domains to protect. Defaults to same-origin.
|
|
1257
|
+
*/
|
|
1258
|
+
initializeFetch(targetDomains = []) {
|
|
1259
|
+
const fingerprintInterceptor = async (resource, options, next) => {
|
|
1260
|
+
const requestUrl = (resource instanceof Request) ? resource.url : String(resource);
|
|
1261
|
+
let shouldProtect = false;
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
const url = new URL(requestUrl, window.location.origin);
|
|
1265
|
+
// Protect if targetDomains is empty and same-origin, or host matches targetDomains
|
|
1266
|
+
shouldProtect = (targetDomains.length === 0 && url.origin === window.location.origin) ||
|
|
1267
|
+
(targetDomains.length > 0 && targetDomains.includes(url.hostname));
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
// Fallback for malformed URLs
|
|
1270
|
+
shouldProtect = targetDomains.length === 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (shouldProtect) {
|
|
1274
|
+
const fp = this.getDeviceFingerprint();
|
|
1275
|
+
const behavior = this.getClientBehaviorMetrics();
|
|
1276
|
+
const headers = new Headers(options.headers || {});
|
|
1277
|
+
headers.set('X-Device-Fingerprint', fp);
|
|
1278
|
+
headers.set('X-Behavior-Metrics', String(behavior));
|
|
1279
|
+
options.headers = headers;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Forward to next interceptor in chain
|
|
1283
|
+
return next(resource, options);
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
this.addFetchInterceptor(fingerprintInterceptor);
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Injects visually hidden "honeypot" links into the DOM to trap bots.
|
|
1291
|
+
* @param {string[]} urls - An array of trap URLs to inject.
|
|
1292
|
+
* @private
|
|
1293
|
+
*/
|
|
1294
|
+
injectTrapLinks(urls) {
|
|
1295
|
+
if (!urls || urls.length === 0 || typeof document === 'undefined' || !document.body) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const host = document.createElement('div');
|
|
1300
|
+
host.setAttribute('aria-hidden', 'true');
|
|
1301
|
+
host.style.position = 'absolute';
|
|
1302
|
+
host.style.width = '0';
|
|
1303
|
+
host.style.height = '0';
|
|
1304
|
+
host.style.overflow = 'hidden';
|
|
1305
|
+
|
|
1306
|
+
const shadow = typeof host.attachShadow === 'function'
|
|
1307
|
+
? host.attachShadow({ mode: 'closed' })
|
|
1308
|
+
: host;
|
|
1309
|
+
|
|
1310
|
+
// Randomize classes, CSS variables, and layout tags
|
|
1311
|
+
const wrapperClass = genRandStr(10);
|
|
1312
|
+
const layoutPosVar = `--${genRandStr(8)}`;
|
|
1313
|
+
const offsetValVar = `--${genRandStr(8)}`;
|
|
1314
|
+
const visibilityStateVar = `--${genRandStr(8)}`;
|
|
1315
|
+
const scaleFactorVar = `--${genRandStr(8)}`;
|
|
1316
|
+
const ptrEventsVar = `--${genRandStr(8)}`;
|
|
1317
|
+
const linkColorClass = genRandStr(6);
|
|
1318
|
+
|
|
1319
|
+
const style = document.createElement('style');
|
|
1320
|
+
style.textContent = `
|
|
1321
|
+
:host {
|
|
1322
|
+
${layoutPosVar}: absolute;
|
|
1323
|
+
${offsetValVar}: -9999px;
|
|
1324
|
+
${visibilityStateVar}: hidden;
|
|
1325
|
+
${scaleFactorVar}: 0;
|
|
1326
|
+
${ptrEventsVar}: none;
|
|
1327
|
+
}
|
|
1328
|
+
.${wrapperClass} {
|
|
1329
|
+
position: var(${layoutPosVar});
|
|
1330
|
+
left: var(${offsetValVar});
|
|
1331
|
+
top: var(${offsetValVar});
|
|
1332
|
+
visibility: var(${visibilityStateVar});
|
|
1333
|
+
transform: scale(var(${scaleFactorVar}));
|
|
1334
|
+
pointer-events: var(${ptrEventsVar});
|
|
1335
|
+
}
|
|
1336
|
+
.${linkColorClass} {
|
|
1337
|
+
color: transparent;
|
|
1338
|
+
text-decoration: none;
|
|
1339
|
+
}
|
|
1340
|
+
`;
|
|
1341
|
+
shadow.appendChild(style);
|
|
1342
|
+
|
|
1343
|
+
const wrapperTags = ['div', 'section', 'p', 'span', 'nav', 'aside'];
|
|
1344
|
+
const selectedWrapperTag = wrapperTags[Math.floor(secureRandom() * wrapperTags.length)];
|
|
1345
|
+
const wrapper = document.createElement(selectedWrapperTag);
|
|
1346
|
+
wrapper.className = wrapperClass;
|
|
1347
|
+
|
|
1348
|
+
urls.forEach((url, i) => {
|
|
1349
|
+
const link = document.createElement('a');
|
|
1350
|
+
link.href = url;
|
|
1351
|
+
link.rel = 'nofollow';
|
|
1352
|
+
link.tabIndex = -1;
|
|
1353
|
+
link.className = linkColorClass;
|
|
1354
|
+
link.id = genRandStr(8);
|
|
1355
|
+
|
|
1356
|
+
// Polymorphic link content structure
|
|
1357
|
+
const contentNestingType = Math.floor(secureRandom() * 3);
|
|
1358
|
+
if (contentNestingType === 1) {
|
|
1359
|
+
const span = document.createElement('span');
|
|
1360
|
+
span.className = genRandStr(5);
|
|
1361
|
+
span.innerHTML = `> ${i + 1}`;
|
|
1362
|
+
link.appendChild(span);
|
|
1363
|
+
} else if (contentNestingType === 2) {
|
|
1364
|
+
const b = document.createElement('b');
|
|
1365
|
+
b.className = genRandStr(5);
|
|
1366
|
+
b.innerHTML = `> ${i + 1}`;
|
|
1367
|
+
link.appendChild(b);
|
|
1368
|
+
} else {
|
|
1369
|
+
link.innerHTML = `<span>> ${i + 1}</span>`;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const trigger = (e) => {
|
|
1373
|
+
if (e && typeof e.preventDefault === 'function') {
|
|
1374
|
+
e.preventDefault();
|
|
1375
|
+
}
|
|
1376
|
+
this.onHoneypotTrigger();
|
|
1377
|
+
};
|
|
1378
|
+
link.addEventListener('click', trigger, { passive: false });
|
|
1379
|
+
link.addEventListener('focus', trigger, { passive: true });
|
|
1380
|
+
link.addEventListener('mouseover', trigger, { passive: true });
|
|
1381
|
+
|
|
1382
|
+
// Polymorphic link nesting
|
|
1383
|
+
if (secureRandom() > 0.5) {
|
|
1384
|
+
const itemContainer = document.createElement('span');
|
|
1385
|
+
itemContainer.className = genRandStr(5);
|
|
1386
|
+
itemContainer.appendChild(link);
|
|
1387
|
+
wrapper.appendChild(itemContainer);
|
|
1388
|
+
} else {
|
|
1389
|
+
wrapper.appendChild(link);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
shadow.appendChild(wrapper);
|
|
1394
|
+
document.body.appendChild(host);
|
|
1395
|
+
},
|
|
1396
|
+
/**
|
|
1397
|
+
* Intercepts a JSON challenge response, solves it, and retries the request.
|
|
1398
|
+
* @param {Response} response - Initial response (status 404/challenge).
|
|
1399
|
+
* @param {RequestInfo} resource - Original request resource.
|
|
1400
|
+
* @param {RequestInit} options - Original request options.
|
|
1401
|
+
* @param {number} [maxRetries=3] - Maximum allowed challenge retry attempts.
|
|
1402
|
+
* @returns {Promise<Response>} - Retried response.
|
|
1403
|
+
* @private
|
|
1404
|
+
*/
|
|
1405
|
+
async solveChallengeAndRetry(response, resource, options = {}, maxRetries = 3) {
|
|
1406
|
+
const effectiveMaxRetries = (options && typeof options._powMaxRetries === 'number')
|
|
1407
|
+
? options._powMaxRetries
|
|
1408
|
+
: (typeof maxRetries === 'number' ? maxRetries : 3);
|
|
1409
|
+
const currentRetries = (options && options._powRetryCount) || (resource && resource._powRetryCount) || 0;
|
|
1410
|
+
if (currentRetries >= effectiveMaxRetries) {
|
|
1411
|
+
console.warn(`[Fingerprint] Challenge retry limit reached (${effectiveMaxRetries}). Aborting challenge retry loop.`);
|
|
1412
|
+
return response;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (!response || response.status !== 404 || !response.headers?.get?.('content-type')?.includes('application/json') || response.bodyUsed) {
|
|
1416
|
+
return response;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
try {
|
|
1420
|
+
const challengeData = await response.json();
|
|
1421
|
+
if (!challengeData.challenge || !challengeData.challenge.type) {
|
|
1422
|
+
return response; // Not a valid JSON challenge
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
1426
|
+
this._dispatchEvent('challengeReceived', { challenge: challengeData.challenge });
|
|
1427
|
+
|
|
1428
|
+
// The device fingerprint solving the challenge is crucial
|
|
1429
|
+
const solverFp = this.getDeviceFingerprint();
|
|
1430
|
+
const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
|
|
1431
|
+
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
1432
|
+
|
|
1433
|
+
this._dispatchEvent('challengeSolved', { solution: solutionWrapper.rawSolution });
|
|
1434
|
+
// Append solution parameters to retry request URL
|
|
1435
|
+
const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
|
|
1436
|
+
solutionWrapper.applyToUrl(url);
|
|
1437
|
+
|
|
1438
|
+
// Attach solver fingerprint to retry request
|
|
1439
|
+
url.searchParams.set('pow_fp', solverFp);
|
|
1440
|
+
|
|
1441
|
+
// Use fetch to retry with incremented retry count to prevent infinite challenge loops
|
|
1442
|
+
const retryOptions = {
|
|
1443
|
+
...options,
|
|
1444
|
+
_powRetryCount: currentRetries + 1,
|
|
1445
|
+
_powMaxRetries: effectiveMaxRetries
|
|
1446
|
+
};
|
|
1447
|
+
const fetchFn = (typeof window !== 'undefined' && typeof window.fetch === 'function')
|
|
1448
|
+
? window.fetch
|
|
1449
|
+
: fetch;
|
|
1450
|
+
return fetchFn(url.toString(), retryOptions);
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
console.error('[Fingerprint] Failed to solve or retry challenge:', e);
|
|
1453
|
+
return response; // Retourne la réponse 429 originale en cas d'échec
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Initializes all client-side protections in one call.
|
|
1459
|
+
* Also loads WASM module if `wasmPath` is provided.
|
|
1460
|
+
* Recommended initialization approach.
|
|
1461
|
+
* @param {ClientConfig} [config={}] - Client configuration options.
|
|
1462
|
+
*/
|
|
1463
|
+
initializeClient(config = {}) {
|
|
1464
|
+
const rawHoneypots = config.honeypots ?? config.honeypotFields ?? [];
|
|
1465
|
+
const honeypots = Array.isArray(rawHoneypots)
|
|
1466
|
+
? rawHoneypots
|
|
1467
|
+
: (rawHoneypots ? [rawHoneypots] : []);
|
|
1468
|
+
const {
|
|
1469
|
+
mouse = true,
|
|
1470
|
+
keystrokes = true,
|
|
1471
|
+
clicks = true, // Add new option
|
|
1472
|
+
touches = true, // Touch event tracking
|
|
1473
|
+
motion = true,
|
|
1474
|
+
rendering = true,
|
|
1475
|
+
phantomTraps = true,
|
|
1476
|
+
trapUrls = [], // Trap URLs
|
|
1477
|
+
wasm = true,
|
|
1478
|
+
wasmPath = (config.wasmPath || (config.wasm ? '/fp.wasm' : undefined)),
|
|
1479
|
+
worker = false,
|
|
1480
|
+
workerPath, // NOUVEAU
|
|
1481
|
+
fetch: fetchConfig = {}
|
|
1482
|
+
} = config;
|
|
1483
|
+
|
|
1484
|
+
negotiateSessionKey();
|
|
1485
|
+
// Load WASM module if path provided
|
|
1486
|
+
if (wasm && wasmPath) {
|
|
1487
|
+
this.initializeWasm(wasmPath);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (mouse) {
|
|
1491
|
+
this.startMouseEntropyTracker();
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Instantiation du Service Worker via initializeClient
|
|
1495
|
+
let resolvedWorkerPath = null;
|
|
1496
|
+
let workerScope = undefined;
|
|
1497
|
+
let workerTargetDomains = fetchConfig.targetDomains || [];
|
|
1498
|
+
|
|
1499
|
+
if (typeof worker === 'string') {
|
|
1500
|
+
resolvedWorkerPath = worker;
|
|
1501
|
+
} else if (typeof worker === 'object' && worker !== null) {
|
|
1502
|
+
resolvedWorkerPath = worker.path || '/fingerprint.worker.js';
|
|
1503
|
+
workerScope = worker.scope;
|
|
1504
|
+
if (Array.isArray(worker.targetDomains)) {
|
|
1505
|
+
workerTargetDomains = worker.targetDomains;
|
|
1506
|
+
}
|
|
1507
|
+
} else if (worker === true) {
|
|
1508
|
+
resolvedWorkerPath = '/fingerprint.worker.js';
|
|
1509
|
+
} else if (workerPath) {
|
|
1510
|
+
resolvedWorkerPath = workerPath;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (resolvedWorkerPath) {
|
|
1514
|
+
this.workerPath = resolvedWorkerPath;
|
|
1515
|
+
this._targetDomains = workerTargetDomains;
|
|
1516
|
+
this.initServiceWorker(resolvedWorkerPath, workerTargetDomains, workerScope);
|
|
1517
|
+
}
|
|
1518
|
+
if (keystrokes) {
|
|
1519
|
+
this.startKeystrokeDynamicsTracker();
|
|
1520
|
+
}
|
|
1521
|
+
if (clicks) {
|
|
1522
|
+
this.startClickTracker();
|
|
1523
|
+
}
|
|
1524
|
+
if (touches) {
|
|
1525
|
+
this.startTouchEventTracker();
|
|
1526
|
+
}
|
|
1527
|
+
if (motion) {
|
|
1528
|
+
this.startMotionTracker();
|
|
1529
|
+
}
|
|
1530
|
+
if (ClientLibrary.detectTamperedPrototypes()) {
|
|
1531
|
+
metrics.prototypeTampered = true;
|
|
1532
|
+
}
|
|
1533
|
+
if (rendering) {
|
|
1534
|
+
this.startRenderingTracker();
|
|
1535
|
+
}
|
|
1536
|
+
if (phantomTraps) {
|
|
1537
|
+
this.injectPhantomTraps();
|
|
1538
|
+
}
|
|
1539
|
+
if (honeypots.length > 0) {
|
|
1540
|
+
this.initializeHoneypots(honeypots);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Dynamically inject trap links on startup
|
|
1544
|
+
if (trapUrls.length > 0) {
|
|
1545
|
+
this.injectTrapLinks(trapUrls);
|
|
1546
|
+
}
|
|
1547
|
+
// Enable fetch monkey-patching only if explicitly configured and no Service Worker is used
|
|
1548
|
+
if (config.fetch && !resolvedWorkerPath) {
|
|
1549
|
+
this.initializeFetch(fetchConfig.targetDomains);
|
|
1550
|
+
|
|
1551
|
+
// Add challenge resolution interceptor
|
|
1552
|
+
if (fetchConfig.handleChallenges !== false) {
|
|
1553
|
+
const maxRetries = typeof fetchConfig.maxRetries === 'number' ? fetchConfig.maxRetries : 3;
|
|
1554
|
+
this.addFetchInterceptor(async (resource, options, next) => {
|
|
1555
|
+
const originalResponse = await next(resource, options);
|
|
1556
|
+
// Clone response to prevent draining the body stream for original caller
|
|
1557
|
+
return this.solveChallengeAndRetry(originalResponse.clone(), resource, options, maxRetries);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Attempts to load and initialize WebAssembly module for accelerated hashing.
|
|
1565
|
+
* Falls back to JavaScript implementation on error.
|
|
1566
|
+
* @param {string} wasmPath - Path to WASM loader script or binary.
|
|
1567
|
+
*/
|
|
1568
|
+
async initializeWasm(wasmPath) {
|
|
1569
|
+
try {
|
|
1570
|
+
// Direct standalone polymorphic WebAssembly loading
|
|
1571
|
+
if (wasmPath.endsWith('.wasm')) {
|
|
1572
|
+
let instance;
|
|
1573
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
1574
|
+
try {
|
|
1575
|
+
const response = await fetch(wasmPath);
|
|
1576
|
+
const result = await WebAssembly.instantiateStreaming(response, {});
|
|
1577
|
+
instance = result.instance;
|
|
1578
|
+
} catch (streamingError) {
|
|
1579
|
+
console.warn('[Fingerprint] WebAssembly.instantiateStreaming failed, falling back to compile:', streamingError);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (!instance) {
|
|
1583
|
+
const response = await fetch(wasmPath);
|
|
1584
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1585
|
+
const module = await WebAssembly.compile(arrayBuffer);
|
|
1586
|
+
instance = await WebAssembly.instantiate(module, {});
|
|
1587
|
+
}
|
|
1588
|
+
const exports = instance.exports;
|
|
1589
|
+
const memory = exports.memory;
|
|
1590
|
+
activeCyrb53 = (str) => {
|
|
1591
|
+
const encoder = new TextEncoder();
|
|
1592
|
+
const bytes = encoder.encode(str);
|
|
1593
|
+
const view = new Uint8Array(memory.buffer, 0, bytes.length);
|
|
1594
|
+
view.set(bytes);
|
|
1595
|
+
return exports.hash(0, bytes.length);
|
|
1596
|
+
};
|
|
1597
|
+
console.log('[Fingerprint] Standalone polymorphic WASM loaded successfully. Using fast dynamic hashing.');
|
|
1598
|
+
if (this._cachedBuilder) {
|
|
1599
|
+
this._cachedBuilder.addRaw('wasm', 'true');
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
// 1. Inject WASM loader script
|
|
1604
|
+
const script = document.createElement('script');
|
|
1605
|
+
script.src = wasmPath;
|
|
1606
|
+
await new Promise((resolve, reject) => {
|
|
1607
|
+
script.onload = resolve;
|
|
1608
|
+
script.onerror = reject;
|
|
1609
|
+
document.head.appendChild(script);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// 2. Await global module factory
|
|
1613
|
+
if (typeof window.createFingerprintModule !== 'function') {
|
|
1614
|
+
throw new Error('WASM loader script did not expose createFingerprintModule.');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// 3. Initialize module
|
|
1618
|
+
const wasmUrl = wasmPath.replace(/\.js$/, '.wasm');
|
|
1619
|
+
const wasmModule = await window.createFingerprintModule({
|
|
1620
|
+
instantiateWasm: (imports, successCallback) => {
|
|
1621
|
+
(async () => {
|
|
1622
|
+
try {
|
|
1623
|
+
const cached = await getCachedWasm(wasmUrl);
|
|
1624
|
+
if (cached) {
|
|
1625
|
+
let instance;
|
|
1626
|
+
if (cached instanceof WebAssembly.Module) {
|
|
1627
|
+
instance = await WebAssembly.instantiate(cached, imports);
|
|
1628
|
+
} else {
|
|
1629
|
+
const result = await WebAssembly.instantiate(cached, imports);
|
|
1630
|
+
instance = result.instance;
|
|
1631
|
+
}
|
|
1632
|
+
successCallback(instance, cached);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const response = await fetch(wasmUrl);
|
|
1637
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1638
|
+
|
|
1639
|
+
let cachedData = arrayBuffer;
|
|
1640
|
+
let isModuleCached = false;
|
|
1641
|
+
try {
|
|
1642
|
+
const compiledModule = await WebAssembly.compile(arrayBuffer);
|
|
1643
|
+
const success = await cacheWasm(wasmUrl, compiledModule);
|
|
1644
|
+
if (success) {
|
|
1645
|
+
cachedData = compiledModule;
|
|
1646
|
+
isModuleCached = true;
|
|
1647
|
+
}
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
// Fallback if browser doesn't allow structured cloning of Compiled Modules
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (!isModuleCached) {
|
|
1653
|
+
await cacheWasm(wasmUrl, arrayBuffer);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
let instance;
|
|
1657
|
+
if (cachedData instanceof WebAssembly.Module) {
|
|
1658
|
+
instance = await WebAssembly.instantiate(cachedData, imports);
|
|
1659
|
+
} else {
|
|
1660
|
+
const result = await WebAssembly.instantiate(arrayBuffer, imports);
|
|
1661
|
+
instance = result.instance;
|
|
1662
|
+
}
|
|
1663
|
+
successCallback(instance, cachedData);
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
console.warn('[Fingerprint] Custom WASM instantiation failed, falling back to default Emscripten loader:', err);
|
|
1666
|
+
successCallback(null);
|
|
1667
|
+
}
|
|
1668
|
+
})();
|
|
1669
|
+
return {}; // Async instantiation indicator for Emscripten
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
if (typeof wasmModule._hash_string !== 'function') {
|
|
1673
|
+
throw new Error('WASM module did not export _hash_string.');
|
|
1674
|
+
}
|
|
1675
|
+
window.wasmModule = wasmModule;
|
|
1676
|
+
ClientLibrary.wasmModule = wasmModule;
|
|
1677
|
+
|
|
1678
|
+
// Static memory pool: 4KB pre-allocated buffer
|
|
1679
|
+
// Avoids heap malloc/free churn on rapid mouse and keyboard tracking
|
|
1680
|
+
const staticBufferSize = 4096;
|
|
1681
|
+
const staticBufferPtr = wasmModule._malloc(staticBufferSize);
|
|
1682
|
+
const encoder = new TextEncoder();
|
|
1683
|
+
|
|
1684
|
+
// 4. Swap hash function with WASM implementation
|
|
1685
|
+
activeCyrb53 = (str) => {
|
|
1686
|
+
const bytes = encoder.encode(str);
|
|
1687
|
+
if (bytes.length < staticBufferSize) {
|
|
1688
|
+
wasmModule.HEAPU8.set(bytes, staticBufferPtr);
|
|
1689
|
+
wasmModule.HEAPU8[staticBufferPtr + bytes.length] = 0; // null-terminator
|
|
1690
|
+
return wasmModule._hash_string(staticBufferPtr);
|
|
1691
|
+
}
|
|
1692
|
+
// Fallback if string exceeds static buffer size
|
|
1693
|
+
return wasmModule._hash_string(str);
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
console.log('[Fingerprint] WASM module loaded successfully. Using fast hashing.');
|
|
1697
|
+
// Tag fingerprint with wasm marker
|
|
1698
|
+
if (this._cachedBuilder) {
|
|
1699
|
+
this._cachedBuilder.addRaw('wasm', 'true');
|
|
1700
|
+
}
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
console.warn('[Fingerprint] WASM module failed to load. Falling back to JS implementation. Error:', error);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* @typedef {object} ClientBehaviorMetrics
|
|
1709
|
+
* @property {number} mouseEntropy - Mouse entropy estimate.
|
|
1710
|
+
* @property {Array<{x: number, y: number, t: number}>} mouseMovementsHistory - Mouse movement coordinates.
|
|
1711
|
+
* @property {number} keystrokeLatency - Average latency between keystrokes.
|
|
1712
|
+
* @property {boolean} honeypotInteraction - True if honeypot was triggered.
|
|
1713
|
+
* @property {Array<{x: number, y: number, t: number, targetId: string}>} clicksHistory - Click interaction history.
|
|
1714
|
+
* @property {Array<{x: number, y: number, t: number, p: number, r: number, num: number}>} touchMovementsHistory - Touch movement trajectory.
|
|
1715
|
+
* @property {number} historyLength - Browser window.history.length.
|
|
1716
|
+
* @property {number} clientTimestamp - Metrics collection timestamp.
|
|
1717
|
+
* @property {string[]} [trapUrls] - Dynamically injected trap URLs.
|
|
1718
|
+
*/
|
|
1719
|
+
/** @type {ClientBehaviorMetrics} */
|
|
1720
|
+
const metrics = {
|
|
1721
|
+
mouseEntropy: 0, // Retained for compatibility; analysis primarily evaluates trajectory history
|
|
1722
|
+
mouseMovementsHistory: [],
|
|
1723
|
+
touchMovementsHistory: [],
|
|
1724
|
+
clicksHistory: [],
|
|
1725
|
+
keystrokeLatency: 0,
|
|
1726
|
+
honeypotInteraction: false,
|
|
1727
|
+
historyLength: 0,
|
|
1728
|
+
clientTimestamp: 0,
|
|
1729
|
+
rendering: { fps: 0, jitter: 0, offscreenAnom: false },
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
let lastMousePos = { x: 0, y: 0 };
|
|
1733
|
+
let mouseMovementsHistory = []; // Mouse movement trajectory
|
|
1734
|
+
let touchMovementsHistory = []; // Touch movement trajectory
|
|
1735
|
+
const TOUCH_HISTORY_MAX = 100;
|
|
1736
|
+
const MOUSE_HISTORY_MAX = 100;
|
|
1737
|
+
let clicksHistory = [];
|
|
1738
|
+
const CLICKS_HISTORY_MAX = 50;
|
|
1739
|
+
let activeHoneypotListeners = new Map();
|
|
1740
|
+
let keystrokeTimestamps = [];
|
|
1741
|
+
let keystrokeLatencies = [];
|
|
1742
|
+
let keystrokeDwellTimes = [];
|
|
1743
|
+
let keystrokeFlightTimes = [];
|
|
1744
|
+
const KEYSTROKE_HISTORY_MAX = 20; // Keep the last 20 keystroke events
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
// Exporter les fonctions individuellement pour la compatibilité ascendante
|
|
1749
|
+
export const getDeviceFingerprint = ClientLibrary.getDeviceFingerprint.bind(ClientLibrary);
|
|
1750
|
+
export const generateRequestSignature = ClientLibrary.generateRequestSignature.bind(ClientLibrary);
|
|
1751
|
+
export const generateClientSideSignature = ClientLibrary.generateClientSideSignature.bind(ClientLibrary);
|
|
1752
|
+
export const _resetCache = ClientLibrary._resetCache.bind(ClientLibrary);
|
|
1753
|
+
export const startMouseEntropyTracker = ClientLibrary.startMouseEntropyTracker.bind(ClientLibrary);
|
|
1754
|
+
export const startKeystrokeDynamicsTracker = ClientLibrary.startKeystrokeDynamicsTracker.bind(ClientLibrary);
|
|
1755
|
+
export const startClickTracker = ClientLibrary.startClickTracker.bind(ClientLibrary);
|
|
1756
|
+
export const startTouchEventTracker = ClientLibrary.startTouchEventTracker.bind(ClientLibrary);
|
|
1757
|
+
export const startRenderingTracker = ClientLibrary.startRenderingTracker.bind(ClientLibrary);
|
|
1758
|
+
export const startMotionTracker = ClientLibrary.startMotionTracker.bind(ClientLibrary);
|
|
1759
|
+
export const detectTamperedPrototypes = ClientLibrary.detectTamperedPrototypes.bind(ClientLibrary);
|
|
1760
|
+
export const initializeHoneypots = ClientLibrary.initializeHoneypots.bind(ClientLibrary);
|
|
1761
|
+
export const getClientBehaviorMetrics = ClientLibrary.getClientBehaviorMetrics.bind(ClientLibrary);
|
|
1762
|
+
export const protectedFetch = ClientLibrary.protectedFetch.bind(ClientLibrary);
|
|
1763
|
+
export const addFetchInterceptor = ClientLibrary.addFetchInterceptor.bind(ClientLibrary);
|
|
1764
|
+
export const patchGlobalFetch = ClientLibrary.patchGlobalFetch.bind(ClientLibrary);
|
|
1765
|
+
export const initializeFetch = ClientLibrary.initializeFetch.bind(ClientLibrary);
|
|
1766
|
+
export const initializeClient = ClientLibrary.initializeClient.bind(ClientLibrary);
|
|
1767
|
+
export const initializeWasm = ClientLibrary.initializeWasm.bind(ClientLibrary);
|
|
1768
|
+
export const injectTrapLinks = ClientLibrary.injectTrapLinks.bind(ClientLibrary);
|
|
1769
|
+
export const injectPhantomTraps = ClientLibrary.injectPhantomTraps.bind(ClientLibrary);
|
|
1770
|
+
export const solveChallengeAndRetry = ClientLibrary.solveChallengeAndRetry.bind(ClientLibrary);
|
|
1771
|
+
export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrary);
|
|
1772
|
+
export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
|
|
1773
|
+
export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
|
|
1774
|
+
export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
|
|
1775
|
+
export const initServiceWorker = ClientLibrary.initServiceWorker.bind(ClientLibrary);
|
|
1776
|
+
export const syncServiceWorkerCredentials = ClientLibrary.syncServiceWorkerCredentials.bind(ClientLibrary);
|
|
1777
|
+
|
|
1778
|
+
// Export the internal object for testing purposes
|
|
1779
|
+
export default ClientLibrary;
|
|
1780
|
+
|
|
1781
|
+
// --- Global Export for Browser ---
|
|
1782
|
+
// Attach the library to the window object to make it accessible from inline scripts.
|
|
1783
|
+
if (typeof window !== 'undefined') {
|
|
1784
|
+
window.ClientLibrary = ClientLibrary;
|
|
1773
1785
|
}
|