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.
- package/CHANGELOG.md +65 -75
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/discord.md +74 -0
- package/mesh/beacon-broadcast.js +655 -0
- package/mesh/echo-ranging.js +612 -0
- package/mesh/phantom-routing.js +700 -0
- package/mesh/pulse-sync.js +618 -0
- package/mesh/temporal-encoder.js +383 -0
- package/package.json +82 -51
- package/test-novel-systems.mjs +398 -0
- package/test-tme.mjs +383 -0
|
@@ -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
|
+
};
|