@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.
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +221 -23
- package/src/js/fingerprint.js +148 -9
- package/src/js/gpu_pow.solver.js +217 -0
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.test.js +63 -0
- package/src/js/tests/gpu_pow.test.js +118 -0
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +42 -0
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +21 -0
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +77 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/RequestUtils.php +120 -0
|
@@ -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
|
/**
|