@anonympins/fingerprint 0.4.5 → 0.5.0
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 +39 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +767 -215
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +389 -285
- package/src/js/pow.solver.js +105 -1
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +191 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +72 -1
- package/src/php/Challenge/ChallengeUtils.php +395 -5
- package/src/php/FingerprintEngine.php +176 -10
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +45 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {afterEach, assert, beforeEach, describe, expect, it, test, vi} from 'vitest';
|
|
2
|
-
import {createHash, createHmac} from 'node:crypto';
|
|
2
|
+
import {createHash, createHmac, generateKeyPairSync} from 'node:crypto';
|
|
3
3
|
import {solveCpuTargetInline, solveMemory} from '../pow.solver.js';
|
|
4
4
|
import {readFileSync} from 'node:fs';
|
|
5
5
|
import {cyrb53, FingerprintBuilder} from '../fingerprint.builder.js';
|
|
@@ -66,6 +66,18 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
66
66
|
expect(FingerprintBuilder.compare(fp1, ''), "Comparison with empty string should be 0").toBe(0);
|
|
67
67
|
expect(FingerprintBuilder.compare(null, fp2), "Comparison with null should be 0").toBe(0);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it('should calculate a high crossLayerInconsistencyScore when viewport width exceeds screen width', () => {
|
|
71
|
+
const context = {
|
|
72
|
+
headers: {
|
|
73
|
+
'x-device-fingerprint': `scr:${cyrb53("1920x1080_24")}`,
|
|
74
|
+
'sec-ch-viewport-width': '2560'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { crossLayerInconsistencyScore } = __internal.getCrossLayerInconsistency(context);
|
|
79
|
+
expect(crossLayerInconsistencyScore).toBe(20);
|
|
80
|
+
});
|
|
69
81
|
});
|
|
70
82
|
|
|
71
83
|
describe('JA3 Fingerprinting', () => {
|
|
@@ -1080,6 +1092,63 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1080
1092
|
stopThresholdAutoTuning(); // Ensure cleanup after each test
|
|
1081
1093
|
});
|
|
1082
1094
|
|
|
1095
|
+
test('should prune traffic data based on time, size, clearAfterTuning and invoke onCleanup', async () => {
|
|
1096
|
+
const trafficData = [
|
|
1097
|
+
{ type: 'challenge_solved', timestamp: Date.now() - 10000 }, // 10s old
|
|
1098
|
+
{ type: 'challenge_solved', timestamp: Date.now() - 5000 }, // 5s old
|
|
1099
|
+
{ type: 'challenge_solved', timestamp: Date.now() } // fresh
|
|
1100
|
+
];
|
|
1101
|
+
|
|
1102
|
+
const securityConfig = {
|
|
1103
|
+
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
1104
|
+
weights: { historyScore: 1 },
|
|
1105
|
+
patterns: {}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const cleanedLogs = [];
|
|
1109
|
+
const onCleanup = (removed) => {
|
|
1110
|
+
cleanedLogs.push(...removed);
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// Test 1: Time-based pruning (older than 8 seconds)
|
|
1114
|
+
__internal.pruneTrafficData(trafficData, 10, 8000, onCleanup);
|
|
1115
|
+
|
|
1116
|
+
expect(trafficData.length).toBe(2);
|
|
1117
|
+
expect(cleanedLogs.length).toBe(1);
|
|
1118
|
+
expect(cleanedLogs[0].timestamp).toBeLessThan(Date.now() - 8000);
|
|
1119
|
+
|
|
1120
|
+
// Test 2: Size-based pruning (max size = 1)
|
|
1121
|
+
__internal.pruneTrafficData(trafficData, 1, 0, onCleanup);
|
|
1122
|
+
expect(trafficData.length).toBe(1);
|
|
1123
|
+
expect(cleanedLogs.length).toBe(2);
|
|
1124
|
+
|
|
1125
|
+
// Test 3: Clear after tuning
|
|
1126
|
+
const autoTuningTraffic = [];
|
|
1127
|
+
for (let i = 0; i < 110; i++) {
|
|
1128
|
+
autoTuningTraffic.push({ type: 'challenge_solved', timestamp: Date.now(), deviceId: `dev-solved-${i}` });
|
|
1129
|
+
autoTuningTraffic.push({ type: 'request_passed', timestamp: Date.now(), deviceId: `dev-passed-${i}` });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const tuningCleaned = [];
|
|
1133
|
+
startThresholdAutoTuning({
|
|
1134
|
+
securityConfig,
|
|
1135
|
+
trafficData: autoTuningTraffic,
|
|
1136
|
+
interval: 10000,
|
|
1137
|
+
minDataPoints: 100,
|
|
1138
|
+
maxDataPoints: 500,
|
|
1139
|
+
clearAfterTuning: true,
|
|
1140
|
+
onCleanup: (removed) => tuningCleaned.push(...removed)
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
const intervalCallback = setIntervalSpy.mock.calls[0][0];
|
|
1144
|
+
intervalCallback();
|
|
1145
|
+
|
|
1146
|
+
expect(autoTuningTraffic.length).toBe(0);
|
|
1147
|
+
expect(tuningCleaned.length).toBeGreaterThan(0);
|
|
1148
|
+
|
|
1149
|
+
stopThresholdAutoTuning();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1083
1152
|
test('should start, run an optimization cycle, and update thresholds', () => {
|
|
1084
1153
|
const trafficData = [];
|
|
1085
1154
|
const securityConfig = {
|
|
@@ -1635,6 +1704,53 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1635
1704
|
});
|
|
1636
1705
|
});
|
|
1637
1706
|
|
|
1707
|
+
describe('TLS Session Resumption (Cookieless Identity)', () => {
|
|
1708
|
+
const inMemoryStore = {
|
|
1709
|
+
_map: new Map(),
|
|
1710
|
+
async get(key) { return this._map.get(key); },
|
|
1711
|
+
async set(key, value) { this._map.set(key, value); },
|
|
1712
|
+
async has(key) { return this._map.has(key); },
|
|
1713
|
+
async delete(key) { this._map.delete(key); },
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
beforeEach(() => {
|
|
1717
|
+
inMemoryStore._map.clear();
|
|
1718
|
+
configureStore(inMemoryStore);
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
it('should resume device ID using TLS Session ID even if cookies are cleared', async () => {
|
|
1722
|
+
const securityConfig = {
|
|
1723
|
+
weights: { historyScore: 1.0 },
|
|
1724
|
+
thresholds: { low: 20, block: 95 }
|
|
1725
|
+
};
|
|
1726
|
+
const engine = new FingerprintEngine(securityConfig);
|
|
1727
|
+
|
|
1728
|
+
const req1 = {
|
|
1729
|
+
clientIp: '1.2.3.4',
|
|
1730
|
+
path: '/',
|
|
1731
|
+
cookies: {},
|
|
1732
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1733
|
+
rawHeaders: [],
|
|
1734
|
+
httpVersion: '1.1'
|
|
1735
|
+
};
|
|
1736
|
+
const decision1 = await engine.processRequest(req1);
|
|
1737
|
+
const deviceId = decision1.newCookieForResponse.value;
|
|
1738
|
+
expect(deviceId).toBeDefined();
|
|
1739
|
+
|
|
1740
|
+
const req2 = {
|
|
1741
|
+
clientIp: '1.2.3.4',
|
|
1742
|
+
path: '/',
|
|
1743
|
+
cookies: {}, // Cookies cleared!
|
|
1744
|
+
headers: { 'x-tls-session-id': 'session-xyz-123', 'user-agent': 'test-agent' },
|
|
1745
|
+
rawHeaders: [],
|
|
1746
|
+
httpVersion: '1.1'
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
const decision2 = await engine.processRequest(req2);
|
|
1750
|
+
expect(decision2.newCookieForResponse).toBeUndefined(); // Resumed successfully, no new cookie needed
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1638
1754
|
describe('getTlsSpoofingScore', () => {
|
|
1639
1755
|
let getTlsFingerprintMock; // Renamed to reflect it's the mock function
|
|
1640
1756
|
let getTlsSpoofingScore;
|
|
@@ -2367,6 +2483,80 @@ describe('Additional Suspicion Vectors Coverage', () => {
|
|
|
2367
2483
|
});
|
|
2368
2484
|
});
|
|
2369
2485
|
|
|
2486
|
+
describe('Ed25519 Asymmetric Tickets', () => {
|
|
2487
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
|
|
2488
|
+
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
|
|
2489
|
+
publicKeyEncoding: { format: 'pem', type: 'spki' }
|
|
2490
|
+
});
|
|
2491
|
+
const privateKeyPem = privateKey;
|
|
2492
|
+
const publicKeyPem = publicKey;
|
|
2493
|
+
|
|
2494
|
+
beforeEach(() => {
|
|
2495
|
+
process.env.ED25519_PRIVATE_KEY = privateKeyPem;
|
|
2496
|
+
process.env.ED25519_PUBLIC_KEY = publicKeyPem;
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
afterEach(() => {
|
|
2500
|
+
delete process.env.ED25519_PRIVATE_KEY;
|
|
2501
|
+
delete process.env.ED25519_PUBLIC_KEY;
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
it('should generate and validate an Ed25519 ticket successfully', async () => {
|
|
2505
|
+
const payload = {
|
|
2506
|
+
expiry: Date.now() + 3600000,
|
|
2507
|
+
originalIp: '127.0.0.1',
|
|
2508
|
+
deviceId: 'device-123',
|
|
2509
|
+
deviceHash: 'hash-abc'
|
|
2510
|
+
};
|
|
2511
|
+
|
|
2512
|
+
const ticket = fingerprint.generateStatelessTicket(payload);
|
|
2513
|
+
expect(ticket).toBeDefined();
|
|
2514
|
+
expect(ticket.startsWith('ed25519.')).toBe(true);
|
|
2515
|
+
|
|
2516
|
+
const isValid = await fingerprint.isTicketValid('127.0.0.1', ticket, 'device-123', 'hash-abc');
|
|
2517
|
+
expect(isValid).toBe(true);
|
|
2518
|
+
|
|
2519
|
+
const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
|
|
2520
|
+
expect(isDiffIpValid).toBe(false);
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
test('Cooperative PoSpace Workflow', async () => {
|
|
2524
|
+
const clientIp = '127.0.0.1';
|
|
2525
|
+
const nodeIdA = 'node-a';
|
|
2526
|
+
const seedA = 'seed-a';
|
|
2527
|
+
|
|
2528
|
+
// 1. Enregistrement du nœud A (pair)
|
|
2529
|
+
await __internal.registerCooperativeNode(clientIp, nodeIdA, seedA);
|
|
2530
|
+
|
|
2531
|
+
// 2. Recherche de pair pour le nœud B (dans le même sous-réseau)
|
|
2532
|
+
const peer = await __internal.findPeerInSubnet(clientIp, 'node-b');
|
|
2533
|
+
expect(peer).not.toBeNull();
|
|
2534
|
+
expect(peer.nodeId).toBe(nodeIdA);
|
|
2535
|
+
expect(peer.seed).toBe(seedA);
|
|
2536
|
+
|
|
2537
|
+
// 3. Demande de bloc du nœud B vers le nœud A
|
|
2538
|
+
const paramsReq = {
|
|
2539
|
+
coop_op: 'request_peer_block',
|
|
2540
|
+
node_id: 'node-b',
|
|
2541
|
+
peer_id: 'node-a',
|
|
2542
|
+
block_idx: '42',
|
|
2543
|
+
req_id: 'req-123'
|
|
2544
|
+
};
|
|
2545
|
+
const resReq = await __internal.handleCooperativeRequest(paramsReq, clientIp);
|
|
2546
|
+
expect(resReq.status).toBe('queued');
|
|
2547
|
+
|
|
2548
|
+
// 4. Récupération de la demande par le nœud A
|
|
2549
|
+
const paramsPoll = {
|
|
2550
|
+
coop_op: 'poll_requests',
|
|
2551
|
+
node_id: 'node-a'
|
|
2552
|
+
};
|
|
2553
|
+
const resPoll = await __internal.handleCooperativeRequest(paramsPoll, clientIp);
|
|
2554
|
+
expect(resPoll.requests.length).toBe(1);
|
|
2555
|
+
expect(resPoll.requests[0].req_id).toBe('req-123');
|
|
2556
|
+
expect(resPoll.requests[0].block_idx).toBe(42);
|
|
2557
|
+
});
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2370
2560
|
describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
2371
2561
|
const inMemoryStore = {
|
|
2372
2562
|
_map: new Map(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import {getTlsSpoofingScore, parseJa3} from '../fingerprint.js';
|
|
3
3
|
import {vi} from 'vitest';
|
|
4
|
+
import { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "../../js/fingerprint.utils.js";
|
|
4
5
|
|
|
5
6
|
describe('JA3 Anomaly Detector (Node.js)', () => {
|
|
6
7
|
|
package/src/php/AutoTuner.php
CHANGED
|
@@ -25,6 +25,11 @@ class AutoTuner
|
|
|
25
25
|
|
|
26
26
|
private int $minDataPoints;
|
|
27
27
|
private int $maxDataPoints;
|
|
28
|
+
private ?int $maxAgeMs;
|
|
29
|
+
private bool $clearAfterTuning;
|
|
30
|
+
/** @var ?callable */
|
|
31
|
+
private $onCleanup;
|
|
32
|
+
private ?string $savePath;
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
|
|
@@ -42,6 +47,48 @@ class AutoTuner
|
|
|
42
47
|
$this->trafficData = &$trafficData;
|
|
43
48
|
$this->minDataPoints = $options['minDataPoints'] ?? 200;
|
|
44
49
|
$this->maxDataPoints = $options['maxDataPoints'] ?? 10000;
|
|
50
|
+
$this->maxAgeMs = $options['maxAgeMs'] ?? null;
|
|
51
|
+
$this->clearAfterTuning = $options['clearAfterTuning'] ?? false;
|
|
52
|
+
$this->onCleanup = $options['onCleanup'] ?? null;
|
|
53
|
+
$this->savePath = $options['savePath'] ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prunes old or excess traffic logs to prevent memory leaks.
|
|
58
|
+
*/
|
|
59
|
+
private function pruneTrafficData(): void
|
|
60
|
+
{
|
|
61
|
+
$now = (int)(microtime(true) * 1000);
|
|
62
|
+
$removed = [];
|
|
63
|
+
|
|
64
|
+
// 1. Expire par temps (maxAgeMs)
|
|
65
|
+
if ($this->maxAgeMs !== null && $this->maxAgeMs > 0) {
|
|
66
|
+
$threshold = $now - $this->maxAgeMs;
|
|
67
|
+
foreach ($this->trafficData as $key => $log) {
|
|
68
|
+
$logTs = $log['timestamp'] ?? $log['requestTimestamp'] ?? $now;
|
|
69
|
+
if ($logTs < $threshold) {
|
|
70
|
+
$removed[] = $log;
|
|
71
|
+
unset($this->trafficData[$key]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
$this->trafficData = array_values($this->trafficData);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Politique de taille maximale (maxDataPoints)
|
|
78
|
+
if ($this->maxDataPoints > 0 && count($this->trafficData) > $this->maxDataPoints) {
|
|
79
|
+
$overflowCount = count($this->trafficData) - $this->maxDataPoints;
|
|
80
|
+
$spliced = array_splice($this->trafficData, 0, $overflowCount);
|
|
81
|
+
$removed = array_merge($removed, $spliced);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Invocation du callback onCleanup
|
|
85
|
+
if ($this->onCleanup !== null && is_callable($this->onCleanup) && !empty($removed)) {
|
|
86
|
+
try {
|
|
87
|
+
call_user_func($this->onCleanup, $removed);
|
|
88
|
+
} catch (\Throwable $e) {
|
|
89
|
+
error_log("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
45
92
|
}
|
|
46
93
|
|
|
47
94
|
/**
|
|
@@ -49,6 +96,8 @@ class AutoTuner
|
|
|
49
96
|
*/
|
|
50
97
|
public function runOptimizationCycle(): void
|
|
51
98
|
{
|
|
99
|
+
$this->pruneTrafficData();
|
|
100
|
+
|
|
52
101
|
$sanitizedData = RequestUtils::sanitizeTrafficData($this->trafficData);
|
|
53
102
|
|
|
54
103
|
$highConfidenceLogs = count(array_filter(
|
|
@@ -77,7 +126,7 @@ class AutoTuner
|
|
|
77
126
|
|
|
78
127
|
echo sprintf("[AutoTuning] Démarrage du cycle d'optimisation complet avec %d points de données assainis.\n", count($sanitizedData));
|
|
79
128
|
|
|
80
|
-
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData]);
|
|
129
|
+
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
|
|
81
130
|
|
|
82
131
|
if (empty($paretoFront)) {
|
|
83
132
|
echo "[AutoTuning] L'optimisation n'a retourné aucune solution.\n";
|
|
@@ -176,6 +225,28 @@ class AutoTuner
|
|
|
176
225
|
echo "[AutoTuning] Nouveaux seuils : " . json_encode($this->securityConfig['thresholds']) . "\n";
|
|
177
226
|
echo "[AutoTuning] Nouveaux poids : " . json_encode($this->securityConfig['weights']) . "\n";
|
|
178
227
|
echo "[AutoTuning] Nouveaux patterns : " . json_encode($this->securityConfig['patterns']) . "\n";
|
|
228
|
+
|
|
229
|
+
// NOUVEAU: Sauvegarder la meilleure configuration si un chemin est fourni.
|
|
230
|
+
if ($this->savePath !== null) {
|
|
231
|
+
try {
|
|
232
|
+
file_put_contents($this->savePath, json_encode($bestSolution['solution'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
233
|
+
echo "[AutoTuning] Meilleure configuration sauvegardée dans : {$this->savePath}\n";
|
|
234
|
+
} catch (\Throwable $e) {
|
|
235
|
+
error_log("[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : " . $e->getMessage());
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if ($this->clearAfterTuning) {
|
|
240
|
+
$cleared = array_splice($this->trafficData, 0);
|
|
241
|
+
if ($this->onCleanup !== null && is_callable($this->onCleanup) && !empty($cleared)) {
|
|
242
|
+
try {
|
|
243
|
+
call_user_func($this->onCleanup, $cleared);
|
|
244
|
+
} catch (\Throwable $e) {
|
|
245
|
+
error_log("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
echo sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared));
|
|
249
|
+
}
|
|
179
250
|
}
|
|
180
251
|
|
|
181
252
|
/**
|