@anonympins/fingerprint 0.2.0 → 0.2.2
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/README.md +60 -4
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +11 -22
- package/fingerprint.js +324 -355
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +89 -33
- package/pow.worker.js +27 -0
- package/problem-manager.js +247 -0
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
package/fingerprint.js
CHANGED
|
@@ -27,111 +27,12 @@ const getPowSecret = () => {
|
|
|
27
27
|
* @returns {string} The solver JavaScript code.
|
|
28
28
|
*/
|
|
29
29
|
const getPowSolverCode = () => {
|
|
30
|
-
try
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
|
|
37
|
-
// Fallback inline code if file cannot be loaded
|
|
38
|
-
return `(function(global){
|
|
39
|
-
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
40
|
-
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
41
|
-
let cpuSolution = 0;
|
|
42
|
-
const ipPart = clientIp || '';
|
|
43
|
-
while(true){
|
|
44
|
-
// When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
|
|
45
|
-
const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
|
|
46
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
47
|
-
const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
48
|
-
if(BigInt('0x'+hashHex) < cpuTarget) break;
|
|
49
|
-
cpuSolution++;
|
|
50
|
-
if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
|
|
51
|
-
}
|
|
52
|
-
return cpuSolution;
|
|
53
|
-
}
|
|
54
|
-
async function solveMemory(seed, difficulty){
|
|
55
|
-
const size = difficulty * 1024 * 1024;
|
|
56
|
-
const buffer = new Uint32Array(size / 4);
|
|
57
|
-
let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
|
|
58
|
-
for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
|
|
59
|
-
let solution = 0;
|
|
60
|
-
const iterations = size / 16;
|
|
61
|
-
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
62
|
-
for(let i=0;i<iterations;i++){
|
|
63
|
-
addr = buffer[addr] % buffer.length;
|
|
64
|
-
solution ^= addr;
|
|
65
|
-
}
|
|
66
|
-
return solution;
|
|
67
|
-
}
|
|
68
|
-
async function solveTsp(cities, targetMaxDistance){
|
|
69
|
-
function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
|
|
70
|
-
function evaluatePathDistance(cities,path){
|
|
71
|
-
let total=0;
|
|
72
|
-
for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
|
|
73
|
-
total+=distance(cities[path[path.length-1]],cities[path[0]]);
|
|
74
|
-
return total;
|
|
75
|
-
}
|
|
76
|
-
function solveTspNearestNeighbor(cities){
|
|
77
|
-
const n=cities.length;
|
|
78
|
-
if(n===0)return[];
|
|
79
|
-
let path=[0];
|
|
80
|
-
let visited=new Array(n).fill(false);
|
|
81
|
-
visited[0]=true;
|
|
82
|
-
for(let i=1;i<n;i++){
|
|
83
|
-
let nearest=-1, minDist=Infinity;
|
|
84
|
-
for(let j=0;j<n;j++){
|
|
85
|
-
if(!visited[j]){
|
|
86
|
-
const d=distance(cities[path[i-1]],cities[j]);
|
|
87
|
-
if(d<minDist){minDist=d;nearest=j;}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
path.push(nearest);
|
|
91
|
-
visited[nearest]=true;
|
|
92
|
-
}
|
|
93
|
-
return path;
|
|
94
|
-
}
|
|
95
|
-
await new Promise(r=>setTimeout(r,10));
|
|
96
|
-
const solutionPath=solveTspNearestNeighbor(cities);
|
|
97
|
-
const solutionDistance=evaluatePathDistance(cities,solutionPath);
|
|
98
|
-
return{path:solutionPath,distance:solutionDistance};
|
|
99
|
-
}
|
|
100
|
-
async function solveChallenge(challenge) {
|
|
101
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
102
|
-
const solutions = {};
|
|
103
|
-
|
|
104
|
-
switch (type) {
|
|
105
|
-
case 'cpu_target':
|
|
106
|
-
solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
|
|
107
|
-
break;
|
|
108
|
-
case 'cpu_mem':
|
|
109
|
-
case 'cpu_mem_inline':
|
|
110
|
-
const memSeed = nonce + ":" + clientSecret;
|
|
111
|
-
const [cpuSol, memSol] = await Promise.all([
|
|
112
|
-
solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
|
|
113
|
-
solveMemory(memSeed, memDifficulty)
|
|
114
|
-
]);
|
|
115
|
-
solutions.cpu = cpuSol;
|
|
116
|
-
solutions.mem = memSol;
|
|
117
|
-
break;
|
|
118
|
-
case 'tsp':
|
|
119
|
-
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
120
|
-
solutions.tsp = tspResult.path;
|
|
121
|
-
solutions.distance = tspResult.distance;
|
|
122
|
-
break;
|
|
123
|
-
default:
|
|
124
|
-
throw new Error(\`Unknown challenge type: \${type}\`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return solutions;
|
|
128
|
-
}
|
|
129
|
-
global.solveCpuChallengeInline=solveCpuTargetInline;
|
|
130
|
-
global.solveMemoryChallenge=solveMemory;
|
|
131
|
-
global.solveTspChallenge=solveTsp;
|
|
132
|
-
global.solveChallenge=solveChallenge;
|
|
133
|
-
})(typeof window!=='undefined'?window:global);`;
|
|
134
|
-
}
|
|
30
|
+
// On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
|
|
31
|
+
// ce qui est préférable à servir un code de secours potentiellement désynchronisé.
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
|
|
35
|
+
return readFileSync(solverPath, 'utf-8');
|
|
135
36
|
};
|
|
136
37
|
|
|
137
38
|
/**
|
|
@@ -190,7 +91,7 @@ function getHeaderSignature(context) {
|
|
|
190
91
|
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
191
92
|
headerKeys.push(context.rawHeaders[i]);
|
|
192
93
|
}
|
|
193
|
-
return cyrb53(headerKeys.join(','));
|
|
94
|
+
return cyrb53(headerKeys.sort().join(','));
|
|
194
95
|
}
|
|
195
96
|
|
|
196
97
|
/**
|
|
@@ -227,11 +128,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
227
128
|
const ua = context.headers["user-agent"];
|
|
228
129
|
if (ua) {
|
|
229
130
|
srv.add("ua", ua);
|
|
230
|
-
// Extraire des infos supplémentaires du UA
|
|
231
|
-
const uaParts = parseUserAgent(ua);
|
|
232
|
-
if (uaParts.browser) srv.add("browser", uaParts.browser);
|
|
233
|
-
if (uaParts.os) srv.add("os_version", uaParts.os);
|
|
234
|
-
if (uaParts.device) srv.add("device_type", uaParts.device);
|
|
235
131
|
}
|
|
236
132
|
|
|
237
133
|
// 2. SIGNAL FORT: JA3 TLS Fingerprint
|
|
@@ -266,9 +162,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
266
162
|
srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
|
|
267
163
|
}
|
|
268
164
|
|
|
269
|
-
// 10. SIGNAL FORT: Ordonnancement des headers
|
|
270
|
-
srv.add("h_ord", getHeaderSignature(context));
|
|
271
|
-
|
|
272
165
|
// 11. SIGNAL AVANCÉ: Cookies (si disponible)
|
|
273
166
|
if (context.cookies) {
|
|
274
167
|
const cookieKeys = Object.keys(context.cookies).sort().join(',');
|
|
@@ -614,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
614
507
|
* Verifies a memory PoW solution.
|
|
615
508
|
* The server performs the same calculation to validate.
|
|
616
509
|
*/
|
|
617
|
-
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
|
|
510
|
+
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
618
511
|
const size = difficulty * 1024 * 1024;
|
|
619
512
|
const iterations = size / 16;
|
|
620
513
|
const buffer = new Uint32Array(size / 4);
|
|
621
|
-
const seed =
|
|
514
|
+
const seed = `:${nonce}:${clientSecret}`;
|
|
622
515
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
623
516
|
|
|
624
517
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -929,113 +822,86 @@ function getCrossLayerInconsistency(context) {
|
|
|
929
822
|
function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
930
823
|
if (!deviceData) return { requestPatternScore: 0 };
|
|
931
824
|
|
|
932
|
-
//
|
|
825
|
+
// (NOUVEAU) Logique de détection de pattern simplifiée et unifiée.
|
|
933
826
|
const {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
//
|
|
941
|
-
benfordMinSamples = 15, benfordWeight = 50,
|
|
942
|
-
// Nouveau paramètre pour la détection de séquences
|
|
943
|
-
sequenceLength = 3, sequenceWeight = 60
|
|
827
|
+
historySize = 20, // Nombre de requêtes à conserver pour l'analyse.
|
|
828
|
+
minSamples = 10, // Nombre d'intervalles de temps à analyser avant de calculer.
|
|
829
|
+
regularityThreshold = 150, // Écart-type (ms) en dessous duquel le comportement est "trop régulier".
|
|
830
|
+
benfordThreshold = 0.15, // Seuil de déviation de Benford au-dessus duquel la distribution est "non naturelle".
|
|
831
|
+
patternWeight = 80, // Pénalité FORTE et unique si un pattern est détecté.
|
|
832
|
+
decayFactor = 0.95, // Décroissance du score dans le temps.
|
|
833
|
+
inactivityReset = 180000 // Réinitialisation du score après 3 minutes d'inactivité.
|
|
944
834
|
} = patternConfig;
|
|
945
835
|
|
|
946
836
|
const now = Date.now();
|
|
947
837
|
const currentPath = context.path;
|
|
948
838
|
// Make the function robust to handle both URLSearchParams and plain objects for query.
|
|
949
|
-
|
|
950
|
-
|
|
839
|
+
const params =
|
|
840
|
+
context.query instanceof URLSearchParams
|
|
841
|
+
? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
|
|
842
|
+
: new URLSearchParams(context.query || {});
|
|
951
843
|
params.sort(); // Sort for deterministic order
|
|
952
844
|
const currentQueryString = params.toString();
|
|
953
845
|
|
|
954
|
-
// Initialize request history if it doesn't exist
|
|
955
846
|
if (!deviceData.requestHistory) deviceData.requestHistory = [];
|
|
956
|
-
// NOUVEAU: S'assurer que timingHistory est toujours initialisé.
|
|
957
|
-
// Cette vérification est séparée car deviceData peut exister avec requestHistory mais sans timingHistory.
|
|
958
847
|
if (!deviceData.timingHistory) deviceData.timingHistory = [];
|
|
959
848
|
|
|
960
849
|
const history = deviceData.requestHistory;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
// --- Analyze patterns based on the last few requests ---
|
|
964
|
-
if (history.length > 0) {
|
|
965
|
-
const lastRequest = history[history.length - 1];
|
|
966
|
-
const timeSinceLast = now - lastRequest.timestamp;
|
|
850
|
+
const lastRequest = history.length > 0 ? history[history.length - 1] : null;
|
|
851
|
+
const timeSinceLast = lastRequest ? now - lastRequest.timestamp : Infinity;
|
|
967
852
|
|
|
968
|
-
|
|
853
|
+
// Mise à jour de l'historique
|
|
854
|
+
history.push({
|
|
855
|
+
timestamp: now,
|
|
856
|
+
path: currentPath,
|
|
857
|
+
queryString: currentQueryString,
|
|
858
|
+
});
|
|
859
|
+
if (lastRequest) {
|
|
969
860
|
deviceData.timingHistory.push(timeSinceLast);
|
|
861
|
+
}
|
|
970
862
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
score += velocityWeight; // score = 30
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// 2. Burst Check: Add additional penalty for identical requests in a very short time frame.
|
|
977
|
-
if (currentPath === lastRequest.path && currentQueryString === lastRequest.queryString && timeSinceLast < burstThreshold) { // 150 < 500 -> true
|
|
978
|
-
score += burstWeight; // score = 30 + 50 = 80
|
|
979
|
-
}
|
|
863
|
+
let instantScore = 0;
|
|
864
|
+
const timings = deviceData.timingHistory;
|
|
980
865
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
score += scrapeWeight; // First sign of a potential scraping pattern
|
|
989
|
-
}
|
|
990
|
-
}
|
|
866
|
+
// Analyse statistique unifiée si nous avons assez de données
|
|
867
|
+
if (timings.length >= minSamples) {
|
|
868
|
+
const timings = deviceData.timingHistory;
|
|
869
|
+
const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
870
|
+
const variance = timings.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / timings.length;
|
|
871
|
+
const stdDev = Math.sqrt(variance);
|
|
872
|
+
const benfordDeviation = Optimization.Operators.benfordTest(timings);
|
|
991
873
|
|
|
992
|
-
//
|
|
993
|
-
if (
|
|
994
|
-
|
|
995
|
-
const previousSequence = history.slice(-sequenceLength * 2, -sequenceLength);
|
|
996
|
-
|
|
997
|
-
const isRepeating = lastSequence.every((req, i) =>
|
|
998
|
-
req.path === previousSequence[i].path && req.queryString === previousSequence[i].queryString
|
|
999
|
-
);
|
|
1000
|
-
if (isRepeating) score += sequenceWeight;
|
|
874
|
+
// Détection de régularité (bots de type "cron")
|
|
875
|
+
if (stdDev < regularityThreshold) {
|
|
876
|
+
instantScore = patternWeight;
|
|
1001
877
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
// On concatène tous les délais en une seule chaîne de chiffres.
|
|
1006
|
-
const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
|
|
1007
|
-
|
|
1008
|
-
// Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
|
|
1009
|
-
// Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
|
|
1010
|
-
score += Math.min(100, (benfordDeviation / 0.3) * benfordWeight);
|
|
878
|
+
// Détection de distribution non-naturelle (bots "faussement aléatoires")
|
|
879
|
+
else if (benfordDeviation > benfordThreshold) {
|
|
880
|
+
instantScore = patternWeight;
|
|
1011
881
|
}
|
|
1012
882
|
}
|
|
1013
883
|
|
|
1014
|
-
//
|
|
1015
|
-
history.push({
|
|
1016
|
-
timestamp: now,
|
|
1017
|
-
path: currentPath,
|
|
1018
|
-
queryString: currentQueryString,
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// Keep history to a reasonable size (e.g., last 10 requests)
|
|
884
|
+
// Garder l'historique à une taille raisonnable
|
|
1022
885
|
if (history.length > historySize) {
|
|
1023
886
|
history.shift();
|
|
1024
887
|
}
|
|
1025
|
-
if (deviceData.timingHistory.length >
|
|
888
|
+
if (deviceData.timingHistory.length > historySize) {
|
|
1026
889
|
deviceData.timingHistory.shift();
|
|
1027
890
|
}
|
|
1028
891
|
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
deviceData.lastPatternScore = Math.min(100, (deviceData.lastPatternScore || 0) * decayFactor + score); // Decay old score and add new, plafonné à 100
|
|
892
|
+
// Logique de décroissance et de score final
|
|
893
|
+
let newPatternScore = deviceData.lastPatternScore || 0;
|
|
1032
894
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
895
|
+
if (timeSinceLast > inactivityReset) {
|
|
896
|
+
newPatternScore = 0; // Réinitialisation complète après une longue inactivité
|
|
897
|
+
} else {
|
|
898
|
+
newPatternScore *= decayFactor;
|
|
1036
899
|
}
|
|
900
|
+
newPatternScore = Math.max(0, newPatternScore);
|
|
1037
901
|
|
|
1038
|
-
|
|
902
|
+
deviceData.lastPatternScore = newPatternScore + instantScore;
|
|
903
|
+
|
|
904
|
+
return { requestPatternScore: Math.min(100, deviceData.lastPatternScore) };
|
|
1039
905
|
}
|
|
1040
906
|
|
|
1041
907
|
const trapUrlTemplates = [
|
|
@@ -1365,20 +1231,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
|
|
|
1365
1231
|
* @param {number} suspicionFactor - A number from 0 to 1.
|
|
1366
1232
|
* @returns {BigInt} The target number.
|
|
1367
1233
|
*/
|
|
1368
|
-
function calculateTarget(suspicionFactor) {
|
|
1234
|
+
function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
1369
1235
|
// Difficulty range adjusted to be realistic.
|
|
1370
1236
|
// MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
|
|
1371
1237
|
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
1372
|
-
|
|
1373
|
-
const
|
|
1238
|
+
// NOUVEAU: La difficulté est maintenant configurable.
|
|
1239
|
+
const { cpu: cpuConfig = {} } = securityConfig;
|
|
1240
|
+
const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
|
|
1241
|
+
const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
|
|
1374
1242
|
|
|
1375
1243
|
// Use linear interpolation between min and max difficulty.
|
|
1376
1244
|
const totalDifficultyBits =
|
|
1377
1245
|
MIN_DIFFICULTY_BITS +
|
|
1378
1246
|
suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
|
|
1247
|
+
|
|
1248
|
+
if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
|
|
1249
|
+
|
|
1250
|
+
// The correct way to calculate the target is to define the number of leading zero bits required.
|
|
1251
|
+
// A target for N bits of difficulty is 2^(256-N).
|
|
1252
|
+
// We can calculate this with a left-shift on 1.
|
|
1253
|
+
const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
|
|
1254
|
+
return 1n << shift;
|
|
1255
|
+
}
|
|
1379
1256
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1257
|
+
/**
|
|
1258
|
+
* @private
|
|
1259
|
+
* Crée le bloc de base pour le challenge CPU.
|
|
1260
|
+
* Ce buffer contient toutes les données sauf la solution.
|
|
1261
|
+
* @param {string} nonce
|
|
1262
|
+
* @param {string} clientSecret
|
|
1263
|
+
* @param {string} fingerprint
|
|
1264
|
+
* @returns {Buffer}
|
|
1265
|
+
*/
|
|
1266
|
+
function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
|
|
1267
|
+
const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
|
|
1268
|
+
// On concatène les chaînes, puis on les convertit en buffer une seule fois.
|
|
1269
|
+
// Cela garantit que le client et le serveur travaillent sur la même base binaire.
|
|
1270
|
+
const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
|
|
1271
|
+
return Buffer.from(messageBase, 'utf8');
|
|
1382
1272
|
}
|
|
1383
1273
|
|
|
1384
1274
|
/**
|
|
@@ -1389,12 +1279,15 @@ export function generateCpuTargetChallenge(
|
|
|
1389
1279
|
nonce,
|
|
1390
1280
|
suspicionFactor,
|
|
1391
1281
|
originalUrl,
|
|
1282
|
+
securityConfig,
|
|
1392
1283
|
) {
|
|
1393
|
-
const target = calculateTarget(suspicionFactor);
|
|
1284
|
+
const target = calculateTarget(suspicionFactor, securityConfig);
|
|
1285
|
+
// Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
|
|
1286
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
|
|
1394
1287
|
return {
|
|
1395
1288
|
type: "cpu_target",
|
|
1396
1289
|
nonce: nonce,
|
|
1397
|
-
target: target.toString(16),
|
|
1290
|
+
target: target.toString(16),
|
|
1398
1291
|
path: originalUrl,
|
|
1399
1292
|
};
|
|
1400
1293
|
}
|
|
@@ -1419,10 +1312,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1419
1312
|
async function solve() {
|
|
1420
1313
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1421
1314
|
const nonce = ${JSON.stringify(nonce)};
|
|
1422
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1315
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1316
|
+
// La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
|
|
1317
|
+
// car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
|
|
1318
|
+
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
1319
|
+
const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
|
|
1426
1320
|
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
1427
1321
|
}
|
|
1428
1322
|
solve();
|
|
@@ -1437,9 +1331,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1437
1331
|
* @param {string} clientIp - The client's IP address.
|
|
1438
1332
|
* @returns {string} HTML content.
|
|
1439
1333
|
*/
|
|
1440
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
|
|
1334
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
|
|
1441
1335
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1442
1336
|
const solverCode = getPowSolverCode();
|
|
1337
|
+
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
1338
|
+
// Le fingerprint est maintenant passé directement en paramètre.
|
|
1339
|
+
const fingerprint = originalFingerprint;
|
|
1340
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
1341
|
+
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
1443
1342
|
|
|
1444
1343
|
const challengeScript = `
|
|
1445
1344
|
async function solve() {
|
|
@@ -1447,22 +1346,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1447
1346
|
const path = ${JSON.stringify(path)};
|
|
1448
1347
|
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1449
1348
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1450
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1349
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1451
1350
|
const memDifficulty = ${memoryDifficulty};
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1455
|
-
const getClientFingerprint = () => (window.ClientLibrary && typeof window.ClientLibrary.getDeviceFingerprint === 'function') ? window.ClientLibrary.getDeviceFingerprint() : '';
|
|
1456
|
-
const fingerprint = getClientFingerprint();
|
|
1351
|
+
// Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
|
|
1352
|
+
// Il n'a plus besoin de construire le message lui-même.
|
|
1353
|
+
const baseBlock = new Uint8Array(${baseBlockBytes});
|
|
1457
1354
|
|
|
1458
1355
|
// --- CPU Challenge ---
|
|
1459
|
-
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
1460
|
-
|
|
1461
|
-
// Optional progress callback
|
|
1462
|
-
});
|
|
1356
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
|
|
1357
|
+
|
|
1463
1358
|
// --- Memory Challenge ---
|
|
1464
1359
|
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
1465
|
-
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1360
|
+
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1466
1361
|
let memSolution = 0;
|
|
1467
1362
|
try {
|
|
1468
1363
|
const memSeed = nonce + ":" + clientSecret;
|
|
@@ -1471,7 +1366,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1471
1366
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1472
1367
|
return;
|
|
1473
1368
|
}
|
|
1474
|
-
|
|
1369
|
+
|
|
1370
|
+
// Redirect with both solutions and the fingerprint used to solve.
|
|
1371
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1372
|
+
window.location.href = finalUrl;
|
|
1475
1373
|
}
|
|
1476
1374
|
solve();
|
|
1477
1375
|
`;
|
|
@@ -1502,23 +1400,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1502
1400
|
*/
|
|
1503
1401
|
export function verifyCpuTargetPoWAndGenerateTicket(
|
|
1504
1402
|
clientIp, // This parameter is crucial and must be the actual client IP
|
|
1505
|
-
ticketTtl,
|
|
1403
|
+
ticketTtl,
|
|
1506
1404
|
nonce,
|
|
1507
1405
|
solution,
|
|
1508
|
-
|
|
1509
|
-
target, // La cible est maintenant passée directement en hexadécimal,
|
|
1510
|
-
fingerprint, // Le fingerprint du SOLVER, soumis par le client
|
|
1406
|
+
challengeContext = {}, // Le contexte complet du challenge est maintenant passé
|
|
1511
1407
|
) {
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1408
|
+
const { cpuTarget, baseBlock } = challengeContext;
|
|
1409
|
+
if (!cpuTarget || !baseBlock) {
|
|
1410
|
+
console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Le baseBlock est déjà un Buffer ou un tableau d'octets.
|
|
1415
|
+
// On s'assure que c'est un Buffer pour la concaténation.
|
|
1416
|
+
const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
|
|
1417
|
+
const solutionBuffer = Buffer.from(String(solution), 'utf8');
|
|
1418
|
+
|
|
1419
|
+
// Concaténation binaire directe. C'est la garantie de cohérence.
|
|
1420
|
+
const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
|
|
1421
|
+
|
|
1515
1422
|
const hash = crypto
|
|
1516
1423
|
.createHash("sha256")
|
|
1517
|
-
.update(
|
|
1424
|
+
.update(finalBlock)
|
|
1518
1425
|
.digest("hex");
|
|
1519
1426
|
const hashAsInt = BigInt("0x" + hash);
|
|
1427
|
+
const targetAsInt = BigInt("0x" + cpuTarget);
|
|
1428
|
+
|
|
1429
|
+
// --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
|
|
1430
|
+
console.log('[FP Server Verify] Intermediate values:', {
|
|
1431
|
+
hashCalculated: `0x${hash}`,
|
|
1432
|
+
hashAsInt: hashAsInt.toString(), // Log as string to see full value
|
|
1433
|
+
target: `0x${cpuTarget}`,
|
|
1434
|
+
targetAsInt: targetAsInt.toString(), // Log as string to see full value
|
|
1435
|
+
});
|
|
1436
|
+
// --- FIN DES NOUVEAUX LOGS ---
|
|
1520
1437
|
|
|
1521
|
-
|
|
1438
|
+
const isValid = hashAsInt < targetAsInt;
|
|
1439
|
+
|
|
1440
|
+
// --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
|
|
1441
|
+
if (!isValid) {
|
|
1442
|
+
console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
|
|
1443
|
+
hashCalculated: `0x${hash}`,
|
|
1444
|
+
target: `0x${cpuTarget}`,
|
|
1445
|
+
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
// --- FIN DES LOGS ---
|
|
1449
|
+
|
|
1450
|
+
if (isValid) {
|
|
1451
|
+
console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
|
|
1452
|
+
});
|
|
1522
1453
|
// The comparison is direct with native BigInts
|
|
1523
1454
|
// The proof is valid, generate the ticket
|
|
1524
1455
|
const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
|
|
@@ -1593,6 +1524,46 @@ export class FingerprintEngine {
|
|
|
1593
1524
|
}
|
|
1594
1525
|
return blockList;
|
|
1595
1526
|
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Checks if the client's IP resolves to any of the hostnames in the hostname allowlist.
|
|
1530
|
+
* The result is cached to avoid repeated DNS lookups.
|
|
1531
|
+
* @private
|
|
1532
|
+
* @param {string} clientIp - The IP address of the client.
|
|
1533
|
+
* @returns {Promise<boolean>} True if the IP is in the hostname allowlist.
|
|
1534
|
+
*/
|
|
1535
|
+
async _isIpInHostnameAllowlist(clientIp) {
|
|
1536
|
+
const { whitelist = [] } = this.securityConfig;
|
|
1537
|
+
const hostnameRule = whitelist.find(rule => rule.type === 'hostname_allowlist');
|
|
1538
|
+
|
|
1539
|
+
if (!hostnameRule || !hostnameRule.entries || hostnameRule.entries.length === 0) {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const cacheKey = `ip-hostname-allowlist:${clientIp}`;
|
|
1544
|
+
const cachedStatus = await store.get(cacheKey);
|
|
1545
|
+
|
|
1546
|
+
if (cachedStatus === 'verified') return true;
|
|
1547
|
+
if (cachedStatus === 'failed') return false;
|
|
1548
|
+
|
|
1549
|
+
try {
|
|
1550
|
+
// Reverse DNS lookup to get hostnames for the IP
|
|
1551
|
+
const hostnames = await dns.reverse(clientIp);
|
|
1552
|
+
|
|
1553
|
+
// Check if any of the resolved hostnames is in our allowlist
|
|
1554
|
+
const isAllowed = hostnames.some(hostname => hostnameRule.entries.includes(hostname));
|
|
1555
|
+
|
|
1556
|
+
if (isAllowed) {
|
|
1557
|
+
await store.set(cacheKey, 'verified', 86400); // Cache success for 24h
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
// DNS errors (like no rDNS record) are treated as a failure.
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
await store.set(cacheKey, 'failed', 86400); // Cache failure for 24h
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1596
1567
|
_isIpInAllowlist(clientIp) {
|
|
1597
1568
|
return this._allowlist.check(clientIp);
|
|
1598
1569
|
}
|
|
@@ -1678,6 +1649,12 @@ export class FingerprintEngine {
|
|
|
1678
1649
|
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
|
|
1679
1650
|
}
|
|
1680
1651
|
|
|
1652
|
+
// 2. Check hostname-based allowlist.
|
|
1653
|
+
if (await this._isIpInHostnameAllowlist(clientIp)) {
|
|
1654
|
+
this._log('IP resolves to a whitelisted hostname - allowing request', { clientIp });
|
|
1655
|
+
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'hostname_allowlist' } };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1681
1658
|
const { pow_nonce } = query;
|
|
1682
1659
|
|
|
1683
1660
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
@@ -1706,7 +1683,7 @@ export class FingerprintEngine {
|
|
|
1706
1683
|
if (deviceData?.condemned) {
|
|
1707
1684
|
this._log('Device condemned - blocking request', { deviceId });
|
|
1708
1685
|
if (onDeviceCompromised) {
|
|
1709
|
-
onDeviceCompromised({ deviceId:
|
|
1686
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1710
1687
|
}
|
|
1711
1688
|
return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1712
1689
|
}
|
|
@@ -1768,7 +1745,7 @@ export class FingerprintEngine {
|
|
|
1768
1745
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
1769
1746
|
// avant même de recalculer le score de suspicion.
|
|
1770
1747
|
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
1771
|
-
if (pow_nonce && (pow_solution ||
|
|
1748
|
+
if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
1772
1749
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
1773
1750
|
|
|
1774
1751
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
@@ -1795,14 +1772,33 @@ export class FingerprintEngine {
|
|
|
1795
1772
|
if (challengeContext) {
|
|
1796
1773
|
// *** NOUVELLE VÉRIFICATION CRUCIALE ***
|
|
1797
1774
|
// On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
|
|
1798
|
-
// lors de l'émission du challenge
|
|
1799
|
-
|
|
1800
|
-
|
|
1775
|
+
// lors de l'émission du challenge.
|
|
1776
|
+
// --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
|
|
1777
|
+
// This handles API clients that might not use the full client-side library but still solve the challenge.
|
|
1778
|
+
const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
|
|
1779
|
+
const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
|
|
1780
|
+
|
|
1781
|
+
let similarity;
|
|
1782
|
+
const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
|
|
1783
|
+
|
|
1784
|
+
// If fingerprints are simple strings (like test placeholders 'fp-probation')
|
|
1785
|
+
// and don't contain the typical structure, fall back to a strict equality check.
|
|
1786
|
+
if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
|
|
1787
|
+
similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
|
|
1788
|
+
} else {
|
|
1789
|
+
// Use the weighted comparison for structured fingerprints.
|
|
1790
|
+
// We compare the fingerprint of the request that triggered the challenge
|
|
1791
|
+
// with the fingerprint of the request that is submitting the solution.
|
|
1792
|
+
// They should be very similar.
|
|
1793
|
+
similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
|
|
1794
|
+
}
|
|
1801
1795
|
|
|
1802
|
-
if (
|
|
1796
|
+
if (similarity < similarityThreshold) {
|
|
1803
1797
|
this._log('Fingerprint mismatch - challenge solved on a different machine!', {
|
|
1804
1798
|
original: originalFingerprint,
|
|
1805
1799
|
solver: solverFingerprint,
|
|
1800
|
+
similarity: similarity.toFixed(4),
|
|
1801
|
+
threshold: similarityThreshold
|
|
1806
1802
|
});
|
|
1807
1803
|
isValid = false;
|
|
1808
1804
|
} else {
|
|
@@ -1810,13 +1806,13 @@ export class FingerprintEngine {
|
|
|
1810
1806
|
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
1811
1807
|
this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
|
|
1812
1808
|
|
|
1813
|
-
if (pow_type === "cpu_target" && pow_solution) {
|
|
1814
|
-
|
|
1809
|
+
if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
|
|
1810
|
+
const cpuSolution = pow_solution_cpu || pow_solution;
|
|
1811
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
|
|
1815
1812
|
isValid = ticket !== null;
|
|
1816
1813
|
this._log('CPU target challenge verification', { isValid });
|
|
1817
|
-
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
1818
|
-
|
|
1819
|
-
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1814
|
+
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
|
|
1815
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
|
|
1820
1816
|
isValid = cpuTicket !== null && isMemValid;
|
|
1821
1817
|
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1822
1818
|
this._log('Combined CPU+Memory challenge verification', {
|
|
@@ -1835,12 +1831,12 @@ export class FingerprintEngine {
|
|
|
1835
1831
|
this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: finalTtl, isProbationary });
|
|
1836
1832
|
|
|
1837
1833
|
if (logger) {
|
|
1838
|
-
logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
|
|
1834
|
+
logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now(), vector: preliminaryVector });
|
|
1839
1835
|
}
|
|
1840
1836
|
|
|
1841
1837
|
// NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
|
|
1842
1838
|
// 1. On part du chemin original stocké, qui peut contenir des query params.
|
|
1843
|
-
const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1839
|
+
const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1844
1840
|
// 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
|
|
1845
1841
|
const finalSearchParams = new URLSearchParams(requestContext.query);
|
|
1846
1842
|
|
|
@@ -1851,6 +1847,10 @@ export class FingerprintEngine {
|
|
|
1851
1847
|
finalSearchParams.delete('pow_solution_cpu');
|
|
1852
1848
|
finalSearchParams.delete('pow_solution_mem');
|
|
1853
1849
|
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
1850
|
+
// NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
|
|
1851
|
+
finalSearchParams.delete('pow_solution_population');
|
|
1852
|
+
finalSearchParams.delete('pow_solution_work_result');
|
|
1853
|
+
finalSearchParams.delete('pow_problem_id');
|
|
1854
1854
|
|
|
1855
1855
|
// 4. On reconstruit le chemin final.
|
|
1856
1856
|
const finalQueryString = finalSearchParams.toString();
|
|
@@ -1873,6 +1873,13 @@ export class FingerprintEngine {
|
|
|
1873
1873
|
this._log('Challenge solution invalid', { pow_nonce });
|
|
1874
1874
|
suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
|
|
1875
1875
|
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1876
|
+
// --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
|
|
1877
|
+
const newBlockThreshold = thresholds.block ?? 95;
|
|
1878
|
+
if (finalScore >= newBlockThreshold) {
|
|
1879
|
+
this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
|
|
1880
|
+
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1881
|
+
}
|
|
1882
|
+
// If not blocked, the request will proceed to be re-challenged.
|
|
1876
1883
|
}
|
|
1877
1884
|
} else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
|
|
1878
1885
|
this._log('Optimization task solution submitted', { pow_nonce });
|
|
@@ -1935,7 +1942,10 @@ export class FingerprintEngine {
|
|
|
1935
1942
|
if (isBlocked) {
|
|
1936
1943
|
this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
|
|
1937
1944
|
if (onDeviceCompromised) {
|
|
1938
|
-
onDeviceCompromised({ deviceId:
|
|
1945
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
|
|
1946
|
+
}
|
|
1947
|
+
if (logger) {
|
|
1948
|
+
logger({ type: 'request_blocked', deviceId: deviceId, score: finalScore, vector: suspicionVector, timestamp: Date.now() });
|
|
1939
1949
|
}
|
|
1940
1950
|
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1941
1951
|
}
|
|
@@ -1947,16 +1957,27 @@ export class FingerprintEngine {
|
|
|
1947
1957
|
this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
|
|
1948
1958
|
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1949
1959
|
if (onDeviceCompromised) {
|
|
1950
|
-
onDeviceCompromised({ deviceId:
|
|
1960
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1951
1961
|
}
|
|
1952
1962
|
if (logger) {
|
|
1953
|
-
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
|
|
1963
|
+
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now(), vector: { honeypotScore: 100 } });
|
|
1954
1964
|
}
|
|
1955
1965
|
await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
|
|
1956
1966
|
return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
|
|
1957
1967
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1968
|
+
|
|
1969
|
+
// --- NOUVELLE LOGIQUE DE RE-CHALLENGE ---
|
|
1970
|
+
// Un challenge est nécessaire si :
|
|
1971
|
+
// 1. La requête est suspecte ET il n'y a pas de ticket valide.
|
|
1972
|
+
// OU
|
|
1973
|
+
// 2. La requête est *très* suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
1974
|
+
const hasValidTicket = isTicketValid(clientIp, powCookie);
|
|
1975
|
+
const mustReChallenge = isSuspiciousHigh && hasValidTicket;
|
|
1976
|
+
|
|
1977
|
+
if (isSuspicious && (!hasValidTicket || mustReChallenge)) {
|
|
1978
|
+
if (mustReChallenge) {
|
|
1979
|
+
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
1980
|
+
}
|
|
1960
1981
|
this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
|
|
1961
1982
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
1962
1983
|
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
@@ -1966,7 +1987,7 @@ export class FingerprintEngine {
|
|
|
1966
1987
|
if (pow_nonce && !isChallengeResponse) {
|
|
1967
1988
|
this._log('Honeypot probe detected - blocking request', { path, pow_nonce });
|
|
1968
1989
|
if (logger) {
|
|
1969
|
-
logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
|
|
1990
|
+
logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now(), vector: suspicionVector });
|
|
1970
1991
|
}
|
|
1971
1992
|
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
1972
1993
|
// Recalculate the final score with the updated vector.
|
|
@@ -2002,8 +2023,8 @@ export class FingerprintEngine {
|
|
|
2002
2023
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
2003
2024
|
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
2004
2025
|
|
|
2005
|
-
|
|
2006
|
-
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
2026
|
+
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
2027
|
+
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
2007
2028
|
|
|
2008
2029
|
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
2009
2030
|
// Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
|
|
@@ -2019,16 +2040,18 @@ export class FingerprintEngine {
|
|
|
2019
2040
|
memDifficulty,
|
|
2020
2041
|
cpuTarget: cpuChallengeDetails.target
|
|
2021
2042
|
});
|
|
2022
|
-
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge.
|
|
2023
|
-
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || getCompositeDeviceHash(requestContext);
|
|
2043
|
+
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
|
|
2044
|
+
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
|
|
2024
2045
|
|
|
2025
2046
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
2047
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
|
|
2026
2048
|
await store.set(`secret:${nonce}`, {
|
|
2027
2049
|
clientSecret,
|
|
2028
2050
|
cpuTarget: cpuChallengeDetails.target,
|
|
2029
2051
|
suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
|
|
2030
2052
|
fingerprint: originalFingerprint, // *** NOUVEAU ***
|
|
2031
2053
|
memDifficulty: memDifficulty,
|
|
2054
|
+
baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
|
|
2032
2055
|
originalPath: path, // *** FIX: Store the original path ***
|
|
2033
2056
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
2034
2057
|
|
|
@@ -2045,7 +2068,7 @@ export class FingerprintEngine {
|
|
|
2045
2068
|
});
|
|
2046
2069
|
|
|
2047
2070
|
if (logger) {
|
|
2048
|
-
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
2071
|
+
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
2049
2072
|
}
|
|
2050
2073
|
|
|
2051
2074
|
// Check if the request is an API request to return a JSON challenge
|
|
@@ -2060,13 +2083,14 @@ export class FingerprintEngine {
|
|
|
2060
2083
|
clientSecret: clientSecret, // The client needs this to solve the challenge
|
|
2061
2084
|
cpuTarget: cpuChallengeDetails.target,
|
|
2062
2085
|
memDifficulty: memDifficulty,
|
|
2086
|
+
baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
|
|
2063
2087
|
}
|
|
2064
2088
|
};
|
|
2065
2089
|
this._log('API challenge response generated', { challengePayload });
|
|
2066
2090
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
2067
2091
|
} else {
|
|
2068
2092
|
// For browsers, send the HTML page.
|
|
2069
|
-
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
|
|
2093
|
+
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
|
|
2070
2094
|
this._log('Browser challenge page generated', {
|
|
2071
2095
|
pageLength: page.length,
|
|
2072
2096
|
hasTrapContainer: true
|
|
@@ -2080,7 +2104,7 @@ export class FingerprintEngine {
|
|
|
2080
2104
|
this._log('Request passed - no challenge required', { finalScore, hasValidTicket: isTicketValid(clientIp, powCookie) });
|
|
2081
2105
|
|
|
2082
2106
|
if (logger) {
|
|
2083
|
-
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
2107
|
+
logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now(), vector: suspicionVector });
|
|
2084
2108
|
}
|
|
2085
2109
|
|
|
2086
2110
|
return { action: 'next', score: finalScore, vector: suspicionVector };
|
|
@@ -2523,115 +2547,58 @@ let autoTuningJobId = null;
|
|
|
2523
2547
|
* @param {object} securityConfig - The security configuration object to update.
|
|
2524
2548
|
* @param {Array<object>} trafficData - The array containing traffic logs.
|
|
2525
2549
|
* @param {number} minDataPoints - The minimum number of data points required to start optimization.
|
|
2550
|
+
* @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
|
|
2526
2551
|
*/
|
|
2527
|
-
function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
console.log(`[AutoTuning] Démarrage du cycle d'optimisation avec ${trafficData.length} points de données.`);
|
|
2533
|
-
|
|
2534
|
-
// Classify historical requests with a confidence weight.
|
|
2535
|
-
const solvedDevices = new Set(trafficData.filter(e => e.type === 'challenge_solved').map(e => e.deviceId));
|
|
2536
|
-
const challengedDevices = new Set(trafficData.filter(e => e.type === 'challenge_issued').map(e => e.deviceId));
|
|
2537
|
-
|
|
2538
|
-
const historicalRequests = trafficData.map(log => {
|
|
2539
|
-
// Assign a label ('bot' or 'human') and a confidence weight to each log entry.
|
|
2540
|
-
switch (log.type) {
|
|
2541
|
-
case 'honeypot_probe':
|
|
2542
|
-
case 'trap_triggered':
|
|
2543
|
-
return { score: log.score, label: 'bot', confidence: 10.0 }; // Very high confidence
|
|
2544
|
-
|
|
2545
|
-
case 'challenge_issued':
|
|
2546
|
-
// A challenge issued to a device that never solved it is a strong bot signal.
|
|
2547
|
-
if (!solvedDevices.has(log.deviceId)) {
|
|
2548
|
-
return { score: log.score, label: 'bot', confidence: 3.0 }; // High confidence
|
|
2549
|
-
}
|
|
2550
|
-
// If the challenge was eventually solved, this specific log is neutral.
|
|
2551
|
-
return null;
|
|
2552
|
+
function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
|
|
2553
|
+
if (trafficData.length < minDataPoints) {
|
|
2554
|
+
console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2552
2557
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2558
|
+
if (trafficData.length > maxDataPoints) {
|
|
2559
|
+
console.log(`[AutoTuning] Le journal de trafic a atteint ${trafficData.length} entrées (max: ${maxDataPoints}). Troncation des données les plus anciennes.`);
|
|
2560
|
+
trafficData.splice(0, trafficData.length - maxDataPoints);
|
|
2561
|
+
}
|
|
2555
2562
|
|
|
2556
|
-
|
|
2557
|
-
// A passed request from a device that was never even challenged is likely a human.
|
|
2558
|
-
if (!challengedDevices.has(log.deviceId)) {
|
|
2559
|
-
return { score: log.score, label: 'human', confidence: 0.5 }; // Low confidence
|
|
2560
|
-
}
|
|
2561
|
-
// If the device was challenged at some point, this log is ambiguous.
|
|
2562
|
-
return null;
|
|
2563
|
-
|
|
2564
|
-
default:
|
|
2565
|
-
return null;
|
|
2566
|
-
}
|
|
2567
|
-
}).filter(Boolean); // Remove null entries
|
|
2568
|
-
|
|
2569
|
-
// The "fitness" function evaluates the quality of a set of thresholds.
|
|
2570
|
-
// A lower score is better.
|
|
2571
|
-
const fitnessFunction = (solution) => {
|
|
2572
|
-
const [low, medium, high, velocityThreshold, burstThreshold, scrapeThreshold] = solution;
|
|
2573
|
-
if (low >= medium || medium >= high || low < 10 || high > 90) return Infinity;
|
|
2574
|
-
if (velocityThreshold < 50 || velocityThreshold > burstThreshold || burstThreshold > scrapeThreshold) return Infinity;
|
|
2575
|
-
|
|
2576
|
-
let weightedFalsePositives = 0; // Humans challenged unnecessarily.
|
|
2577
|
-
let weightedFalseNegatives = 0; // Undetected bots.
|
|
2578
|
-
|
|
2579
|
-
for (const req of historicalRequests) {
|
|
2580
|
-
if (req.label === 'bot') {
|
|
2581
|
-
if (req.score < low) weightedFalseNegatives += req.confidence;
|
|
2582
|
-
} else { // 'human'
|
|
2583
|
-
if (req.score >= low) weightedFalsePositives += req.confidence;
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
// The penalty for false negatives is implicitly higher due to the higher confidence scores of bot signals.
|
|
2587
|
-
return weightedFalsePositives + weightedFalseNegatives;
|
|
2588
|
-
};
|
|
2563
|
+
console.log(`[AutoTuning] Démarrage du cycle d'optimisation complet avec ${trafficData.length} points de données.`);
|
|
2589
2564
|
|
|
2590
|
-
|
|
2591
|
-
const createIndividual = () => [
|
|
2592
|
-
10 + Math.random() * 20, // low
|
|
2593
|
-
30 + Math.random() * 30, // medium
|
|
2594
|
-
60 + Math.random() * 30, // high
|
|
2595
|
-
100 + Math.random() * 150, // velocityThreshold (100-250ms)
|
|
2596
|
-
300 + Math.random() * 400, // burstThreshold (300-700ms)
|
|
2597
|
-
800 + Math.random() * 700, // scrapeThreshold (800-1500ms)
|
|
2598
|
-
];
|
|
2599
|
-
const crossover = (p1, p2) => p1.map((val, i) => (val + p2[i]) / 2);
|
|
2600
|
-
const mutate = (s) => {
|
|
2601
|
-
const n = [...s];
|
|
2602
|
-
const i = Math.floor(Math.random() * n.length);
|
|
2603
|
-
// Adjust mutation range based on parameter
|
|
2604
|
-
const mutationRange = i < 3 ? 5 : 50;
|
|
2605
|
-
n[i] += (Math.random() - 0.5) * mutationRange;
|
|
2606
|
-
return n;
|
|
2607
|
-
};
|
|
2565
|
+
const paretoFront = Optimization.Operators.solveFullSecurityTuning({ trafficData });
|
|
2608
2566
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2567
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
2568
|
+
console.warn("[AutoTuning] L'optimisation n'a retourné aucune solution.");
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Stratégie de sélection : choisir la solution la plus équilibrée du front de Pareto.
|
|
2573
|
+
// On cherche la solution la plus proche de l'origine (0,0) dans l'espace des objectifs.
|
|
2574
|
+
let bestSolution = paretoFront[0];
|
|
2575
|
+
let minDistance = Math.sqrt(Math.pow(bestSolution.objectives[0], 2) + Math.pow(bestSolution.objectives[1], 2));
|
|
2576
|
+
|
|
2577
|
+
for (let i = 1; i < paretoFront.length; i++) {
|
|
2578
|
+
const distance = Math.sqrt(Math.pow(paretoFront[i].objectives[0], 2) + Math.pow(paretoFront[i].objectives[1], 2));
|
|
2579
|
+
if (distance < minDistance) {
|
|
2580
|
+
minDistance = distance;
|
|
2581
|
+
bestSolution = paretoFront[i];
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2614
2584
|
|
|
2615
|
-
|
|
2585
|
+
// Appliquer la nouvelle configuration optimisée
|
|
2586
|
+
const newConfig = bestSolution.solution;
|
|
2616
2587
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
securityConfig.thresholds.medium = Math.round(newMedium);
|
|
2622
|
-
securityConfig.thresholds.high = Math.round(newHigh);
|
|
2588
|
+
// S'assurer que les objets de configuration existent avant d'utiliser Object.assign
|
|
2589
|
+
if (!securityConfig.thresholds) securityConfig.thresholds = {};
|
|
2590
|
+
if (!securityConfig.weights) securityConfig.weights = {};
|
|
2591
|
+
if (!securityConfig.patterns) securityConfig.patterns = {};
|
|
2623
2592
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
securityConfig.patterns.burstThreshold = Math.round(newBurst);
|
|
2628
|
-
securityConfig.patterns.scrapeThreshold = Math.round(newScrape);
|
|
2629
|
-
// Weights could also be optimized, but let's keep it to thresholds for now for simplicity.
|
|
2593
|
+
Object.assign(securityConfig.thresholds, newConfig.thresholds || {});
|
|
2594
|
+
Object.assign(securityConfig.weights, newConfig.weights || {});
|
|
2595
|
+
Object.assign(securityConfig.patterns, newConfig.patterns || {});
|
|
2630
2596
|
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2597
|
+
console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
|
|
2598
|
+
console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
|
|
2599
|
+
console.log("[AutoTuning] Seuils :", securityConfig.thresholds);
|
|
2600
|
+
console.log("[AutoTuning] Poids :", securityConfig.weights);
|
|
2601
|
+
console.log("[AutoTuning] Patterns :", securityConfig.patterns);
|
|
2635
2602
|
}
|
|
2636
2603
|
|
|
2637
2604
|
/**
|
|
@@ -2641,7 +2608,8 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints) {
|
|
|
2641
2608
|
* @param {object} options.securityConfig - The live security configuration object that will be mutated.
|
|
2642
2609
|
* @param {Array<object>} options.trafficData - The array where the logger pushes traffic data.
|
|
2643
2610
|
* @param {number} [options.interval=1800000] - The interval in milliseconds between each optimization cycle (default: 30 minutes).
|
|
2644
|
-
* @param {number} [options.minDataPoints=200] - The minimum number of requests to
|
|
2611
|
+
* @param {number} [options.minDataPoints=200] - The minimum number of requests to have before starting a cycle (default: 200).
|
|
2612
|
+
* @param {number} [options.maxDataPoints=10000] - The maximum number of log entries to keep in memory (default: 10,000).
|
|
2645
2613
|
*/
|
|
2646
2614
|
export function startThresholdAutoTuning(options) {
|
|
2647
2615
|
if (autoTuningJobId) {
|
|
@@ -2652,8 +2620,9 @@ export function startThresholdAutoTuning(options) {
|
|
|
2652
2620
|
const {
|
|
2653
2621
|
securityConfig,
|
|
2654
2622
|
trafficData,
|
|
2655
|
-
interval = 1800000,
|
|
2656
|
-
minDataPoints = 200
|
|
2623
|
+
interval = 1800000, // 30 minutes
|
|
2624
|
+
minDataPoints = 200,
|
|
2625
|
+
maxDataPoints = 10000 // Limite par défaut à 10 000 entrées
|
|
2657
2626
|
} = options;
|
|
2658
2627
|
|
|
2659
2628
|
if (!securityConfig || !trafficData) {
|
|
@@ -2663,7 +2632,7 @@ export function startThresholdAutoTuning(options) {
|
|
|
2663
2632
|
console.log(`[AutoTuning] Job d'optimisation des seuils démarré. Prochain cycle dans ${interval / 60000} minutes.`);
|
|
2664
2633
|
|
|
2665
2634
|
autoTuningJobId = setInterval(() => {
|
|
2666
|
-
runThresholdOptimization(securityConfig, trafficData, minDataPoints);
|
|
2635
|
+
runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints);
|
|
2667
2636
|
}, interval);
|
|
2668
2637
|
}
|
|
2669
2638
|
|