yakmesh 1.1.0 → 1.3.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.
@@ -0,0 +1,700 @@
1
+ /**
2
+ * YAKMESH™ PHANTOM - Post-quantum Hidden Anonymous Network Transmission Over Mesh
3
+ *
4
+ * The first post-quantum secure onion routing implementation featuring:
5
+ * - ML-DSA-65 signatures at every routing layer
6
+ * - Kyber (ML-KEM) key encapsulation for quantum-safe key exchange
7
+ * - Multi-layer encryption with perfect forward secrecy
8
+ * - Timing attack resistance through temporal padding
9
+ *
10
+ * Key Innovation: "Your packets become ghosts"
11
+ * - Each routing layer uses different quantum-resistant keys
12
+ * - Decoy traffic masks real communication patterns
13
+ * - Temporal obfuscation defeats traffic analysis
14
+ *
15
+ * @module mesh/phantom-routing
16
+ * @license MIT
17
+ * @copyright 2026 YAKMESH Contributors
18
+ * @trademark PHANTOM™ is a trademark of YAKMESH
19
+ */
20
+
21
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
22
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
23
+ import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
24
+ import { sha3_256 } from '@noble/hashes/sha3.js';
25
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
26
+
27
+ const PHANTOM_CONFIG = {
28
+ // Circuit settings
29
+ defaultHopCount: 3, // Number of hops (like Tor)
30
+ maxHopCount: 7, // Maximum allowed hops
31
+ circuitTimeout: 300000, // 5 minute circuit lifetime
32
+
33
+ // Encryption
34
+ layerEncryption: 'aes-256-gcm',
35
+ nonceSize: 12,
36
+ authTagLength: 16,
37
+
38
+ // Timing obfuscation
39
+ minPaddingMs: 10, // Minimum random delay
40
+ maxPaddingMs: 100, // Maximum random delay
41
+ decoyProbability: 0.1, // 10% chance of sending decoy
42
+
43
+ // Packet sizing
44
+ fixedPacketSize: 8192, // Fixed size to prevent length analysis
45
+ maxPayloadSize: 7000, // Max actual payload
46
+
47
+ // Key derivation
48
+ keyDerivationSalt: 'PHANTOM-YAKMESH-2026',
49
+ };
50
+
51
+ /**
52
+ * A single routing layer in the onion
53
+ */
54
+ class PhantomLayer {
55
+ constructor(options) {
56
+ this.hopIndex = options.hopIndex;
57
+ this.nodeId = options.nodeId;
58
+ this.nextHop = options.nextHop || null;
59
+ this.isExit = options.isExit || false;
60
+
61
+ // Ephemeral keys for this layer
62
+ this.kemKeyPair = null;
63
+ this.sharedSecret = null;
64
+ this.encryptionKey = null;
65
+ }
66
+
67
+ /**
68
+ * Generate ephemeral key pair for key encapsulation
69
+ */
70
+ async generateKeys() {
71
+ const seed = randomBytes(64);
72
+ this.kemKeyPair = ml_kem768.keygen(seed);
73
+ return {
74
+ publicKey: bytesToHex(this.kemKeyPair.publicKey),
75
+ hopIndex: this.hopIndex,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Encapsulate key using peer's public key
81
+ */
82
+ encapsulateKey(peerPublicKey) {
83
+ const publicKeyBytes = hexToBytes(peerPublicKey);
84
+ const result = ml_kem768.encapsulate(publicKeyBytes);
85
+
86
+ this.sharedSecret = result.sharedSecret;
87
+ this.encryptionKey = this._deriveEncryptionKey(this.sharedSecret);
88
+
89
+ return {
90
+ ciphertext: bytesToHex(result.ciphertext),
91
+ hopIndex: this.hopIndex,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Decapsulate key from received ciphertext
97
+ */
98
+ decapsulateKey(ciphertext) {
99
+ if (!this.kemKeyPair) {
100
+ throw new Error('No key pair generated');
101
+ }
102
+
103
+ const ciphertextBytes = hexToBytes(ciphertext);
104
+ this.sharedSecret = ml_kem768.decapsulate(ciphertextBytes, this.kemKeyPair.secretKey);
105
+ this.encryptionKey = this._deriveEncryptionKey(this.sharedSecret);
106
+
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Encrypt data for this layer
112
+ */
113
+ encrypt(data) {
114
+ if (!this.encryptionKey) {
115
+ throw new Error('No encryption key established');
116
+ }
117
+
118
+ const nonce = randomBytes(PHANTOM_CONFIG.nonceSize);
119
+ const cipher = createCipheriv(
120
+ PHANTOM_CONFIG.layerEncryption,
121
+ this.encryptionKey,
122
+ nonce,
123
+ { authTagLength: PHANTOM_CONFIG.authTagLength }
124
+ );
125
+
126
+ const plaintext = typeof data === 'string' ? data : JSON.stringify(data);
127
+ const encrypted = Buffer.concat([
128
+ cipher.update(plaintext, 'utf8'),
129
+ cipher.final(),
130
+ ]);
131
+
132
+ return {
133
+ nonce: nonce.toString('hex'),
134
+ data: encrypted.toString('hex'),
135
+ tag: cipher.getAuthTag().toString('hex'),
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Decrypt data for this layer
141
+ */
142
+ decrypt(encryptedData) {
143
+ if (!this.encryptionKey) {
144
+ throw new Error('No encryption key established');
145
+ }
146
+
147
+ const nonce = Buffer.from(encryptedData.nonce, 'hex');
148
+ const data = Buffer.from(encryptedData.data, 'hex');
149
+ const tag = Buffer.from(encryptedData.tag, 'hex');
150
+
151
+ const decipher = createDecipheriv(
152
+ PHANTOM_CONFIG.layerEncryption,
153
+ this.encryptionKey,
154
+ nonce,
155
+ { authTagLength: PHANTOM_CONFIG.authTagLength }
156
+ );
157
+ decipher.setAuthTag(tag);
158
+
159
+ const decrypted = Buffer.concat([
160
+ decipher.update(data),
161
+ decipher.final(),
162
+ ]);
163
+
164
+ return decrypted.toString('utf8');
165
+ }
166
+
167
+ _deriveEncryptionKey(sharedSecret) {
168
+ return createHash('sha256')
169
+ .update(sharedSecret)
170
+ .update(PHANTOM_CONFIG.keyDerivationSalt)
171
+ .update(Buffer.from([this.hopIndex]))
172
+ .digest();
173
+ }
174
+ }
175
+
176
+ /**
177
+ * An onion-wrapped packet
178
+ */
179
+ class PhantomPacket {
180
+ constructor(options = {}) {
181
+ this.id = options.id || bytesToHex(randomBytes(16));
182
+ this.circuitId = options.circuitId;
183
+ this.layers = []; // Encrypted layers (outermost first)
184
+ this.timestamp = options.timestamp || Date.now();
185
+ this.isDecoy = options.isDecoy || false;
186
+ this.padding = null; // Random padding for fixed size
187
+ }
188
+
189
+ /**
190
+ * Add encrypted layer (wrap the onion)
191
+ */
192
+ addLayer(encryptedPayload) {
193
+ this.layers.unshift(encryptedPayload);
194
+ }
195
+
196
+ /**
197
+ * Remove outermost layer (peel the onion)
198
+ */
199
+ peelLayer() {
200
+ return this.layers.shift();
201
+ }
202
+
203
+ /**
204
+ * Pad to fixed size to prevent length analysis
205
+ */
206
+ padToFixedSize() {
207
+ const serialized = JSON.stringify({
208
+ id: this.id,
209
+ circuitId: this.circuitId,
210
+ layers: this.layers,
211
+ timestamp: this.timestamp,
212
+ });
213
+
214
+ const currentSize = Buffer.byteLength(serialized, 'utf8');
215
+ const paddingNeeded = PHANTOM_CONFIG.fixedPacketSize - currentSize - 50; // Reserve for padding field
216
+
217
+ if (paddingNeeded > 0) {
218
+ this.padding = randomBytes(Math.max(1, paddingNeeded)).toString('base64');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Create decoy packet
224
+ */
225
+ static createDecoy(circuitId) {
226
+ const decoy = new PhantomPacket({
227
+ circuitId,
228
+ isDecoy: true,
229
+ });
230
+
231
+ // Fill with random encrypted-looking data
232
+ for (let i = 0; i < 3; i++) {
233
+ decoy.addLayer({
234
+ nonce: randomBytes(12).toString('hex'),
235
+ data: randomBytes(256).toString('hex'),
236
+ tag: randomBytes(16).toString('hex'),
237
+ });
238
+ }
239
+
240
+ decoy.padToFixedSize();
241
+ return decoy;
242
+ }
243
+
244
+ serialize() {
245
+ return {
246
+ id: this.id,
247
+ circuitId: this.circuitId,
248
+ layers: this.layers,
249
+ timestamp: this.timestamp,
250
+ padding: this.padding,
251
+ };
252
+ }
253
+
254
+ static deserialize(obj) {
255
+ const packet = new PhantomPacket({
256
+ id: obj.id,
257
+ circuitId: obj.circuitId,
258
+ timestamp: obj.timestamp,
259
+ });
260
+ packet.layers = obj.layers;
261
+ packet.padding = obj.padding;
262
+ return packet;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * A circuit through the mesh (like a Tor circuit)
268
+ */
269
+ class PhantomCircuit {
270
+ constructor(options = {}) {
271
+ this.circuitId = options.circuitId || bytesToHex(randomBytes(16));
272
+ this.hops = []; // Array of PhantomLayer
273
+ this.isEstablished = false;
274
+ this.createdAt = Date.now();
275
+ this.lastUsed = Date.now();
276
+ this.packetsForwarded = 0;
277
+ }
278
+
279
+ /**
280
+ * Build a circuit through specified nodes
281
+ */
282
+ async buildCircuit(nodeIds) {
283
+ if (nodeIds.length > PHANTOM_CONFIG.maxHopCount) {
284
+ throw new Error('Too many hops: max is ' + PHANTOM_CONFIG.maxHopCount);
285
+ }
286
+
287
+ this.hops = [];
288
+
289
+ for (let i = 0; i < nodeIds.length; i++) {
290
+ const layer = new PhantomLayer({
291
+ hopIndex: i,
292
+ nodeId: nodeIds[i],
293
+ nextHop: nodeIds[i + 1] || null,
294
+ isExit: i === nodeIds.length - 1,
295
+ });
296
+
297
+ await layer.generateKeys();
298
+ this.hops.push(layer);
299
+ }
300
+
301
+ return {
302
+ circuitId: this.circuitId,
303
+ hops: this.hops.map(h => ({
304
+ hopIndex: h.hopIndex,
305
+ nodeId: h.nodeId,
306
+ publicKey: bytesToHex(h.kemKeyPair.publicKey),
307
+ })),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Establish keys with each hop using their public keys
313
+ */
314
+ establishKeys(hopPublicKeys) {
315
+ const ciphertexts = [];
316
+
317
+ for (const hop of this.hops) {
318
+ const peerKey = hopPublicKeys.find(k => k.hopIndex === hop.hopIndex);
319
+ if (!peerKey) {
320
+ throw new Error('Missing public key for hop ' + hop.hopIndex);
321
+ }
322
+
323
+ const result = hop.encapsulateKey(peerKey.publicKey);
324
+ ciphertexts.push(result);
325
+ }
326
+
327
+ this.isEstablished = true;
328
+ return ciphertexts;
329
+ }
330
+
331
+ /**
332
+ * Wrap a message in multiple encryption layers
333
+ */
334
+ wrapMessage(message, destination) {
335
+ if (!this.isEstablished) {
336
+ throw new Error('Circuit not established');
337
+ }
338
+
339
+ // Start with the innermost layer (exit node message)
340
+ let payload = {
341
+ type: 'DATA',
342
+ destination,
343
+ message,
344
+ timestamp: Date.now(),
345
+ };
346
+
347
+ // Wrap in layers from inside out (exit first, entry last)
348
+ for (let i = this.hops.length - 1; i >= 0; i--) {
349
+ const hop = this.hops[i];
350
+ const encrypted = hop.encrypt(payload);
351
+
352
+ payload = {
353
+ type: 'RELAY',
354
+ nextHop: hop.nextHop,
355
+ isExit: hop.isExit,
356
+ encrypted,
357
+ };
358
+ }
359
+
360
+ const packet = new PhantomPacket({
361
+ circuitId: this.circuitId,
362
+ });
363
+ packet.addLayer(payload);
364
+ packet.padToFixedSize();
365
+
366
+ this.lastUsed = Date.now();
367
+ this.packetsForwarded++;
368
+
369
+ return packet;
370
+ }
371
+
372
+ isExpired() {
373
+ return Date.now() - this.createdAt > PHANTOM_CONFIG.circuitTimeout;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Relay node handler for forwarding phantom packets
379
+ */
380
+ class PhantomRelay {
381
+ constructor(options = {}) {
382
+ this.nodeId = options.nodeId || bytesToHex(randomBytes(16));
383
+ this.circuits = new Map(); // circuitId -> local layer info
384
+ this.signKeyPair = null; // ML-DSA-65 for signing
385
+
386
+ this.stats = {
387
+ packetsRelayed: 0,
388
+ circuitsHandled: 0,
389
+ decoysInjected: 0,
390
+ };
391
+
392
+ // Initialize signing keys
393
+ this._initKeys();
394
+ }
395
+
396
+ async _initKeys() {
397
+ const seed = randomBytes(32);
398
+ this.signKeyPair = ml_dsa65.keygen(seed);
399
+ }
400
+
401
+ /**
402
+ * Handle incoming circuit creation request
403
+ */
404
+ async handleCircuitCreate(request) {
405
+ const layer = new PhantomLayer({
406
+ hopIndex: request.hopIndex,
407
+ nodeId: this.nodeId,
408
+ nextHop: request.nextHop,
409
+ isExit: request.isExit,
410
+ });
411
+
412
+ await layer.generateKeys();
413
+
414
+ this.circuits.set(request.circuitId, {
415
+ layer,
416
+ createdAt: Date.now(),
417
+ packetsRelayed: 0,
418
+ });
419
+
420
+ this.stats.circuitsHandled++;
421
+
422
+ return {
423
+ circuitId: request.circuitId,
424
+ hopIndex: request.hopIndex,
425
+ publicKey: bytesToHex(layer.kemKeyPair.publicKey),
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Handle key establishment
431
+ */
432
+ handleKeyEstablish(circuitId, ciphertext) {
433
+ const circuit = this.circuits.get(circuitId);
434
+ if (!circuit) {
435
+ return { error: 'Unknown circuit' };
436
+ }
437
+
438
+ circuit.layer.decapsulateKey(ciphertext);
439
+ return { success: true };
440
+ }
441
+
442
+ /**
443
+ * Process incoming packet (peel and forward)
444
+ */
445
+ async processPacket(packet) {
446
+ const circuit = this.circuits.get(packet.circuitId);
447
+ if (!circuit) {
448
+ return { error: 'Unknown circuit' };
449
+ }
450
+
451
+ // Add timing obfuscation
452
+ await this._addTimingDelay();
453
+
454
+ // Peel the outermost layer
455
+ const encryptedLayer = packet.peelLayer();
456
+ if (!encryptedLayer) {
457
+ return { error: 'No layers to peel' };
458
+ }
459
+
460
+ try {
461
+ const decrypted = JSON.parse(circuit.layer.decrypt(encryptedLayer.encrypted));
462
+ circuit.packetsRelayed++;
463
+ this.stats.packetsRelayed++;
464
+
465
+ // Maybe inject a decoy
466
+ const decoy = this._maybeInjectDecoy(packet.circuitId);
467
+
468
+ if (decrypted.isExit) {
469
+ // We're the exit node - deliver the message
470
+ return {
471
+ type: 'EXIT',
472
+ destination: decrypted.destination,
473
+ message: decrypted.message,
474
+ timestamp: decrypted.timestamp,
475
+ decoy,
476
+ };
477
+ } else {
478
+ // Forward to next hop
479
+ return {
480
+ type: 'FORWARD',
481
+ nextHop: decrypted.nextHop,
482
+ packet: packet.serialize(),
483
+ decoy,
484
+ };
485
+ }
486
+ } catch (err) {
487
+ return { error: 'Decryption failed: ' + err.message };
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Add random delay to defeat timing analysis
493
+ */
494
+ async _addTimingDelay() {
495
+ const delay = PHANTOM_CONFIG.minPaddingMs +
496
+ Math.random() * (PHANTOM_CONFIG.maxPaddingMs - PHANTOM_CONFIG.minPaddingMs);
497
+ await new Promise(resolve => setTimeout(resolve, delay));
498
+ }
499
+
500
+ /**
501
+ * Maybe inject a decoy packet to mask traffic patterns
502
+ */
503
+ _maybeInjectDecoy(circuitId) {
504
+ if (Math.random() < PHANTOM_CONFIG.decoyProbability) {
505
+ this.stats.decoysInjected++;
506
+ return PhantomPacket.createDecoy(circuitId).serialize();
507
+ }
508
+ return null;
509
+ }
510
+
511
+ /**
512
+ * Sign data with ML-DSA-65
513
+ */
514
+ sign(data) {
515
+ const message = typeof data === 'string' ? utf8ToBytes(data) : data;
516
+ return bytesToHex(ml_dsa65.sign(this.signKeyPair.secretKey, message));
517
+ }
518
+
519
+ /**
520
+ * Verify signature
521
+ */
522
+ verify(data, signature, publicKey) {
523
+ const message = typeof data === 'string' ? utf8ToBytes(data) : data;
524
+ return ml_dsa65.verify(
525
+ hexToBytes(publicKey),
526
+ message,
527
+ hexToBytes(signature)
528
+ );
529
+ }
530
+
531
+ getPublicKey() {
532
+ return bytesToHex(this.signKeyPair.publicKey);
533
+ }
534
+
535
+ getStats() {
536
+ return { ...this.stats };
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Main PHANTOM routing manager
542
+ */
543
+ class PhantomRouter {
544
+ constructor(options = {}) {
545
+ this.nodeId = options.nodeId || bytesToHex(randomBytes(16));
546
+ this.relay = new PhantomRelay({ nodeId: this.nodeId });
547
+ this.circuits = new Map(); // circuitId -> PhantomCircuit (for circuits we created)
548
+ this.knownNodes = new Map(); // nodeId -> { publicKey, lastSeen }
549
+
550
+ this.stats = {
551
+ circuitsCreated: 0,
552
+ messagesSent: 0,
553
+ messagesReceived: 0,
554
+ };
555
+
556
+ // Callbacks
557
+ this.onMessageReceived = options.onMessageReceived || (() => {});
558
+ this.onForward = options.onForward || (() => {});
559
+ }
560
+
561
+ /**
562
+ * Register a known node for routing
563
+ */
564
+ registerNode(nodeId, publicKey) {
565
+ this.knownNodes.set(nodeId, {
566
+ publicKey,
567
+ lastSeen: Date.now(),
568
+ });
569
+ }
570
+
571
+ /**
572
+ * Create a new circuit through specified hops
573
+ */
574
+ async createCircuit(hopNodeIds) {
575
+ if (hopNodeIds.length === 0) {
576
+ // Auto-select random hops
577
+ const availableNodes = Array.from(this.knownNodes.keys())
578
+ .filter(id => id !== this.nodeId);
579
+
580
+ if (availableNodes.length < PHANTOM_CONFIG.defaultHopCount) {
581
+ throw new Error('Not enough known nodes for circuit');
582
+ }
583
+
584
+ // Shuffle and pick
585
+ for (let i = availableNodes.length - 1; i > 0; i--) {
586
+ const j = Math.floor(Math.random() * (i + 1));
587
+ [availableNodes[i], availableNodes[j]] = [availableNodes[j], availableNodes[i]];
588
+ }
589
+
590
+ hopNodeIds = availableNodes.slice(0, PHANTOM_CONFIG.defaultHopCount);
591
+ }
592
+
593
+ const circuit = new PhantomCircuit();
594
+ const buildResult = await circuit.buildCircuit(hopNodeIds);
595
+
596
+ this.circuits.set(circuit.circuitId, circuit);
597
+ this.stats.circuitsCreated++;
598
+
599
+ return buildResult;
600
+ }
601
+
602
+ /**
603
+ * Complete circuit establishment with hop responses
604
+ */
605
+ establishCircuit(circuitId, hopResponses) {
606
+ const circuit = this.circuits.get(circuitId);
607
+ if (!circuit) {
608
+ throw new Error('Unknown circuit');
609
+ }
610
+
611
+ const ciphertexts = circuit.establishKeys(hopResponses);
612
+ return { circuitId, established: true, ciphertexts };
613
+ }
614
+
615
+ /**
616
+ * Send a message through a circuit
617
+ */
618
+ sendMessage(circuitId, destination, message) {
619
+ const circuit = this.circuits.get(circuitId);
620
+ if (!circuit) {
621
+ throw new Error('Unknown circuit');
622
+ }
623
+
624
+ if (circuit.isExpired()) {
625
+ this.circuits.delete(circuitId);
626
+ throw new Error('Circuit expired');
627
+ }
628
+
629
+ const packet = circuit.wrapMessage(message, destination);
630
+ this.stats.messagesSent++;
631
+
632
+ return {
633
+ packet: packet.serialize(),
634
+ firstHop: circuit.hops[0].nodeId,
635
+ };
636
+ }
637
+
638
+ /**
639
+ * Handle incoming packet (as a relay)
640
+ */
641
+ async handlePacket(packetData) {
642
+ const packet = PhantomPacket.deserialize(packetData);
643
+ const result = await this.relay.processPacket(packet);
644
+
645
+ if (result.type === 'EXIT') {
646
+ this.stats.messagesReceived++;
647
+ this.onMessageReceived(result);
648
+ } else if (result.type === 'FORWARD') {
649
+ this.onForward(result);
650
+ }
651
+
652
+ return result;
653
+ }
654
+
655
+ /**
656
+ * Handle circuit creation request (as a relay)
657
+ */
658
+ async handleCircuitCreate(request) {
659
+ return this.relay.handleCircuitCreate(request);
660
+ }
661
+
662
+ /**
663
+ * Handle key establishment (as a relay)
664
+ */
665
+ handleKeyEstablish(circuitId, ciphertext) {
666
+ return this.relay.handleKeyEstablish(circuitId, ciphertext);
667
+ }
668
+
669
+ /**
670
+ * Cleanup expired circuits
671
+ */
672
+ cleanupCircuits() {
673
+ let cleaned = 0;
674
+ for (const [id, circuit] of this.circuits) {
675
+ if (circuit.isExpired()) {
676
+ this.circuits.delete(id);
677
+ cleaned++;
678
+ }
679
+ }
680
+ return cleaned;
681
+ }
682
+
683
+ getStats() {
684
+ return {
685
+ ...this.stats,
686
+ relayStats: this.relay.getStats(),
687
+ activeCircuits: this.circuits.size,
688
+ knownNodes: this.knownNodes.size,
689
+ };
690
+ }
691
+ }
692
+
693
+ export {
694
+ PHANTOM_CONFIG,
695
+ PhantomLayer,
696
+ PhantomPacket,
697
+ PhantomCircuit,
698
+ PhantomRelay,
699
+ PhantomRouter,
700
+ };