@anonympins/fingerprint 0.7.3 → 0.7.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.
@@ -1,78 +1,78 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { DynamicWasmGenerator } from '../dynamic-wasm.js';
3
-
4
- // Helper function to decode ULEB128 (for verification)
5
- function decodeULEB128(bytes) {
6
- let result = 0;
7
- let shift = 0;
8
- for (const byte of bytes) {
9
- result |= (byte & 0x7f) << shift;
10
- if (!(byte & 0x80)) {
11
- break;
12
- }
13
- shift += 7;
14
- }
15
- return result;
16
- }
17
-
18
- describe('DynamicWasmGenerator', () => {
19
- it('should generate a valid WASM module buffer', () => {
20
- const constants = { seed: 123, multiplier: 33, adder: 7 };
21
- const wasmBuffer = DynamicWasmGenerator.generate(constants);
22
-
23
- // Basic check: ensure it's a Buffer and starts with WASM magic number
24
- expect(wasmBuffer).toBeInstanceOf(Buffer);
25
- expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
26
- expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
27
-
28
- // Attempt to compile and instantiate the module to ensure validity
29
- let instance;
30
- try {
31
- const module = new WebAssembly.Module(wasmBuffer);
32
- instance = new WebAssembly.Instance(module, {});
33
- } catch (e) {
34
- // If compilation/instantiation fails, it's an invalid WASM module
35
- expect.fail(`Generated WASM module is invalid: ${e.message}`);
36
- }
37
-
38
- // Verify the exported hash function exists
39
- expect(instance.exports.hash).toBeInstanceOf(Function);
40
- expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
41
- });
42
-
43
- it('should produce a consistent hash result with the JS fallback', () => {
44
- const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
45
- const testString = "hello world";
46
-
47
- const wasmBuffer = DynamicWasmGenerator.generate(constants);
48
- const module = new WebAssembly.Module(wasmBuffer);
49
- const instance = new WebAssembly.Instance(module, {});
50
- const exports = instance.exports;
51
- const memory = exports.memory;
52
-
53
- // Write string to WASM memory
54
- const encoder = new TextEncoder();
55
- const bytes = encoder.encode(testString);
56
- const view = new Uint8Array(memory.buffer);
57
- view.set(bytes, 0); // Assuming hash function expects string at address 0
58
-
59
- // Calculate hash using WASM
60
- const wasmHash = exports.hash(0, bytes.length);
61
-
62
- // Calculate hash using JS fallback
63
- const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
64
-
65
- // The WASM and JS implementations should yield the same result
66
- expect(wasmHash).toBe(jsHash);
67
- });
68
-
69
- it('should generate different WASM modules for different constants', () => {
70
- const constants1 = { seed: 1, multiplier: 2, adder: 3 };
71
- const constants2 = { seed: 4, multiplier: 5, adder: 6 };
72
-
73
- const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
74
- const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
75
-
76
- expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
77
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DynamicWasmGenerator } from '../dynamic-wasm.js';
3
+
4
+ // Helper function to decode ULEB128 (for verification)
5
+ function decodeULEB128(bytes) {
6
+ let result = 0;
7
+ let shift = 0;
8
+ for (const byte of bytes) {
9
+ result |= (byte & 0x7f) << shift;
10
+ if (!(byte & 0x80)) {
11
+ break;
12
+ }
13
+ shift += 7;
14
+ }
15
+ return result;
16
+ }
17
+
18
+ describe('DynamicWasmGenerator', () => {
19
+ it('should generate a valid WASM module buffer', () => {
20
+ const constants = { seed: 123, multiplier: 33, adder: 7 };
21
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
22
+
23
+ // Basic check: ensure it's a Buffer and starts with WASM magic number
24
+ expect(wasmBuffer).toBeInstanceOf(Buffer);
25
+ expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
26
+ expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
27
+
28
+ // Attempt to compile and instantiate the module to ensure validity
29
+ let instance;
30
+ try {
31
+ const module = new WebAssembly.Module(wasmBuffer);
32
+ instance = new WebAssembly.Instance(module, {});
33
+ } catch (e) {
34
+ // If compilation/instantiation fails, it's an invalid WASM module
35
+ expect.fail(`Generated WASM module is invalid: ${e.message}`);
36
+ }
37
+
38
+ // Verify the exported hash function exists
39
+ expect(instance.exports.hash).toBeInstanceOf(Function);
40
+ expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
41
+ });
42
+
43
+ it('should produce a consistent hash result with the JS fallback', () => {
44
+ const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
45
+ const testString = "hello world";
46
+
47
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
48
+ const module = new WebAssembly.Module(wasmBuffer);
49
+ const instance = new WebAssembly.Instance(module, {});
50
+ const exports = instance.exports;
51
+ const memory = exports.memory;
52
+
53
+ // Write string to WASM memory
54
+ const encoder = new TextEncoder();
55
+ const bytes = encoder.encode(testString);
56
+ const view = new Uint8Array(memory.buffer);
57
+ view.set(bytes, 0); // Assuming hash function expects string at address 0
58
+
59
+ // Calculate hash using WASM
60
+ const wasmHash = exports.hash(0, bytes.length);
61
+
62
+ // Calculate hash using JS fallback
63
+ const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
64
+
65
+ // The WASM and JS implementations should yield the same result
66
+ expect(wasmHash).toBe(jsHash);
67
+ });
68
+
69
+ it('should generate different WASM modules for different constants', () => {
70
+ const constants1 = { seed: 1, multiplier: 2, adder: 3 };
71
+ const constants2 = { seed: 4, multiplier: 5, adder: 6 };
72
+
73
+ const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
74
+ const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
75
+
76
+ expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
77
+ });
78
78
  });
@@ -17,8 +17,14 @@ Object.defineProperty(global, 'document', { value: dom.window.document, writable
17
17
  Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
18
18
  Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
19
19
  Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
20
+ if (typeof globalThis.crypto !== 'undefined') {
21
+ Object.defineProperty(dom.window, 'crypto', { value: globalThis.crypto, writable: true, configurable: true });
22
+ }
23
+ Object.defineProperty(global, 'CustomEvent', { value: dom.window.CustomEvent, writable: true, configurable: true });
24
+ Object.defineProperty(global, 'Event', { value: dom.window.Event, writable: true, configurable: true });
20
25
 
21
26
  global.fetch = vi.fn(); // Mock global fetch
27
+ window.fetch = (...args) => global.fetch(...args);
22
28
  global.Headers = dom.window.Headers;
23
29
  global.Request = dom.window.Request;
24
30
  global.URL = dom.window.URL;
@@ -45,6 +51,7 @@ describe('ClientLibrary.initializeClient', () => {
45
51
  initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
46
52
  injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
47
53
  initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
54
+ window.fetch = (...args) => global.fetch(...args);
48
55
  injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
49
56
  });
50
57
 
@@ -144,4 +151,105 @@ describe('ClientLibrary.initializeClient', () => {
144
151
  expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
145
152
  });
146
153
 
154
+ it('should abort challenge retry loop when maxRetries is reached', async () => {
155
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
156
+ const mockResponse = {
157
+ status: 404,
158
+ headers: new Headers({ 'content-type': 'application/json' }),
159
+ bodyUsed: false,
160
+ json: vi.fn().mockResolvedValue({
161
+ challenge: { type: 'cpu_target', nonce: 'nonce-1', cpuTarget: '0000ffff' }
162
+ }),
163
+ clone() { return this; }
164
+ };
165
+
166
+ const result = await ClientLibrary.solveChallengeAndRetry(
167
+ mockResponse,
168
+ 'http://localhost/api/data',
169
+ { _powRetryCount: 3 },
170
+ 3
171
+ );
172
+
173
+ expect(result).toBe(mockResponse);
174
+ expect(mockResponse.json).not.toHaveBeenCalled();
175
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Challenge retry limit reached (3)'));
176
+ });
177
+
178
+ it('should inject phantom traps within closed shadow root and fire trigger on interaction', () => {
179
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
180
+ let capturedShadow = null;
181
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
182
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
183
+ capturedShadow = origAttachShadow.call(this, init);
184
+ return capturedShadow;
185
+ });
186
+
187
+ // Ensure document.body exists
188
+ document.body.innerHTML = '';
189
+
190
+ ClientLibrary.injectPhantomTraps();
191
+
192
+ const host = document.body.lastElementChild;
193
+ expect(host).not.toBeNull();
194
+ expect(host.getAttribute('aria-hidden')).toBe('true');
195
+ // Under closed mode, host.shadowRoot is inaccessible (null) from external callers
196
+ expect(host.shadowRoot).toBeNull();
197
+ expect(capturedShadow).not.toBeNull();
198
+
199
+ // Dispatch events on attached phantom elements inside closed shadow root
200
+ const link = capturedShadow.querySelector('a');
201
+ expect(link).not.toBeNull();
202
+ link.dispatchEvent(new Event('focus'));
203
+ expect(onTriggerSpy).toHaveBeenCalled();
204
+ });
205
+
206
+ it('should inject trap links within closed shadow root and trigger on honeypot click', () => {
207
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
208
+ let capturedShadow = null;
209
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
210
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
211
+ capturedShadow = origAttachShadow.call(this, init);
212
+ return capturedShadow;
213
+ });
214
+
215
+ document.body.innerHTML = '';
216
+
217
+ ClientLibrary.injectTrapLinks(['/trap-url-1', '/trap-url-2']);
218
+
219
+ const host = document.body.lastElementChild;
220
+ expect(host).not.toBeNull();
221
+ expect(host.getAttribute('aria-hidden')).toBe('true');
222
+ expect(host.shadowRoot).toBeNull(); // Closed Shadow DOM verification
223
+ expect(capturedShadow).not.toBeNull();
224
+
225
+ const link = capturedShadow.querySelector('a');
226
+ expect(link).not.toBeNull();
227
+ link.dispatchEvent(new Event('click'));
228
+ expect(onTriggerSpy).toHaveBeenCalled();
229
+ });
230
+
231
+ it('should initialize honeypot form inputs within closed shadow root', () => {
232
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
233
+ let capturedShadow = null;
234
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
235
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
236
+ capturedShadow = origAttachShadow.call(this, init);
237
+ return capturedShadow;
238
+ });
239
+
240
+ document.body.innerHTML = '';
241
+
242
+ ClientLibrary.initializeHoneypots(['bot_field_trap']);
243
+
244
+ const host = document.body.lastElementChild;
245
+ expect(host).not.toBeNull();
246
+ expect(host.getAttribute('aria-hidden')).toBe('true');
247
+ expect(host.shadowRoot).toBeNull();
248
+ expect(capturedShadow).not.toBeNull();
249
+
250
+ const input = capturedShadow.querySelector('input');
251
+ expect(input).not.toBeNull();
252
+ input.dispatchEvent(new Event('input'));
253
+ expect(onTriggerSpy).toHaveBeenCalled();
254
+ });
147
255
  });
