@anonympins/fingerprint 0.5.5 → 0.6.1
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 +21 -0
- package/README.md +7 -6
- package/package.json +1 -1
- 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 +19 -5
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +430 -74
- package/src/js/problem-manager.js +22 -0
- package/src/js/tests/cross_parity.test.js +17 -11
- package/src/js/tests/fingerprint.client.test.js +130 -128
- package/src/js/tests/fingerprint.test.js +17 -2
- package/src/js/ttl-optimization.worker.js +51 -0
- package/src/php/Challenge/ChallengeUtils.php +120 -8
- package/src/php/FingerprintBuilder.php +193 -185
- package/src/php/FingerprintEngine.php +245 -3
- package/src/php/ProblemManager.php +20 -0
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +446 -285
- package/src/php/Utils/RequestUtils.php +26 -1
|
@@ -341,6 +341,18 @@ class ProblemManager {
|
|
|
341
341
|
console.error(`[ProblemManager] Propriété 'solution' manquante dans solutionData pour ${problemId}.`);
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
|
+
// DoS mitigation: validate solution size and structure
|
|
345
|
+
if (Array.isArray(solutionData.solution) && solutionData.solution.length > 500) {
|
|
346
|
+
console.error(`[ProblemManager] Solution array too large for ${problemId} verification.`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const serializedSolution = JSON.stringify(solutionData.solution);
|
|
351
|
+
if (serializedSolution && serializedSolution.length > 65536) {
|
|
352
|
+
console.error(`[ProblemManager] Solution payload size exceeds safe limit for ${problemId}.`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {}
|
|
344
356
|
// 1. Ne JAMAIS faire confiance au score du client. Recalculer systématiquement.
|
|
345
357
|
const scoreFunction = problem.workUnit.scoreFunction;
|
|
346
358
|
if (!scoreFunction) {
|
|
@@ -368,6 +380,16 @@ class ProblemManager {
|
|
|
368
380
|
return; // Ne rien faire si la vérification est impossible.
|
|
369
381
|
}
|
|
370
382
|
|
|
383
|
+
if (solutionData.population.length > 150) {
|
|
384
|
+
console.error(`[ProblemManager] Population size exceeds safe limit for ${problemId}.`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
for (const individual of solutionData.population) {
|
|
388
|
+
if (individual && individual.chromosome && Array.isArray(individual.chromosome) && individual.chromosome.length > 100) {
|
|
389
|
+
console.error(`[ProblemManager] Chromosome size too large for ${problemId}.`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
371
393
|
// 1. On choisit un petit échantillon aléatoire de la population soumise.
|
|
372
394
|
const sampleSize = Math.min(5, solutionData.population.length);
|
|
373
395
|
const sampleIndices = new Set();
|
|
@@ -140,21 +140,27 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
140
140
|
it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
|
|
141
141
|
const iterations = 100;
|
|
142
142
|
for (let i = 0; i < 50; i++) {
|
|
143
|
-
const seed = `seed_parity_val_${i}`;
|
|
144
|
-
const jsTrajectory = jsGenerateTrajectory(seed, iterations);
|
|
145
|
-
const phpTrajectory = runPhpTrajectory(seed, iterations);
|
|
143
|
+
const seed = `seed_parity_val_${i}`; // Use a unique seed for each iteration
|
|
144
|
+
const jsTrajectory = jsGenerateTrajectory(seed, iterations); // Generate trajectory using JS
|
|
145
|
+
const phpTrajectory = runPhpTrajectory(seed, iterations); // Generate trajectory using PHP
|
|
146
146
|
|
|
147
147
|
for (let idx = 0; idx < 64; idx++) {
|
|
148
148
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
149
149
|
const phpVal = Math.fround(phpTrajectory[idx]);
|
|
150
|
-
expect(jsVal).toBe(phpVal);
|
|
150
|
+
expect(jsVal).toBe(phpVal, `PHP and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Ensure _malloc and HEAP8 are available on the wasmModule
|
|
155
|
+
if (typeof wasmModule._malloc !== 'function' || wasmModule.HEAP8 == null) {
|
|
156
|
+
console.warn("WASM module is missing _malloc or HEAP8. Skipping WASM parity test.");
|
|
157
|
+
continue; // Skip WASM part of the test if prerequisites are not met
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const outputPtr = wasmModule._malloc(256); // Allocate memory for 64 floats (64 * 4 bytes)
|
|
161
|
+
const seedPtr = wasmModule._malloc(seed.length + 1); // Allocate memory for seed string + null terminator
|
|
162
|
+
for (let s = 0; s < seed.length; s++) { // Copy seed string to WASM memory
|
|
163
|
+
wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s); // HEAP8 is Int8Array
|
|
158
164
|
}
|
|
159
165
|
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
160
166
|
|
|
@@ -163,13 +169,13 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
163
169
|
const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
|
|
164
170
|
for (let idx = 0; idx < 64; idx++) {
|
|
165
171
|
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
166
|
-
const wasmVal = Math.fround(wasmOutput[idx]);
|
|
167
|
-
expect(jsVal).toBe(wasmVal);
|
|
172
|
+
const wasmVal = Math.fround(wasmOutput[idx]); // Ensure float32 precision for comparison
|
|
173
|
+
expect(jsVal).toBe(wasmVal, `WASM and JS trajectories mismatch at index ${idx} for seed "${seed}"`);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
wasmModule._free(outputPtr);
|
|
171
177
|
wasmModule._free(seedPtr);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
|
-
},
|
|
180
|
+
}, 30000); // Increased timeout for potentially longer WASM compilation/execution
|
|
175
181
|
});
|
|
@@ -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
|
});
|
|
@@ -2643,6 +2643,11 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2643
2643
|
const nodeIdA = 'node-a';
|
|
2644
2644
|
const seedA = 'seed-a';
|
|
2645
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
|
+
|
|
2646
2651
|
// 1. Enregistrement du nœud A (pair)
|
|
2647
2652
|
await __internal.registerCooperativeNode(clientIp, nodeIdA, seedA);
|
|
2648
2653
|
|
|
@@ -2652,21 +2657,31 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2652
2657
|
expect(peer.nodeId).toBe(nodeIdA);
|
|
2653
2658
|
expect(peer.seed).toBe(seedA);
|
|
2654
2659
|
|
|
2660
|
+
const sigReq = createHash('sha256')
|
|
2661
|
+
.update('secret-b:request_peer_block:node-b:node-a:42:req-123')
|
|
2662
|
+
.digest('hex');
|
|
2663
|
+
|
|
2655
2664
|
// 3. Demande de bloc du nœud B vers le nœud A
|
|
2656
2665
|
const paramsReq = {
|
|
2657
2666
|
coop_op: 'request_peer_block',
|
|
2658
2667
|
node_id: 'node-b',
|
|
2659
2668
|
peer_id: 'node-a',
|
|
2660
2669
|
block_idx: '42',
|
|
2661
|
-
req_id: 'req-123'
|
|
2670
|
+
req_id: 'req-123',
|
|
2671
|
+
coop_sig: sigReq
|
|
2662
2672
|
};
|
|
2663
2673
|
const resReq = await __internal.handleCooperativeRequest(paramsReq, clientIp);
|
|
2664
2674
|
expect(resReq.status).toBe('queued');
|
|
2665
2675
|
|
|
2676
|
+
const sigPoll = createHash('sha256')
|
|
2677
|
+
.update('secret-a:poll_requests:node-a')
|
|
2678
|
+
.digest('hex');
|
|
2679
|
+
|
|
2666
2680
|
// 4. Récupération de la demande par le nœud A
|
|
2667
2681
|
const paramsPoll = {
|
|
2668
2682
|
coop_op: 'poll_requests',
|
|
2669
|
-
node_id: 'node-a'
|
|
2683
|
+
node_id: 'node-a',
|
|
2684
|
+
coop_sig: sigPoll
|
|
2670
2685
|
};
|
|
2671
2686
|
const resPoll = await __internal.handleCooperativeRequest(paramsPoll, clientIp);
|
|
2672
2687
|
expect(resPoll.requests.length).toBe(1);
|
|
@@ -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);
|
|
@@ -138,7 +138,7 @@ class ChallengeUtils
|
|
|
138
138
|
return $activePeers[array_rand($activePeers)];
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
public static function handleCooperativeRequest(array $params): ?array
|
|
141
|
+
public static function handleCooperativeRequest(array $params, string $clientIp = '127.0.0.1', array $config = []): ?array
|
|
142
142
|
{
|
|
143
143
|
$op = $params['coop_op'] ?? null;
|
|
144
144
|
if (!$op) {
|
|
@@ -146,11 +146,108 @@ class ChallengeUtils
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
$store = StoreManager::getStore();
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if ($op === 'share_threat_intel') {
|
|
152
|
+
$peers = $config['federatedPeers'] ?? [];
|
|
153
|
+
if (!empty($peers)) {
|
|
154
|
+
$allowedHosts = array_map(function ($url) {
|
|
155
|
+
$host = parse_url($url, PHP_URL_HOST);
|
|
156
|
+
return !empty($host) ? $host : $url;
|
|
157
|
+
}, $peers);
|
|
158
|
+
|
|
159
|
+
if (!in_array($clientIp, $allowedHosts, true)) {
|
|
160
|
+
return ['error' => 'Unauthorized federation sender IP'];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
$zkpY = $params['zkpY'] ?? '';
|
|
164
|
+
$headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : [];
|
|
165
|
+
$sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE_ED25519'] ?? '';
|
|
166
|
+
$sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE'] ?? '';
|
|
167
|
+
$timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? $_SERVER['HTTP_X_FEDERATION_TIMESTAMP'] ?? 0);
|
|
168
|
+
|
|
169
|
+
if (empty($zkpY) || empty($timestamp)) {
|
|
170
|
+
return ['error' => 'Missing threat intel parameters'];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Time window check (5 minutes anti-replay)
|
|
174
|
+
$now = (int)(microtime(true) * 1000);
|
|
175
|
+
if (abs($now - $timestamp) > 300000) {
|
|
176
|
+
return ['error' => 'Message expired or clock skew too high'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
$msg = "{$timestamp}:{$zkpY}";
|
|
180
|
+
|
|
181
|
+
if (!empty($sigEd25519)) {
|
|
182
|
+
$publicKey = $config['ed25519_public_key'] ?? $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
|
|
183
|
+
if (!$publicKey) {
|
|
184
|
+
return ['error' => 'Missing public key for asymmetric verification'];
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
$cleanKey = str_replace('\n', "\n", $publicKey);
|
|
188
|
+
$pubKeyObj = openssl_pkey_get_public($cleanKey);
|
|
189
|
+
if (!$pubKeyObj || openssl_verify($msg, hex2bin($sigEd25519), $pubKeyObj, null) !== 1) {
|
|
190
|
+
return ['error' => 'Invalid asymmetric federation signature'];
|
|
191
|
+
}
|
|
192
|
+
} catch (\Throwable $e) {
|
|
193
|
+
return ['error' => 'Asymmetric signature verification failed'];
|
|
194
|
+
}
|
|
195
|
+
} elseif (!empty($sigHmac)) {
|
|
196
|
+
$secret = $params['federationSecret'] ?? $config['federationSecret'] ?? self::getPowSecret();
|
|
197
|
+
$expectedSig = hash_hmac('sha256', $msg, $secret);
|
|
198
|
+
if (!hash_equals($expectedSig, $sigHmac)) {
|
|
199
|
+
return ['error' => 'Invalid federation signature'];
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
return ['error' => 'Missing signature'];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Ban the ZKP public key for 30 days
|
|
206
|
+
$store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
|
|
207
|
+
return ['status' => 'synchronized'];
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
$nodeId = $params['node_id'] ?? '';
|
|
150
211
|
if (empty($nodeId)) {
|
|
151
212
|
return ['error' => 'Missing node_id'];
|
|
152
213
|
}
|
|
153
214
|
|
|
215
|
+
// --- VÉRIFICATION DE LA SIGNATURE COOPÉRATIVE ---
|
|
216
|
+
$challengeContext = $store->get("secret:{$nodeId}");
|
|
217
|
+
if (!$challengeContext || empty($challengeContext['clientSecret'])) {
|
|
218
|
+
return ['error' => 'Invalid or expired node_id'];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
$clientSecret = $challengeContext['clientSecret'];
|
|
222
|
+
$coopSig = $params['coop_sig'] ?? '';
|
|
223
|
+
|
|
224
|
+
$expectedMsg = '';
|
|
225
|
+
switch ($op) {
|
|
226
|
+
case 'register':
|
|
227
|
+
$expectedMsg = "{$clientSecret}:register:{$nodeId}:" . ($params['seed'] ?? '');
|
|
228
|
+
break;
|
|
229
|
+
case 'request_peer_block':
|
|
230
|
+
$expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . ($params['peer_id'] ?? '') . ":" . ($params['block_idx'] ?? '0') . ":" . ($params['req_id'] ?? '');
|
|
231
|
+
break;
|
|
232
|
+
case 'poll_requests':
|
|
233
|
+
$expectedMsg = "{$clientSecret}:poll_requests:{$nodeId}";
|
|
234
|
+
break;
|
|
235
|
+
case 'respond_block':
|
|
236
|
+
$expectedMsg = "{$clientSecret}:respond_block:{$nodeId}:" . ($params['requester_id'] ?? '') . ":" . ($params['req_id'] ?? '') . ":" . ($params['block_data'] ?? '');
|
|
237
|
+
break;
|
|
238
|
+
case 'poll_response':
|
|
239
|
+
$expectedMsg = "{$clientSecret}:poll_response:{$nodeId}:" . ($params['req_id'] ?? '');
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
return ['error' => 'Invalid cooperative operation'];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
$expectedSig = hash('sha256', $expectedMsg);
|
|
246
|
+
if (!hash_equals($expectedSig, $coopSig)) {
|
|
247
|
+
return ['error' => 'Invalid cooperative signature'];
|
|
248
|
+
}
|
|
249
|
+
// --- FIN DE LA VÉRIFICATION ---
|
|
250
|
+
|
|
154
251
|
switch ($op) {
|
|
155
252
|
case 'register':
|
|
156
253
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
@@ -298,7 +395,7 @@ class ChallengeUtils
|
|
|
298
395
|
/**
|
|
299
396
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
300
397
|
*/
|
|
301
|
-
|
|
398
|
+
public static function getPowSecret(): string
|
|
302
399
|
{
|
|
303
400
|
$secret = $_ENV['POW_SECRET'] ?? getenv('POW_SECRET');
|
|
304
401
|
if (!$secret && ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production') {
|
|
@@ -857,25 +954,38 @@ class ChallengeUtils
|
|
|
857
954
|
const peerBlockIdx = {$peerBlockIdx};
|
|
858
955
|
const coopTimeout = {$coopTimeout};
|
|
859
956
|
|
|
957
|
+
async function signCoop(op, nid, extra = "") {
|
|
958
|
+
const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
|
|
959
|
+
const encoder = new TextEncoder();
|
|
960
|
+
const data = encoder.encode(msg);
|
|
961
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
962
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
963
|
+
}
|
|
964
|
+
|
|
860
965
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
861
966
|
await new Promise(r => setTimeout(r, 10));
|
|
862
967
|
|
|
863
968
|
try {
|
|
864
969
|
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
865
970
|
|
|
866
|
-
|
|
867
|
-
|
|
971
|
+
if (peerId && peerBlockIdx !== -1) {
|
|
972
|
+
// Enregistrement coopératif
|
|
973
|
+
const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
|
|
974
|
+
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
|
|
975
|
+
}
|
|
868
976
|
|
|
869
977
|
// Écoute des requêtes entrantes de nos pairs suspects
|
|
870
978
|
setInterval(async () => {
|
|
871
979
|
try {
|
|
872
|
-
const
|
|
980
|
+
const sig = await signCoop("poll_requests", nodeId);
|
|
981
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
|
|
873
982
|
const data = await res.json();
|
|
874
983
|
if (data.requests && data.requests.length > 0) {
|
|
875
984
|
for (const req of data.requests) {
|
|
876
985
|
document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
|
|
877
986
|
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
878
|
-
await
|
|
987
|
+
const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
|
|
988
|
+
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
989
|
}
|
|
880
990
|
}
|
|
881
991
|
} catch (e) {
|
|
@@ -888,11 +998,13 @@ class ChallengeUtils
|
|
|
888
998
|
if (peerId && peerBlockIdx !== -1) {
|
|
889
999
|
document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
|
|
890
1000
|
const reqId = Math.random().toString(36).substring(2);
|
|
891
|
-
await
|
|
1001
|
+
const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
|
|
1002
|
+
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
1003
|
|
|
893
1004
|
let attempts = 0;
|
|
894
1005
|
while (attempts < coopTimeout) {
|
|
895
|
-
const
|
|
1006
|
+
const pollSig = await signCoop("poll_response", nodeId, reqId);
|
|
1007
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
|
|
896
1008
|
const data = await res.json();
|
|
897
1009
|
if (data.status === 'ready') {
|
|
898
1010
|
peerBlock = data.block_data;
|