@anonympins/fingerprint 0.4.4 → 0.4.6
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 +40 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +180 -0
- package/src/js/fingerprint.client.js +21 -0
- package/src/js/fingerprint.js +763 -56
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/dynamic-wasm.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +128 -104
- package/src/js/tests/fingerprint.test.js +79 -1
- package/src/js/tests/ip-reputation.test.js +141 -131
- package/src/js/tests/tcpFingerprint.test.js +78 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +174 -12
- package/src/php/Config/SecurityProfiles.php +10 -0
- package/src/php/FingerprintEngine.php +140 -8
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/FingerprintEngineTest.php +132 -1
- package/src/php/Tests/IpReputationTest.php +175 -156
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +319 -5
|
@@ -1,132 +1,142 @@
|
|
|
1
|
-
import {beforeEach, describe, expect, it} from 'vitest';
|
|
2
|
-
import {__internal, configureStore, FingerprintEngine} from '../fingerprint.js';
|
|
3
|
-
|
|
4
|
-
describe('IP Reputation Local System (Node.js)', () => {
|
|
5
|
-
let mockStore;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
mockStore = {
|
|
9
|
-
_map: new Map(),
|
|
10
|
-
async get(key) { return this._map.get(key); },
|
|
11
|
-
async set(key, value) { this._map.set(key, value); },
|
|
12
|
-
async has(key) { return this._map.has(key); },
|
|
13
|
-
async delete(key) { this._map.delete(key); }
|
|
14
|
-
};
|
|
15
|
-
configureStore(mockStore);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should return a default score of 0 for unknown IPs', async () => {
|
|
19
|
-
const score = await __internal.getIpReputationScore('1.1.1.1');
|
|
20
|
-
expect(score).toBe(0);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should correctly increment and decrement score values', async () => {
|
|
24
|
-
const ip = '192.168.1.1';
|
|
25
|
-
await __internal.updateIpReputationScore(ip, 30);
|
|
26
|
-
let score = await __internal.getIpReputationScore(ip);
|
|
27
|
-
expect(score).toBe(30);
|
|
28
|
-
|
|
29
|
-
await __internal.updateIpReputationScore(ip, -10);
|
|
30
|
-
score = await __internal.getIpReputationScore(ip);
|
|
31
|
-
expect(score).toBe(20);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should clamp the reputation score within bounds [0, 100]', async () => {
|
|
35
|
-
const ip = '10.0.0.1';
|
|
36
|
-
await __internal.updateIpReputationScore(ip, 150);
|
|
37
|
-
let score = await __internal.getIpReputationScore(ip);
|
|
38
|
-
expect(score).toBe(100);
|
|
39
|
-
|
|
40
|
-
await __internal.updateIpReputationScore(ip, -200);
|
|
41
|
-
score = await __internal.getIpReputationScore(ip);
|
|
42
|
-
expect(score).toBe(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should apply passive decay of 2 points per hour of inactivity', async () => {
|
|
46
|
-
const ip = '172.16.0.1';
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
|
|
49
|
-
// Simulation d'un score de 50 mis à jour il y a 3 heures (3h * 2 points/heure = 6 points de perte)
|
|
50
|
-
await mockStore.set(`ip-reputation:${ip}`, {
|
|
51
|
-
score: 50,
|
|
52
|
-
lastUpdate: now - (3 * 60 * 60 * 1000)
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const score = await __internal.getIpReputationScore(ip);
|
|
56
|
-
expect(score).toBe(44);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should integrate ipReputationScore into the final score calculation', async () => {
|
|
60
|
-
const ip = '1.2.3.4';
|
|
61
|
-
await __internal.updateIpReputationScore(ip, 60); // Set IP reputation to 60
|
|
62
|
-
|
|
63
|
-
const mockSecurityConfig = {
|
|
64
|
-
weights: {
|
|
65
|
-
ipReputationScore: 0.5, // Give it a weight
|
|
66
|
-
historyScore: 0.0, // Other weights to 0 for isolation
|
|
67
|
-
rotationScore: 0.0,
|
|
68
|
-
headerAnomalyScore: 0.0,
|
|
69
|
-
requestPatternScore: 0.0,
|
|
70
|
-
inconsistencyScore: 0.0,
|
|
71
|
-
honeypotScore: 0.0,
|
|
72
|
-
behaviorScore: 0.0,
|
|
73
|
-
botScore: 0.0,
|
|
74
|
-
crossLayerInconsistencyScore: 0.0,
|
|
75
|
-
tlsSpoofingScore: 0.0,
|
|
76
|
-
timeInconsistencyScore: 0.0,
|
|
77
|
-
clickVarianceScore: 0.0,
|
|
78
|
-
clientHintsInconsistencyScore: 0.0,
|
|
79
|
-
subnetScore: 0.0,
|
|
80
|
-
},
|
|
81
|
-
thresholds: { low: 0, medium: 0, high: 0, block: 100 }, // Irrelevant for this test
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const ipRepScore = await __internal.getIpReputationScore(ip);
|
|
85
|
-
const suspicionVector = { ipReputationScore: ipRepScore };
|
|
86
|
-
|
|
87
|
-
const engine = new FingerprintEngine(mockSecurityConfig);
|
|
88
|
-
const finalScore = engine.calculateFinalScore(suspicionVector);
|
|
89
|
-
|
|
90
|
-
// Expected score: 60 (ipRepScore) * 0.5 (weight) = 30
|
|
91
|
-
expect(finalScore).toBe(30.0);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should return correct subnet for IPv4 and IPv6', async () => {
|
|
95
|
-
const ipv4Subnet = __internal.getIpSubnet('192.168.1.50', 24, 48);
|
|
96
|
-
expect(ipv4Subnet).toBe('192.168.1.0/24');
|
|
97
|
-
|
|
98
|
-
const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
|
|
99
|
-
expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should calculate client hints inconsistency score correctly', async () => {
|
|
103
|
-
const mockContextMismatch = {
|
|
104
|
-
headers: {
|
|
105
|
-
'user-agent': 'Mozilla/5.0 Firefox/117.0',
|
|
106
|
-
'sec-ch-ua': '"Google Chrome";v="117"'
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
const scoreMismatch = __internal.getClientHintsInconsistencyScore(mockContextMismatch);
|
|
110
|
-
expect(scoreMismatch.clientHintsInconsistencyScore).toBe(90);
|
|
111
|
-
|
|
112
|
-
const mockContextVersionDrift = {
|
|
113
|
-
headers: {
|
|
114
|
-
'user-agent': 'Mozilla/5.0 Chrome/110.0',
|
|
115
|
-
'sec-ch-ua': '"Google Chrome";v="117"'
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const scoreVersionDrift = __internal.getClientHintsInconsistencyScore(mockContextVersionDrift);
|
|
119
|
-
expect(scoreVersionDrift.clientHintsInconsistencyScore).toBe(80);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1
|
+
import {beforeEach, describe, expect, it} from 'vitest';
|
|
2
|
+
import {__internal, configureStore, FingerprintEngine} from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('IP Reputation Local System (Node.js)', () => {
|
|
5
|
+
let mockStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockStore = {
|
|
9
|
+
_map: new Map(),
|
|
10
|
+
async get(key) { return this._map.get(key); },
|
|
11
|
+
async set(key, value) { this._map.set(key, value); },
|
|
12
|
+
async has(key) { return this._map.has(key); },
|
|
13
|
+
async delete(key) { this._map.delete(key); }
|
|
14
|
+
};
|
|
15
|
+
configureStore(mockStore);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return a default score of 0 for unknown IPs', async () => {
|
|
19
|
+
const score = await __internal.getIpReputationScore('1.1.1.1');
|
|
20
|
+
expect(score).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should correctly increment and decrement score values', async () => {
|
|
24
|
+
const ip = '192.168.1.1';
|
|
25
|
+
await __internal.updateIpReputationScore(ip, 30);
|
|
26
|
+
let score = await __internal.getIpReputationScore(ip);
|
|
27
|
+
expect(score).toBe(30);
|
|
28
|
+
|
|
29
|
+
await __internal.updateIpReputationScore(ip, -10);
|
|
30
|
+
score = await __internal.getIpReputationScore(ip);
|
|
31
|
+
expect(score).toBe(20);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should clamp the reputation score within bounds [0, 100]', async () => {
|
|
35
|
+
const ip = '10.0.0.1';
|
|
36
|
+
await __internal.updateIpReputationScore(ip, 150);
|
|
37
|
+
let score = await __internal.getIpReputationScore(ip);
|
|
38
|
+
expect(score).toBe(100);
|
|
39
|
+
|
|
40
|
+
await __internal.updateIpReputationScore(ip, -200);
|
|
41
|
+
score = await __internal.getIpReputationScore(ip);
|
|
42
|
+
expect(score).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should apply passive decay of 2 points per hour of inactivity', async () => {
|
|
46
|
+
const ip = '172.16.0.1';
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
|
|
49
|
+
// Simulation d'un score de 50 mis à jour il y a 3 heures (3h * 2 points/heure = 6 points de perte)
|
|
50
|
+
await mockStore.set(`ip-reputation:${ip}`, {
|
|
51
|
+
score: 50,
|
|
52
|
+
lastUpdate: now - (3 * 60 * 60 * 1000)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const score = await __internal.getIpReputationScore(ip);
|
|
56
|
+
expect(score).toBe(44);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should integrate ipReputationScore into the final score calculation', async () => {
|
|
60
|
+
const ip = '1.2.3.4';
|
|
61
|
+
await __internal.updateIpReputationScore(ip, 60); // Set IP reputation to 60
|
|
62
|
+
|
|
63
|
+
const mockSecurityConfig = {
|
|
64
|
+
weights: {
|
|
65
|
+
ipReputationScore: 0.5, // Give it a weight
|
|
66
|
+
historyScore: 0.0, // Other weights to 0 for isolation
|
|
67
|
+
rotationScore: 0.0,
|
|
68
|
+
headerAnomalyScore: 0.0,
|
|
69
|
+
requestPatternScore: 0.0,
|
|
70
|
+
inconsistencyScore: 0.0,
|
|
71
|
+
honeypotScore: 0.0,
|
|
72
|
+
behaviorScore: 0.0,
|
|
73
|
+
botScore: 0.0,
|
|
74
|
+
crossLayerInconsistencyScore: 0.0,
|
|
75
|
+
tlsSpoofingScore: 0.0,
|
|
76
|
+
timeInconsistencyScore: 0.0,
|
|
77
|
+
clickVarianceScore: 0.0,
|
|
78
|
+
clientHintsInconsistencyScore: 0.0,
|
|
79
|
+
subnetScore: 0.0,
|
|
80
|
+
},
|
|
81
|
+
thresholds: { low: 0, medium: 0, high: 0, block: 100 }, // Irrelevant for this test
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ipRepScore = await __internal.getIpReputationScore(ip);
|
|
85
|
+
const suspicionVector = { ipReputationScore: ipRepScore };
|
|
86
|
+
|
|
87
|
+
const engine = new FingerprintEngine(mockSecurityConfig);
|
|
88
|
+
const finalScore = engine.calculateFinalScore(suspicionVector);
|
|
89
|
+
|
|
90
|
+
// Expected score: 60 (ipRepScore) * 0.5 (weight) = 30
|
|
91
|
+
expect(finalScore).toBe(30.0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return correct subnet for IPv4 and IPv6', async () => {
|
|
95
|
+
const ipv4Subnet = __internal.getIpSubnet('192.168.1.50', 24, 48);
|
|
96
|
+
expect(ipv4Subnet).toBe('192.168.1.0/24');
|
|
97
|
+
|
|
98
|
+
const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
|
|
99
|
+
expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should calculate client hints inconsistency score correctly', async () => {
|
|
103
|
+
const mockContextMismatch = {
|
|
104
|
+
headers: {
|
|
105
|
+
'user-agent': 'Mozilla/5.0 Firefox/117.0',
|
|
106
|
+
'sec-ch-ua': '"Google Chrome";v="117"'
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const scoreMismatch = __internal.getClientHintsInconsistencyScore(mockContextMismatch);
|
|
110
|
+
expect(scoreMismatch.clientHintsInconsistencyScore).toBe(90);
|
|
111
|
+
|
|
112
|
+
const mockContextVersionDrift = {
|
|
113
|
+
headers: {
|
|
114
|
+
'user-agent': 'Mozilla/5.0 Chrome/110.0',
|
|
115
|
+
'sec-ch-ua': '"Google Chrome";v="117"'
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const scoreVersionDrift = __internal.getClientHintsInconsistencyScore(mockContextVersionDrift);
|
|
119
|
+
expect(scoreVersionDrift.clientHintsInconsistencyScore).toBe(80);
|
|
120
|
+
|
|
121
|
+
const mockContextFullVersionMismatch = {
|
|
122
|
+
headers: {
|
|
123
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.1.2 Safari/537.36',
|
|
124
|
+
'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
125
|
+
'sec-ch-ua-full-version-list': '"Not_A Brand";v="8.0.0.0", "Chromium";v="120.0.1.3", "Google Chrome";v="120.0.1.3"'
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const scoreFullVersionMismatch = __internal.getClientHintsInconsistencyScore(mockContextFullVersionMismatch);
|
|
129
|
+
expect(scoreFullVersionMismatch.clientHintsInconsistencyScore).toBe(85);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should integrate subnet history into subnet score', async () => {
|
|
133
|
+
const mockContext = { clientIp: '192.168.1.50' };
|
|
134
|
+
let score = await __internal.getSubnetScore(mockContext);
|
|
135
|
+
expect(score.subnetScore).toBe(0);
|
|
136
|
+
for (let i = 1; i <= 12; i++) {
|
|
137
|
+
await __internal.updateSubnetMetrics(mockContext, `dev-${i}`, 40);
|
|
138
|
+
}
|
|
139
|
+
score = await __internal.getSubnetScore(mockContext);
|
|
140
|
+
expect(score.subnetScore).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
132
142
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
const { parseTcpSyn, classifyTcpOs, getTcpAnomalyScore } = __internal;
|
|
5
|
+
|
|
6
|
+
describe('Passive TCP/IP Fingerprinting (Type p0f)', () => {
|
|
7
|
+
|
|
8
|
+
// Mock d'un paquet IP + TCP SYN d'un système Linux (TTL=64, WS=7)
|
|
9
|
+
const mockLinuxSynPacket = Buffer.from([
|
|
10
|
+
0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
|
|
11
|
+
0x40, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x40 (64)
|
|
12
|
+
0x7f, 0x00, 0x00, 0x01,
|
|
13
|
+
0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01,
|
|
14
|
+
0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0x72, 0x10, // Window=0x7210 (29200), TCP header len = 40 (0xa0)
|
|
15
|
+
0x3c, 0x1a, 0x00, 0x00,
|
|
16
|
+
0x02, 0x04, 0x05, 0xb4, // Option MSS: 1460 (0x05b4)
|
|
17
|
+
0x04, 0x02, // Option SACK Permitted
|
|
18
|
+
0x01, // NOP
|
|
19
|
+
0x03, 0x03, 0x07 // Option WS: 7
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Mock d'un paquet IP + TCP SYN d'un système Windows (TTL=128, WS=8)
|
|
23
|
+
const mockWindowsSynPacket = Buffer.from([
|
|
24
|
+
0x45, 0x00, 0x00, 0x3c, 0x1a, 0x2b, 0x40, 0x00,
|
|
25
|
+
0x80, 0x06, 0x3c, 0x1a, 0x7f, 0x00, 0x00, 0x01, // TTL=0x80 (128)
|
|
26
|
+
0x7f, 0x00, 0x00, 0x01,
|
|
27
|
+
0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01,
|
|
28
|
+
0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfa, 0xf0, // Window=64240 (0xfaf0)
|
|
29
|
+
0x3c, 0x1a, 0x00, 0x00,
|
|
30
|
+
0x02, 0x04, 0x05, 0xb4, // Option MSS: 1460 (0x05b4)
|
|
31
|
+
0x04, 0x02, // Option SACK Permitted
|
|
32
|
+
0x01, // NOP
|
|
33
|
+
0x03, 0x03, 0x08 // Option WS: 8
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
it('should correctly parse raw TCP SYN binary packets', () => {
|
|
37
|
+
const linuxFp = parseTcpSyn(mockLinuxSynPacket);
|
|
38
|
+
expect(linuxFp).not.toBeNull();
|
|
39
|
+
expect(linuxFp.ttl).toBe(64);
|
|
40
|
+
expect(linuxFp.windowSize).toBe(29200);
|
|
41
|
+
expect(linuxFp.mss).toBe(1460);
|
|
42
|
+
expect(linuxFp.ws).toBe(7);
|
|
43
|
+
expect(linuxFp.sack).toBe(true);
|
|
44
|
+
|
|
45
|
+
const windowsFp = parseTcpSyn(mockWindowsSynPacket);
|
|
46
|
+
expect(windowsFp.ttl).toBe(128);
|
|
47
|
+
expect(windowsFp.windowSize).toBe(64240);
|
|
48
|
+
expect(windowsFp.ws).toBe(8);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should classify OS correctly based on parsed parameters', () => {
|
|
52
|
+
const linuxFp = parseTcpSyn(mockLinuxSynPacket);
|
|
53
|
+
expect(classifyTcpOs(linuxFp)).toBe('Linux');
|
|
54
|
+
|
|
55
|
+
const windowsFp = parseTcpSyn(mockWindowsSynPacket);
|
|
56
|
+
expect(classifyTcpOs(windowsFp)).toBe('Windows');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should calculate correct TCP/IP anomaly scores', () => {
|
|
60
|
+
// Cas 1 : Le User-Agent prétend être Windows, mais la pile TCP/IP est Linux
|
|
61
|
+
const contextAnomaly = {
|
|
62
|
+
headers: {
|
|
63
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
|
|
64
|
+
},
|
|
65
|
+
rawTcpBinary: mockLinuxSynPacket
|
|
66
|
+
};
|
|
67
|
+
expect(getTcpAnomalyScore(contextAnomaly).tcpAnomalyScore).toBe(80.1);
|
|
68
|
+
|
|
69
|
+
// Cas 2 : Cohérence complète (UA Windows et pile TCP/IP Windows)
|
|
70
|
+
const contextCoherent = {
|
|
71
|
+
headers: {
|
|
72
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0',
|
|
73
|
+
},
|
|
74
|
+
rawTcpBinary: mockWindowsSynPacket
|
|
75
|
+
};
|
|
76
|
+
expect(getTcpAnomalyScore(contextCoherent).tcpAnomalyScore).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -77,7 +77,7 @@ class AutoTuner
|
|
|
77
77
|
|
|
78
78
|
echo sprintf("[AutoTuning] Démarrage du cycle d'optimisation complet avec %d points de données assainis.\n", count($sanitizedData));
|
|
79
79
|
|
|
80
|
-
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData]);
|
|
80
|
+
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
|
|
81
81
|
|
|
82
82
|
if (empty($paretoFront)) {
|
|
83
83
|
echo "[AutoTuning] L'optimisation n'a retourné aucune solution.\n";
|
|
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Challenge;
|
|
6
6
|
|
|
7
7
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
8
|
+
use Anonympins\Fingerprint\FingerprintBuilder;
|
|
8
9
|
use Anonympins\Fingerprint\Utils\BigInt;
|
|
9
10
|
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
10
11
|
|
|
@@ -23,6 +24,65 @@ class ChallengeUtils
|
|
|
23
24
|
'/.git/config_{RANDOM}'
|
|
24
25
|
];
|
|
25
26
|
|
|
27
|
+
private static function imul(int $a, int $b): int
|
|
28
|
+
{
|
|
29
|
+
$ah = ($a >> 16) & 0xffff;
|
|
30
|
+
$al = $a & 0xffff;
|
|
31
|
+
$bh = ($b >> 16) & 0xffff;
|
|
32
|
+
$bl = $b & 0xffff;
|
|
33
|
+
$lo = $al * $bl;
|
|
34
|
+
$hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
|
|
35
|
+
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private static function generateBlock(string $seed, int $blockIndex, int $blockSize = 1024): string
|
|
39
|
+
{
|
|
40
|
+
$block = str_repeat("\x00", $blockSize);
|
|
41
|
+
$h = FingerprintBuilder::cyrb53($seed . ":" . $blockIndex);
|
|
42
|
+
|
|
43
|
+
$h_int = (int)bcmod($h, '4294967296');
|
|
44
|
+
for ($i = 0; $i < $blockSize; $i++) {
|
|
45
|
+
$h_int = self::imul($h_int ^ $i, 1597334677);
|
|
46
|
+
$block[$i] = chr($h_int & 0xff);
|
|
47
|
+
}
|
|
48
|
+
return $block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static function generateSpaceChallenge(string $clientIp, string $nonce, float $suspicionFactor, string $originalUrl, array $securityConfig): array
|
|
52
|
+
{
|
|
53
|
+
$pospaceConfig = $securityConfig['pospace'] ?? [];
|
|
54
|
+
$sizeMb = $pospaceConfig['sizeMb'] ?? 100;
|
|
55
|
+
$numQueries = $pospaceConfig['numQueries'] ?? 10;
|
|
56
|
+
|
|
57
|
+
$queries = [];
|
|
58
|
+
$maxBlocks = $sizeMb * 1024;
|
|
59
|
+
while (count($queries) < $numQueries) {
|
|
60
|
+
$idx = random_int(0, $maxBlocks - 1);
|
|
61
|
+
if (!in_array($idx, $queries, true)) {
|
|
62
|
+
$queries[] = $idx;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
'type' => 'pospace',
|
|
68
|
+
'nonce' => $nonce,
|
|
69
|
+
'sizeMb' => $sizeMb,
|
|
70
|
+
'queries' => $queries,
|
|
71
|
+
'path' => $originalUrl
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public static function verifySpacePoW(string $nonce, string $solution, array $queries, string $seed, string $clientSecret): bool
|
|
76
|
+
{
|
|
77
|
+
$combined = '';
|
|
78
|
+
foreach ($queries as $idx) {
|
|
79
|
+
$combined .= self::generateBlock($seed, (int)$idx);
|
|
80
|
+
}
|
|
81
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
82
|
+
$hash = hash('sha256', $finalBlock);
|
|
83
|
+
return hash_equals($hash, $solution);
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
/**
|
|
27
87
|
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
28
88
|
*/
|
|
@@ -35,6 +95,52 @@ class ChallengeUtils
|
|
|
35
95
|
return $secret ?: "fallback-dev-secret-32-chars-minimum";
|
|
36
96
|
}
|
|
37
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Génère un ticket stateless chiffré et signé contenant le contexte d'autorisation.
|
|
100
|
+
* @param array $payload
|
|
101
|
+
* @return string
|
|
102
|
+
*/
|
|
103
|
+
public static function generateStatelessTicket(array $payload): string
|
|
104
|
+
{
|
|
105
|
+
$key = hash('sha256', self::getPowSecret(), true);
|
|
106
|
+
$iv = random_bytes(16);
|
|
107
|
+
$encrypted = openssl_encrypt(json_encode($payload), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
108
|
+
$signature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
109
|
+
|
|
110
|
+
return rtrim(strtr(base64_encode($iv), '+/', '-_'), '=') . '.' .
|
|
111
|
+
rtrim(strtr(base64_encode($encrypted), '+/', '-_'), '=') . '.' .
|
|
112
|
+
rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Décode et valide un ticket stateless chiffré et signé.
|
|
117
|
+
* @param string $ticket
|
|
118
|
+
* @return array|null
|
|
119
|
+
*/
|
|
120
|
+
public static function parseStatelessTicket(string $ticket): ?array
|
|
121
|
+
{
|
|
122
|
+
$parts = explode('.', $ticket);
|
|
123
|
+
if (count($parts) !== 3) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
$base64UrlDecode = function ($input) {
|
|
127
|
+
return base64_decode(strtr($input, '-_', '+/'));
|
|
128
|
+
};
|
|
129
|
+
$iv = $base64UrlDecode($parts[0]);
|
|
130
|
+
$encrypted = $base64UrlDecode($parts[1]);
|
|
131
|
+
$signature = $base64UrlDecode($parts[2]);
|
|
132
|
+
if (!$iv || !$encrypted || !$signature || strlen($iv) !== 16) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
$key = hash('sha256', self::getPowSecret(), true);
|
|
136
|
+
$expectedSignature = hash_hmac('sha256', $iv . $encrypted, $key, true);
|
|
137
|
+
if (!hash_equals($expectedSignature, $signature)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
$decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
|
141
|
+
return $decrypted !== false ? json_decode($decrypted, true) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
38
144
|
/**
|
|
39
145
|
* Vérifie si un ticket de passage est valide (supporte les tickets opaques via store et le fallback legacy).
|
|
40
146
|
*/
|
|
@@ -49,6 +155,31 @@ class ChallengeUtils
|
|
|
49
155
|
return false;
|
|
50
156
|
}
|
|
51
157
|
|
|
158
|
+
// Tentative de validation stateless d'abord
|
|
159
|
+
$ticketData = self::parseStatelessTicket($ticket);
|
|
160
|
+
if ($ticketData !== null) {
|
|
161
|
+
$expiry = $ticketData['expiry'] ?? null;
|
|
162
|
+
$originalIp = $ticketData['originalIp'] ?? null;
|
|
163
|
+
$storedDeviceId = $ticketData['deviceId'] ?? '';
|
|
164
|
+
$storedDeviceHash = $ticketData['deviceHash'] ?? '';
|
|
165
|
+
|
|
166
|
+
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if ($ip === $originalIp) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
$currentSubnet = RequestUtils::getIpSubnet($ip);
|
|
173
|
+
$originalSubnet = RequestUtils::getIpSubnet($originalIp);
|
|
174
|
+
if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (!$allowCrossNetworkRoaming) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return !empty($deviceId) && $deviceId === $storedDeviceId && !empty($deviceHash) && $deviceHash === $storedDeviceHash;
|
|
181
|
+
}
|
|
182
|
+
|
|
52
183
|
$store = StoreManager::getStore();
|
|
53
184
|
$ticketData = $store->get("ticket:{$ticket}");
|
|
54
185
|
|
|
@@ -152,27 +283,21 @@ class ChallengeUtils
|
|
|
152
283
|
$finalBlock = $baseBlock . $solution;
|
|
153
284
|
$hash = hash('sha256', $finalBlock);
|
|
154
285
|
|
|
155
|
-
|
|
156
|
-
$
|
|
157
|
-
|
|
158
|
-
$isValid = $hashAsInt->compareTo($targetAsInt) < 0;
|
|
286
|
+
// Pad target to 64 hex characters to allow direct O(1) lexicographical comparison
|
|
287
|
+
$paddedTarget = str_pad($cpuTargetHex, 64, '0', STR_PAD_LEFT);
|
|
288
|
+
$isValid = strcmp($hash, $paddedTarget) < 0;
|
|
159
289
|
|
|
160
290
|
if ($isValid) {
|
|
161
291
|
error_log('[FP Server Verify] CPU PoW verification PASSED.');
|
|
162
292
|
|
|
163
|
-
// Génération d'un jeton opaque et unique
|
|
164
|
-
$ticketId = bin2hex(random_bytes(16));
|
|
165
293
|
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
166
|
-
|
|
167
|
-
$store = StoreManager::getStore();
|
|
168
|
-
$store->set("ticket:{$ticketId}", [
|
|
294
|
+
$payload = [
|
|
169
295
|
'expiry' => $expiry,
|
|
170
296
|
'originalIp' => $clientIp,
|
|
171
297
|
'deviceId' => $deviceId,
|
|
172
298
|
'deviceHash' => $deviceHash
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
return $ticketId;
|
|
299
|
+
];
|
|
300
|
+
return self::generateStatelessTicket($payload);
|
|
176
301
|
}
|
|
177
302
|
|
|
178
303
|
// Log details on failure
|
|
@@ -336,6 +461,43 @@ class ChallengeUtils
|
|
|
336
461
|
return file_get_contents($solverPath) ?: '';
|
|
337
462
|
}
|
|
338
463
|
|
|
464
|
+
public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
|
|
465
|
+
{
|
|
466
|
+
$nonce = $challengeDetails['nonce'];
|
|
467
|
+
$sizeMb = $challengeDetails['sizeMb'];
|
|
468
|
+
$queries = $challengeDetails['queries'];
|
|
469
|
+
$path = $challengeDetails['path'];
|
|
470
|
+
|
|
471
|
+
$solverCode = self::getPowSolverCode();
|
|
472
|
+
$queriesJson = json_encode($queries);
|
|
473
|
+
|
|
474
|
+
$challengeScript = <<<JS
|
|
475
|
+
async function solve() {
|
|
476
|
+
const nonce = "{$nonce}";
|
|
477
|
+
const path = "{$path}";
|
|
478
|
+
const clientSecret = "{$clientSecret}";
|
|
479
|
+
const queries = {$queriesJson};
|
|
480
|
+
const sizeMb = {$sizeMb};
|
|
481
|
+
|
|
482
|
+
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
483
|
+
await new Promise(r => setTimeout(r, 10));
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
|
|
487
|
+
document.getElementById('loader').innerText = '⚙️ Generating Proof of Space...';
|
|
488
|
+
const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret);
|
|
489
|
+
|
|
490
|
+
window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;
|
|
491
|
+
} catch(e) {
|
|
492
|
+
document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
solve();
|
|
496
|
+
JS;
|
|
497
|
+
|
|
498
|
+
return "<html><head><title>Security Check</title></head><body style=\"font-family:sans-serif; text-align:center; padding-top:50px;\"><h1>Security Check (Level 2)</h1><p>We are verifying your storage allocation. This may take a few seconds on first load.</p><div id=\"loader\" style=\"margin:20px;\">⚙️ Initializing storage space...</div><script>{$solverCode}</script><script>{$challengeScript}</script></body></html>";
|
|
499
|
+
}
|
|
500
|
+
|
|
339
501
|
/**
|
|
340
502
|
* Génère le contenu HTML pour un challenge combiné CPU + Mémoire.
|
|
341
503
|
* @param array $cpuChallengeDetails
|
|
@@ -40,6 +40,8 @@ class SecurityProfiles
|
|
|
40
40
|
'clickVarianceScore' => 0.6, // Poids pour la variance des clics
|
|
41
41
|
'subnetScore' => 0.5, // Pénalise les sous-réseaux IP avec une activité suspecte agrégée
|
|
42
42
|
'botnetClusterScore' => 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
43
|
+
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
44
|
+
|
|
43
45
|
],
|
|
44
46
|
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
45
47
|
'patterns' => [
|
|
@@ -84,6 +86,8 @@ class SecurityProfiles
|
|
|
84
86
|
'clickVarianceScore' => 0.7, // High weight for click variance
|
|
85
87
|
'subnetScore' => 0.7, // Poids plus élevé en mode strict
|
|
86
88
|
'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
89
|
+
'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
|
|
90
|
+
|
|
87
91
|
],
|
|
88
92
|
'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
|
|
89
93
|
'patterns' => [
|
|
@@ -128,6 +132,8 @@ class SecurityProfiles
|
|
|
128
132
|
'clickVarianceScore' => 0.3, // Low weight as not applicable to APIs
|
|
129
133
|
'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
|
|
130
134
|
'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
135
|
+
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
136
|
+
|
|
131
137
|
],
|
|
132
138
|
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
133
139
|
'patterns' => [
|
|
@@ -174,6 +180,8 @@ class SecurityProfiles
|
|
|
174
180
|
'clickVarianceScore' => 0.5, // Moderate weight for click variance
|
|
175
181
|
'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
|
|
176
182
|
'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
183
|
+
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
184
|
+
|
|
177
185
|
],
|
|
178
186
|
'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
|
|
179
187
|
'patterns' => [
|
|
@@ -219,6 +227,8 @@ class SecurityProfiles
|
|
|
219
227
|
'clickVarianceScore' => 0.8, // Very high weight for click variance
|
|
220
228
|
'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
|
|
221
229
|
'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
230
|
+
'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
|
|
231
|
+
|
|
222
232
|
],
|
|
223
233
|
'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
|
|
224
234
|
'patterns' => [
|