@@ -906,8 +906,76 @@ describe('Fingerprint & PoW Security Suite', () => {
906
906
  expect(req.fingerprint.score).toBeGreaterThanOrEqual(blockThreshold);
907
907
  });
908
908
 
909
- test('should redirect after a valid COMBINED (CPU+Mem) PoW solution is provided', async (context) => {
909
+ test('should redirect after a valid COMBINED (CPU+Mem) PoW solution is provided', async () => {
910
+ const ip = '127.0.0.1';
911
+ const originalPath = '/protected/combined';
912
+ const solverFingerprint = 'fp-combined-solution';
913
+ const userAgent = 'test-ua-combined';
914
+
915
+ vi.spyOn(__internal, 'getSuspicionVector').mockResolvedValue({
916
+ historyScore: 35,
917
+ });
918
+
919
+ const securityConfigWithLowDiff = {
920
+ ...securityConfig,
921
+ cpu: { minDifficultyBits: 4, ...securityConfig.cpu },
922
+ };
923
+ const middleware = powMiddleware(securityConfigWithLowDiff);
924
+
925
+ // 1. Déclencher le challenge
926
+ const req1 = {
927
+ path: originalPath, ip, cookies: {}, query: {},
928
+ headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
929
+ rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
930
+ };
931
+ let challengeBody;
932
+ const res1 = {
933
+ status: () => res1,
934
+ send: (body) => { challengeBody = body; },
935
+ cookie: vi.fn()
936
+ };
937
+ req1.fingerprint = {};
938
+ await middleware(req1, res1, vi.fn());
939
+
940
+ expect(challengeBody).toContain('Enhanced Verification');
910
941
 
942
+ // 2. Résoudre le challenge
943
+ const nonce = challengeBody.match(/const nonce = "([^"]+)"/)[1];
944
+ const clientSecret = challengeBody.match(/const clientSecret = "([^"]+)"/)[1];
945
+ const cpuTargetHex = challengeBody.match(/const cpuTarget = BigInt\("0x" \+ "([^"]+)"\);/)[1];
946
+ const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
947
+
948
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
949
+ const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
950
+ const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
951
+
952
+ // 3. Soumettre la solution
953
+ const req2 = {
954
+ path: originalPath, ip, cookies: {},
955
+ query: {
956
+ pow_type: 'cpu_mem',
957
+ pow_nonce: nonce,
958
+ pow_solution_cpu: String(cpuSolution),
959
+ pow_solution_mem: JSON.stringify(memSolution),
960
+ pow_fp: solverFingerprint
961
+ },
962
+ headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
963
+ rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
964
+ };
965
+ let capturedCookie;
966
+ const res2 = {
967
+ cookie: (name, value, options) => { capturedCookie = { name, value, options }; },
968
+ redirect: vi.fn(),
969
+ status: vi.fn(() => res2),
970
+ send: vi.fn(),
971
+ };
972
+
973
+ req2.fingerprint = {};
974
+ await middleware(req2, res2, vi.fn());
975
+
976
+ expect(res2.redirect).toHaveBeenCalledWith(originalPath);
977
+ expect(capturedCookie).toBeDefined();
978
+ expect(capturedCookie.name).toBe('pow_clearance');
911
979
  });
