@anonympins/fingerprint 0.6.7 → 0.7.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 +33 -0
- package/README.md +3 -7
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +9 -11
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +236 -152
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +517 -101
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +355 -384
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +206 -62
- package/src/js/pow.solver.js +235 -101
- package/src/js/pow.worker.js +48 -26
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +101 -40
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +204 -32
- package/src/php/Config/SecurityProfiles.php +21 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +24 -25
- package/src/php/FingerprintEngine.php +1836 -1880
- package/src/php/Ja3AnomalyDetector.php +258 -227
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +28 -27
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -693
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +236 -19
- package/src/php/Utils/TLSClientHelloParser.php +369 -212
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
package/src/js/pow.worker.js
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file @/pow.worker.js
|
|
3
|
-
* @description Web Worker
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
self.onmessage = async (event) => {
|
|
8
|
-
const { message, target } = event.data;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file @/pow.worker.js
|
|
3
|
+
* @description Dedicated Web Worker for CPU challenge solving.
|
|
4
|
+
* Executes on a separate thread to avoid blocking the main UI thread.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
self.onmessage = async (event) => {
|
|
8
|
+
const { message, baseBlock, target } = event.data;
|
|
9
|
+
const cpuTarget = BigInt(target);
|
|
10
|
+
let solution = 0;
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
if (baseBlock) {
|
|
13
|
+
// Mode solveCpuTargetInline (Uint8Array base block)
|
|
14
|
+
const blockArray = new Uint8Array(baseBlock);
|
|
15
|
+
while (true) {
|
|
16
|
+
const solutionBytes = encoder.encode(String(solution));
|
|
17
|
+
const finalBlock = new Uint8Array(blockArray.length + solutionBytes.length);
|
|
18
|
+
finalBlock.set(blockArray);
|
|
19
|
+
finalBlock.set(solutionBytes, blockArray.length);
|
|
20
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', finalBlock);
|
|
21
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
22
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
23
|
+
|
|
24
|
+
if (BigInt('0x' + hashHex) < cpuTarget) {
|
|
25
|
+
self.postMessage({ type: 'success', solution });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
solution++;
|
|
29
|
+
if (solution % 25000 === 0) {
|
|
30
|
+
self.postMessage({ type: 'progress', solution });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Mode solveCpuTarget standard/legacy (Message string)
|
|
35
|
+
while (true) {
|
|
36
|
+
const currentMessage = `${message}:${solution}`;
|
|
37
|
+
const data = encoder.encode(currentMessage);
|
|
38
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
39
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
40
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
41
|
+
|
|
42
|
+
if (BigInt('0x' + hashHex) < cpuTarget) {
|
|
43
|
+
self.postMessage({ solution });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
solution++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
};
|
package/src/js/redis-store.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createRedisStore(redisClient) {
|
|
|
35
35
|
reconnecting = false;
|
|
36
36
|
clearInterval(interval);
|
|
37
37
|
} catch (err) {
|
|
38
|
-
//
|
|
38
|
+
// Keep retrying connection every 5 seconds
|
|
39
39
|
}
|
|
40
40
|
}, 5000);
|
|
41
41
|
if (interval.unref) interval.unref();
|
|
@@ -1,144 +1,147 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
-
import {JSDOM} from 'jsdom';
|
|
3
|
-
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
-
|
|
5
|
-
// --- Setup JSDOM Environment ---
|
|
6
|
-
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
-
// ici rend le test explicite et portable.
|
|
8
|
-
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
-
url: 'http://localhost',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
-
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
-
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
-
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
-
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
-
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
-
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
-
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
-
|
|
21
|
-
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
-
global.Headers = dom.window.Headers;
|
|
23
|
-
global.Request = dom.window.Request;
|
|
24
|
-
global.URL = dom.window.URL;
|
|
25
|
-
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
-
|
|
30
|
-
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
-
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
// Réinitialiser l'état du client avant chaque test
|
|
35
|
-
ClientLibrary._resetCache();
|
|
36
|
-
|
|
37
|
-
// Créer les espions sur les méthodes internes
|
|
38
|
-
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
-
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
-
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
-
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
expect(
|
|
61
|
-
expect(
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {JSDOM} from 'jsdom';
|
|
3
|
+
import ClientLibrary from '../fingerprint.client.js';
|
|
4
|
+
|
|
5
|
+
// --- Setup JSDOM Environment ---
|
|
6
|
+
// Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
|
|
7
|
+
// ici rend le test explicite et portable.
|
|
8
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
9
|
+
url: 'http://localhost',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// In recent Node.js versions, some global properties like 'navigator', 'performance',
|
|
13
|
+
// and 'screen' are read-only. To ensure our JSDOM environment works correctly across
|
|
14
|
+
// all versions, we use Object.defineProperty to make these properties writable.
|
|
15
|
+
Object.defineProperty(global, 'window', { value: dom.window, writable: true });
|
|
16
|
+
Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
|
|
17
|
+
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
+
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
|
+
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
+
|
|
21
|
+
global.fetch = vi.fn(); // Mock global fetch
|
|
22
|
+
global.Headers = dom.window.Headers;
|
|
23
|
+
global.Request = dom.window.Request;
|
|
24
|
+
global.URL = dom.window.URL;
|
|
25
|
+
global.TextEncoder = dom.window.TextEncoder;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
describe('ClientLibrary.initializeClient', () => {
|
|
29
|
+
|
|
30
|
+
// On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
|
|
31
|
+
let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startMotionSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Réinitialiser l'état du client avant chaque test
|
|
35
|
+
ClientLibrary._resetCache();
|
|
36
|
+
|
|
37
|
+
// Créer les espions sur les méthodes internes
|
|
38
|
+
startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
|
|
39
|
+
startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
|
|
40
|
+
startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
|
|
41
|
+
startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
|
|
42
|
+
startMotionSpy = vi.spyOn(ClientLibrary, 'startMotionTracker');
|
|
43
|
+
startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
|
|
44
|
+
initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
|
|
45
|
+
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
46
|
+
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
47
|
+
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
48
|
+
injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// Restaurer les espions après chaque test pour ne pas affecter les autres tests
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should enable all trackers by default', () => {
|
|
57
|
+
ClientLibrary.initializeClient();
|
|
58
|
+
|
|
59
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
60
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
61
|
+
expect(startClickSpy).toHaveBeenCalled();
|
|
62
|
+
expect(startTouchSpy).toHaveBeenCalled();
|
|
63
|
+
expect(startMotionSpy).toHaveBeenCalled();
|
|
64
|
+
expect(startRenderingSpy).toHaveBeenCalled();
|
|
65
|
+
expect(injectPhantomTrapsSpy).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should disable trackers when configured', () => {
|
|
69
|
+
ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, motion: false, rendering: false });
|
|
70
|
+
|
|
71
|
+
expect(startMouseSpy).not.toHaveBeenCalled();
|
|
72
|
+
expect(startKeystrokeSpy).not.toHaveBeenCalled();
|
|
73
|
+
expect(startClickSpy).not.toHaveBeenCalled();
|
|
74
|
+
expect(startTouchSpy).not.toHaveBeenCalled();
|
|
75
|
+
expect(startMotionSpy).not.toHaveBeenCalled();
|
|
76
|
+
expect(startRenderingSpy).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should initialize WASM if wasmPath is configured', () => {
|
|
80
|
+
ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
|
|
81
|
+
|
|
82
|
+
expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should disable phantom traps when configured', () => {
|
|
86
|
+
ClientLibrary.initializeClient({ phantomTraps: false });
|
|
87
|
+
|
|
88
|
+
expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should initialize honeypots with the provided field names', () => {
|
|
92
|
+
const honeypotFields = ['email_confirm', 'user_nickname'];
|
|
93
|
+
ClientLibrary.initializeClient({ honeypots: honeypotFields });
|
|
94
|
+
|
|
95
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not initialize honeypots if the array is empty', () => {
|
|
99
|
+
ClientLibrary.initializeClient({ honeypots: [] });
|
|
100
|
+
|
|
101
|
+
expect(initHoneypotsSpy).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should inject trap URLs when provided', () => {
|
|
105
|
+
const urls = ['/trap1?sig=123', '/trap2?sig=456'];
|
|
106
|
+
ClientLibrary.initializeClient({ trapUrls: urls });
|
|
107
|
+
|
|
108
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not inject trap URLs if the array is empty', () => {
|
|
112
|
+
ClientLibrary.initializeClient({ trapUrls: [] });
|
|
113
|
+
|
|
114
|
+
expect(injectTrapsSpy).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should initialize fetch interception when fetch config is present', () => {
|
|
118
|
+
const targetDomains = ['api.example.com'];
|
|
119
|
+
ClientLibrary.initializeClient({ fetch: { targetDomains } });
|
|
120
|
+
|
|
121
|
+
expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should not initialize fetch interception if fetch config is absent', () => {
|
|
125
|
+
ClientLibrary.initializeClient(); // No fetch config
|
|
126
|
+
|
|
127
|
+
expect(initFetchSpy).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should call all initializers correctly when a full config is provided', () => {
|
|
131
|
+
const config = {
|
|
132
|
+
mouse: true,
|
|
133
|
+
keystrokes: true,
|
|
134
|
+
honeypots: ['field1'],
|
|
135
|
+
trapUrls: ['/trap1'],
|
|
136
|
+
fetch: { targetDomains: ['api.com'] }
|
|
137
|
+
};
|
|
138
|
+
ClientLibrary.initializeClient(config);
|
|
139
|
+
|
|
140
|
+
expect(startMouseSpy).toHaveBeenCalled();
|
|
141
|
+
expect(startKeystrokeSpy).toHaveBeenCalled();
|
|
142
|
+
expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
|
|
143
|
+
expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
|
|
144
|
+
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
145
|
+
});
|
|
146
|
+
|
|
144
147
|
});
|
|
@@ -665,38 +665,6 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
665
665
|
expect(next, 'next() should have been called for a request with a valid ticket').toHaveBeenCalled();
|
|
666
666
|
});
|
|
667
667
|
|
|
668
|
-
test('should issue a new challenge (re-challenge) if suspicion score is high even with a valid ticket', async () => {
|
|
669
|
-
vi.spyOn(__internal, 'getSuspicionVector').mockResolvedValue({
|
|
670
|
-
historyScore: 80,
|
|
671
|
-
rotationScore: 80,
|
|
672
|
-
headerAnomalyScore: 80,
|
|
673
|
-
inconsistencyScore: 80,
|
|
674
|
-
requestPatternScore: 80,
|
|
675
|
-
honeypotScore: 0
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
const ip = '127.0.0.1';
|
|
679
|
-
const expiry = Date.now() + 3600000;
|
|
680
|
-
const signature = createHmac("sha256", process.env.POW_SECRET || "fallback-dev-secret-32-chars-minimum").update(`${ip}:${expiry}`).digest("hex");
|
|
681
|
-
const validTicket = `${expiry}:${signature}`;
|
|
682
|
-
|
|
683
|
-
const req = { path: '/', ip, cookies: { pow_clearance: validTicket }, query: {}, headers: { 'user-agent': 'test-ua' } };
|
|
684
|
-
let sentStatus, sentBody;
|
|
685
|
-
const res = {
|
|
686
|
-
status: (s) => { sentStatus = s; return res; },
|
|
687
|
-
send: (b) => { sentBody = b; },
|
|
688
|
-
cookie: vi.fn()
|
|
689
|
-
};
|
|
690
|
-
const next = vi.fn();
|
|
691
|
-
|
|
692
|
-
req.fingerprint = {};
|
|
693
|
-
await powMiddleware(securityConfig)(req, res, next);
|
|
694
|
-
|
|
695
|
-
expect(next).not.toHaveBeenCalled();
|
|
696
|
-
expect(sentStatus).toBe(404);
|
|
697
|
-
expect(sentBody).toContain('Enhanced Verification');
|
|
698
|
-
});
|
|
699
|
-
|
|
700
668
|
test('should redirect after a valid PoW solution is provided', async () => {
|
|
701
669
|
// --- 1. Setup: Define context and mock a suspicious score ---
|
|
702
670
|
const ip = '127.0.0.1';
|
|
@@ -2401,20 +2369,47 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2401
2369
|
// 2. Some high scores, few devices. The subnet is calculated from the context.
|
|
2402
2370
|
// The key is hardcoded here to match what the implementation would store.
|
|
2403
2371
|
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2404
|
-
highScoreCount: 5,
|
|
2405
|
-
deviceIds: ['d1', 'd2'],
|
|
2372
|
+
highScoreCount: 5,
|
|
2373
|
+
deviceIds: ['d1', 'd2'],
|
|
2406
2374
|
});
|
|
2407
2375
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2408
|
-
expect(subnetScore).
|
|
2376
|
+
expect(subnetScore).toBeGreaterThan(0);
|
|
2377
|
+
expect(subnetScore).toBeLessThanOrEqual(45.0); // Ambient score capped in Medium zone
|
|
2409
2378
|
|
|
2410
|
-
// 3. Many devices and high scores
|
|
2379
|
+
// 3. Many devices and high scores, ambient score still capped at 45.0 (Medium asymptote)
|
|
2411
2380
|
const deviceIds = Array.from({ length: 20 }, (_, i) => `d${i}`);
|
|
2412
2381
|
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2413
|
-
highScoreCount: 30,
|
|
2414
|
-
deviceIds: deviceIds,
|
|
2382
|
+
highScoreCount: 30,
|
|
2383
|
+
deviceIds: deviceIds,
|
|
2384
|
+
});
|
|
2385
|
+
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2386
|
+
expect(subnetScore).toBeLessThanOrEqual(45.0);
|
|
2387
|
+
expect(subnetScore).toBeGreaterThanOrEqual(30.0);
|
|
2388
|
+
|
|
2389
|
+
// 4. Nearby attacker in /30 (+45 boost), escalates score into High territory
|
|
2390
|
+
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2391
|
+
highScoreCount: 30,
|
|
2392
|
+
deviceIds: deviceIds,
|
|
2393
|
+
attackerIps: [{ ip: '10.0.0.26', lastSeen: Date.now(), score: 85 }]
|
|
2415
2394
|
});
|
|
2416
2395
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2417
|
-
|
|
2396
|
+
expect(subnetScore).toBeGreaterThanOrEqual(80.0);
|
|
2397
|
+
|
|
2398
|
+
// 5. Exact IP match (+55 boost), reaches 90-100
|
|
2399
|
+
await inMemoryStore.set('subnet:10.0.0.0/24', {
|
|
2400
|
+
highScoreCount: 30,
|
|
2401
|
+
deviceIds: deviceIds,
|
|
2402
|
+
attackerIps: [{ ip: '10.0.0.25', lastSeen: Date.now(), score: 95 }]
|
|
2403
|
+
});
|
|
2404
|
+
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2405
|
+
expect(subnetScore).toBeGreaterThanOrEqual(90.0);
|
|
2406
|
+
});
|
|
2407
|
+
|
|
2408
|
+
it('getIpCommonPrefixLength should accurately calculate common prefix bits', () => {
|
|
2409
|
+
const { getIpCommonPrefixLength } = __internal;
|
|
2410
|
+
expect(getIpCommonPrefixLength('10.0.0.25', '10.0.0.25')).toBe(32);
|
|
2411
|
+
expect(getIpCommonPrefixLength('10.0.0.24', '10.0.0.25')).toBe(31);
|
|
2412
|
+
expect(getIpCommonPrefixLength('192.168.1.10', '192.168.2.10')).toBe(22);
|
|
2418
2413
|
});
|
|
2419
2414
|
});
|
|
2420
2415
|
|
|
@@ -2611,6 +2606,14 @@ describe('Additional Suspicion Vectors Coverage', () => {
|
|
|
2611
2606
|
});
|
|
2612
2607
|
|
|
2613
2608
|
describe('Ed25519 Asymmetric Tickets', () => {
|
|
2609
|
+
const localStore = {
|
|
2610
|
+
_map: new Map(),
|
|
2611
|
+
async get(key) { return this._map.get(key); },
|
|
2612
|
+
async set(key, value) { this._map.set(key, value); },
|
|
2613
|
+
async has(key) { return this._map.has(key); },
|
|
2614
|
+
async delete(key) { this._map.delete(key); },
|
|
2615
|
+
async clear() { this._map.clear(); }
|
|
2616
|
+
};
|
|
2614
2617
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
|
|
2615
2618
|
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
|
|
2616
2619
|
publicKeyEncoding: { format: 'pem', type: 'spki' }
|
|
@@ -2618,7 +2621,9 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2618
2621
|
const privateKeyPem = privateKey;
|
|
2619
2622
|
const publicKeyPem = publicKey;
|
|
2620
2623
|
|
|
2621
|
-
beforeEach(() => {
|
|
2624
|
+
beforeEach(async () => {
|
|
2625
|
+
await localStore.clear();
|
|
2626
|
+
configureStore(localStore);
|
|
2622
2627
|
process.env.ED25519_PRIVATE_KEY = privateKeyPem;
|
|
2623
2628
|
process.env.ED25519_PUBLIC_KEY = publicKeyPem;
|
|
2624
2629
|
});
|
|
@@ -2697,6 +2702,62 @@ describe('Ed25519 Asymmetric Tickets', () => {
|
|
|
2697
2702
|
expect(resPoll.requests[0].req_id).toBe('req-123');
|
|
2698
2703
|
expect(resPoll.requests[0].block_idx).toBe(42);
|
|
2699
2704
|
});
|
|
2705
|
+
|
|
2706
|
+
test('WebRTC P2P Signaling Flow (Node.js)', async () => {
|
|
2707
|
+
const clientIp = '127.0.0.1';
|
|
2708
|
+
const nodeIdA = 'node-a-webrtc';
|
|
2709
|
+
const nodeIdB = 'node-b-webrtc';
|
|
2710
|
+
const clientSecretA = 'secret-a-webrtc';
|
|
2711
|
+
const clientSecretB = 'secret-b-webrtc';
|
|
2712
|
+
|
|
2713
|
+
const store = __internal.store;
|
|
2714
|
+
await store.set(`secret:${nodeIdA}`, { clientSecret: clientSecretA });
|
|
2715
|
+
await store.set(`secret:${nodeIdB}`, { clientSecret: clientSecretB });
|
|
2716
|
+
|
|
2717
|
+
await __internal.registerCooperativeNode(clientIp, nodeIdA, 'seed-a');
|
|
2718
|
+
await __internal.registerCooperativeNode(clientIp, nodeIdB, 'seed-b');
|
|
2719
|
+
|
|
2720
|
+
// 1. Découverte d'un pair dans le sous-réseau
|
|
2721
|
+
const sigFind = createHash('sha256')
|
|
2722
|
+
.update(`${clientSecretA}:find_peer:${nodeIdA}`)
|
|
2723
|
+
.digest('hex');
|
|
2724
|
+
const findRes = await __internal.handleCooperativeRequest({
|
|
2725
|
+
coop_op: 'find_peer',
|
|
2726
|
+
node_id: nodeIdA,
|
|
2727
|
+
coop_sig: sigFind
|
|
2728
|
+
}, clientIp);
|
|
2729
|
+
expect(findRes.status).toBe('peer_found');
|
|
2730
|
+
expect(findRes.peer_id).toBe(nodeIdB);
|
|
2731
|
+
|
|
2732
|
+
// 2. Envoi d'une offre SDP WebRTC (A -> B)
|
|
2733
|
+
const offerData = '{"type":"offer","sdp":"v=0..."}';
|
|
2734
|
+
const sigSignal = createHash('sha256')
|
|
2735
|
+
.update(`${clientSecretA}:webrtc_signal:${nodeIdA}:${nodeIdB}:offer:${offerData}`)
|
|
2736
|
+
.digest('hex');
|
|
2737
|
+
const signalRes = await __internal.handleCooperativeRequest({
|
|
2738
|
+
coop_op: 'webrtc_signal',
|
|
2739
|
+
node_id: nodeIdA,
|
|
2740
|
+
target_peer_id: nodeIdB,
|
|
2741
|
+
signal_type: 'offer',
|
|
2742
|
+
signal_data: offerData,
|
|
2743
|
+
coop_sig: sigSignal
|
|
2744
|
+
}, clientIp);
|
|
2745
|
+
expect(signalRes.status).toBe('signal_queued');
|
|
2746
|
+
|
|
2747
|
+
// 3. Récupération des signaux par le nœud B
|
|
2748
|
+
const sigPoll = createHash('sha256')
|
|
2749
|
+
.update(`${clientSecretB}:poll_signals:${nodeIdB}`)
|
|
2750
|
+
.digest('hex');
|
|
2751
|
+
const pollRes = await __internal.handleCooperativeRequest({
|
|
2752
|
+
coop_op: 'poll_signals',
|
|
2753
|
+
node_id: nodeIdB,
|
|
2754
|
+
coop_sig: sigPoll
|
|
2755
|
+
}, clientIp);
|
|
2756
|
+
expect(pollRes.status).toBe('ok');
|
|
2757
|
+
expect(pollRes.signals.length).toBe(1);
|
|
2758
|
+
expect(pollRes.signals[0].from_peer_id).toBe(nodeIdA);
|
|
2759
|
+
expect(pollRes.signals[0].signal_type).toBe('offer');
|
|
2760
|
+
});
|
|
2700
2761
|
});
|
|
2701
2762
|
|
|
2702
2763
|
describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
@@ -138,5 +138,6 @@ describe('IP Reputation Local System (Node.js)', () => {
|
|
|
138
138
|
}
|
|
139
139
|
score = await __internal.getSubnetScore(mockContext);
|
|
140
140
|
expect(score.subnetScore).toBeGreaterThan(0);
|
|
141
|
+
expect(score.subnetScore).toBeLessThanOrEqual(45.0); // Ambient cap
|
|
141
142
|
});
|
|
142
143
|
});
|