@anonympins/fingerprint 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/js/build-client.js +67 -67
- package/src/js/fingerprint.client.js +1433 -1271
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +272 -55
- package/src/js/obfuscation.worker.js +23 -0
- package/src/js/pow.solver.inline.js +37 -28
- package/src/js/pow.solver.js +37 -28
- package/src/js/tests/cross_parity.test.js +175 -0
- package/src/js/tests/fingerprint.test.js +25 -0
- package/src/js/tests/obfuscation.test.js +48 -0
- package/src/php/AutoTuner.php +42 -0
- package/src/php/FingerprintEngine.php +1 -1
package/src/js/pow.solver.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
+
function secureRandom() {
|
|
13
|
+
if (typeof window !== 'undefined' && (window.crypto || window.msCrypto)) {
|
|
14
|
+
const array = new Uint32Array(1);
|
|
15
|
+
(window.crypto || window.msCrypto).getRandomValues(array);
|
|
16
|
+
return array[0] / 0x100000000;
|
|
17
|
+
}
|
|
18
|
+
return Math.random();
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function cyrb53(str, seed = 0) {
|
|
13
22
|
const safeStr = (typeof str === 'string' ? str : String(str || '')).slice(0, 10000);
|
|
14
23
|
let h1 = 0xdeadbeef ^ seed,
|
|
@@ -518,7 +527,7 @@ const ClientOptimizers = {
|
|
|
518
527
|
for (let i = 0; i < iterations; i++) {
|
|
519
528
|
const newSolution = neighbor(currentSolution);
|
|
520
529
|
const newEnergy = evaluator(newSolution);
|
|
521
|
-
if (newEnergy < currentEnergy ||
|
|
530
|
+
if (newEnergy < currentEnergy || secureRandom() < Math.exp((currentEnergy - newEnergy) / temperature)) {
|
|
522
531
|
currentSolution = newSolution;
|
|
523
532
|
currentEnergy = newEnergy;
|
|
524
533
|
}
|
|
@@ -537,10 +546,10 @@ const ClientOptimizers = {
|
|
|
537
546
|
population.sort((a, b) => a.fitness - b.fitness);
|
|
538
547
|
const newPopulation = [population[0]]; // Elitism
|
|
539
548
|
while (newPopulation.length < popSize) {
|
|
540
|
-
const p1 = population[Math.floor(
|
|
541
|
-
const p2 = population[Math.floor(
|
|
549
|
+
const p1 = population[Math.floor(secureRandom() * (popSize / 2))];
|
|
550
|
+
const p2 = population[Math.floor(secureRandom() * (popSize / 2))];
|
|
542
551
|
let offspring = crossover(p1.chromosome, p2.chromosome);
|
|
543
|
-
if (
|
|
552
|
+
if (secureRandom() < 0.1) offspring = mutate(offspring);
|
|
544
553
|
newPopulation.push({ chromosome: offspring, fitness: fitness(offspring) });
|
|
545
554
|
}
|
|
546
555
|
population = newPopulation;
|
|
@@ -561,10 +570,10 @@ const ClientOptimizers = {
|
|
|
561
570
|
for (let gen = 0; gen < generations; gen++) {
|
|
562
571
|
const offspring = [];
|
|
563
572
|
for (let i = 0; i < populationSize; i++) {
|
|
564
|
-
const parent1 = population[Math.floor(
|
|
565
|
-
const parent2 = population[Math.floor(
|
|
573
|
+
const parent1 = population[Math.floor(secureRandom() * population.length)];
|
|
574
|
+
const parent2 = population[Math.floor(secureRandom() * population.length)];
|
|
566
575
|
let childIndividual = crossover(parent1.individual, parent2.individual);
|
|
567
|
-
if (
|
|
576
|
+
if (secureRandom() < mutationRate) {
|
|
568
577
|
childIndividual = mutate(childIndividual);
|
|
569
578
|
}
|
|
570
579
|
const child = { individual: childIndividual };
|
|
@@ -632,9 +641,9 @@ const ClientOptimizers = {
|
|
|
632
641
|
return [-platformRevenue, -advertiserValue, marketImbalance];
|
|
633
642
|
};
|
|
634
643
|
|
|
635
|
-
const createIndividual = () => 0.5 +
|
|
644
|
+
const createIndividual = () => 0.5 + secureRandom() * 4.5;
|
|
636
645
|
const crossover = (cpc1, cpc2) => (cpc1 + cpc2) / 2;
|
|
637
|
-
const mutate = (cpc) => Math.max(0.1, cpc + (
|
|
646
|
+
const mutate = (cpc) => Math.max(0.1, cpc + (secureRandom() - 0.5) * 0.5);
|
|
638
647
|
|
|
639
648
|
const gaOptions = {
|
|
640
649
|
generations: 50,
|
|
@@ -738,10 +747,10 @@ const ClientOptimizers = {
|
|
|
738
747
|
};
|
|
739
748
|
|
|
740
749
|
const createIndividual = () => [
|
|
741
|
-
100 +
|
|
742
|
-
1 +
|
|
743
|
-
|
|
744
|
-
Math.floor(
|
|
750
|
+
100 + secureRandom() * 4900,
|
|
751
|
+
1 + secureRandom() * 9999,
|
|
752
|
+
secureRandom() * 0.5,
|
|
753
|
+
Math.floor(secureRandom() * 10)
|
|
745
754
|
];
|
|
746
755
|
|
|
747
756
|
const crossover = (s1, s2) => [
|
|
@@ -755,7 +764,7 @@ const ClientOptimizers = {
|
|
|
755
764
|
const newSolution = [...solution];
|
|
756
765
|
const i = Math.floor(Math.random() * 4);
|
|
757
766
|
const mutationFactors = [500, 1000, 0.1, 2];
|
|
758
|
-
newSolution[i] += (
|
|
767
|
+
newSolution[i] += (secureRandom() - 0.5) * mutationFactors[i];
|
|
759
768
|
return newSolution;
|
|
760
769
|
};
|
|
761
770
|
|
|
@@ -809,16 +818,16 @@ async function solveUsefulWorkTask(task) {
|
|
|
809
818
|
};
|
|
810
819
|
const neighbor = (facilities) => {
|
|
811
820
|
const newFacilities = facilities.map((f) => ({ ...f }));
|
|
812
|
-
const i = Math.floor(
|
|
813
|
-
const moveX = (
|
|
814
|
-
const moveY = (
|
|
821
|
+
const i = Math.floor(secureRandom() * numFacilities);
|
|
822
|
+
const moveX = (secureRandom() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
|
|
823
|
+
const moveY = (secureRandom() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
|
|
815
824
|
newFacilities[i].x = Math.max(bounds.minX, Math.min(bounds.maxX, newFacilities[i].x + moveX));
|
|
816
825
|
newFacilities[i].y = Math.max(bounds.minY, Math.min(bounds.maxY, newFacilities[i].y + moveY));
|
|
817
826
|
return newFacilities;
|
|
818
827
|
};
|
|
819
828
|
const initialSolution = task.initialSolution || Array.from({ length: numFacilities }, () => ({
|
|
820
|
-
x: bounds.minX +
|
|
821
|
-
y: bounds.minY +
|
|
829
|
+
x: bounds.minX + secureRandom() * (bounds.maxX - bounds.minX),
|
|
830
|
+
y: bounds.minY + secureRandom() * (bounds.maxY - bounds.minY),
|
|
822
831
|
}));
|
|
823
832
|
return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
|
|
824
833
|
}
|
|
@@ -833,11 +842,11 @@ async function solveUsefulWorkTask(task) {
|
|
|
833
842
|
};
|
|
834
843
|
const neighbor = (path) => {
|
|
835
844
|
const newPath = [...path];
|
|
836
|
-
const [i, j] = [Math.floor(
|
|
845
|
+
const [i, j] = [Math.floor(secureRandom() * path.length), Math.floor(secureRandom() * path.length)];
|
|
837
846
|
[newPath[i], newPath[j]] = [newPath[j], newPath[i]];
|
|
838
847
|
return newPath;
|
|
839
848
|
};
|
|
840
|
-
const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 -
|
|
849
|
+
const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 - secureRandom());
|
|
841
850
|
|
|
842
851
|
return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
|
|
843
852
|
}
|
|
@@ -853,9 +862,9 @@ async function solveUsefulWorkTask(task) {
|
|
|
853
862
|
if (vol > maxVolatility) return 1000 + (vol - maxVolatility);
|
|
854
863
|
return -ret;
|
|
855
864
|
};
|
|
856
|
-
const createIndividual = () => Array.from({ length: assets.length },
|
|
865
|
+
const createIndividual = () => Array.from({ length: assets.length }, secureRandom);
|
|
857
866
|
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
858
|
-
const mutate = p => { const n = [...p], i = Math.floor(
|
|
867
|
+
const mutate = p => { const n = [...p], i = Math.floor(secureRandom() * n.length); n[i] += (secureRandom() - 0.5) * 0.2; return n.map(v => Math.max(0, v)); };
|
|
859
868
|
|
|
860
869
|
// Le client doit recréer la population si elle n'est pas fournie
|
|
861
870
|
const initialPopulation = task.initialPopulation || Array.from({ length: task.payload.options.populationSize }, () => ({ chromosome: createIndividual(), fitness: 0 }));
|
|
@@ -907,8 +916,8 @@ export async function solveOptimizationTask(initialPopulation, generations) {
|
|
|
907
916
|
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
908
917
|
const mutate = (p) => {
|
|
909
918
|
const newP = [...p];
|
|
910
|
-
const i = Math.floor(
|
|
911
|
-
newP[i] += (
|
|
919
|
+
const i = Math.floor(secureRandom() * newP.length);
|
|
920
|
+
newP[i] += (secureRandom() - 0.5) * 0.2;
|
|
912
921
|
return newP;
|
|
913
922
|
};
|
|
914
923
|
|
|
@@ -920,10 +929,10 @@ export async function solveOptimizationTask(initialPopulation, generations) {
|
|
|
920
929
|
const newPopulation = [...parents]; // Élitisme
|
|
921
930
|
|
|
922
931
|
while (newPopulation.length < population.length) {
|
|
923
|
-
const parent1 = parents[Math.floor(
|
|
924
|
-
const parent2 = parents[Math.floor(
|
|
932
|
+
const parent1 = parents[Math.floor(secureRandom() * parents.length)];
|
|
933
|
+
const parent2 = parents[Math.floor(secureRandom() * parents.length)];
|
|
925
934
|
let offspring = crossover(parent1.chromosome, parent2.chromosome);
|
|
926
|
-
if (
|
|
935
|
+
if (secureRandom() < 0.1) offspring = mutate(offspring);
|
|
927
936
|
// La fitness sera recalculée côté serveur pour la vérification.
|
|
928
937
|
newPopulation.push({ chromosome: offspring, fitness: -1 });
|
|
929
938
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it, beforeAll } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const fpJsPath = path.resolve(__dirname, '../../../public/fp.js');
|
|
9
|
+
const fpWasmPath = path.resolve(__dirname, '../../../public/fp.wasm');
|
|
10
|
+
|
|
11
|
+
describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', () => {
|
|
12
|
+
let wasmModule = null;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// Essai de compilation WASM à la volée s'il y a em++
|
|
16
|
+
try {
|
|
17
|
+
execSync('node src/js/build-client.js', { stdio: 'ignore' });
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Ignorer si Emscripten n'est pas présent dans l'env
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Instancier le WASM réel si compilé
|
|
23
|
+
if (fs.existsSync(fpJsPath) && fs.existsSync(fpWasmPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const module = await import(fpJsPath);
|
|
26
|
+
const createFingerprintModule = module.default || module.createFingerprintModule;
|
|
27
|
+
if (typeof createFingerprintModule === 'function') {
|
|
28
|
+
wasmModule = await createFingerprintModule({
|
|
29
|
+
locateFile: () => fpWasmPath
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
try {
|
|
34
|
+
const require = (await import('module')).createRequire(import.meta.url);
|
|
35
|
+
const createFingerprintModule = require(fpJsPath);
|
|
36
|
+
wasmModule = await createFingerprintModule({
|
|
37
|
+
locateFile: () => fpWasmPath
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.warn('WASM could not be initialized in test:', err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function jsHashSeedToFloat(seed) {
|
|
47
|
+
let hash = 0;
|
|
48
|
+
for (let i = 0; i < seed.length; i++) {
|
|
49
|
+
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
50
|
+
hash |= 0;
|
|
51
|
+
}
|
|
52
|
+
return Math.abs(hash % 1000000) / 1000000;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function jsGenerateTrajectory(seed, iterations) {
|
|
56
|
+
const numericSeed = jsHashSeedToFloat(seed);
|
|
57
|
+
const r = 3.9999;
|
|
58
|
+
const rFloat = Math.fround(r);
|
|
59
|
+
const output = new Float32Array(64);
|
|
60
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
61
|
+
let x = Math.fround(numericSeed + idx * 0.015);
|
|
62
|
+
for (let i = 0; i < iterations; i++) {
|
|
63
|
+
x = Math.fround(rFloat * x * Math.fround(1.0 - x));
|
|
64
|
+
}
|
|
65
|
+
output[idx] = x;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runPhpTrajectory(seed, iterations) {
|
|
71
|
+
const phpCode = `<?php
|
|
72
|
+
error_reporting(E_ALL);
|
|
73
|
+
ini_set('display_errors', '1');
|
|
74
|
+
|
|
75
|
+
\$seed = ${JSON.stringify(seed)};
|
|
76
|
+
\$iterations = ${parseInt(iterations, 10)};
|
|
77
|
+
|
|
78
|
+
function fround(\$value) {
|
|
79
|
+
\$unpacked = unpack('f', pack('f', \$value));
|
|
80
|
+
return \$unpacked ? \$unpacked[1] : \$value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hashSeedToFloat(\$seed) {
|
|
84
|
+
\$hash = 0;
|
|
85
|
+
\$len = strlen(\$seed);
|
|
86
|
+
for (\$i = 0; \$i < \$len; \$i++) {
|
|
87
|
+
\$hash = ((\$hash << 5) - \$hash + ord(\$seed[\$i])) & 0xffffffff;
|
|
88
|
+
if (\$hash & 0x80000000) {
|
|
89
|
+
\$hash = \$hash - 0x100000000;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return abs(\$hash % 1000000) / 1000000;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
\$numericSeed = hashSeedToFloat(\$seed);
|
|
96
|
+
\$r = 3.9999;
|
|
97
|
+
\$solutions = [];
|
|
98
|
+
for (\$idx = 0; \$idx < 64; \$idx++) {
|
|
99
|
+
\$x = fround(\$numericSeed + \$idx * 0.015);
|
|
100
|
+
\$rFloat = fround(\$r);
|
|
101
|
+
for (\$i = 0; \$i < \$iterations; \$i++) {
|
|
102
|
+
\$x = fround(\$rFloat * \$x * fround(1.0 - \$x));
|
|
103
|
+
}
|
|
104
|
+
\$solutions[] = \$x;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
\$sanitized = [];
|
|
108
|
+
foreach (\$solutions as \$val) {
|
|
109
|
+
if (is_nan(\$val)) {
|
|
110
|
+
\$sanitized[] = "NaN";
|
|
111
|
+
} elseif (is_infinite(\$val)) {
|
|
112
|
+
\$sanitized[] = \$val > 0 ? "Infinity" : "-Infinity";
|
|
113
|
+
} else {
|
|
114
|
+
\$sanitized[] = \$val;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
echo json_encode(\$sanitized);
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const tempFile = path.resolve(__dirname, 'temp_trajectory.php').replace(/\\/g, '/');
|
|
122
|
+
fs.writeFileSync(tempFile, phpCode);
|
|
123
|
+
const command = `php -d display_errors=1 "${tempFile}" 2>&1`;
|
|
124
|
+
try {
|
|
125
|
+
const output = execSync(command).toString();
|
|
126
|
+
const parsed = JSON.parse(output.trim());
|
|
127
|
+
return parsed.map(v => {
|
|
128
|
+
if (v === "NaN") return NaN;
|
|
129
|
+
if (v === "Infinity") return Infinity;
|
|
130
|
+
if (v === "-Infinity") return -Infinity;
|
|
131
|
+
return v;
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
if (fs.existsSync(tempFile)) {
|
|
135
|
+
fs.unlinkSync(tempFile);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
|
|
141
|
+
const iterations = 100;
|
|
142
|
+
for (let i = 0; i < 50; i++) {
|
|
143
|
+
const seed = `seed_parity_val_${i}`;
|
|
144
|
+
const jsTrajectory = jsGenerateTrajectory(seed, iterations);
|
|
145
|
+
const phpTrajectory = runPhpTrajectory(seed, iterations);
|
|
146
|
+
|
|
147
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
148
|
+
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
149
|
+
const phpVal = Math.fround(phpTrajectory[idx]);
|
|
150
|
+
expect(jsVal).toBe(phpVal);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
|
|
154
|
+
const outputPtr = wasmModule._malloc(256);
|
|
155
|
+
const seedPtr = wasmModule._malloc(seed.length + 1);
|
|
156
|
+
for (let s = 0; s < seed.length; s++) {
|
|
157
|
+
wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s);
|
|
158
|
+
}
|
|
159
|
+
wasmModule.HEAP8[seedPtr + seed.length] = 0;
|
|
160
|
+
|
|
161
|
+
wasmModule._generate_gpu_pow_trajectory(seedPtr, iterations, outputPtr);
|
|
162
|
+
|
|
163
|
+
const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
|
|
164
|
+
for (let idx = 0; idx < 64; idx++) {
|
|
165
|
+
const jsVal = Math.fround(jsTrajectory[idx]);
|
|
166
|
+
const wasmVal = Math.fround(wasmOutput[idx]);
|
|
167
|
+
expect(jsVal).toBe(wasmVal);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
wasmModule._free(outputPtr);
|
|
171
|
+
wasmModule._free(seedPtr);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, 20000);
|
|
175
|
+
});
|
|
@@ -2075,6 +2075,31 @@ describe('getTimeInconsistencyScore', () => {
|
|
|
2075
2075
|
expect(timeInconsistencyScore).toBe(0);
|
|
2076
2076
|
});
|
|
2077
2077
|
|
|
2078
|
+
it('should return 100 if the signature is invalid or missing when sessionHmacKey is negotiated', () => {
|
|
2079
|
+
const requestTimestamp = Date.now();
|
|
2080
|
+
const clientTimestamp = requestTimestamp - 100;
|
|
2081
|
+
const context = { requestTimestamp };
|
|
2082
|
+
const metrics = { clientTimestamp, signature: 'bad-signature' };
|
|
2083
|
+
const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
|
|
2084
|
+
|
|
2085
|
+
const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
|
|
2086
|
+
expect(timeInconsistencyScore).toBe(100);
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
it('should return 0 if the signature is valid when sessionHmacKey is negotiated', () => {
|
|
2090
|
+
const requestTimestamp = Date.now();
|
|
2091
|
+
const clientTimestamp = requestTimestamp - 100;
|
|
2092
|
+
const context = { requestTimestamp };
|
|
2093
|
+
const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
|
|
2094
|
+
const metrics = { clientTimestamp };
|
|
2095
|
+
const dataToSign = JSON.stringify(metrics);
|
|
2096
|
+
const signature = createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex')).update(dataToSign).digest('hex');
|
|
2097
|
+
metrics.signature = signature;
|
|
2098
|
+
|
|
2099
|
+
const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
|
|
2100
|
+
expect(timeInconsistencyScore).toBe(0);
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2078
2103
|
it('should return a score > 0 for a replayed request (clientTimestamp significantly before requestTimestamp)', () => {
|
|
2079
2104
|
const requestTimestamp = Date.now();
|
|
2080
2105
|
const clientTimestamp = requestTimestamp - (REPLAY_THRESHOLD_MS + 1000); // 1 second beyond threshold
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __internal } from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
|
|
5
|
+
|
|
6
|
+
it('should generate a valid session mapping with randomized headers and globals', () => {
|
|
7
|
+
const mapping1 = __internal.generateSessionMapping();
|
|
8
|
+
const mapping2 = __internal.generateSessionMapping();
|
|
9
|
+
|
|
10
|
+
// Vérification de la présence des structures attendues
|
|
11
|
+
expect(mapping1).toBeDefined();
|
|
12
|
+
expect(mapping1.headers).toHaveProperty('x-device-fingerprint');
|
|
13
|
+
expect(mapping1.headers).toHaveProperty('x-behavior-metrics');
|
|
14
|
+
expect(mapping1.globals).toHaveProperty('ClientLibrary');
|
|
15
|
+
expect(mapping1.globals).toHaveProperty('getDeviceFingerprint');
|
|
16
|
+
expect(mapping1.globals).toHaveProperty('getClientBehaviorMetrics');
|
|
17
|
+
expect(mapping1.keys).toHaveProperty('ua');
|
|
18
|
+
expect(mapping1.wasmConstants).toHaveProperty('seed');
|
|
19
|
+
|
|
20
|
+
// Vérification de l'aléa (deux sessions consécutives doivent avoir des mappings uniques)
|
|
21
|
+
expect(mapping1.headers['x-device-fingerprint']).not.toBe(mapping2.headers['x-device-fingerprint']);
|
|
22
|
+
expect(mapping1.globals['ClientLibrary']).not.toBe(mapping2.globals['ClientLibrary']);
|
|
23
|
+
expect(mapping1.keys['ua']).not.toBe(mapping2.keys['ua']);
|
|
24
|
+
expect(mapping1.wasmConstants.seed).not.toBe(mapping2.wasmConstants.seed);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should compile and obfuscate client script asynchronously using the obfuscation worker', async () => {
|
|
28
|
+
const mapping = __internal.generateSessionMapping();
|
|
29
|
+
|
|
30
|
+
// On lance la compilation lourde qui instancie le Worker d'arrière-plan
|
|
31
|
+
const obfuscatedCode = await __internal.compilePolymorphicJs(mapping);
|
|
32
|
+
|
|
33
|
+
expect(obfuscatedCode).toBeDefined();
|
|
34
|
+
expect(typeof obfuscatedCode).toBe('string');
|
|
35
|
+
expect(obfuscatedCode.length).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
// Vérification que les placeholders originaux ont bien été purgés du code client final
|
|
38
|
+
expect(obfuscatedCode).not.toContain('X-Device-Fingerprint');
|
|
39
|
+
expect(obfuscatedCode).not.toContain('X-Behavior-Metrics');
|
|
40
|
+
expect(obfuscatedCode).not.toContain('ClientLibrary');
|
|
41
|
+
|
|
42
|
+
// Vérification que le code est minifié/obfusqué (ne contient pas de commentaires de développement par exemple)
|
|
43
|
+
expect(obfuscatedCode).not.toContain('// Nom trompeur aléatoire pour attirer les analyseurs');
|
|
44
|
+
|
|
45
|
+
// La structure globale de javascript-obfuscator génère typiquement un tableau de chaînes au début
|
|
46
|
+
expect(obfuscatedCode).toMatch(/_0x/);
|
|
47
|
+
}, 15000); // Augmentation du timeout à 15s car l'obfuscation via Worker thread consomme du CPU
|
|
48
|
+
});
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -30,6 +30,7 @@ class AutoTuner
|
|
|
30
30
|
/** @var ?callable */
|
|
31
31
|
private $onCleanup;
|
|
32
32
|
private ?string $savePath;
|
|
33
|
+
private float $validationTolerance;
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
|
|
@@ -51,6 +52,7 @@ class AutoTuner
|
|
|
51
52
|
$this->clearAfterTuning = $options['clearAfterTuning'] ?? false;
|
|
52
53
|
$this->onCleanup = $options['onCleanup'] ?? null;
|
|
53
54
|
$this->savePath = $options['savePath'] ?? null;
|
|
55
|
+
$this->validationTolerance = $options['validationTolerance'] ?? 0.15;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -211,6 +213,46 @@ class AutoTuner
|
|
|
211
213
|
}
|
|
212
214
|
};
|
|
213
215
|
|
|
216
|
+
// --- VALIDATION POST-CALCUL (PHP Rollback & Seuil de Tolérance) ---
|
|
217
|
+
$tempConfig = [
|
|
218
|
+
'thresholds' => $this->securityConfig['thresholds'],
|
|
219
|
+
'weights' => $this->securityConfig['weights'],
|
|
220
|
+
'patterns' => $this->securityConfig['patterns'],
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
$applyInertialUpdate($tempConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
|
|
224
|
+
$applyInertialUpdate($tempConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
|
|
225
|
+
$applyInertialUpdate($tempConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
|
|
226
|
+
|
|
227
|
+
$evaluator = OptimizationOperators::createFullSecurityConfigEvaluator(['trafficData' => $sanitizedData]);
|
|
228
|
+
$currentObjectives = $evaluator($this->securityConfig);
|
|
229
|
+
$proposedObjectives = $evaluator($tempConfig);
|
|
230
|
+
|
|
231
|
+
$currentFPR = $currentObjectives[0];
|
|
232
|
+
$currentFNR = $currentObjectives[1];
|
|
233
|
+
$proposedFPR = $proposedObjectives[0];
|
|
234
|
+
$proposedFNR = $proposedObjectives[1];
|
|
235
|
+
|
|
236
|
+
$validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
|
|
237
|
+
|
|
238
|
+
if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
|
|
239
|
+
error_log(sprintf(
|
|
240
|
+
"[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
|
|
241
|
+
$proposedFPR, $currentFPR, $proposedFNR, $currentFNR
|
|
242
|
+
));
|
|
243
|
+
if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger'])) {
|
|
244
|
+
call_user_func($this->securityConfig['logger'], [
|
|
245
|
+
'type' => 'autotuning_instability_alert',
|
|
246
|
+
'proposedFPR' => $proposedFPR,
|
|
247
|
+
'currentFPR' => $currentFPR,
|
|
248
|
+
'proposedFNR' => $proposedFNR,
|
|
249
|
+
'currentFNR' => $currentFNR,
|
|
250
|
+
'timestamp' => (int)(microtime(true) * 1000)
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
return; // Rollback
|
|
254
|
+
}
|
|
255
|
+
|
|
214
256
|
$applyInertialUpdate($this->securityConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
|
|
215
257
|
$applyInertialUpdate($this->securityConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
|
|
216
258
|
$applyInertialUpdate($this->securityConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
|
|
@@ -1153,7 +1153,7 @@
|
|
|
1153
1153
|
}
|
|
1154
1154
|
|
|
1155
1155
|
$highThreshold = $thresholds['high'] ?? 75;
|
|
1156
|
-
$mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket;
|
|
1156
|
+
$mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket && $finalScore > 0;
|
|
1157
1157
|
|
|
1158
1158
|
$lowThreshold = $thresholds['low'] ?? 20;
|
|
1159
1159
|
if (($finalScore >= $lowThreshold && !$hasValidTicket) || $mustReChallenge) {
|