912
980
  });
913
981
 
@@ -1553,6 +1621,18 @@ describe('Fingerprint & PoW Security Suite', () => {
1553
1621
  expect(behaviorScore).toBe(0);
1554
1622
  });
1555
1623
 
1624
+ it('should return a score of 0 when x-behavior-metrics is 1 (human verified client-side)', () => {
1625
+ const context = { headers: { 'x-behavior-metrics': '1' } };
1626
+ const { behaviorScore } = getBehaviorScore(context);
1627
+ expect(behaviorScore).toBe(0);
1628
+ });
1629
+
1630
+ it('should return a score of 100 when x-behavior-metrics is 0 (bot detected client-side)', () => {
1631
+ const context = { headers: { 'x-behavior-metrics': '0' } };
1632
+ const { behaviorScore } = getBehaviorScore(context);
1633
+ expect(behaviorScore).toBe(100);
1634
+ });
1635
+
1556
1636
  it('should return a score of 100 for honeypot interaction', () => {
1557
1637
  const metrics = { honeypotInteraction: true, mouseEntropy: 50, keystrokeLatency: 120 };
1558
1638
  const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
@@ -1560,11 +1640,11 @@ describe('Fingerprint & PoW Security Suite', () => {
1560
1640
  expect(behaviorScore).toBe(100);
1561
1641
  });
1562
1642
 
1563
- it('should return a score of 40 for no mouse or keyboard activity', () => {
1643
+ it('should return a score of 5 for no mouse or keyboard activity', () => {
1564
1644
  const metrics = { honeypotInteraction: false, mouseMovementsHistory: [], keystrokeLatency: 0 };
1565
1645
  const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
1566
1646
  const { behaviorScore } = getBehaviorScore(context);
1567
- expect(behaviorScore).toBe(40);
1647
+ expect(behaviorScore).toBe(5);
1568
1648
  });
1569
1649
 
1570
1650
  it('should return a score of 0 for normal user activity', () => {
@@ -2394,6 +2474,58 @@ describe('getClientHintsInconsistencyScore', () => {
2394
2474
  });
2395
2475
  });
2396
2476
 
2477
+ describe('getProtocolAnomalyScore (HTTP/2 Frame Analysis)', () => {
2478
+ const { getProtocolAnomalyScore } = __internal;
2479
+
2480
+ test('should penalize spoofed Chromium with no PRIORITY frames', () => {
2481
+ const context = {
2482
+ headers: {
2483
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
2484
+ 'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:0,w:4,c:1'
2485
+ },
2486
+ httpVersion: '2.0'
2487
+ };
2488
+ const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
2489
+ expect(protocolAnomalyScore).toBe(25.0);
2490
+ });
2491
+
2492
+ test('should penalize spoofed Chromium with too few WINDOW_UPDATE frames', () => {
2493
+ const context = {
2494
+ headers: {
2495
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
2496
+ 'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:3,w:1,c:1'
2497
+ },
2498
+ httpVersion: '2.0'
2499
+ };
2500
+ const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
2501
+ expect(protocolAnomalyScore).toBe(20.0);
2502
+ });
2503
+
2504
+ test('should penalize spoofed Firefox with too many PRIORITY frames', () => {
2505
+ const context = {
2506
+ headers: {
2507
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
2508
+ 'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,s,p,a|p:3,w:2,c:0'
2509
+ },
2510
+ httpVersion: '2.0'
2511
+ };
2512
+ const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
2513
+ expect(protocolAnomalyScore).toBe(20.0);
2514
+ });
2515
+
2516
+ test('should not penalize a legitimate Chromium fingerprint', () => {
2517
+ const context = {
2518
+ headers: {
2519
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
2520
+ 'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:3,w:4,c:1'
2521
+ },
2522
+ httpVersion: '2.0'
2523
+ };
2524
+ const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
2525
+ expect(protocolAnomalyScore).toBe(0.0);
2526
+ });
2527
+ });
2528
+
2397
2529
  describe('Subnet Scoring (Node.js)', () => {
2398
2530
  const inMemoryStore = {
2399
2531
  _map: new Map(),
@@ -3068,4 +3200,22 @@ describe('IP Registration and Filtering', () => {
3068
3200
  expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
3069
3201
  });
3070
3202
  });
3203
+
3204
+ describe('Closed-Shadow DOM Honeypot Traps (Feature 19)', () => {
3205
+ it('should generate closed declarative shadow DOM trap container for trap URLs', () => {
3206
+ const trapUrls = ['/trap-1', '/trap-2'];
3207
+ const markup = __internal.generateClosedShadowTraps(trapUrls);
3208
+
3209
+ expect(markup).toContain('aria-hidden="true"');
3210
+ expect(markup).toContain('shadowrootmode="closed"');
3211
+ expect(markup).toContain('shadowroot="closed"');
3212
+ expect(markup).toContain('href="/trap-1"');
3213
+ expect(markup).toContain('href="/trap-2"');
3214
+ });
3215
+
3216
+ it('should return an empty string if trapUrls array is empty or invalid', () => {
3217
+ expect(__internal.generateClosedShadowTraps([])).toBe('');
3218
+ expect(__internal.generateClosedShadowTraps(null)).toBe('');
3219
+ });
3220
+ });
3071
3221
  });
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { __internal, configureStore } from '../fingerprint.js';
3
3
 
4
4
  describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
@@ -73,4 +73,43 @@ describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
73
73
  expect(vector.threatIntelScore).toBeGreaterThan(70.0);
74
74
  expect(vector.threatIntelScore).toBeLessThanOrEqual(95.0);
75
75
  });
76
+
77
+ it('should broadcast banned ZKP with differential privacy (perturbed timestamps and decoy injection)', async () => {
78
+ const calls = [];
79
+ const originalFetch = globalThis.fetch;
80
+ globalThis.fetch = vi.fn(async (url, init) => {
81
+ calls.push({ url, init });
82
+ return { ok: true, json: async () => ({ status: 'synchronized' }) };
83
+ });
84
+
85
+ const config = {
86
+ federatedPeers: ['https://peer1.example.com', 'https://peer2.example.com'],
87
+ federationSecret: 'test-secret-key-32-chars-long!!',
88
+ differentialPrivacy: {
89
+ enabled: true,
90
+ epsilon: 1.0,
91
+ dummyRate: 1.0 // Force decoy generation for deterministic testing
92
+ }
93
+ };
94
+
95
+ const realZkpY = 'deadbeef12345678';
96
+ await __internal.broadcastBannedZkp(realZkpY, config);
97
+
98
+ // Should broadcast to 2 peers with real key + 2 peers with decoy key
99
+ expect(calls.length).toBe(4);
100
+
101
+ const bodies = calls.map(c => JSON.parse(c.init.body));
102
+ const zkpYs = bodies.map(b => b.zkpY);
103
+
104
+ expect(zkpYs).toContain(realZkpY);
105
+ const decoyKey = zkpYs.find(k => k !== realZkpY);
106
+ expect(decoyKey).toBeDefined();
107
+ expect(decoyKey).not.toBe(realZkpY);
108
+
109
+ for (const call of calls) {
110
+ const timestamp = Number(call.init.headers['X-Federation-Timestamp']);
111
+ expect(timestamp).toBeGreaterThan(0);
112
+ }
113
+ globalThis.fetch = originalFetch;
114
+ });
76
115
  });
