@anonympins/fingerprint 0.5.4 → 0.6.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 +16 -0
- package/README.md +7 -6
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +1445 -1271
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +499 -116
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +37 -28
- package/src/js/pow.solver.js +37 -28
- package/src/js/problem-manager.js +22 -0
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/fingerprint.client.test.js +130 -128
- package/src/js/tests/fingerprint.test.js +42 -2
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/js/ttl-optimization.worker.js +51 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/Challenge/ChallengeUtils.php +57 -6
- package/src/php/FingerprintBuilder.php +193 -185
- package/src/php/FingerprintEngine.php +147 -1
- package/src/php/ProblemManager.php +20 -0
- package/src/php/Tests/ChallengeUtilsTest.php +446 -285
- package/src/php/Utils/RequestUtils.php +19 -0
|
@@ -1,129 +1,131 @@
|
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
expect(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
vi.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expect(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
expect(
|
|
127
|
-
|
|
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
|
+
_malloc: vi.fn().mockReturnValue(0),
|
|
28
|
+
HEAPU8: new Uint8Array(4096)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 2. Simuler le chargement du script et l'initialisation du module
|
|
32
|
+
// On attache les fonctions de simulation à `window` car c'est ce que le code cherche
|
|
33
|
+
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
34
|
+
|
|
35
|
+
// On simule l'injection du script en appelant directement `onload`
|
|
36
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
37
|
+
// Simule le chargement réussi du script
|
|
38
|
+
script.onload();
|
|
39
|
+
return script;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 3. Appeler la fonction d'initialisation
|
|
43
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
44
|
+
|
|
45
|
+
// 4. Vérifier que le hasher a été remplacé
|
|
46
|
+
const wasmHash = ClientLibrary._hasher("test");
|
|
47
|
+
expect(wasmHash).toBe(99999);
|
|
48
|
+
expect(mockWasmModule._hash_string).toHaveBeenCalledWith(0);
|
|
49
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
|
|
53
|
+
// 1. Simuler un échec de chargement du script
|
|
54
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
55
|
+
script.onerror(new Error('Script loading failed'));
|
|
56
|
+
return script;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 2. Appeler la fonction d'initialisation
|
|
60
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
61
|
+
|
|
62
|
+
// 3. Vérifier que le hasher est toujours l'implémentation JS
|
|
63
|
+
const jsHash = ClientLibrary._hasher("test");
|
|
64
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
65
|
+
|
|
66
|
+
expect(jsHash).toBe(originalJsHash);
|
|
67
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should gracefully fall back if WASM module does not export _hash_string', async () => {
|
|
71
|
+
// 1. Simuler un module WASM malformé (sans la fonction attendue)
|
|
72
|
+
const mockWasmModule = {};
|
|
73
|
+
window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
|
|
74
|
+
|
|
75
|
+
vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
|
|
76
|
+
script.onload();
|
|
77
|
+
return script;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 2. Appeler la fonction d'initialisation
|
|
81
|
+
await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
|
|
82
|
+
|
|
83
|
+
// 3. Vérifier le fallback
|
|
84
|
+
const jsHash = ClientLibrary._hasher("test");
|
|
85
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
|
|
86
|
+
expect(jsHash).toBe(originalJsHash);
|
|
87
|
+
// L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
|
|
88
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
89
|
+
expect.stringContaining('WASM module failed to load'),
|
|
90
|
+
expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should successfully load raw WASM and use standalone polymorphic hasher', async () => {
|
|
95
|
+
const mockWasmInstance = {
|
|
96
|
+
exports: {
|
|
97
|
+
memory: { buffer: new ArrayBuffer(65536) },
|
|
98
|
+
hash: vi.fn().mockReturnValue(123456)
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
global.WebAssembly = {
|
|
103
|
+
compile: vi.fn().mockResolvedValue({}),
|
|
104
|
+
instantiate: vi.fn().mockResolvedValue(mockWasmInstance)
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
108
|
+
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(100))
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await ClientLibrary.initializeWasm('/fp.wasm');
|
|
112
|
+
|
|
113
|
+
const hash = ClientLibrary._hasher("test");
|
|
114
|
+
expect(hash).toBe(123456);
|
|
115
|
+
expect(mockWasmInstance.exports.hash).toHaveBeenCalledWith(0, 4);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should use the default JS hasher if WASM is not configured', async () => {
|
|
119
|
+
// Ensure no WASM path is provided in the config
|
|
120
|
+
ClientLibrary.initializeClient({});
|
|
121
|
+
|
|
122
|
+
// Verify that the active hasher is the original JS implementation
|
|
123
|
+
const jsHash = ClientLibrary._hasher("another test string");
|
|
124
|
+
const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
|
|
125
|
+
|
|
126
|
+
expect(jsHash).toBe(originalJsHash);
|
|
127
|
+
// Ensure no WASM-related console logs or warnings were made
|
|
128
|
+
expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
|
|
129
|
+
expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
|
|
130
|
+
});
|
|
129
131
|
});
|
|
@@ -2075,6 +2075,31 @@ describe('getTimeInconsistencyScore', () => {
|
|
|
2075
2075
|
expect(timeInconsistencyScore).toBe(0);
|
|
2076
2076
|
});
|
|
2077
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
|
+
|
|
2078
2103
|
it('should return a score > 0 for a replayed request (clientTimestamp significantly before requestTimestamp)', () => {
|
|
2079
2104
|
const requestTimestamp = Date.now();
|
|
2080
2105
|
const clientTimestamp = requestTimestamp - (REPLAY_THRESHOLD_MS + 1000); // 1 second beyond threshold
|
|
@@ -2618,6 +2643,11 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2618
2643
|
const nodeIdA = 'node-a';
|
|
2619
2644
|
const seedA = 'seed-a';
|
|
2620
2645
|
|
|
2646
|
+
// Configurer les secrets dans le store pour passer la validation de signature
|
|
2647
|
+
const store = __internal.store;
|
|
2648
|
+
await store.set('secret:node-a', { clientSecret: 'secret-a' });
|
|
2649
|
+
await store.set('secret:node-b', { clientSecret: 'secret-b' });
|
|
2650
|
+
|
|
2621
2651
|
// 1. Enregistrement du nœud A (pair)
|
|
2622
2652
|
await __internal.registerCooperativeNode(clientIp, nodeIdA, seedA);
|
|
2623
2653
|
|
|
@@ -2627,21 +2657,31 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2627
2657
|
expect(peer.nodeId).toBe(nodeIdA);
|
|
2628
2658
|
expect(peer.seed).toBe(seedA);
|
|
2629
2659
|
|
|
2660
|
+
const sigReq = createHash('sha256')
|
|
2661
|
+
.update('secret-b:request_peer_block:node-b:node-a:42:req-123')
|
|
2662
|
+
.digest('hex');
|
|
2663
|
+
|
|
2630
2664
|
// 3. Demande de bloc du nœud B vers le nœud A
|
|
2631
2665
|
const paramsReq = {
|
|
2632
2666
|
coop_op: 'request_peer_block',
|
|
2633
2667
|
node_id: 'node-b',
|
|
2634
2668
|
peer_id: 'node-a',
|
|
2635
2669
|
block_idx: '42',
|
|
2636
|
-
req_id: 'req-123'
|
|
2670
|
+
req_id: 'req-123',
|
|
2671
|
+
coop_sig: sigReq
|
|
2637
2672
|
};
|
|
2638
2673
|
const resReq = await __internal.handleCooperativeRequest(paramsReq, clientIp);
|
|
2639
2674
|
expect(resReq.status).toBe('queued');
|
|
2640
2675
|
|
|
2676
|
+
const sigPoll = createHash('sha256')
|
|
2677
|
+
.update('secret-a:poll_requests:node-a')
|
|
2678
|
+
.digest('hex');
|
|
2679
|
+
|
|
2641
2680
|
// 4. Récupération de la demande par le nœud A
|
|
2642
2681
|
const paramsPoll = {
|
|
2643
2682
|
coop_op: 'poll_requests',
|
|
2644
|
-
node_id: 'node-a'
|
|
2683
|
+
node_id: 'node-a',
|
|
2684
|
+
coop_sig: sigPoll
|
|
2645
2685
|
};
|
|
2646
2686
|
const resPoll = await __internal.handleCooperativeRequest(paramsPoll, clientIp);
|
|
2647
2687
|
expect(resPoll.requests.length).toBe(1);
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { Optimization } from "./library.js";
|
|
3
|
+
|
|
4
|
+
const MIN_TTL = 300000;
|
|
5
|
+
const MAX_TTL = 86400000;
|
|
6
|
+
const keyScores = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
7
|
+
const tempCache = {};
|
|
8
|
+
|
|
9
|
+
for (const suspicionScore of keyScores) {
|
|
10
|
+
const solverFunction = () => {
|
|
11
|
+
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
12
|
+
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
13
|
+
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
14
|
+
const mutate = (ttl) => {
|
|
15
|
+
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1;
|
|
16
|
+
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
20
|
+
createIndividual,
|
|
21
|
+
fitnessFunction,
|
|
22
|
+
crossover,
|
|
23
|
+
mutate,
|
|
24
|
+
{
|
|
25
|
+
generations: 40,
|
|
26
|
+
populationSize: 30,
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
31
|
+
return { solution: null, fitness: Infinity };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let bestSolutionInFront;
|
|
35
|
+
if (suspicionScore < 50) {
|
|
36
|
+
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
37
|
+
} else {
|
|
38
|
+
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
39
|
+
}
|
|
40
|
+
return { solution: bestSolutionInFront, fitness: 0 };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
44
|
+
if (bestResult && bestResult.solution && bestResult.solution !== Infinity) {
|
|
45
|
+
tempCache[suspicionScore] = Math.round(bestResult.solution);
|
|
46
|
+
} else {
|
|
47
|
+
tempCache[suspicionScore] = Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
parentPort.postMessage(tempCache);
|
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);
|
|
@@ -151,6 +151,42 @@ class ChallengeUtils
|
|
|
151
151
|
return ['error' => 'Missing node_id'];
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// --- VÉRIFICATION DE LA SIGNATURE COOPÉRATIVE ---
|
|
155
|
+
$challengeContext = $store->get("secret:{$nodeId}");
|
|
156
|
+
if (!$challengeContext || empty($challengeContext['clientSecret'])) {
|
|
157
|
+
return ['error' => 'Invalid or expired node_id'];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$clientSecret = $challengeContext['clientSecret'];
|
|
161
|
+
$coopSig = $params['coop_sig'] ?? '';
|
|
162
|
+
|
|
163
|
+
$expectedMsg = '';
|
|
164
|
+
switch ($op) {
|
|
165
|
+
case 'register':
|
|
166
|
+
$expectedMsg = "{$clientSecret}:register:{$nodeId}:" . ($params['seed'] ?? '');
|
|
167
|
+
break;
|
|
168
|
+
case 'request_peer_block':
|
|
169
|
+
$expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . ($params['peer_id'] ?? '') . ":" . ($params['block_idx'] ?? '0') . ":" . ($params['req_id'] ?? '');
|
|
170
|
+
break;
|
|
171
|
+
case 'poll_requests':
|
|
172
|
+
$expectedMsg = "{$clientSecret}:poll_requests:{$nodeId}";
|
|
173
|
+
break;
|
|
174
|
+
case 'respond_block':
|
|
175
|
+
$expectedMsg = "{$clientSecret}:respond_block:{$nodeId}:" . ($params['requester_id'] ?? '') . ":" . ($params['req_id'] ?? '') . ":" . ($params['block_data'] ?? '');
|
|
176
|
+
break;
|
|
177
|
+
case 'poll_response':
|
|
178
|
+
$expectedMsg = "{$clientSecret}:poll_response:{$nodeId}:" . ($params['req_id'] ?? '');
|
|
179
|
+
break;
|
|
180
|
+
default:
|
|
181
|
+
return ['error' => 'Invalid cooperative operation'];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$expectedSig = hash('sha256', $expectedMsg);
|
|
185
|
+
if (!hash_equals($expectedSig, $coopSig)) {
|
|
186
|
+
return ['error' => 'Invalid cooperative signature'];
|
|
187
|
+
}
|
|
188
|
+
// --- FIN DE LA VÉRIFICATION ---
|
|
189
|
+
|
|
154
190
|
switch ($op) {
|
|
155
191
|
case 'register':
|
|
156
192
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
@@ -857,25 +893,38 @@ class ChallengeUtils
|
|
|
857
893
|
const peerBlockIdx = {$peerBlockIdx};
|
|
858
894
|
const coopTimeout = {$coopTimeout};
|
|
859
895
|
|
|
896
|
+
async function signCoop(op, nid, extra = "") {
|
|
897
|
+
const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
|
|
898
|
+
const encoder = new TextEncoder();
|
|
899
|
+
const data = encoder.encode(msg);
|
|
900
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
901
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
902
|
+
}
|
|
903
|
+
|
|
860
904
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
861
905
|
await new Promise(r => setTimeout(r, 10));
|
|
862
906
|
|
|
863
907
|
try {
|
|
864
908
|
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
865
909
|
|
|
866
|
-
|
|
867
|
-
|
|
910
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
911
|
+
// Enregistrement coopératif
|
|
912
|
+
const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
|
|
913
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
|
|
914
|
+
}
|
|
868
915
|
|
|
869
916
|
// Écoute des requêtes entrantes de nos pairs suspects
|
|
870
917
|
setInterval(async () => {
|
|
871
918
|
try {
|
|
872
|
-
const
|
|
919
|
+
const sig = await signCoop("poll_requests", nodeId);
|
|
920
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
|
|
873
921
|
const data = await res.json();
|
|
874
922
|
if (data.requests && data.requests.length > 0) {
|
|
875
923
|
for (const req of data.requests) {
|
|
876
924
|
document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
|
|
877
925
|
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
878
|
-
await
|
|
926
|
+
const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
|
|
927
|
+
await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData) + "&coop_sig=" + respSig);
|
|
879
928
|
}
|
|
880
929
|
}
|
|
881
930
|
} catch (e) {
|
|
@@ -888,11 +937,13 @@ class ChallengeUtils
|
|
|
888
937
|
if (peerId && peerBlockIdx !== -1) {
|
|
889
938
|
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
890
939
|
const reqId = Math.random().toString(36).substring(2);
|
|
891
|
-
await
|
|
940
|
+
const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
|
|
941
|
+
await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
|
|
892
942
|
|
|
893
943
|
let attempts = 0;
|
|
894
944
|
while (attempts < coopTimeout) {
|
|
895
|
-
const
|
|
945
|
+
const pollSig = await signCoop("poll_response", nodeId, reqId);
|
|
946
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
|
|
896
947
|
const data = await res.json();
|
|
897
948
|
if (data.status === 'ready') {
|
|
898
949
|
peerBlock = data.block_data;
|