@anonympins/fingerprint 0.4.4 → 0.4.6
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 +40 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +180 -0
- package/src/js/fingerprint.client.js +21 -0
- package/src/js/fingerprint.js +763 -56
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +79 -1
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +174 -12
- package/src/php/Config/SecurityProfiles.php +10 -0
- package/src/php/FingerprintEngine.php +140 -8
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/FingerprintEngineTest.php +132 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +319 -5
package/src/js/pow.solver.js
CHANGED
|
@@ -9,6 +9,98 @@
|
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
+
function cyrb53(str, seed = 0) {
|
|
13
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
14
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
15
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
16
|
+
ch = str.charCodeAt(i);
|
|
17
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
18
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
19
|
+
}
|
|
20
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
21
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
22
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function openDb() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const request = indexedDB.open("pospace-db", 1);
|
|
28
|
+
request.onupgradeneeded = (e) => {
|
|
29
|
+
const db = e.target.result;
|
|
30
|
+
if (!db.objectStoreNames.contains("blocks")) {
|
|
31
|
+
db.createObjectStore("blocks");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
35
|
+
request.onerror = (e) => reject(e.target.error);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
40
|
+
const block = new Uint8Array(blockSize);
|
|
41
|
+
let h = cyrb53(seed + ":" + blockIndex);
|
|
42
|
+
for (let i = 0; i < blockSize; i++) {
|
|
43
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
44
|
+
block[i] = h & 0xff;
|
|
45
|
+
}
|
|
46
|
+
return block;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function initializeSpace(seed, sizeMb) {
|
|
50
|
+
const db = await openDb();
|
|
51
|
+
const transaction = db.transaction("blocks", "readwrite");
|
|
52
|
+
const store = transaction.objectStore("blocks");
|
|
53
|
+
const numBlocks = sizeMb * 1024;
|
|
54
|
+
const metadataKey = "pospace-metadata";
|
|
55
|
+
|
|
56
|
+
const metaReq = store.get(metadataKey);
|
|
57
|
+
const meta = await new Promise((resolve) => {
|
|
58
|
+
metaReq.onsuccess = () => resolve(metaReq.result);
|
|
59
|
+
});
|
|
60
|
+
if (meta && meta.sizeMb === sizeMb && meta.seed === seed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CHUNK_SIZE = 1000;
|
|
65
|
+
for (let i = 0; i < numBlocks; i += CHUNK_SIZE) {
|
|
66
|
+
const end = Math.min(numBlocks, i + CHUNK_SIZE);
|
|
67
|
+
for (let j = i; j < end; j++) {
|
|
68
|
+
const block = generateBlock(seed, j);
|
|
69
|
+
store.put(block, j);
|
|
70
|
+
}
|
|
71
|
+
await new Promise(r => setTimeout(r, 0));
|
|
72
|
+
}
|
|
73
|
+
store.put({ sizeMb, seed }, metadataKey);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
77
|
+
const db = await openDb();
|
|
78
|
+
const transaction = db.transaction("blocks", "readonly");
|
|
79
|
+
const store = transaction.objectStore("blocks");
|
|
80
|
+
|
|
81
|
+
let combined = new Uint8Array(queries.length * 1024);
|
|
82
|
+
for (let i = 0; i < queries.length; i++) {
|
|
83
|
+
const idx = queries[i];
|
|
84
|
+
const getReq = store.get(idx);
|
|
85
|
+
let block = await new Promise((resolve) => {
|
|
86
|
+
getReq.onsuccess = () => resolve(getReq.result);
|
|
87
|
+
});
|
|
88
|
+
if (!block) {
|
|
89
|
+
block = generateBlock(seed, idx);
|
|
90
|
+
}
|
|
91
|
+
combined.set(block, i * 1024);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const encoder = new TextEncoder();
|
|
95
|
+
const nonceBytes = encoder.encode(nonce + ":" + clientSecret);
|
|
96
|
+
const finalBlock = new Uint8Array(combined.length + nonceBytes.length);
|
|
97
|
+
finalBlock.set(combined);
|
|
98
|
+
finalBlock.set(nonceBytes, combined.length);
|
|
99
|
+
|
|
100
|
+
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
101
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
102
|
+
}
|
|
103
|
+
|
|
12
104
|
/**
|
|
13
105
|
* Résout un challenge CPU basé sur une cible en utilisant un bloc de base binaire.
|
|
14
106
|
* @param {Uint8Array} baseBlock - Le bloc de données initial (nonce, secret, fp) fourni par le serveur.
|
|
@@ -458,7 +550,7 @@ class ChallengeSolution {
|
|
|
458
550
|
* @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
|
|
459
551
|
*/
|
|
460
552
|
export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
|
|
461
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
553
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask, queries, sizeMb } = challenge;
|
|
462
554
|
let rawSolution = {};
|
|
463
555
|
|
|
464
556
|
switch (type) {
|
|
@@ -515,6 +607,10 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
|
|
|
515
607
|
rawSolution.work_result = workResult;
|
|
516
608
|
rawSolution.problem_id = usefulWorkTask.problemId;
|
|
517
609
|
break;
|
|
610
|
+
case 'pospace':
|
|
611
|
+
await initializeSpace(nonce + ":" + clientSecret, sizeMb || 100);
|
|
612
|
+
rawSolution.hash = await solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
613
|
+
break;
|
|
518
614
|
default:
|
|
519
615
|
throw new Error(`Unknown challenge type: ${type}`);
|
|
520
616
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DynamicWasmGenerator } from '../dynamic-wasm.js';
|
|
3
|
+
|
|
4
|
+
// Helper function to decode ULEB128 (for verification)
|
|
5
|
+
function decodeULEB128(bytes) {
|
|
6
|
+
let result = 0;
|
|
7
|
+
let shift = 0;
|
|
8
|
+
for (const byte of bytes) {
|
|
9
|
+
result |= (byte & 0x7f) << shift;
|
|
10
|
+
if (!(byte & 0x80)) {
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
shift += 7;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('DynamicWasmGenerator', () => {
|
|
19
|
+
it('should generate a valid WASM module buffer', () => {
|
|
20
|
+
const constants = { seed: 123, multiplier: 33, adder: 7 };
|
|
21
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
22
|
+
|
|
23
|
+
// Basic check: ensure it's a Buffer and starts with WASM magic number
|
|
24
|
+
expect(wasmBuffer).toBeInstanceOf(Buffer);
|
|
25
|
+
expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
|
|
26
|
+
expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
|
|
27
|
+
|
|
28
|
+
// Attempt to compile and instantiate the module to ensure validity
|
|
29
|
+
let instance;
|
|
30
|
+
try {
|
|
31
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
32
|
+
instance = new WebAssembly.Instance(module, {});
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// If compilation/instantiation fails, it's an invalid WASM module
|
|
35
|
+
expect.fail(`Generated WASM module is invalid: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verify the exported hash function exists
|
|
39
|
+
expect(instance.exports.hash).toBeInstanceOf(Function);
|
|
40
|
+
expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should produce a consistent hash result with the JS fallback', () => {
|
|
44
|
+
const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
|
|
45
|
+
const testString = "hello world";
|
|
46
|
+
|
|
47
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
48
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
49
|
+
const instance = new WebAssembly.Instance(module, {});
|
|
50
|
+
const exports = instance.exports;
|
|
51
|
+
const memory = exports.memory;
|
|
52
|
+
|
|
53
|
+
// Write string to WASM memory
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
const bytes = encoder.encode(testString);
|
|
56
|
+
const view = new Uint8Array(memory.buffer);
|
|
57
|
+
view.set(bytes, 0); // Assuming hash function expects string at address 0
|
|
58
|
+
|
|
59
|
+
// Calculate hash using WASM
|
|
60
|
+
const wasmHash = exports.hash(0, bytes.length);
|
|
61
|
+
|
|
62
|
+
// Calculate hash using JS fallback
|
|
63
|
+
const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
|
|
64
|
+
|
|
65
|
+
// The WASM and JS implementations should yield the same result
|
|
66
|
+
expect(wasmHash).toBe(jsHash);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate different WASM modules for different constants', () => {
|
|
70
|
+
const constants1 = { seed: 1, multiplier: 2, adder: 3 };
|
|
71
|
+
const constants2 = { seed: 4, multiplier: 5, adder: 6 };
|
|
72
|
+
|
|
73
|
+
const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
|
|
74
|
+
const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
|
|
75
|
+
|
|
76
|
+
expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -1,105 +1,129 @@
|
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 successfully load raw WASM and use standalone polymorphic hasher', async () => {
|
|
93
|
+
const mockWasmInstance = {
|
|
94
|
+
exports: {
|
|
95
|
+
memory: { buffer: new ArrayBuffer(65536) },
|
|
96
|
+
hash: vi.fn().mockReturnValue(123456)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
global.WebAssembly = {
|
|
101
|
+
compile: vi.fn().mockResolvedValue({}),
|
|
102
|
+
instantiate: vi.fn().mockResolvedValue(mockWasmInstance)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
106
|
+
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(100))
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await ClientLibrary.initializeWasm('/fp.wasm');
|
|
110
|
+
|
|
111
|
+
const hash = ClientLibrary._hasher("test");
|
|
112
|
+
expect(hash).toBe(123456);
|
|
113
|
+
expect(mockWasmInstance.exports.hash).toHaveBeenCalledWith(0, 4);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should use the default JS hasher if WASM is not configured', async () => {
|
|
117
|
+
// Ensure no WASM path is provided in the config
|
|
118
|
+
ClientLibrary.initializeClient({});
|
|
119
|
+
|
|
120
|
+
// Verify that the active hasher is the original JS implementation
|
|
121
|
+
const jsHash = ClientLibrary._hasher("another test string");
|
|
122
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
|
|
123
|
+
|
|
124
|
+
expect(jsHash).toBe(originalJsHash);
|
|
125
|
+
// Ensure no WASM-related console logs or warnings were made
|
|
126
|
+
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
|
|
127
|
+
expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
|
|
128
|
+
});
|
|
105
129
|
});
|
|
@@ -66,6 +66,18 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
66
66
|
expect(FingerprintBuilder.compare(fp1, ''), "Comparison with empty string should be 0").toBe(0);
|
|
67
67
|
expect(FingerprintBuilder.compare(null, fp2), "Comparison with null should be 0").toBe(0);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it('should calculate a high crossLayerInconsistencyScore when viewport width exceeds screen width', () => {
|
|
71
|
+
const context = {
|
|
72
|
+
headers: {
|
|
73
|
+
'x-device-fingerprint': `scr:${cyrb53("1920x1080_24")}`,
|
|
74
|
+
'sec-ch-viewport-width': '2560'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { crossLayerInconsistencyScore } = __internal.getCrossLayerInconsistency(context);
|
|
79
|
+
expect(crossLayerInconsistencyScore).toBe(20);
|
|
80
|
+
});
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
describe('JA3 Fingerprinting', () => {
|
|
@@ -1635,6 +1647,53 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1635
1647
|
});
|
|
1636
1648
|
});
|
|
1637
1649
|
|
|
1650
|
+
describe('TLS Session Resumption (Cookieless Identity)', () => {
|
|
1651
|
+
const inMemoryStore = {
|
|
1652
|
+
_map: new Map(),
|
|
1653
|
+
async get(key) { return this._map.get(key); },
|
|
1654
|
+
async set(key, value) { this._map.set(key, value); },
|
|
1655
|
+
async has(key) { return this._map.has(key); },
|
|
1656
|
+
async delete(key) { this._map.delete(key); },
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
beforeEach(() => {
|
|
1660
|
+
inMemoryStore._map.clear();
|
|
1661
|
+
configureStore(inMemoryStore);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('should resume device ID using TLS Session ID even if cookies are cleared', async () => {
|
|
1665
|
+
const securityConfig = {
|
|
1666
|
+
weights: { historyScore: 1.0 },
|
|
1667
|
+
thresholds: { low: 20, block: 95 }
|
|
1668
|
+
};
|
|
1669
|
+
const engine = new FingerprintEngine(securityConfig);
|
|
1670
|
+
|
|
1671
|
+
const req1 = {
|
|
1672
|
+
clientIp: '1.2.3.4',
|
|
1673
|
+
path: '/',
|
|
1674
|
+
cookies: {},
|
|
1675
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1676
|
+
rawHeaders: [],
|
|
1677
|
+
httpVersion: '1.1'
|
|
1678
|
+
};
|
|
1679
|
+
const decision1 = await engine.processRequest(req1);
|
|
1680
|
+
const deviceId = decision1.newCookieForResponse.value;
|
|
1681
|
+
expect(deviceId).toBeDefined();
|
|
1682
|
+
|
|
1683
|
+
const req2 = {
|
|
1684
|
+
clientIp: '1.2.3.4',
|
|
1685
|
+
path: '/',
|
|
1686
|
+
cookies: {}, // Cookies cleared!
|
|
1687
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1688
|
+
rawHeaders: [],
|
|
1689
|
+
httpVersion: '1.1'
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
const decision2 = await engine.processRequest(req2);
|
|
1693
|
+
expect(decision2.newCookieForResponse).toBeUndefined(); // Resumed successfully, no new cookie needed
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1638
1697
|
describe('getTlsSpoofingScore', () => {
|
|
1639
1698
|
let getTlsFingerprintMock; // Renamed to reflect it's the mock function
|
|
1640
1699
|
let getTlsSpoofingScore;
|
|
@@ -2069,7 +2128,7 @@ describe('getClientHintsInconsistencyScore', () => {
|
|
|
2069
2128
|
}
|
|
2070
2129
|
};
|
|
2071
2130
|
const { clientHintsInconsistencyScore } = getClientHintsInconsistencyScore(context);
|
|
2072
|
-
expect(clientHintsInconsistencyScore).toBe(
|
|
2131
|
+
expect(clientHintsInconsistencyScore).toBe(97);
|
|
2073
2132
|
});
|
|
2074
2133
|
|
|
2075
2134
|
it('should return 40 for a small version mismatch', () => {
|
|
@@ -2449,3 +2508,22 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
|
2449
2508
|
}
|
|
2450
2509
|
});
|
|
2451
2510
|
});
|
|
2511
|
+
|
|
2512
|
+
test('Stateless Ticket Generation and Validation', async () => {
|
|
2513
|
+
const payload = {
|
|
2514
|
+
expiry: Date.now() + 3600000,
|
|
2515
|
+
originalIp: '127.0.0.1',
|
|
2516
|
+
deviceId: 'device-123',
|
|
2517
|
+
deviceHash: 'hash-abc'
|
|
2518
|
+
};
|
|
2519
|
+
|
|
2520
|
+
const ticket = fingerprint.generateStatelessTicket(payload);
|
|
2521
|
+
expect(ticket).toBeTruthy();
|
|
2522
|
+
expect(ticket.split('.').length).toBe(3);
|
|
2523
|
+
|
|
2524
|
+
const isValid = await fingerprint.isTicketValid('127.0.0.1', ticket, 'device-123', 'hash-abc');
|
|
2525
|
+
expect(isValid).toBe(true);
|
|
2526
|
+
|
|
2527
|
+
const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
|
|
2528
|
+
expect(isDiffIpValid).toBe(false);
|
|
2529
|
+
});
|