@@ -605,6 +605,11 @@ class ChallengeUtils
605
605
 
606
606
  if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
607
607
  return false;
608
+ }
609
+ if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
610
+ if (empty($deviceId) || $deviceId === $storedDeviceId) {
611
+ return true;
612
+ }
608
613
  }
609
614
  if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
610
615
  $expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
@@ -644,6 +649,11 @@ class ChallengeUtils
644
649
  if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
645
650
  $store->delete("ticket:{$ticket}");
646
651
  return false;
652
+ }
653
+ if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
654
+ if (empty($deviceId) || $deviceId === $storedDeviceId) {
655
+ return true;
656
+ }
647
657
  }
648
658
  if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
649
659
  $expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
@@ -789,39 +799,38 @@ class ChallengeUtils
789
799
  /**
790
800
  * Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
791
801
  */
792
- public static function checkChallengeRateLimit(string $clientIp): bool
802
+ public static function checkChallengeRateLimit(string $clientIp, float $capacity = 5.0, float $refillRate = 0.1): bool
793
803
  {
794
- $subnet = RequestUtils::getIpSubnet($clientIp);
795
- if ($subnet === null) {
796
- return false;
797
- }
804
+ $subnet = RequestUtils::getIpSubnet($clientIp) ?? $clientIp;
798
805
 
799
806
  $store = StoreManager::getStore();
800
807
  $key = "rate-limit:{$subnet}";
801
- $rateLimitData = $store->get($key) ?? [
802
- 'tokens' => 5.0,
803
- 'lastRefill' => microtime(true)
804
- ];
805
-
806
- $capacity = 5.0;
807
- $refillRate = 0.1; // 1 token toutes les 10 secondes
808
808
  $now = microtime(true);
809
809
 
810
- $elapsed = $now - $rateLimitData['lastRefill'];
811
- $tokens = min($capacity, $rateLimitData['tokens'] + $elapsed * $refillRate);
810
+ $rateLimitData = $store->get($key);
811
+ if (!is_array($rateLimitData) || !isset($rateLimitData['tokens'], $rateLimitData['lastRefill'])) {
812
+ $rateLimitData = [
813
+ 'tokens' => $capacity,
814
+ 'lastRefill' => $now
815
+ ];
816
+ }
817
+
818
+ $elapsed = max(0.0, $now - (float)$rateLimitData['lastRefill']);
819
+ $tokens = min($capacity, (float)$rateLimitData['tokens'] + $elapsed * $refillRate);
820
+ $ttl = (int)max(60, ceil($capacity / max(0.1, $refillRate)));
812
821
 
813
822
  if ($tokens < 1.0) {
814
823
  $store->set($key, [
815
824
  'tokens' => $tokens,
816
825
  'lastRefill' => $now
817
- ], 60);
826
+ ], $ttl);
818
827
  return false;
