@anonympins/fingerprint 0.3.2 → 0.3.3
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 +172 -0
- package/README.md +276 -34
- package/composer.json +38 -0
- package/index.js +5 -0
- package/package.json +23 -18
- package/phpunit.xml +20 -0
- package/public/fp.js +2 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +255 -101
- package/{library.js → src/js/library.js} +1729 -1729
- package/{problem-manager.js → src/js/problem-manager.js} +539 -522
- package/src/php/AutoTuner.php +155 -0
- package/src/php/Challenge/ChallengeUtils.php +306 -0
- package/src/php/Config/SecurityProfiles.php +257 -0
- package/src/php/DirectFingerprint.php +81 -0
- package/src/php/FingerprintBuilder.php +185 -0
- package/src/php/FingerprintClient.php +118 -0
- package/src/php/FingerprintEngine.php +850 -0
- package/src/php/Optimization/FunctionRegistry.php +63 -0
- package/src/php/Optimization/Optimization.php +256 -0
- package/src/php/Optimization/OptimizationOperators.php +305 -0
- package/src/php/Optimization/ProblemInitializers.php +53 -0
- package/src/php/ProblemManager.php +255 -0
- package/src/php/RequestContext.php +87 -0
- package/src/php/Store/IStore.php +42 -0
- package/src/php/Store/InMemoryStore.php +67 -0
- package/src/php/Store/StoreManager.php +26 -0
- package/src/php/Tests/ChallengeUtilsTest.php +82 -0
- package/src/php/Tests/FingerprintBuilderTest.php +58 -0
- package/src/php/Tests/FingerprintEngineTest.php +219 -0
- package/src/php/Tests/PowTest.php +40 -0
- package/src/php/Tests/ProblemManagerTest.php +295 -0
- package/src/php/Tests/RequestUtilsTest.php +81 -0
- package/src/php/Tests/problems.config.json +9 -0
- package/src/php/Utils/BigInt.php +102 -0
- package/src/php/Utils/BlockList.php +100 -0
- package/src/php/Utils/Logger.php +30 -0
- package/src/php/Utils/MaliciousPatterns.php +59 -0
- package/src/php/Utils/RequestUtils.php +673 -0
- package/fingerprint.client.obfuscated.js +0 -1
- /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
- /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
- /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
- /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
- /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
- /package/{redis-store.js → src/js/redis-store.js} +0 -0
- /package/{sql-store.js → src/js/sql-store.js} +0 -0
package/package.json
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Advanced anti-bot library for Node.js using multi-layer fingerprinting (JA3, client-side, headers), behavioral analysis, and adaptive Proof-of-Work (PoW) challenges to mitigate scraping, scalping, and automated threats.",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.0.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "vitest run --reporter=verbose",
|
|
12
|
-
"build": "node build-client.js"
|
|
12
|
+
"build": "node src/js/build-client.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./index.js",
|
|
16
|
+
"./client": {
|
|
17
|
+
"import": "./src/js/fingerprint.client.obfuscated.js",
|
|
18
|
+
"require": "./src/js/fingerprint.client.obfuscated.js",
|
|
19
|
+
"browser": "./src/js/fingerprint.client.obfuscated.js"
|
|
20
|
+
},
|
|
21
|
+
"./client/src/*": "./src/js/*",
|
|
22
|
+
"./fp.wasm": "./public/fp.wasm",
|
|
23
|
+
"./fp.js": "./public/fp.js",
|
|
24
|
+
"./package.json": "./package.json"
|
|
13
25
|
},
|
|
14
26
|
"files": [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"fingerprint.builder.js",
|
|
19
|
-
"pow.solver.js",
|
|
20
|
-
"pow.worker.js",
|
|
21
|
-
"pow.solver.inline.js",
|
|
22
|
-
"problem-manager.js",
|
|
23
|
-
"optimization.worker.js",
|
|
24
|
-
"library.js",
|
|
25
|
-
"redis-store.js",
|
|
26
|
-
"mongodb-store.js",
|
|
27
|
-
"sql-store.js",
|
|
28
|
-
"public/fp.wasm",
|
|
27
|
+
"index.js",
|
|
28
|
+
"src/js/",
|
|
29
|
+
"public/",
|
|
29
30
|
"README.md",
|
|
30
|
-
"LICENSE"
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"src/php/",
|
|
34
|
+
"composer.json",
|
|
35
|
+
"phpunit.xml"
|
|
31
36
|
],
|
|
32
37
|
"repository": {
|
|
33
38
|
"type": "git",
|
package/phpunit.xml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
3
|
+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
|
|
4
|
+
bootstrap="vendor/autoload.php"
|
|
5
|
+
colors="true">
|
|
6
|
+
<testsuites>
|
|
7
|
+
<testsuite name="default">
|
|
8
|
+
<directory>src/php/Tests</directory>
|
|
9
|
+
</testsuite>
|
|
10
|
+
</testsuites>
|
|
11
|
+
|
|
12
|
+
<coverage>
|
|
13
|
+
<include>
|
|
14
|
+
<directory suffix=".php">./src/php</directory>
|
|
15
|
+
</include>
|
|
16
|
+
<exclude>
|
|
17
|
+
<directory>./src/php/Tests</directory>
|
|
18
|
+
</exclude>
|
|
19
|
+
</coverage>
|
|
20
|
+
</phpunit>
|
package/public/fp.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var createFingerprintModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAPU8=new Uint8Array(b);HEAPU32=new Uint32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("fp.wasm")}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAP8;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}var _hash_string,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_hash_string=Module["_hash_string"]=wasmExports["f"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={c:___cxa_throw,a:__abort_js,b:_emscripten_resize_heap};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
|
|
2
|
+
;return Module}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createFingerprintModule;module.exports.default=createFingerprintModule}else if(typeof define==="function"&&define["amd"])define([],()=>createFingerprintModule);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
5
|
+
import JavaScriptObfuscator from 'javascript-obfuscator';
|
|
6
|
+
import { minify } from 'terser';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tente d'exécuter la commande de build pour le module WASM.
|
|
13
|
+
* Ne bloque pas le build si la commande échoue (ex: em++ non trouvé).
|
|
14
|
+
*/
|
|
15
|
+
function buildWasm() {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
console.log('Attempting to build WASM module (optional)...');
|
|
18
|
+
const command = "em++ src/cpp/main.cpp src/cpp/utils.cpp -o public/fp.js -s WASM=1 -s MODULARIZE=1 -s EXPORT_NAME='createFingerprintModule' -s \"EXPORTED_FUNCTIONS=['_hash_string']\" -O3 --no-entry";
|
|
19
|
+
|
|
20
|
+
exec(command, (error, stdout, stderr) => {
|
|
21
|
+
if (error) {
|
|
22
|
+
console.warn('WASM build failed (this is optional and can be ignored):', stderr);
|
|
23
|
+
} else {
|
|
24
|
+
console.log('WASM module built successfully.');
|
|
25
|
+
}
|
|
26
|
+
resolve(); // Toujours résoudre la promesse pour ne pas bloquer le build principal.
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildClientScript() {
|
|
32
|
+
try {
|
|
33
|
+
console.log('Reading client script...');
|
|
34
|
+
const clientScriptPath = join(__dirname, 'fingerprint.client.js');
|
|
35
|
+
const clientScriptContent = await fs.readFile(clientScriptPath, 'utf-8');
|
|
36
|
+
|
|
37
|
+
console.log('Obfuscating client script...');
|
|
38
|
+
const obfuscationResult = JavaScriptObfuscator.obfuscate(clientScriptContent, {
|
|
39
|
+
compact: true,
|
|
40
|
+
controlFlowFlattening: true, // Aplatit le flux de contrôle
|
|
41
|
+
deadCodeInjection: true, // Injecte du code mort
|
|
42
|
+
stringArray: true,
|
|
43
|
+
stringArrayRotate: true, // Fait tourner le tableau de chaînes
|
|
44
|
+
stringArrayShuffle: true, // Mélange le tableau de chaînes
|
|
45
|
+
// L'utilisation d'une graine (seed) de 0 signifie que l'obfuscation sera déterministe
|
|
46
|
+
// pour une même entrée. Pour une obfuscation unique à chaque build, on peut utiliser
|
|
47
|
+
// une graine aléatoire, par exemple : seed: Math.random()
|
|
48
|
+
seed: Math.random(),
|
|
49
|
+
selfDefending: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const obfuscatedCode = obfuscationResult.getObfuscatedCode();
|
|
53
|
+
|
|
54
|
+
console.log('Writing obfuscated script to fingerprint.client.obfuscated.js...');
|
|
55
|
+
await fs.writeFile(join(__dirname, './fingerprint.client.obfuscated.js'), obfuscatedCode);
|
|
56
|
+
|
|
57
|
+
console.log('Client script build process completed successfully.');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Error during client script build:', error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
await buildClientScript();
|
|
66
|
+
await buildWasm();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main();
|
|
@@ -1,176 +1,169 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
-
*/
|
|
4
|
-
// Exporté pour être utilisé comme fallback par fingerprint.client.js
|
|
5
|
-
export const cyrb53 = (str, seed = 0) => {
|
|
6
|
-
let h1 = 0xdeadbeef ^ seed,
|
|
7
|
-
h2 = 0x41c6ce57 ^ seed;
|
|
8
|
-
for (let i = 0, ch; i < str.length; i++) {
|
|
9
|
-
ch = str.charCodeAt(i);
|
|
10
|
-
h1 = Math.imul(h1 ^ ch, 2654435761); // Use Math.imul for 32-bit multiplication
|
|
11
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
12
|
-
}
|
|
13
|
-
h1 =
|
|
14
|
-
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
15
|
-
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
16
|
-
h2 =
|
|
17
|
-
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
18
|
-
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
19
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
24
|
-
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
25
|
-
*/
|
|
26
|
-
export class FingerprintBuilder {
|
|
27
|
-
constructor() {
|
|
28
|
-
// Le hasher est maintenant une propriété pour pouvoir être surchargé par le client WASM.
|
|
29
|
-
this.components = new Map();
|
|
30
|
-
// FIX: Initialiser le hasher par défaut à l'implémentation JS.
|
|
31
|
-
this.hasher = cyrb53;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Ajoute un composant au hash global.
|
|
36
|
-
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
37
|
-
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
38
|
-
*/
|
|
39
|
-
add(group, value) {
|
|
40
|
-
if (value === undefined || value === null) return this;
|
|
41
|
-
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
42
|
-
this.components.set(group, this.hasher(String(value)));
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Adds a raw component without hashing it.
|
|
48
|
-
* Useful for metrics that need to be read on the server.
|
|
49
|
-
* @param {string} group - The name of the group.
|
|
50
|
-
* @param {string|number} value - The raw value.
|
|
51
|
-
*/
|
|
52
|
-
addRaw(group, value) {
|
|
53
|
-
if (value === undefined || value === null) return this;
|
|
54
|
-
this.components.set(group, value);
|
|
55
|
-
return this;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Affiche les composants actuels dans la console.
|
|
60
|
-
* @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
|
|
61
|
-
*/
|
|
62
|
-
log(title = 'FingerprintBuilder Components') {
|
|
63
|
-
console.log(`--- ${title} ---`);
|
|
64
|
-
const sortedComponents = Array.from(this.components.entries())
|
|
65
|
-
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
66
|
-
|
|
67
|
-
console.table(Object.fromEntries(sortedComponents));
|
|
68
|
-
console.log(`Final string: ${this.toString()}`);
|
|
69
|
-
console.log(`---------------------------------${'-'.repeat(title.length)}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Génère la chaîne de signature finale.
|
|
74
|
-
* Trie les clés pour garantir un ordre déterministe.
|
|
75
|
-
*/
|
|
76
|
-
toString() {
|
|
77
|
-
return Array.from(this.components.entries())
|
|
78
|
-
.sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
|
|
79
|
-
.map(([key, hash]) => `${key}:${hash}`)
|
|
80
|
-
.join("|");
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Adds a raw component without hashing it.
|
|
84
|
-
* Useful for metrics that need to be read on the server.
|
|
85
|
-
* @param {string} group - The name of the group.
|
|
86
|
-
* @param {string|number} value - The raw value.
|
|
87
|
-
*/
|
|
88
|
-
addRaw(group, value) {
|
|
89
|
-
if (value === undefined || value === null) return this;
|
|
90
|
-
this.components.set(group, value);
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Compares two fingerprints and returns a similarity score (0 to 1).
|
|
96
|
-
* Uses weights to give more importance to strong invariants (Canvas, GPU).
|
|
97
|
-
* @param {string} fpString1 - Fingerprint A
|
|
98
|
-
* @param {string} fpString2 - Fingerprint B
|
|
99
|
-
*/
|
|
100
|
-
// Note on `volatileKeys`: These keys are ignored during the comparison between the fingerprint
|
|
101
|
-
// of the request that *triggered* a challenge and the fingerprint of the request that *submits*
|
|
102
|
-
// the solution. This is because headers like Client-Hints (ch_*), cookie presence, and upgrade-insecure-requests
|
|
103
|
-
// can legitimately change or be absent on the subsequent request, especially after a redirect.
|
|
104
|
-
// By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
|
|
105
|
-
static compare(fpString1, fpString2) {
|
|
106
|
-
if (!fpString1 || !fpString2) return 0;
|
|
107
|
-
|
|
108
|
-
const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
|
|
109
|
-
|
|
110
|
-
const map1 = parse(fpString1);
|
|
111
|
-
const map2 = parse(fpString2);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// --- Signaux
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// On
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (map1.get(key) === map2.get(key)) {
|
|
170
|
-
weightedMatches += weight;
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
|
|
175
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
+
*/
|
|
4
|
+
// Exporté pour être utilisé comme fallback par fingerprint.client.js
|
|
5
|
+
export const cyrb53 = (str, seed = 0) => {
|
|
6
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
7
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
8
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
9
|
+
ch = str.charCodeAt(i);
|
|
10
|
+
h1 = Math.imul(h1 ^ ch, 2654435761); // Use Math.imul for 32-bit multiplication
|
|
11
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
12
|
+
}
|
|
13
|
+
h1 =
|
|
14
|
+
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
15
|
+
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
16
|
+
h2 =
|
|
17
|
+
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
18
|
+
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
19
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
24
|
+
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
25
|
+
*/
|
|
26
|
+
export class FingerprintBuilder {
|
|
27
|
+
constructor() {
|
|
28
|
+
// Le hasher est maintenant une propriété pour pouvoir être surchargé par le client WASM.
|
|
29
|
+
this.components = new Map();
|
|
30
|
+
// FIX: Initialiser le hasher par défaut à l'implémentation JS.
|
|
31
|
+
this.hasher = cyrb53;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ajoute un composant au hash global.
|
|
36
|
+
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
37
|
+
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
38
|
+
*/
|
|
39
|
+
add(group, value) {
|
|
40
|
+
if (value === undefined || value === null) return this;
|
|
41
|
+
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
42
|
+
this.components.set(group, this.hasher(String(value)));
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds a raw component without hashing it.
|
|
48
|
+
* Useful for metrics that need to be read on the server.
|
|
49
|
+
* @param {string} group - The name of the group.
|
|
50
|
+
* @param {string|number} value - The raw value.
|
|
51
|
+
*/
|
|
52
|
+
addRaw(group, value) {
|
|
53
|
+
if (value === undefined || value === null) return this;
|
|
54
|
+
this.components.set(group, value);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Affiche les composants actuels dans la console.
|
|
60
|
+
* @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
|
|
61
|
+
*/
|
|
62
|
+
log(title = 'FingerprintBuilder Components') {
|
|
63
|
+
console.log(`--- ${title} ---`);
|
|
64
|
+
const sortedComponents = Array.from(this.components.entries())
|
|
65
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
66
|
+
|
|
67
|
+
console.table(Object.fromEntries(sortedComponents));
|
|
68
|
+
console.log(`Final string: ${this.toString()}`);
|
|
69
|
+
console.log(`---------------------------------${'-'.repeat(title.length)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Génère la chaîne de signature finale.
|
|
74
|
+
* Trie les clés pour garantir un ordre déterministe.
|
|
75
|
+
*/
|
|
76
|
+
toString() {
|
|
77
|
+
return Array.from(this.components.entries())
|
|
78
|
+
.sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
|
|
79
|
+
.map(([key, hash]) => `${key}:${hash}`)
|
|
80
|
+
.join("|");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Adds a raw component without hashing it.
|
|
84
|
+
* Useful for metrics that need to be read on the server.
|
|
85
|
+
* @param {string} group - The name of the group.
|
|
86
|
+
* @param {string|number} value - The raw value.
|
|
87
|
+
*/
|
|
88
|
+
addRaw(group, value) {
|
|
89
|
+
if (value === undefined || value === null) return this;
|
|
90
|
+
this.components.set(group, value);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compares two fingerprints and returns a similarity score (0 to 1).
|
|
96
|
+
* Uses weights to give more importance to strong invariants (Canvas, GPU).
|
|
97
|
+
* @param {string} fpString1 - Fingerprint A
|
|
98
|
+
* @param {string} fpString2 - Fingerprint B
|
|
99
|
+
*/
|
|
100
|
+
// Note on `volatileKeys`: These keys are ignored during the comparison between the fingerprint
|
|
101
|
+
// of the request that *triggered* a challenge and the fingerprint of the request that *submits*
|
|
102
|
+
// the solution. This is because headers like Client-Hints (ch_*), cookie presence, and upgrade-insecure-requests
|
|
103
|
+
// can legitimately change or be absent on the subsequent request, especially after a redirect.
|
|
104
|
+
// By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
|
|
105
|
+
static compare(fpString1, fpString2) {
|
|
106
|
+
if (!fpString1 || !fpString2) return 0;
|
|
107
|
+
|
|
108
|
+
const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
|
|
109
|
+
|
|
110
|
+
const map1 = parse(fpString1);
|
|
111
|
+
const map2 = parse(fpString2);
|
|
112
|
+
|
|
113
|
+
const volatileKeys = new Set([
|
|
114
|
+
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
115
|
+
'cookie_keys', 'upgrade',
|
|
116
|
+
'network', 'http_ver',
|
|
117
|
+
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// Poids de "véracité" (Entropie/Stabilité)
|
|
121
|
+
// Les poids sont augmentés pour donner plus d'importance aux signaux forts.
|
|
122
|
+
const weights = {
|
|
123
|
+
// --- Signaux très forts (difficiles à usurper) ---
|
|
124
|
+
cvs: 5.0, // Canvas: Très haute entropie (Rendu unique du GPU/driver)
|
|
125
|
+
gpu: 4.0, // GPU: Haute entropie (Matériel spécifique)
|
|
126
|
+
ja3: 3.5, // JA3: Identifie la librairie TLS (très stable pour un client donné)
|
|
127
|
+
ja4: 4.0, // JA4: Plus moderne, inclut HTTP/2
|
|
128
|
+
h2: 3.0, // HTTP/2 settings frame fingerprint
|
|
129
|
+
tcp: 2.5, // TCP/IP fingerprint
|
|
130
|
+
ua: 2.0, // User-Agent: Signal fort, bien que modifiable
|
|
131
|
+
|
|
132
|
+
// --- Signaux composites et dérivés ---
|
|
133
|
+
client_fp_hash: 3.0, // Le hash de l'empreinte client est un signal très fort.
|
|
134
|
+
browser: 1.5, // Le navigateur extrait du UA.
|
|
135
|
+
os_version: 1.5, // L'OS extrait du UA.
|
|
136
|
+
device_type: 1.0, // Le type d'appareil extrait du UA.
|
|
137
|
+
|
|
138
|
+
// --- Signaux moyens ---
|
|
139
|
+
hw: 1.5, // Hardware (CPU, RAM): Stabilité moyenne
|
|
140
|
+
scr: 1.0, // Screen: Stabilité moyenne
|
|
141
|
+
// 'os' est souvent la même chose que 'ch_platform', on peut le déprécier ou lui donner un poids faible.
|
|
142
|
+
os: 0.8, // OS (nav.platform): Assez stable
|
|
143
|
+
geo: 0.5, // Geo/Langue: Peut changer (VPN, voyage)
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let weightedMatches = 0;
|
|
147
|
+
let totalWeight = 0;
|
|
148
|
+
|
|
149
|
+
const allKeys = new Set([...map1.keys(), ...map2.keys()]);
|
|
150
|
+
|
|
151
|
+
allKeys.forEach((key) => {
|
|
152
|
+
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
153
|
+
if (volatileKeys.has(key)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const weight = weights[key] ?? 0;
|
|
158
|
+
// On ne compte une clé dans le poids total que si elle est présente dans au moins une des deux empreintes.
|
|
159
|
+
if (!map1.has(key) && !map2.has(key)) return;
|
|
160
|
+
|
|
161
|
+
totalWeight += weight; // N'incrémenter que si la clé est pertinente.
|
|
162
|
+
if (map1.get(key) === map2.get(key)) {
|
|
163
|
+
weightedMatches += weight;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
|
|
168
|
+
}
|
|
176
169
|
}
|