@anonympins/fingerprint 0.5.5 → 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.
@@ -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
- // 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
+ _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);
@@ -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
- // Enregistrement coopératif
867
- await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret));
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 res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId);
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 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));
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 fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId);
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 res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId);
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;