@anonympins/fingerprint 0.5.2 → 0.5.5
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 +27 -0
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +280 -38
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +474 -163
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +24 -8
- package/src/js/mongodb-store.js +165 -79
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +509 -14
- package/src/js/pow.solver.js +350 -32
- package/src/js/problem-manager.js +1 -1
- package/src/js/redis-store.js +114 -42
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/dns-circuit-breaker.test.js +55 -0
- package/src/js/tests/fingerprint.client.init.test.js +143 -143
- package/src/js/tests/fingerprint.client.test.js +128 -128
- package/src/js/tests/fingerprint.test.js +57 -2
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/js/tests/pow.solver.test.js +5 -2
- package/src/js/tests/usefulWorkSolver.test.js +162 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/Challenge/ChallengeUtils.php +21 -8
- package/src/php/FingerprintClient.php +22 -5
- package/src/php/FingerprintEngine.php +233 -57
- package/src/php/Optimization/Optimization.php +23 -16
- package/src/php/Optimization/OptimizationOperators.php +27 -0
- package/src/php/ProblemManager.php +1 -1
- package/src/php/Store/MongoDbStore.php +184 -104
- package/src/php/Store/RedisStore.php +91 -11
- package/src/php/Tests/ChallengeUtilsTest.php +285 -285
- package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/PowTest.php +1 -1
- package/src/php/Tests/RequestUtilsTest.php +29 -0
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/RequestUtils.php +74 -21
- package/src/php/Utils/TLSClientHelloParser.php +212 -116
|
@@ -1,129 +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 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
|
-
});
|
|
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
|
+
});
|
|
129
129
|
});
|
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
default_whitelist,
|
|
32
32
|
stopThresholdAutoTuning,
|
|
33
33
|
getCompositeDeviceHash,
|
|
34
|
+
sanitizeTrafficData,
|
|
34
35
|
} = fingerprint;
|
|
35
36
|
const { store, getRequestPatternScore, getDeviceHash } = __internal;
|
|
36
37
|
const { getRenderingAnomalyScore } = __internal;
|
|
@@ -1083,6 +1084,35 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1083
1084
|
describe('Threshold Auto-Tuning', () => {
|
|
1084
1085
|
let setIntervalSpy, clearIntervalSpy, consoleLogSpy;
|
|
1085
1086
|
|
|
1087
|
+
test('should sanitize traffic data based on stable hardware cluster', async () => {
|
|
1088
|
+
const trafficData = [];
|
|
1089
|
+
// Attacker rotating IPs and deviceIds but having the same hardware fingerprint
|
|
1090
|
+
for (let i = 0; i < 100; i++) {
|
|
1091
|
+
trafficData.push({
|
|
1092
|
+
type: 'trap_triggered',
|
|
1093
|
+
deviceId: `attacker-dev-${i}`,
|
|
1094
|
+
clientIp: `192.168.1.${i}`,
|
|
1095
|
+
fingerprint: 'gpu:spoofed-nvidia-gpu|cvs:spoofed-canvas-data'
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Unique legit users
|
|
1100
|
+
for (let i = 0; i < 10; i++) {
|
|
1101
|
+
trafficData.push({
|
|
1102
|
+
type: 'challenge_solved',
|
|
1103
|
+
deviceId: `legit-dev-${i}`,
|
|
1104
|
+
clientIp: `10.0.0.${i}`,
|
|
1105
|
+
fingerprint: `gpu:legit-gpu-${i}|cvs:legit-canvas-${i}`
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const sanitized = sanitizeTrafficData(trafficData);
|
|
1110
|
+
const attackerLogs = sanitized.filter(log => log.deviceId.startsWith('attacker-dev-'));
|
|
1111
|
+
|
|
1112
|
+
// Total 110 logs. 2% limit = Math.max(3, 2) = 3.
|
|
1113
|
+
expect(attackerLogs.length).toBeLessThanOrEqual(3);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1086
1116
|
beforeEach(() => {
|
|
1087
1117
|
setIntervalSpy = vi.spyOn(global, 'setInterval');
|
|
1088
1118
|
clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
@@ -2045,6 +2075,31 @@ describe('getTimeInconsistencyScore', () => {
|
|
|
2045
2075
|
expect(timeInconsistencyScore).toBe(0);
|
|
2046
2076
|
});
|
|
2047
2077
|
|
|
2078
|
+
it('should return 100 if the signature is invalid or missing when sessionHmacKey is negotiated', () => {
|
|
2079
|
+
const requestTimestamp = Date.now();
|
|
2080
|
+
const clientTimestamp = requestTimestamp - 100;
|
|
2081
|
+
const context = { requestTimestamp };
|
|
2082
|
+
const metrics = { clientTimestamp, signature: 'bad-signature' };
|
|
2083
|
+
const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
|
|
2084
|
+
|
|
2085
|
+
const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
|
|
2086
|
+
expect(timeInconsistencyScore).toBe(100);
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
it('should return 0 if the signature is valid when sessionHmacKey is negotiated', () => {
|
|
2090
|
+
const requestTimestamp = Date.now();
|
|
2091
|
+
const clientTimestamp = requestTimestamp - 100;
|
|
2092
|
+
const context = { requestTimestamp };
|
|
2093
|
+
const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
|
|
2094
|
+
const metrics = { clientTimestamp };
|
|
2095
|
+
const dataToSign = JSON.stringify(metrics);
|
|
2096
|
+
const signature = createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex')).update(dataToSign).digest('hex');
|
|
2097
|
+
metrics.signature = signature;
|
|
2098
|
+
|
|
2099
|
+
const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
|
|
2100
|
+
expect(timeInconsistencyScore).toBe(0);
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2048
2103
|
it('should return a score > 0 for a replayed request (clientTimestamp significantly before requestTimestamp)', () => {
|
|
2049
2104
|
const requestTimestamp = Date.now();
|
|
2050
2105
|
const clientTimestamp = requestTimestamp - (REPLAY_THRESHOLD_MS + 1000); // 1 second beyond threshold
|
|
@@ -2305,9 +2360,9 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2305
2360
|
// The new version handles other prefixes
|
|
2306
2361
|
expect(getIpSubnet('10.20.30.40', 16)).toBe('10.20.0.0/16');
|
|
2307
2362
|
// IPv6
|
|
2308
|
-
expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:
|
|
2363
|
+
expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
|
|
2309
2364
|
// The new version handles other prefixes
|
|
2310
|
-
expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:
|
|
2365
|
+
expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:0e0a:0129:0000:0000:0000:0000:0000/48');
|
|
2311
2366
|
// Invalid IPs
|
|
2312
2367
|
expect(getIpSubnet('not-an-ip')).toBeNull();
|
|
2313
2368
|
});
|
|
@@ -96,7 +96,7 @@ describe('IP Reputation Local System (Node.js)', () => {
|
|
|
96
96
|
expect(ipv4Subnet).toBe('192.168.1.0/24');
|
|
97
97
|
|
|
98
98
|
const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
|
|
99
|
-
expect(ipv6Subnet).toBe('2001:
|
|
99
|
+
expect(ipv6Subnet).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('should calculate client hints inconsistency score correctly', async () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
|
|
5
|
+
|
|
6
|
+
it('should generate a valid session mapping with randomized headers and globals', () => {
|
|
7
|
+
const mapping1 = __internal.generateSessionMapping();
|
|
8
|
+
const mapping2 = __internal.generateSessionMapping();
|
|
9
|
+
|
|
10
|
+
// Vérification de la présence des structures attendues
|
|
11
|
+
expect(mapping1).toBeDefined();
|
|
12
|
+
expect(mapping1.headers).toHaveProperty('x-device-fingerprint');
|
|
13
|
+
expect(mapping1.headers).toHaveProperty('x-behavior-metrics');
|
|
14
|
+
expect(mapping1.globals).toHaveProperty('ClientLibrary');
|
|
15
|
+
expect(mapping1.globals).toHaveProperty('getDeviceFingerprint');
|
|
16
|
+
expect(mapping1.globals).toHaveProperty('getClientBehaviorMetrics');
|
|
17
|
+
expect(mapping1.keys).toHaveProperty('ua');
|
|
18
|
+
expect(mapping1.wasmConstants).toHaveProperty('seed');
|
|
19
|
+
|
|
20
|
+
// Vérification de l'aléa (deux sessions consécutives doivent avoir des mappings uniques)
|
|
21
|
+
expect(mapping1.headers['x-device-fingerprint']).not.toBe(mapping2.headers['x-device-fingerprint']);
|
|
22
|
+
expect(mapping1.globals['ClientLibrary']).not.toBe(mapping2.globals['ClientLibrary']);
|
|
23
|
+
expect(mapping1.keys['ua']).not.toBe(mapping2.keys['ua']);
|
|
24
|
+
expect(mapping1.wasmConstants.seed).not.toBe(mapping2.wasmConstants.seed);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should compile and obfuscate client script asynchronously using the obfuscation worker', async () => {
|
|
28
|
+
const mapping = __internal.generateSessionMapping();
|
|
29
|
+
|
|
30
|
+
// On lance la compilation lourde qui instancie le Worker d'arrière-plan
|
|
31
|
+
const obfuscatedCode = await __internal.compilePolymorphicJs(mapping);
|
|
32
|
+
|
|
33
|
+
expect(obfuscatedCode).toBeDefined();
|
|
34
|
+
expect(typeof obfuscatedCode).toBe('string');
|
|
35
|
+
expect(obfuscatedCode.length).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
// Vérification que les placeholders originaux ont bien été purgés du code client final
|
|
38
|
+
expect(obfuscatedCode).not.toContain('X-Device-Fingerprint');
|
|
39
|
+
expect(obfuscatedCode).not.toContain('X-Behavior-Metrics');
|
|
40
|
+
expect(obfuscatedCode).not.toContain('ClientLibrary');
|
|
41
|
+
|
|
42
|
+
// Vérification que le code est minifié/obfusqué (ne contient pas de commentaires de développement par exemple)
|
|
43
|
+
expect(obfuscatedCode).not.toContain('// Nom trompeur aléatoire pour attirer les analyseurs');
|
|
44
|
+
|
|
45
|
+
// La structure globale de javascript-obfuscator génère typiquement un tableau de chaînes au début
|
|
46
|
+
expect(obfuscatedCode).toMatch(/_0x/);
|
|
47
|
+
}, 15000); // Augmentation du timeout à 15s car l'obfuscation via Worker thread consomme du CPU
|
|
48
|
+
});
|
|
@@ -223,9 +223,12 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
223
223
|
const seed = 'test-wasm-seed';
|
|
224
224
|
const difficulty = 4; // 4 MB
|
|
225
225
|
|
|
226
|
-
const
|
|
226
|
+
const result = await solveMemory(seed, difficulty);
|
|
227
227
|
|
|
228
|
-
expect(
|
|
228
|
+
expect(result).toBeInstanceOf(Object);
|
|
229
|
+
expect(result.solution).toBe(777);
|
|
230
|
+
expect(result.merkleRoot).toBeDefined();
|
|
231
|
+
expect(result.proofs).toBeDefined();
|
|
229
232
|
expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
|
|
230
233
|
expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
|
|
231
234
|
expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { solveChallenge } from '../pow.solver.js'; // Assuming pow.solver.js is the ES module version
|
|
3
|
+
|
|
4
|
+
// Mock crypto.subtle for hashing operations
|
|
5
|
+
const mockSubtleDigest = vi.fn(async (algorithm, data) => {
|
|
6
|
+
// Return a consistent mock hash for testing purposes
|
|
7
|
+
const hash = new Uint8Array(32); // 32 bytes for SHA-256
|
|
8
|
+
for (let i = 0; i < hash.length; i++) {
|
|
9
|
+
hash[i] = i; // Simple predictable hash
|
|
10
|
+
}
|
|
11
|
+
return hash.buffer;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Mock Math.random for deterministic optimization results
|
|
15
|
+
const mockMathRandom = vi.fn(() => 0.5);
|
|
16
|
+
|
|
17
|
+
// Mock IndexedDB for pospace challenges, even if not directly used by this test
|
|
18
|
+
const mockIndexedDB = {
|
|
19
|
+
open: vi.fn(() => ({
|
|
20
|
+
onupgradeneeded: null,
|
|
21
|
+
onsuccess: null,
|
|
22
|
+
onerror: null,
|
|
23
|
+
result: {
|
|
24
|
+
objectStoreNames: { contains: vi.fn(() => true) },
|
|
25
|
+
createObjectStore: vi.fn(),
|
|
26
|
+
transaction: vi.fn(() => ({
|
|
27
|
+
objectStore: vi.fn(() => ({
|
|
28
|
+
put: vi.fn(),
|
|
29
|
+
get: vi.fn(() => ({ onsuccess: null, result: null }))
|
|
30
|
+
}))
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
}))
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Useful Work Solver for Optimization Challenges', () => {
|
|
37
|
+
let originalMathRandom;
|
|
38
|
+
let originalIndexedDB;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Save original implementations
|
|
42
|
+
originalMathRandom = global.Math.random;
|
|
43
|
+
originalIndexedDB = global.indexedDB;
|
|
44
|
+
|
|
45
|
+
// Mock global objects
|
|
46
|
+
vi.stubGlobal('crypto', {
|
|
47
|
+
subtle: {
|
|
48
|
+
digest: mockSubtleDigest,
|
|
49
|
+
},
|
|
50
|
+
randomInt: vi.fn((min, max) => Math.floor(mockMathRandom() * (max - min) + min)), // Mock randomInt for crypto
|
|
51
|
+
randomBytes: vi.fn((length) => new Uint8Array(length).fill(0x01)), // Mock randomBytes for crypto
|
|
52
|
+
});
|
|
53
|
+
global.Math.random = mockMathRandom;
|
|
54
|
+
global.indexedDB = mockIndexedDB;
|
|
55
|
+
|
|
56
|
+
// Reset mocks
|
|
57
|
+
mockSubtleDigest.mockClear();
|
|
58
|
+
mockMathRandom.mockClear();
|
|
59
|
+
mockIndexedDB.open.mockClear();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
// Restore original implementations
|
|
64
|
+
vi.unstubAllGlobals();
|
|
65
|
+
global.Math.random = originalMathRandom;
|
|
66
|
+
global.indexedDB = originalIndexedDB;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should solve a "useful_work_task" with fraud.solve and return a Pareto front', async () => {
|
|
70
|
+
const mockChallenge = {
|
|
71
|
+
type: 'useful_work_task',
|
|
72
|
+
nonce: 'test-nonce-fraud',
|
|
73
|
+
usefulWorkTask: {
|
|
74
|
+
problemId: 'fraud_detection_challenge',
|
|
75
|
+
task: {
|
|
76
|
+
type: 'multi_objective_genetic_algorithm',
|
|
77
|
+
solverName: 'fraud.solve',
|
|
78
|
+
generations: 5, // Keep generations low for quick test
|
|
79
|
+
populationSize: 10, // Keep population low for quick test
|
|
80
|
+
payload: {
|
|
81
|
+
legitimateClicks: [
|
|
82
|
+
{ fingerprint: 'fp1', timeToClick: 500, clickX: 10, clickY: 10, mouseEntropy: 0.8, scrollEvents: 5 },
|
|
83
|
+
{ fingerprint: 'fp1', timeToClick: 600, clickX: 12, clickY: 12, mouseEntropy: 0.7, scrollEvents: 6 },
|
|
84
|
+
{ fingerprint: 'fp2', timeToClick: 700, clickX: 20, clickY: 20, mouseEntropy: 0.9, scrollEvents: 7 },
|
|
85
|
+
],
|
|
86
|
+
fraudulentClicks: [
|
|
87
|
+
{ fingerprint: 'fp3', timeToClick: 50, clickX: 5, clickY: 5, mouseEntropy: 0.1, scrollEvents: 0 },
|
|
88
|
+
{ fingerprint: 'fp3', timeToClick: 60, clickX: 6, clickY: 6, mouseEntropy: 0.2, scrollEvents: 1 },
|
|
89
|
+
{ fingerprint: 'fp4', timeToClick: 100, clickX: 10, clickY: 10, mouseEntropy: 0.3, scrollEvents: 2 },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const solution = await solveChallenge(mockChallenge);
|
|
97
|
+
|
|
98
|
+
// Assert the structure of the returned solution
|
|
99
|
+
expect(solution.type).toBe('useful_work_task');
|
|
100
|
+
expect(solution.nonce).toBe('test-nonce-fraud');
|
|
101
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'fraud_detection_challenge');
|
|
102
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
103
|
+
|
|
104
|
+
const workResult = solution.rawSolution.work_result;
|
|
105
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
106
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
107
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
// Check the structure of a single solution in the Pareto front
|
|
110
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
111
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
112
|
+
expect(firstParetoSolution.solution).toBeInstanceOf(Array); // fraud.solve returns an array of thresholds
|
|
113
|
+
expect(firstParetoSolution.solution.length).toBe(4); // [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
|
|
114
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
115
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [objective1, objective2]
|
|
116
|
+
expect(firstParetoSolution.objectives.length).toBe(2);
|
|
117
|
+
}, 10000); // Increase timeout for GA, even with low generations/population
|
|
118
|
+
|
|
119
|
+
it('should solve a "useful_work_task" with cpc.solve and return a Pareto front', async () => {
|
|
120
|
+
const mockChallenge = {
|
|
121
|
+
type: 'useful_work_task',
|
|
122
|
+
nonce: 'test-nonce-cpc',
|
|
123
|
+
usefulWorkTask: {
|
|
124
|
+
problemId: 'cpc_optimization_challenge',
|
|
125
|
+
task: {
|
|
126
|
+
type: 'multi_objective_genetic_algorithm',
|
|
127
|
+
solverName: 'cpc.solve',
|
|
128
|
+
generations: 5, // Keep generations low for quick test
|
|
129
|
+
populationSize: 10, // Keep population low for quick test
|
|
130
|
+
payload: {
|
|
131
|
+
platformParams: { optimalBaseCommission: 0.3, optimalBonusFactor: 0.1 },
|
|
132
|
+
website: { relevanceScore: 70 },
|
|
133
|
+
advertiser: { credits: 1000 },
|
|
134
|
+
estimatedImpressions: 500,
|
|
135
|
+
competingAds: [{ id: 'ad1' }, { id: 'ad2' }],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const solution = await solveChallenge(mockChallenge);
|
|
142
|
+
|
|
143
|
+
// Assert the structure of the returned solution
|
|
144
|
+
expect(solution.type).toBe('useful_work_task');
|
|
145
|
+
expect(solution.nonce).toBe('test-nonce-cpc');
|
|
146
|
+
expect(solution.rawSolution).toHaveProperty('problem_id', 'cpc_optimization_challenge');
|
|
147
|
+
expect(solution.rawSolution).toHaveProperty('work_result');
|
|
148
|
+
|
|
149
|
+
const workResult = solution.rawSolution.work_result;
|
|
150
|
+
expect(workResult).toHaveProperty('paretoFront');
|
|
151
|
+
expect(workResult.paretoFront).toBeInstanceOf(Array);
|
|
152
|
+
expect(workResult.paretoFront.length).toBeGreaterThan(0);
|
|
153
|
+
|
|
154
|
+
// Check the structure of a single solution in the Pareto front
|
|
155
|
+
const firstParetoSolution = workResult.paretoFront[0];
|
|
156
|
+
expect(firstParetoSolution).toHaveProperty('solution');
|
|
157
|
+
expect(typeof firstParetoSolution.solution).toBe('number'); // cpc.solve returns a single CPC value
|
|
158
|
+
expect(firstParetoSolution).toHaveProperty('objectives');
|
|
159
|
+
expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [platformRevenue, advertiserValue, marketImbalance]
|
|
160
|
+
expect(firstParetoSolution.objectives.length).toBe(3);
|
|
161
|
+
}, 10000);
|
|
162
|
+
});
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -30,6 +30,7 @@ class AutoTuner
|
|
|
30
30
|
/** @var ?callable */
|
|
31
31
|
private $onCleanup;
|
|
32
32
|
private ?string $savePath;
|
|
33
|
+
private float $validationTolerance;
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
|
|
@@ -51,6 +52,7 @@ class AutoTuner
|
|
|
51
52
|
$this->clearAfterTuning = $options['clearAfterTuning'] ?? false;
|
|
52
53
|
$this->onCleanup = $options['onCleanup'] ?? null;
|
|
53
54
|
$this->savePath = $options['savePath'] ?? null;
|
|
55
|
+
$this->validationTolerance = $options['validationTolerance'] ?? 0.15;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -211,6 +213,46 @@ class AutoTuner
|
|
|
211
213
|
}
|
|
212
214
|
};
|
|
213
215
|
|
|
216
|
+
// --- VALIDATION POST-CALCUL (PHP Rollback & Seuil de Tolérance) ---
|
|
217
|
+
$tempConfig = [
|
|
218
|
+
'thresholds' => $this->securityConfig['thresholds'],
|
|
219
|
+
'weights' => $this->securityConfig['weights'],
|
|
220
|
+
'patterns' => $this->securityConfig['patterns'],
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
$applyInertialUpdate($tempConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
|
|
224
|
+
$applyInertialUpdate($tempConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
|
|
225
|
+
$applyInertialUpdate($tempConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
|
|
226
|
+
|
|
227
|
+
$evaluator = OptimizationOperators::createFullSecurityConfigEvaluator(['trafficData' => $sanitizedData]);
|
|
228
|
+
$currentObjectives = $evaluator($this->securityConfig);
|
|
229
|
+
$proposedObjectives = $evaluator($tempConfig);
|
|
230
|
+
|
|
231
|
+
$currentFPR = $currentObjectives[0];
|
|
232
|
+
$currentFNR = $currentObjectives[1];
|
|
233
|
+
$proposedFPR = $proposedObjectives[0];
|
|
234
|
+
$proposedFNR = $proposedObjectives[1];
|
|
235
|
+
|
|
236
|
+
$validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
|
|
237
|
+
|
|
238
|
+
if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
|
|
239
|
+
error_log(sprintf(
|
|
240
|
+
"[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
|
|
241
|
+
$proposedFPR, $currentFPR, $proposedFNR, $currentFNR
|
|
242
|
+
));
|
|
243
|
+
if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger'])) {
|
|
244
|
+
call_user_func($this->securityConfig['logger'], [
|
|
245
|
+
'type' => 'autotuning_instability_alert',
|
|
246
|
+
'proposedFPR' => $proposedFPR,
|
|
247
|
+
'currentFPR' => $currentFPR,
|
|
248
|
+
'proposedFNR' => $proposedFNR,
|
|
249
|
+
'currentFNR' => $currentFNR,
|
|
250
|
+
'timestamp' => (int)(microtime(true) * 1000)
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
return; // Rollback
|
|
254
|
+
}
|
|
255
|
+
|
|
214
256
|
$applyInertialUpdate($this->securityConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
|
|
215
257
|
$applyInertialUpdate($this->securityConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
|
|
216
258
|
$applyInertialUpdate($this->securityConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
|