@anonympins/fingerprint 0.5.0 → 0.5.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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * GPU Proof-of-Work Solver (WebGPU with WebGL2 Fallback)
3
+ * Forces massive parallel floats computation to exhaust CPU emulators (SwiftShader).
4
+ */
5
+ export class GpuPowSolver {
6
+ /**
7
+ * Solve the GPU challenge.
8
+ * @param {string} seed - The hexadecimal/string challenge seed from the server.
9
+ * @param {number} iterations - Number of chaotic map iterations per thread.
10
+ * @returns {Promise<{solution: string, platform: string, duration: number}>}
11
+ */
12
+ static async solve(seed, iterations = 200000) {
13
+ const start = performance.now();
14
+ const numericSeed = this._hashSeedToFloat(seed);
15
+
16
+ try {
17
+ if (navigator.gpu) {
18
+ const result = await this._solveWebGPU(numericSeed, iterations);
19
+ return {
20
+ solution: result,
21
+ platform: 'webgpu',
22
+ duration: performance.now() - start
23
+ };
24
+ }
25
+ } catch (e) {
26
+ console.warn('[GPU-PoW] WebGPU failed or disabled, falling back to WebGL2:', e);
27
+ }
28
+
29
+ // Fallback to WebGL2
30
+ try {
31
+ const result = await this._solveWebGL2(numericSeed, iterations);
32
+ return {
33
+ solution: result,
34
+ platform: 'webgl2',
35
+ duration: performance.now() - start
36
+ };
37
+ } catch (e) {
38
+ throw new Error(`[GPU-PoW] Both WebGPU and WebGL2 solvers failed: ${e.message}`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Verifies a GPU PoW solution.
44
+ * To prevent server-side DoS, it verifies a sample of the 64 channels.
45
+ * @param {string} seed - The challenge seed.
46
+ * @param {number} iterations - Number of iterations.
47
+ * @param {string} solution - The comma-separated solution string.
48
+ * @param {Array<number>} [sampleIndices=[0, 12, 35, 57]] - Indices to verify.
49
+ * @returns {boolean} True if the solution is valid.
50
+ */
51
+ static verify(seed, iterations, solution, sampleIndices = [0, 12, 35, 57]) {
52
+ if (!solution || typeof solution !== 'string') return false;
53
+ const values = solution.split(',');
54
+ if (values.length !== 64) return false;
55
+
56
+ const numericSeed = this._hashSeedToFloat(seed);
57
+ const r = 3.9999;
58
+
59
+ for (const idx of sampleIndices) {
60
+ if (idx < 0 || idx >= 64) return false;
61
+ let x = Math.fround(numericSeed + idx * 0.015);
62
+ const rFloat = Math.fround(r);
63
+ for (let i = 0; i < iterations; i++) {
64
+ x = Math.fround(rFloat * x * Math.fround(1.0 - x));
65
+ }
66
+ const clientVal = parseFloat(values[idx]);
67
+ if (isNaN(clientVal) || Math.abs(clientVal - x) > 1e-4) {
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ }
73
+
74
+ static _hashSeedToFloat(seed) {
75
+ let hash = 0;
76
+ for (let i = 0; i < seed.length; i++) {
77
+ hash = (hash << 5) - hash + seed.charCodeAt(i);
78
+ hash |= 0;
79
+ }
80
+ return Math.abs(hash % 1000000) / 1000000;
81
+ }
82
+
83
+ static async _solveWebGPU(seed, iterations) {
84
+ const adapter = await navigator.gpu.requestAdapter();
85
+ if (!adapter) throw new Error('No compatible GPU adapter found.');
86
+ const device = await adapter.requestDevice();
87
+
88
+ // Compute shader performing a chaotic logistic map iteration
89
+ const shaderCode = `
90
+ @group(0) @binding(0) var<storage, read_write> data: array<f32>;
91
+ @compute @workgroup_size(64)
92
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
93
+ let index = global_id.x;
94
+ if (index >= 64) { return; }
95
+
96
+ var x: f32 = data[index];
97
+ let r: f32 = 3.9999; // Chaotic regime
98
+
99
+ for (var i: u32 = 0u; i < ${iterations}u; i = i + 1u) {
100
+ x = r * x * (1.0 - x);
101
+ }
102
+ data[index] = x;
103
+ }
104
+ `;
105
+
106
+ const shaderModule = device.createShaderModule({ code: shaderCode });
107
+ const pipeline = device.createComputePipeline({
108
+ layout: 'auto',
109
+ compute: { module: shaderModule, entryPoint: 'main' }
110
+ });
111
+
112
+ const inputData = new Float32Array(64);
113
+ for (let i = 0; i < 64; i++) {
114
+ inputData[i] = seed + (i * 0.015);
115
+ }
116
+
117
+ const gpuBuffer = device.createBuffer({
118
+ size: inputData.byteLength,
119
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
120
+ mappedAtCreation: true
121
+ });
122
+ new Float32Array(gpuBuffer.getMappedRange()).set(inputData);
123
+ gpuBuffer.unmap();
124
+
125
+ const readBuffer = device.createBuffer({
126
+ size: inputData.byteLength,
127
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
128
+ });
129
+
130
+ const bindGroup = device.createBindGroup({
131
+ layout: pipeline.getBindGroupLayout(0),
132
+ entries: [{ binding: 0, resource: { buffer: gpuBuffer } }]
133
+ });
134
+
135
+ const commandEncoder = device.createCommandEncoder();
136
+ const passEncoder = commandEncoder.beginComputePass();
137
+ passEncoder.setPipeline(pipeline);
138
+ passEncoder.setBindGroup(0, bindGroup);
139
+ passEncoder.dispatchWorkgroups(1);
140
+ passEncoder.end();
141
+
142
+ commandEncoder.copyBufferToBuffer(gpuBuffer, 0, readBuffer, 0, inputData.byteLength);
143
+ device.queue.submit([commandEncoder.finish()]);
144
+
145
+ await readBuffer.mapAsync(GPUMapMode.READ);
146
+ const result = new Float32Array(readBuffer.getMappedRange());
147
+ const solutionHash = Array.from(result).map(v => v.toFixed(6)).join(',');
148
+ readBuffer.unmap();
149
+
150
+ return solutionHash;
151
+ }
152
+
153
+ static async _solveWebGL2(seed, iterations) {
154
+ const canvas = document.createElement('canvas');
155
+ canvas.width = 8;
156
+ canvas.height = 8; // 64 pixels total matching WebGPU size
157
+ const gl = canvas.getContext('webgl2');
158
+ if (!gl) throw new Error('WebGL2 context not supported.');
159
+
160
+ const vs = `#version 300 es\nin vec4 pos; void main() { gl_Position = pos; }`;
161
+ const fs = `#version 300 es
162
+ precision highp float;
163
+ out vec4 outColor;
164
+ uniform float uSeed;
165
+ uniform int uIterations;
166
+ void main() {
167
+ float index = gl_FragCoord.x + (gl_FragCoord.y * 8.0);
168
+ float x = uSeed + (index * 0.015);
169
+ float r = 3.9999;
170
+ for(int i = 0; i < uIterations; i++) {
171
+ x = r * x * (1.0 - x);
172
+ }
173
+ outColor = vec4(x, 0.0, 0.0, 1.0);
174
+ }`;
175
+
176
+ // Setup programs, draw fullscreen quad, etc.
177
+ const program = this._createProgram(gl, vs, fs);
178
+ gl.useProgram(program);
179
+
180
+ const posAttr = gl.getAttribLocation(program, 'pos');
181
+ const buffer = gl.createBuffer();
182
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
183
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
184
+ gl.enableVertexAttribArray(posAttr);
185
+ gl.vertexAttribPointer(posAttr, 2, gl.FLOAT, false, 0, 0);
186
+
187
+ gl.uniform1f(gl.getUniformLocation(program, 'uSeed'), seed);
188
+ gl.uniform1i(gl.getUniformLocation(program, 'uIterations'), iterations);
189
+
190
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
191
+
192
+ const pixels = new Float32Array(8 * 8 * 4);
193
+ gl.readPixels(0, 0, 8, 8, gl.RGBA, gl.FLOAT, pixels);
194
+
195
+ const result = [];
196
+ for (let i = 0; i < 64; i++) {
197
+ result.push(pixels[i * 4]);
198
+ }
199
+
200
+ return result.map(v => v.toFixed(6)).join(',');
201
+ }
202
+
203
+ static _createProgram(gl, vsSource, fsSource) {
204
+ const vs = gl.createShader(gl.VERTEX_SHADER);
205
+ gl.shaderSource(vs, vsSource);
206
+ gl.compileShader(vs);
207
+ const fs = gl.createShader(gl.FRAGMENT_SHADER);
208
+ gl.shaderSource(fs, fsSource);
209
+ gl.compileShader(fs);
210
+
211
+ const program = gl.createProgram();
212
+ gl.attachShader(program, vs);
213
+ gl.attachShader(program, fs);
214
+ gl.linkProgram(program);
215
+ return program;
216
+ }
217
+ }
@@ -28,7 +28,7 @@ global.TextEncoder = dom.window.TextEncoder;
28
28
  describe('ClientLibrary.initializeClient', () => {
29
29
 
30
30
  // On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
31
- let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
31
+ let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
32
32
 
33
33
  beforeEach(() => {
34
34
  // Réinitialiser l'état du client avant chaque test
@@ -39,6 +39,7 @@ describe('ClientLibrary.initializeClient', () => {
39
39
  startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
40
40
  startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
41
41
  startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
42
+ startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
42
43
  initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
43
44
  initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
44
45
  injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
@@ -58,16 +59,18 @@ describe('ClientLibrary.initializeClient', () => {
58
59
  expect(startKeystrokeSpy).toHaveBeenCalled();
59
60
  expect(startClickSpy).toHaveBeenCalled();
60
61
  expect(startTouchSpy).toHaveBeenCalled();
62
+ expect(startRenderingSpy).toHaveBeenCalled();
61
63
  expect(injectPhantomTrapsSpy).toHaveBeenCalled();
62
64
  });
63
65
 
64
66
  it('should disable trackers when configured', () => {
65
- ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false });
67
+ ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, rendering: false });
66
68
 
67
69
  expect(startMouseSpy).not.toHaveBeenCalled();
68
70
  expect(startKeystrokeSpy).not.toHaveBeenCalled();
69
71
  expect(startClickSpy).not.toHaveBeenCalled();
70
72
  expect(startTouchSpy).not.toHaveBeenCalled();
73
+ expect(startRenderingSpy).not.toHaveBeenCalled();
71
74
  });
72
75
 
73
76
  it('should initialize WASM if wasmPath is configured', () => {
@@ -33,6 +33,7 @@ const {
33
33
  getCompositeDeviceHash,
34
34
  } = fingerprint;
35
35
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
36
+ const { getRenderingAnomalyScore } = __internal;
36
37
  let { getBehaviorScore, getClickVarianceScore } = __internal;
37
38
  // Mock the entire dns module
38
39
  vi.mock('node:dns/promises');
@@ -1492,6 +1493,50 @@ describe('Fingerprint & PoW Security Suite', () => {
1492
1493
  });
1493
1494
  });
1494
1495
 
1496
+ describe('getRenderingAnomalyScore', () => {
1497
+ it('should return 0 if rendering metrics are missing', () => {
1498
+ const context = { headers: {} };
1499
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1500
+ expect(renderingAnomalyScore).toBe(0);
1501
+ });
1502
+
1503
+ it('should return 100 if offscreen canvas spoofing is detected', () => {
1504
+ const context = {
1505
+ headers: {
1506
+ 'x-behavior-metrics': JSON.stringify({
1507
+ rendering: { fps: 60, jitter: 0.1, offscreenAnom: true }
1508
+ })
1509
+ }
1510
+ };
1511
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1512
+ expect(renderingAnomalyScore).toBe(100);
1513
+ });
1514
+
1515
+ it('should return a high score if jitter is very high', () => {
1516
+ const context = {
1517
+ headers: {
1518
+ 'x-behavior-metrics': JSON.stringify({
1519
+ rendering: { fps: 60, jitter: 12.5, offscreenAnom: false }
1520
+ })
1521
+ }
1522
+ };
1523
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1524
+ expect(renderingAnomalyScore).toBe(65); // (12.5 - 6.0) * 10 = 65
1525
+ });
1526
+
1527
+ it('should return a high score if FPS is abnormal', () => {
1528
+ const context = {
1529
+ headers: {
1530
+ 'x-behavior-metrics': JSON.stringify({
1531
+ rendering: { fps: 300, jitter: 1.0, offscreenAnom: false }
1532
+ })
1533
+ }
1534
+ };
1535
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1536
+ expect(renderingAnomalyScore).toBe(50);
1537
+ });
1538
+ });
1539
+
1495
1540
  describe('getBehaviorScore', () => {
1496
1541
  // La fonction est privée, on la récupère via l'export __internal
1497
1542
  // FIX: Correctly assign the function before tests run.
@@ -1542,6 +1587,24 @@ describe('Fingerprint & PoW Security Suite', () => {
1542
1587
  });
1543
1588
  });
1544
1589
 
1590
+ it('should return a high score for keystroke dynamics with very low dwell/flight variance (bot emulation)', () => {
1591
+ const metrics = {
1592
+ honeypotInteraction: false,
1593
+ keystrokeLatency: 100.0,
1594
+ keystrokeDwellTimes: [50, 50, 50, 50, 50], // stdDev = 0
1595
+ keystrokeFlightTimes: [
1596
+ { digraph: 'ab', time: 100 },
1597
+ { digraph: 'bc', time: 100 },
1598
+ { digraph: 'cd', time: 100 },
1599
+ { digraph: 'de', time: 100 },
1600
+ { digraph: 'ef', time: 100 }
1601
+ ] // stdDev = 0
1602
+ };
1603
+ const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
1604
+ const { behaviorScore } = getBehaviorScore(context);
1605
+ expect(behaviorScore).toBeGreaterThanOrEqual(70); // 35 (stdDevDwell) + 35 (stdDevFlight) = 70
1606
+ });
1607
+
1545
1608
  describe('getClickVarianceScore', () => {
1546
1609
  // FIX: Correctly assign the function before tests run.
1547
1610
  beforeEach(() => {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import { GpuPowSolver } from '../gpu_pow.solver.js';
7
+
8
+ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
9
+ const seed = 'chaotic-pow-seed-test';
10
+ const iterations = 1000;
11
+
12
+ it('should hash seed deterministically to a float between 0 and 1', () => {
13
+ const f1 = GpuPowSolver._hashSeedToFloat(seed);
14
+ const f2 = GpuPowSolver._hashSeedToFloat(seed);
15
+ const f3 = GpuPowSolver._hashSeedToFloat('another-seed');
16
+
17
+ expect(f1).toBe(f2);
18
+ expect(f1).toBeGreaterThanOrEqual(0);
19
+ expect(f1).toBeLessThan(1);
20
+ expect(f1).not.toBe(f3);
21
+ });
22
+
23
+ it('should verify a mathematically correct trajectory solution', () => {
24
+ const numericSeed = GpuPowSolver._hashSeedToFloat(seed);
25
+ const r = 3.9999;
26
+ const rFloat = Math.fround(r);
27
+ const solutions = [];
28
+
29
+ for (let idx = 0; idx < 64; idx++) {
30
+ let x = Math.fround(numericSeed + idx * 0.015);
31
+ for (let i = 0; i < iterations; i++) {
32
+ x = Math.fround(rFloat * x * Math.fround(1.0 - x));
33
+ }
34
+ solutions.push(x.toFixed(6));
35
+ }
36
+
37
+ const validSolution = solutions.join(',');
38
+
39
+ // Verify the correct solution
40
+ expect(GpuPowSolver.verify(seed, iterations, validSolution)).toBe(true);
41
+ });
42
+
43
+ it('should reject invalid solutions', () => {
44
+ expect(GpuPowSolver.verify(seed, iterations, '')).toBe(false);
45
+ expect(GpuPowSolver.verify(seed, iterations, null)).toBe(false);
46
+ expect(GpuPowSolver.verify(seed, iterations, '1.0,2.0')).toBe(false); // Wrong length (should be 64)
47
+ });
48
+
49
+ it('should reject tampered solutions exceeding the threshold', () => {
50
+ const numericSeed = GpuPowSolver._hashSeedToFloat(seed);
51
+ const r = 3.9999;
52
+ const rFloat = Math.fround(r);
53
+ const solutions = [];
54
+
55
+ for (let idx = 0; idx < 64; idx++) {
56
+ let x = Math.fround(numericSeed + idx * 0.015);
57
+ for (let i = 0; i < iterations; i++) {
58
+ x = Math.fround(rFloat * x * Math.fround(1.0 - x));
59
+ }
60
+ solutions.push(x.toFixed(6));
61
+ }
62
+
63
+ // Tamper with the 12th channel (one of the default sample indices [0, 12, 35, 57])
64
+ const tamperedSolutions = [...solutions];
65
+ const originalVal = parseFloat(tamperedSolutions[12]);
66
+ tamperedSolutions[12] = (originalVal + 0.0002).toFixed(6); // exceeding 1e-4 tolerance
67
+
68
+ expect(GpuPowSolver.verify(seed, iterations, tamperedSolutions.join(','))).toBe(false);
69
+ });
70
+
71
+ it('should fallback to WebGL2 if WebGPU is not supported', async () => {
72
+ // Mock WebGPU as undefined
73
+ const originalGpu = global.navigator.gpu;
74
+ Object.defineProperty(global.navigator, 'gpu', {
75
+ value: undefined,
76
+ writable: true,
77
+ configurable: true
78
+ });
79
+
80
+ // Mock WebGL2 context
81
+ const mockGl = {
82
+ useProgram: vi.fn(),
83
+ createBuffer: vi.fn(),
84
+ bindBuffer: vi.fn(),
85
+ bufferData: vi.fn(),
86
+ enableVertexAttribArray: vi.fn(),
87
+ vertexAttribPointer: vi.fn(),
88
+ uniform1f: vi.fn(),
89
+ uniform1i: vi.fn(),
90
+ getUniformLocation: vi.fn(),
91
+ getAttribLocation: vi.fn(),
92
+ drawArrays: vi.fn(),
93
+ readPixels: vi.fn((x, y, w, h, format, type, pixels) => {
94
+ // Populate mock pixels with arbitrary valid floats
95
+ for (let i = 0; i < pixels.length; i++) {
96
+ pixels[i] = 0.5;
97
+ }
98
+ })
99
+ };
100
+
101
+ const mockCanvas = {
102
+ getContext: vi.fn().mockReturnValue(mockGl)
103
+ };
104
+
105
+ vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
106
+ if (tagName === 'canvas') return mockCanvas;
107
+ return {};
108
+ });
109
+
110
+ vi.spyOn(GpuPowSolver, '_createProgram').mockReturnValue({});
111
+
112
+ const result = await GpuPowSolver.solve(seed, 100);
113
+
114
+ expect(result.platform).toBe('webgl2');
115
+ expect(result.solution.split(',').length).toBe(64);
116
+ vi.restoreAllMocks();
117
+ });
118
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __internal } from '../fingerprint.js';
3
+
4
+ const { getQuicAnomalyScore } = __internal;
5
+
6
+ describe('QUIC / HTTP/3 Flow Profiling', () => {
7
+ it('should return 0.0 if no QUIC fingerprint is present', () => {
8
+ const context = { headers: {} };
9
+ const score = getQuicAnomalyScore(context);
10
+ expect(score.quicAnomalyScore).toBe(0.0);
11
+ });
12
+
13
+ it('should detect Chrome spoofed with non-default stream/priority values', () => {
14
+ // Chrome attend un initial_max_data (1) >= 1MB, initial_max_streams_bidi (4) == 100, et extensible Priority (contient u=)
15
+ const context = {
16
+ headers: {
17
+ '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',
18
+ 'x-quic-fp': '1;1=65536,4=50;i=0' // max_data trop bas, max_streams != 100, pas de priorité u=
19
+ }
20
+ };
21
+ const score = getQuicAnomalyScore(context);
22
+ expect(score.quicAnomalyScore).toBe(100.0);
23
+ });
24
+
25
+ it('should return 0.0 for a legitimate Chrome QUIC flow profile', () => {
26
+ const context = {
27
+ headers: {
28
+ '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',
29
+ 'x-quic-fp': '1;1=1572864,4=100;u=2,i'
30
+ }
31
+ };
32
+ const score = getQuicAnomalyScore(context);
33
+ expect(score.quicAnomalyScore).toBe(0.0);
34
+ });
35
+ });
@@ -14,6 +14,48 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
14
14
  */
15
15
  class ChallengeUtils
16
16
  {
17
+ private static function fround(float $value): float
18
+ {
19
+ return unpack('f', pack('f', $value))[1];
20
+ }
21
+
22
+ public static function hashSeedToFloat(string $seed): float
23
+ {
24
+ $hash = 0;
25
+ for ($i = 0; $i < strlen($seed); $i++) {
26
+ $hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
27
+ if ($hash & 0x80000000) {
28
+ $hash = $hash - 0x100000000;
29
+ }
30
+ }
31
+ return abs($hash % 1000000) / 1000000;
32
+ }
33
+
34
+ public static function verifyGpuPow(string $seed, int $iterations, string $solution, array $sampleIndices = [0, 12, 35, 57]): bool
35
+ {
36
+ $values = explode(',', $solution);
37
+ if (count($values) !== 64) {
38
+ return false;
39
+ }
40
+ $numericSeed = self::hashSeedToFloat($seed);
41
+ $r = 3.9999;
42
+ foreach ($sampleIndices as $idx) {
43
+ if ($idx < 0 || $idx >= 64) {
44
+ return false;
45
+ }
46
+ $x = self::fround($numericSeed + $idx * 0.015);
47
+ $rFloat = self::fround($r);
48
+ for ($i = 0; $i < $iterations; $i++) {
49
+ $x = self::fround($rFloat * $x * self::fround(1.0 - $x));
50
+ }
51
+ $clientVal = (float)$values[$idx];
52
+ if (abs($clientVal - $x) > 1e-4) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+
17
59
  private const TRAP_URL_TEMPLATES = [
18
60
  '/includes/config-{RANDOM}.php',
19
61
  '/.env.{RANDOM}',
@@ -41,6 +41,8 @@ class SecurityProfiles
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
43
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
44
+ 'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
45
+ 'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
44
46
 
45
47
  ],
46
48
  'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
@@ -87,6 +89,8 @@ class SecurityProfiles
87
89
  'subnetScore' => 0.7, // Poids plus élevé en mode strict
88
90
  'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
89
91
  'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
92
+ 'quicAnomalyScore' => 1.0, // NEW: Anomalie QUIC
93
+ 'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
90
94
 
91
95
  ],
92
96
  'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
@@ -133,6 +137,7 @@ class SecurityProfiles
133
137
  'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
134
138
  'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
135
139
  'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
140
+ 'quicAnomalyScore' => 0.8, // NEW: Anomalie QUIC
136
141
 
137
142
  ],
138
143
  'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
@@ -181,6 +186,8 @@ class SecurityProfiles
181
186
  'subnetScore' => 0.4, // Utile contre le spam de commentaires coordonné
182
187
  'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
183
188
  'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
189
+ 'quicAnomalyScore' => 0.5, // NEW: Anomalie QUIC
190
+ 'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
184
191
 
185
192
  ],
186
193
  'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
@@ -228,6 +235,8 @@ class SecurityProfiles
228
235
  'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
229
236
  'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
230
237
  'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
238
+ 'quicAnomalyScore' => 0.9, // NEW: Anomalie QUIC
239
+ 'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
231
240
 
232
241
  ],
233
242
  'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
@@ -61,6 +61,19 @@
61
61
  $this->validateConfig($securityConfig);
62
62
  }
63
63
 
64
+ /**
65
+ * Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
66
+ * sans nécessiter de redémarrage.
67
+ *
68
+ * @param array $newConfig La nouvelle configuration (partielle ou complète).
69
+ */
70
+ public function updateConfig(array $newConfig): void
71
+ {
72
+ $this->validateConfig($newConfig);
73
+ $this->securityConfig = SecurityProfiles::deepMerge($this->securityConfig, $newConfig);
74
+ $this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
75
+ }
76
+
64
77
  private function validateConfig(array $config): void
65
78
  {
66
79
  if (empty($config)) {
@@ -467,6 +480,12 @@
467
480
  // Score d'anomalie de pile TCP/IP
468
481
  $tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
469
482
 
483
+ // Score d'anomalie de flux QUIC/HTTP3
484
+ $quicAnomaly = RequestUtils::getQuicAnomalyScore($context);
485
+
486
+ // Score d'anomalie de rendu d'affichage (V-Sync)
487
+ $renderingAnomaly = RequestUtils::getRenderingAnomalyScore($context);
488
+
470
489
  // Assemblage du vecteur de suspicion final
471
490
  $suspicionVector = array_merge($suspicionVector, [
472
491
  'inconsistencyScore' => $inconsistencyScore,
@@ -486,6 +505,8 @@
486
505
  'subnetScore' => $subnetScore['subnetScore'],
487
506
  'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
488
507
  'tcpAnomalyScore' => $tcpAnomaly['tcpAnomalyScore'],
508
+ 'quicAnomalyScore' => $quicAnomaly['quicAnomalyScore'],
509
+ 'renderingAnomalyScore' => $renderingAnomaly['renderingAnomalyScore'],
489
510
  ]);
490
511
 
491
512
  // Sauvegarder l'état mis à jour de l'appareil dans le store
@@ -38,6 +38,7 @@ class RequestContext
38
38
  public ?string $ja4s = null;
39
39
  public ?string $ja4h = null;
40
40
  public ?string $http2Fingerprint = null;
41
+ public ?string $quicFingerprint = null;
41
42
  public ?string $tcpFingerprint = null;
42
43
 
43
44
  /**
@@ -77,6 +78,7 @@ class RequestContext
77
78
  $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
78
79
  $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
79
80
  $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
81
+ $this->quicFingerprint = $this->headers['x-quic-fp'] ?? null;
80
82
  $this->tlsSessionId = $this->headers['x-tls-session-id'] ?? $this->headers['x-ssl-session-id'] ?? null;
81
83
  }
82
84
  /**