819
828
  }
820
829
 
821
830
  $store->set($key, [
822
831
  'tokens' => $tokens - 1.0,
823
832
  'lastRefill' => $now
824
- ], 60);
833
+ ], $ttl);
825
834
 
826
835
  return true;
827
836
  }
@@ -43,6 +43,7 @@ class SecurityProfiles
43
43
  'tcpAnomalyScore' => 0.8,
44
44
  'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
45
45
  'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
46
+ 'mtuAnomalyScore' => 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
46
47
  'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
47
48
  'virtualizationScore' => 0.8,
48
49
  ],
@@ -97,6 +98,7 @@ class SecurityProfiles
97
98
  'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
98
99
  'protocolAnomalyScore' => 1.0, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
99
100
  'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
101
+ 'mtuAnomalyScore' => 0.9,
100
102
 
101
103
  ],
102
104
  'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
@@ -149,6 +151,7 @@ class SecurityProfiles
149
151
  'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
150
152
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
151
153
  'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
154
+ 'mtuAnomalyScore' => 0.9,
152
155
 
153
156
  ],
154
157
  'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
@@ -205,6 +208,7 @@ class SecurityProfiles
205
208
  'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
206
209
  'protocolAnomalyScore' => 0.5, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
207
210
  'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
211
+ 'mtuAnomalyScore' => 0.9,
208
212
 
209
213
  ],
210
214
  'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
@@ -259,6 +263,7 @@ class SecurityProfiles
259
263
  'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
260
264
  'protocolAnomalyScore' => 0.9, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
261
265
  'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
266
+ 'mtuAnomalyScore' => 0.9,
262
267
 
263
268
  ],
264
269
  'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],