@anonympins/fingerprint 0.3.7 → 0.4.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 +331 -244
- package/README.md +63 -1201
- package/composer.json +38 -38
- package/index.js +4 -4
- package/package.json +103 -103
- package/phpunit.xml +20 -20
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +2 -0
- package/src/js/fingerprint.js +4429 -4220
- package/src/js/mongodb-store.js +79 -79
- package/src/js/pow.solver.inline.js +31 -0
- package/src/js/pow.solver.js +31 -0
- package/src/js/tests/fingerprint.builder.test.js +79 -0
- package/src/js/tests/fingerprint.client.init.test.js +120 -0
- package/src/js/tests/fingerprint.client.test.js +105 -0
- package/src/js/tests/fingerprint.engine.test.js +371 -0
- package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
- package/src/js/tests/fingerprint.test.js +2319 -0
- package/src/js/tests/ip-reputation.test.js +132 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
- package/src/js/tests/library.test.js +96 -0
- package/src/js/tests/metrics.test.js +104 -0
- package/src/js/tests/pow.solver.test.js +198 -0
- package/src/js/tests/problem-manager.test.js +323 -0
- package/src/js/tests/stores.test.js +118 -0
- package/src/php/Challenge/ChallengeUtils.php +361 -305
- package/src/php/Config/SecurityProfiles.php +271 -266
- package/src/php/FingerprintBuilder.php +185 -185
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +1006 -1006
- package/src/php/Ja3AnomalyDetector.php +227 -227
- package/src/php/Optimization/FunctionRegistry.php +62 -62
- package/src/php/Optimization/Optimization.php +255 -255
- package/src/php/Optimization/OptimizationOperators.php +304 -304
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +104 -104
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/ChallengeUtilsTest.php +81 -81
- package/src/php/Tests/FingerprintBuilderTest.php +57 -57
- package/src/php/Tests/FingerprintClientTest.php +71 -0
- package/src/php/Tests/FingerprintEngineTest.php +299 -299
- package/src/php/Tests/IpReputationTest.php +156 -156
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
- package/src/php/Tests/MetricsTest.php +45 -45
- package/src/php/Tests/PowTest.php +39 -39
- package/src/php/Tests/ProblemManagerTest.php +296 -296
- package/src/php/Tests/RequestUtilsTest.php +253 -145
- package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
- package/src/php/Tests/problems.config.json +8 -8
- package/src/php/Utils/BigInt.php +144 -144
- package/src/php/Utils/Logger.php +29 -29
- package/src/php/Utils/MaliciousPatterns.php +58 -58
- package/src/php/Utils/MetricsManager.php +166 -166
- package/src/php/Utils/RequestUtils.php +31 -4
- package/src/php/Utils/TLSClientHelloParser.php +117 -0
- package/src/php/bin/auto-tune.php +117 -117
package/src/js/mongodb-store.js
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for MongoDB.
|
|
3
|
-
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
-
* for automatic expiration of documents.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a MongoDB collection.
|
|
9
|
-
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
-
*
|
|
11
|
-
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
-
* In the mongo shell, run:
|
|
13
|
-
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
-
*
|
|
15
|
-
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
-
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
-
*/
|
|
19
|
-
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
-
const collection = db.collection(collectionName);
|
|
21
|
-
|
|
22
|
-
// Custom replacer/reviver to handle Set serialization (identical to Redis/SQL stores)
|
|
23
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
24
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
async get(key) {
|
|
28
|
-
const doc = await collection.findOne({ _id: key });
|
|
29
|
-
if (!doc) return null;
|
|
30
|
-
|
|
31
|
-
// Active expiration check to bypass eventual consistency of MongoDB's 60s TTL cleanup daemon
|
|
32
|
-
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
33
|
-
await this.delete(key);
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(doc.value, reviver);
|
|
39
|
-
} catch (e) {
|
|
40
|
-
// Fallback for legacy un-serialized raw values
|
|
41
|
-
return doc.value;
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
async set(key, value, ttl) {
|
|
45
|
-
const stringValue = JSON.stringify(value, replacer);
|
|
46
|
-
const doc = {
|
|
47
|
-
_id: key,
|
|
48
|
-
value: stringValue,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
if (ttl && ttl > 0) {
|
|
52
|
-
// Set the expiration date for the TTL index.
|
|
53
|
-
doc.expiresAt = new Date(Date.now() + ttl * 1000);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await collection.updateOne(
|
|
57
|
-
{ _id: key },
|
|
58
|
-
{ $set: doc },
|
|
59
|
-
{ upsert: true }
|
|
60
|
-
);
|
|
61
|
-
},
|
|
62
|
-
async has(key) {
|
|
63
|
-
const doc = await collection.findOne({ _id: key }, { projection: { expiresAt: 1 } });
|
|
64
|
-
if (!doc) return false;
|
|
65
|
-
|
|
66
|
-
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
67
|
-
await this.delete(key);
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
71
|
-
},
|
|
72
|
-
async delete(key) {
|
|
73
|
-
await collection.deleteOne({ _id: key });
|
|
74
|
-
},
|
|
75
|
-
async init() {
|
|
76
|
-
// Automates index configuration
|
|
77
|
-
await collection.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 });
|
|
78
|
-
}
|
|
79
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for MongoDB.
|
|
3
|
+
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
+
* for automatic expiration of documents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a MongoDB collection.
|
|
9
|
+
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
+
*
|
|
11
|
+
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
+
* In the mongo shell, run:
|
|
13
|
+
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
+
*
|
|
15
|
+
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
+
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
+
*/
|
|
19
|
+
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
+
const collection = db.collection(collectionName);
|
|
21
|
+
|
|
22
|
+
// Custom replacer/reviver to handle Set serialization (identical to Redis/SQL stores)
|
|
23
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
24
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
async get(key) {
|
|
28
|
+
const doc = await collection.findOne({ _id: key });
|
|
29
|
+
if (!doc) return null;
|
|
30
|
+
|
|
31
|
+
// Active expiration check to bypass eventual consistency of MongoDB's 60s TTL cleanup daemon
|
|
32
|
+
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
33
|
+
await this.delete(key);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(doc.value, reviver);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Fallback for legacy un-serialized raw values
|
|
41
|
+
return doc.value;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async set(key, value, ttl) {
|
|
45
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
46
|
+
const doc = {
|
|
47
|
+
_id: key,
|
|
48
|
+
value: stringValue,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (ttl && ttl > 0) {
|
|
52
|
+
// Set the expiration date for the TTL index.
|
|
53
|
+
doc.expiresAt = new Date(Date.now() + ttl * 1000);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await collection.updateOne(
|
|
57
|
+
{ _id: key },
|
|
58
|
+
{ $set: doc },
|
|
59
|
+
{ upsert: true }
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
async has(key) {
|
|
63
|
+
const doc = await collection.findOne({ _id: key }, { projection: { expiresAt: 1 } });
|
|
64
|
+
if (!doc) return false;
|
|
65
|
+
|
|
66
|
+
if (doc.expiresAt && new Date(doc.expiresAt) < new Date()) {
|
|
67
|
+
await this.delete(key);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
async delete(key) {
|
|
73
|
+
await collection.deleteOne({ _id: key });
|
|
74
|
+
},
|
|
75
|
+
async init() {
|
|
76
|
+
// Automates index configuration
|
|
77
|
+
await collection.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
80
|
}
|
|
@@ -22,6 +22,24 @@ async function solveCpuTargetInline(baseBlock, target, progressCallback) {
|
|
|
22
22
|
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
23
23
|
// --- END FIX ---
|
|
24
24
|
const encoder = new TextEncoder();
|
|
25
|
+
|
|
26
|
+
const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
|
|
27
|
+
if (wasmModule && typeof wasmModule._solve_cpu_target === 'function') {
|
|
28
|
+
const len = baseBlock.length;
|
|
29
|
+
const ptr = wasmModule._malloc(len);
|
|
30
|
+
wasmModule.HEAPU8.set(baseBlock, ptr);
|
|
31
|
+
const targetStr = typeof target === 'string' ? target : target.toString(16);
|
|
32
|
+
const targetPtr = wasmModule._malloc(targetStr.length + 1);
|
|
33
|
+
for (let i = 0; i < targetStr.length; i++) {
|
|
34
|
+
wasmModule.HEAP8[targetPtr + i] = targetStr.charCodeAt(i);
|
|
35
|
+
}
|
|
36
|
+
wasmModule.HEAP8[targetPtr + targetStr.length] = 0;
|
|
37
|
+
const solution = wasmModule._solve_cpu_target(ptr, len, targetPtr);
|
|
38
|
+
wasmModule._free(ptr);
|
|
39
|
+
wasmModule._free(targetPtr);
|
|
40
|
+
return solution;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
let cpuSolution = 0;
|
|
26
44
|
|
|
27
45
|
while (true) {
|
|
@@ -101,6 +119,19 @@ async function solveMemory(seed, difficulty) {
|
|
|
101
119
|
const YIELD_THRESHOLD = 100000;
|
|
102
120
|
const size = difficulty * 1024 * 1024;
|
|
103
121
|
const buffer = new Uint32Array(size / 4);
|
|
122
|
+
|
|
123
|
+
const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
|
|
124
|
+
if (wasmModule && typeof wasmModule._solve_memory_challenge === 'function') {
|
|
125
|
+
const seedPtr = wasmModule._malloc(seed.length + 1);
|
|
126
|
+
for (let i = 0; i < seed.length; i++) {
|
|
127
|
+
wasmModule.HEAP8[seedPtr + i] = seed.charCodeAt(i);
|
|
128
|
+
}
|
|
129
|
+
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
130
|
+
const solution = wasmModule._solve_memory_challenge(seedPtr, difficulty);
|
|
131
|
+
wasmModule._free(seedPtr);
|
|
132
|
+
return solution;
|
|
133
|
+
}
|
|
134
|
+
|
|
104
135
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
105
136
|
for (let i = 0; i < buffer.length; i++) {
|
|
106
137
|
buffer[i] = (h = Math.imul(h ^ i, 1597334677));
|
package/src/js/pow.solver.js
CHANGED
|
@@ -25,6 +25,24 @@ export async function solveCpuTargetInline(baseBlock, target, progressCallback)
|
|
|
25
25
|
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
26
26
|
// --- END FIX ---
|
|
27
27
|
const encoder = new TextEncoder();
|
|
28
|
+
|
|
29
|
+
const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
|
|
30
|
+
if (wasmModule && typeof wasmModule._solve_cpu_target === 'function') {
|
|
31
|
+
const len = baseBlock.length;
|
|
32
|
+
const ptr = wasmModule._malloc(len);
|
|
33
|
+
wasmModule.HEAPU8.set(baseBlock, ptr);
|
|
34
|
+
const targetStr = typeof target === 'string' ? target : target.toString(16);
|
|
35
|
+
const targetPtr = wasmModule._malloc(targetStr.length + 1);
|
|
36
|
+
for (let i = 0; i < targetStr.length; i++) {
|
|
37
|
+
wasmModule.HEAP8[targetPtr + i] = targetStr.charCodeAt(i);
|
|
38
|
+
}
|
|
39
|
+
wasmModule.HEAP8[targetPtr + targetStr.length] = 0;
|
|
40
|
+
const solution = wasmModule._solve_cpu_target(ptr, len, targetPtr);
|
|
41
|
+
wasmModule._free(ptr);
|
|
42
|
+
wasmModule._free(targetPtr);
|
|
43
|
+
return solution;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
let cpuSolution = 0;
|
|
29
47
|
|
|
30
48
|
while (true) {
|
|
@@ -102,6 +120,19 @@ export async function solveMemory(seed, difficulty) {
|
|
|
102
120
|
const YIELD_THRESHOLD = 100000;
|
|
103
121
|
const size = difficulty * 1024 * 1024;
|
|
104
122
|
const buffer = new Uint32Array(size / 4);
|
|
123
|
+
|
|
124
|
+
const wasmModule = typeof window !== 'undefined' ? (window.wasmModule || (window.ClientLibrary && window.ClientLibrary.wasmModule)) : null;
|
|
125
|
+
if (wasmModule && typeof wasmModule._solve_memory_challenge === 'function') {
|
|
126
|
+
const seedPtr = wasmModule._malloc(seed.length + 1);
|
|
127
|
+
for (let i = 0; i < seed.length; i++) {
|
|
128
|
+
wasmModule.HEAP8[seedPtr + i] = seed.charCodeAt(i);
|
|
129
|
+
}
|
|
130
|
+
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
131
|
+
const solution = wasmModule._solve_memory_challenge(seedPtr, difficulty);
|
|
132
|
+
wasmModule._free(seedPtr);
|
|
133
|
+
return solution;
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
106
137
|
|
|
107
138
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {FingerprintBuilder} from '../fingerprint.builder.js';
|
|
3
|
+
|
|
4
|
+
describe('FingerprintBuilder.compare', () => {
|
|
5
|
+
|
|
6
|
+
// Empreinte réaliste d'un utilisateur légitime (ex: Chrome sur Windows)
|
|
7
|
+
// C'est l'empreinte qui serait stockée lors de la première visite.
|
|
8
|
+
const realisticOriginalFp = new FingerprintBuilder()
|
|
9
|
+
.add('cvs', 'mock-canvas-data-v1')
|
|
10
|
+
.add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)')
|
|
11
|
+
.add('hw', '16_8_0') // 16 cores, 8GB RAM, no touch
|
|
12
|
+
.add('os', 'Win32')
|
|
13
|
+
.add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
|
|
14
|
+
.add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0')
|
|
15
|
+
// En-têtes qui peuvent changer lors de la requête de résolution du challenge
|
|
16
|
+
.add('cookie_keys', '_ga,session,device_id')
|
|
17
|
+
.add('upgrade', '1')
|
|
18
|
+
.toString();
|
|
19
|
+
|
|
20
|
+
it('should return a similarity score of 1.0 for the same device with minor volatile changes', () => {
|
|
21
|
+
// Scénario de succès : L'utilisateur résout un challenge.
|
|
22
|
+
// L'empreinte du solveur est presque identique, mais certains en-têtes "volatils"
|
|
23
|
+
// (comme la présence de cookies ou 'upgrade-insecure-requests') ont changé ou disparu.
|
|
24
|
+
// La fonction `compare` est conçue pour ignorer ces clés volatiles.
|
|
25
|
+
const realisticSolverFp = new FingerprintBuilder()
|
|
26
|
+
.add('cvs', 'mock-canvas-data-v1') // Identique
|
|
27
|
+
.add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)') // Identique
|
|
28
|
+
.add('hw', '16_8_0') // Identique
|
|
29
|
+
.add('os', 'Win32') // Identique
|
|
30
|
+
.add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36') // Identique
|
|
31
|
+
.add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0') // Identique
|
|
32
|
+
// La clé 'cookie_keys' est absente, simulant une requête sans cookies.
|
|
33
|
+
// La clé 'upgrade' est également absente.
|
|
34
|
+
.toString();
|
|
35
|
+
|
|
36
|
+
const similarity = FingerprintBuilder.compare(realisticOriginalFp, realisticSolverFp);
|
|
37
|
+
|
|
38
|
+
// La similarité doit être de 1.0 car toutes les différences concernent des clés volatiles
|
|
39
|
+
// qui sont ignorées par la comparaison.
|
|
40
|
+
expect(similarity).toBe(1.0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return a low similarity score for two completely different devices', () => {
|
|
44
|
+
// Scénario d'échec : Un attaquant a volé le cookie `device_id` et tente de
|
|
45
|
+
// résoudre un challenge depuis une machine différente (ex: un serveur Linux avec Firefox).
|
|
46
|
+
const differentSolverFp = new FingerprintBuilder()
|
|
47
|
+
.add('cvs', 'different-canvas-data') // Différent
|
|
48
|
+
.add('gpu', 'llvmpipe (LLVM 15.0.7, 256 bits)') // Différent (GPU de VM)
|
|
49
|
+
.add('hw', '8_4_0') // Différent
|
|
50
|
+
.add('os', 'Linux x86_64') // Différent
|
|
51
|
+
.add('ua', 'Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0') // Différent
|
|
52
|
+
.add('ja3', '771,49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,65281-11-10-35-16-5-13-51-45-43-27-23-17513,29-23-24,0') // Différent (JA3 de Firefox)
|
|
53
|
+
.toString();
|
|
54
|
+
|
|
55
|
+
const similarity = FingerprintBuilder.compare(realisticOriginalFp, differentSolverFp);
|
|
56
|
+
|
|
57
|
+
// La similarité doit être très faible (proche de 0) car tous les signaux forts sont différents.
|
|
58
|
+
expect(similarity).toBeLessThan(0.1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return 0 if one of the fingerprints is null or empty', () => {
|
|
62
|
+
expect(FingerprintBuilder.compare(realisticOriginalFp, null)).toBe(0);
|
|
63
|
+
expect(FingerprintBuilder.compare(null, realisticOriginalFp)).toBe(0);
|
|
64
|
+
expect(FingerprintBuilder.compare(realisticOriginalFp, '')).toBe(0);
|
|
65
|
+
expect(FingerprintBuilder.compare('', realisticOriginalFp)).toBe(0);
|
|
66
|
+
expect(FingerprintBuilder.compare(null, null)).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle fingerprints with missing components gracefully', () => {
|
|
70
|
+
const partialFp = new FingerprintBuilder()
|
|
71
|
+
.add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
|
|
72
|
+
.add('os', 'Win32')
|
|
73
|
+
.toString(); // Manque GPU, canvas, etc.
|
|
74
|
+
|
|
75
|
+
const similarity = FingerprintBuilder.compare(realisticOriginalFp, partialFp);
|
|
76
|
+
expect(similarity).toBeGreaterThan(0);
|
|
77
|
+
expect(similarity).toBeLessThan(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {JSDOM} from 'jsdom';
|
|
3
|
+
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
+
|
|
5
|
+
// --- Setup JSDOM Environment ---
|
|
6
|
+
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
+
// ici rend le test explicite et portable.
|
|
8
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
+
url: 'http://localhost',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
+
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
+
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
+
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
+
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
+
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
+
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
+
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
+
|
|
21
|
+
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
+
global.Headers = dom.window.Headers;
|
|
23
|
+
global.Request = dom.window.Request;
|
|
24
|
+
global.URL = dom.window.URL;
|
|
25
|
+
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
+
|
|
30
|
+
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
+
let startMouseSpy, startKeystrokeSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Réinitialiser l'état du client avant chaque test
|
|
35
|
+
ClientLibrary._resetCache();
|
|
36
|
+
|
|
37
|
+
// Créer les espions sur les méthodes internes
|
|
38
|
+
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
+
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
+
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
41
|
+
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
42
|
+
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
// Restaurer les espions après chaque test pour ne pas affecter les autres tests
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should enable all trackers by default', () => {
|
|
51
|
+
ClientLibrary.initializeClient();
|
|
52
|
+
|
|
53
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
54
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should disable mouse and keystroke trackers when configured', () => {
|
|
58
|
+
ClientLibrary.initializeClient({ mouse: false, keystrokes: false });
|
|
59
|
+
|
|
60
|
+
expect(startMouseSpy).not.toHaveBeenCalled();
|
|
61
|
+
expect(startKeystrokeSpy).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should initialize honeypots with the provided field names', () => {
|
|
65
|
+
const honeypotFields = ['email_confirm', 'user_nickname'];
|
|
66
|
+
ClientLibrary.initializeClient({ honeypots: honeypotFields });
|
|
67
|
+
|
|
68
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not initialize honeypots if the array is empty', () => {
|
|
72
|
+
ClientLibrary.initializeClient({ honeypots: [] });
|
|
73
|
+
|
|
74
|
+
expect(initHoneypotsSpy).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should inject trap URLs when provided', () => {
|
|
78
|
+
const urls = ['/trap1?sig=123', '/trap2?sig=456'];
|
|
79
|
+
ClientLibrary.initializeClient({ trapUrls: urls });
|
|
80
|
+
|
|
81
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not inject trap URLs if the array is empty', () => {
|
|
85
|
+
ClientLibrary.initializeClient({ trapUrls: [] });
|
|
86
|
+
|
|
87
|
+
expect(injectTrapsSpy).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should initialize fetch interception when fetch config is present', () => {
|
|
91
|
+
const targetDomains = ['api.example.com'];
|
|
92
|
+
ClientLibrary.initializeClient({ fetch: { targetDomains } });
|
|
93
|
+
|
|
94
|
+
expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not initialize fetch interception if fetch config is absent', () => {
|
|
98
|
+
ClientLibrary.initializeClient(); // No fetch config
|
|
99
|
+
|
|
100
|
+
expect(initFetchSpy).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should call all initializers correctly when a full config is provided', () => {
|
|
104
|
+
const config = {
|
|
105
|
+
mouse: true,
|
|
106
|
+
keystrokes: true,
|
|
107
|
+
honeypots: ['field1'],
|
|
108
|
+
trapUrls: ['/trap1'],
|
|
109
|
+
fetch: { targetDomains: ['api.com'] }
|
|
110
|
+
};
|
|
111
|
+
ClientLibrary.initializeClient(config);
|
|
112
|
+
|
|
113
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
114
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
115
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
|
|
116
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
|
|
117
|
+
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
6
|
+
import ClientLibrary from '../fingerprint.client.js';
|
|
7
|
+
|
|
8
|
+
describe('ClientLibrary WASM Integration', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Réinitialise le cache et les mocks avant chaque test
|
|
11
|
+
ClientLibrary._resetCache();
|
|
12
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
13
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
14
|
+
|
|
15
|
+
// Supprime les mocks globaux pour éviter les fuites entre les tests
|
|
16
|
+
delete window.createFingerprintModule;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should successfully load WASM module and switch hasher', async () => {
|
|
24
|
+
// 1. Simuler un module WASM fonctionnel
|
|
25
|
+
const mockWasmModule = {
|
|
26
|
+
_hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 2. Simuler le chargement du script et l'initialisation du module
|
|
30
|
+
// On attache les fonctions de simulation à `window` car c'est ce que le code cherche
|
|
31
|
+
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
32
|
+
|
|
33
|
+
// On simule l'injection du script en appelant directement `onload`
|
|
34
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
35
|
+
// Simule le chargement réussi du script
|
|
36
|
+
script.onload();
|
|
37
|
+
return script;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 3. Appeler la fonction d'initialisation
|
|
41
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
42
|
+
|
|
43
|
+
// 4. Vérifier que le hasher a été remplacé
|
|
44
|
+
const wasmHash = ClientLibrary._hasher("test");
|
|
45
|
+
expect(wasmHash).toBe(99999);
|
|
46
|
+
expect(mockWasmModule._hash_string).toHaveBeenCalledWith("test");
|
|
47
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
|
|
51
|
+
// 1. Simuler un échec de chargement du script
|
|
52
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
53
|
+
script.onerror(new Error('Script loading failed'));
|
|
54
|
+
return script;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 2. Appeler la fonction d'initialisation
|
|
58
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
59
|
+
|
|
60
|
+
// 3. Vérifier que le hasher est toujours l'implémentation JS
|
|
61
|
+
const jsHash = ClientLibrary._hasher("test");
|
|
62
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
63
|
+
|
|
64
|
+
expect(jsHash).toBe(originalJsHash);
|
|
65
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should gracefully fall back if WASM module does not export _hash_string', async () => {
|
|
69
|
+
// 1. Simuler un module WASM malformé (sans la fonction attendue)
|
|
70
|
+
const mockWasmModule = {};
|
|
71
|
+
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
72
|
+
|
|
73
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
74
|
+
script.onload();
|
|
75
|
+
return script;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 2. Appeler la fonction d'initialisation
|
|
79
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
80
|
+
|
|
81
|
+
// 3. Vérifier le fallback
|
|
82
|
+
const jsHash = ClientLibrary._hasher("test");
|
|
83
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
84
|
+
expect(jsHash).toBe(originalJsHash);
|
|
85
|
+
// L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
|
|
86
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
87
|
+
expect.stringContaining('WASM module failed to load'),
|
|
88
|
+
expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should use the default JS hasher if WASM is not configured', async () => {
|
|
93
|
+
// Ensure no WASM path is provided in the config
|
|
94
|
+
ClientLibrary.initializeClient({});
|
|
95
|
+
|
|
96
|
+
// Verify that the active hasher is the original JS implementation
|
|
97
|
+
const jsHash = ClientLibrary._hasher("another test string");
|
|
98
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
|
|
99
|
+
|
|
100
|
+
expect(jsHash).toBe(originalJsHash);
|
|
101
|
+
// Ensure no WASM-related console logs or warnings were made
|
|
102
|
+
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
|
|
103
|
+
expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
|
|
104
|
+
});
|
|
105
|
+
});
|