@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.
@@ -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
 
@@ -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
  /**