yakmesh 1.3.1 β 1.4.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 +20 -0
- package/README.md +6 -0
- package/announcements/discord-v1.3.1.md +55 -0
- package/content/api.js +348 -0
- package/content/index.js +19 -0
- package/content/store.js +683 -0
- package/deploy-packages/README.md +114 -0
- package/deploy-packages/build-packages.ps1 +205 -0
- package/deploy-packages/yakmesh-full/README.md +122 -0
- package/deploy-packages/yakmesh-full/config/Caddyfile +61 -0
- package/deploy-packages/yakmesh-full/config/php.ini +55 -0
- package/deploy-packages/yakmesh-full/config/yakmesh.config.js +100 -0
- package/deploy-packages/yakmesh-full/start.ps1 +193 -0
- package/deploy-packages/yakmesh-full/stop.ps1 +35 -0
- package/deploy-packages/yakmesh-minimal/README.md +69 -0
- package/deploy-packages/yakmesh-minimal/config/Caddyfile +46 -0
- package/deploy-packages/yakmesh-minimal/config/yakmesh.config.js +79 -0
- package/deploy-packages/yakmesh-minimal/start.ps1 +165 -0
- package/deploy-packages/yakmesh-minimal/stop.ps1 +29 -0
- package/discord.md +13 -58
- package/gossip/protocol.js +13 -2
- package/mesh/annex.js +743 -0
- package/mesh/beacon-broadcast.js +4 -5
- package/mesh/echo-ranging.js +3 -4
- package/mesh/phantom-routing.js +3 -4
- package/mesh/pulse-sync.js +3 -4
- package/mesh/temporal-encoder.js +3 -3
- package/package.json +1 -1
- package/server/index.js +64 -3
- package/yakmesh.config.js +8 -0
package/mesh/annex.js
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yakmesh Annex - Autonomous Network Negotiated Encrypted eXchange
|
|
3
|
+
*
|
|
4
|
+
* End-to-end encrypted point-to-point communication between mesh peers using:
|
|
5
|
+
* - ML-KEM768 (Kyber) for quantum-resistant key encapsulation
|
|
6
|
+
* - AES-256-GCM for authenticated symmetric encryption
|
|
7
|
+
* - ML-DSA-65 signatures for sender authentication
|
|
8
|
+
* - Perfect forward secrecy via ephemeral key exchange
|
|
9
|
+
*
|
|
10
|
+
* Key Innovation: "Changes pass through math alone"
|
|
11
|
+
* - All payload transformations are cryptographically autonomous
|
|
12
|
+
* - No plaintext touches the network - only ciphertext
|
|
13
|
+
* - Recipients prove possession of private key to decrypt
|
|
14
|
+
* - Sovereignty over your data channel
|
|
15
|
+
*
|
|
16
|
+
* Use Cases:
|
|
17
|
+
* - Authenticated content delivery (vs public gossip)
|
|
18
|
+
* - Beacon acknowledgments with encryption
|
|
19
|
+
* - App-specific private payloads
|
|
20
|
+
* - Site/CDN authentication tokens
|
|
21
|
+
* - Direct peer-to-peer secure messaging
|
|
22
|
+
*
|
|
23
|
+
* @module mesh/annex
|
|
24
|
+
* @license MIT
|
|
25
|
+
* @copyright 2026 YAKMESHβ’ Contributors
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
29
|
+
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
30
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
31
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
32
|
+
|
|
33
|
+
const ANNEX_CONFIG = {
|
|
34
|
+
// Encryption
|
|
35
|
+
symmetricAlgorithm: 'aes-256-gcm',
|
|
36
|
+
nonceSize: 12,
|
|
37
|
+
authTagLength: 16,
|
|
38
|
+
|
|
39
|
+
// Key derivation
|
|
40
|
+
keyDerivationSalt: 'YAKMESH-ANNEX-2026',
|
|
41
|
+
contextSeparator: ':',
|
|
42
|
+
|
|
43
|
+
// Session management
|
|
44
|
+
sessionTimeout: 3600000, // 1 hour session lifetime
|
|
45
|
+
rekeyInterval: 300000, // Re-key every 5 minutes for PFS
|
|
46
|
+
maxMessagesPerKey: 10000, // Force re-key after N messages
|
|
47
|
+
|
|
48
|
+
// Message types
|
|
49
|
+
messageTypes: {
|
|
50
|
+
KEY_EXCHANGE: 'annex:key_exchange',
|
|
51
|
+
KEY_RESPONSE: 'annex:key_response',
|
|
52
|
+
ENCRYPTED: 'annex:encrypted',
|
|
53
|
+
REKEY: 'annex:rekey',
|
|
54
|
+
CLOSE: 'annex:close',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Encrypted message envelope - the quantum-sealed diplomatic pouch
|
|
60
|
+
*/
|
|
61
|
+
class AnnexEnvelope {
|
|
62
|
+
constructor(options) {
|
|
63
|
+
this.id = options.id || bytesToHex(randomBytes(16));
|
|
64
|
+
this.type = options.type || ANNEX_CONFIG.messageTypes.ENCRYPTED;
|
|
65
|
+
this.senderId = options.senderId;
|
|
66
|
+
this.recipientId = options.recipientId;
|
|
67
|
+
this.sessionId = options.sessionId;
|
|
68
|
+
this.sequence = options.sequence || 0;
|
|
69
|
+
this.timestamp = options.timestamp || Date.now();
|
|
70
|
+
|
|
71
|
+
// Encrypted payload components
|
|
72
|
+
this.nonce = options.nonce || null;
|
|
73
|
+
this.ciphertext = options.ciphertext || null;
|
|
74
|
+
this.authTag = options.authTag || null;
|
|
75
|
+
|
|
76
|
+
// Key exchange components (only for key exchange messages)
|
|
77
|
+
this.kemCiphertext = options.kemCiphertext || null;
|
|
78
|
+
this.kemPublicKey = options.kemPublicKey || null;
|
|
79
|
+
|
|
80
|
+
// Signature (ML-DSA-65)
|
|
81
|
+
this.signature = options.signature || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Compute hash for signing
|
|
86
|
+
*/
|
|
87
|
+
getSigningPayload() {
|
|
88
|
+
return JSON.stringify({
|
|
89
|
+
id: this.id,
|
|
90
|
+
type: this.type,
|
|
91
|
+
senderId: this.senderId,
|
|
92
|
+
recipientId: this.recipientId,
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
sequence: this.sequence,
|
|
95
|
+
timestamp: this.timestamp,
|
|
96
|
+
nonce: this.nonce,
|
|
97
|
+
ciphertext: this.ciphertext,
|
|
98
|
+
authTag: this.authTag,
|
|
99
|
+
kemCiphertext: this.kemCiphertext,
|
|
100
|
+
kemPublicKey: this.kemPublicKey,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Serialize for transport
|
|
106
|
+
*/
|
|
107
|
+
toJSON() {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
type: this.type,
|
|
111
|
+
senderId: this.senderId,
|
|
112
|
+
recipientId: this.recipientId,
|
|
113
|
+
sessionId: this.sessionId,
|
|
114
|
+
sequence: this.sequence,
|
|
115
|
+
timestamp: this.timestamp,
|
|
116
|
+
nonce: this.nonce,
|
|
117
|
+
ciphertext: this.ciphertext,
|
|
118
|
+
authTag: this.authTag,
|
|
119
|
+
kemCiphertext: this.kemCiphertext,
|
|
120
|
+
kemPublicKey: this.kemPublicKey,
|
|
121
|
+
signature: this.signature,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static fromJSON(data) {
|
|
126
|
+
return new AnnexEnvelope(data);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* An annexed territory - a secure session with a single peer
|
|
132
|
+
*/
|
|
133
|
+
class AnnexSession {
|
|
134
|
+
constructor(options) {
|
|
135
|
+
this.sessionId = options.sessionId || bytesToHex(randomBytes(16));
|
|
136
|
+
this.localNodeId = options.localNodeId;
|
|
137
|
+
this.remoteNodeId = options.remoteNodeId;
|
|
138
|
+
this.initiator = options.initiator || false;
|
|
139
|
+
|
|
140
|
+
// Key material
|
|
141
|
+
this.kemKeyPair = null; // Our ephemeral KEM key pair
|
|
142
|
+
this.sharedSecret = null; // Derived shared secret
|
|
143
|
+
this.encryptionKey = null; // Current symmetric key
|
|
144
|
+
this.sendSequence = 0; // Outbound message counter
|
|
145
|
+
this.recvSequence = 0; // Inbound message counter
|
|
146
|
+
this.messageCount = 0; // Total messages with current key
|
|
147
|
+
|
|
148
|
+
// State
|
|
149
|
+
this.established = false;
|
|
150
|
+
this.createdAt = Date.now();
|
|
151
|
+
this.lastActivity = Date.now();
|
|
152
|
+
this.lastRekey = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate ephemeral KEM key pair for this session
|
|
157
|
+
*/
|
|
158
|
+
generateKeyPair() {
|
|
159
|
+
const seed = randomBytes(64);
|
|
160
|
+
this.kemKeyPair = ml_kem768.keygen(seed);
|
|
161
|
+
return bytesToHex(this.kemKeyPair.publicKey);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Complete key exchange as initiator (encapsulate with peer's public key)
|
|
166
|
+
*/
|
|
167
|
+
encapsulate(peerPublicKey) {
|
|
168
|
+
const publicKeyBytes = hexToBytes(peerPublicKey);
|
|
169
|
+
const result = ml_kem768.encapsulate(publicKeyBytes);
|
|
170
|
+
|
|
171
|
+
this.sharedSecret = result.sharedSecret;
|
|
172
|
+
this.encryptionKey = this._deriveEncryptionKey();
|
|
173
|
+
this.established = true;
|
|
174
|
+
this.lastRekey = Date.now();
|
|
175
|
+
|
|
176
|
+
return bytesToHex(result.ciphertext);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Complete key exchange as responder (decapsulate received ciphertext)
|
|
181
|
+
*/
|
|
182
|
+
decapsulate(ciphertext) {
|
|
183
|
+
if (!this.kemKeyPair) {
|
|
184
|
+
throw new Error('No key pair generated');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ciphertextBytes = hexToBytes(ciphertext);
|
|
188
|
+
this.sharedSecret = ml_kem768.decapsulate(ciphertextBytes, this.kemKeyPair.secretKey);
|
|
189
|
+
this.encryptionKey = this._deriveEncryptionKey();
|
|
190
|
+
this.established = true;
|
|
191
|
+
this.lastRekey = Date.now();
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Encrypt a message for this session
|
|
198
|
+
*/
|
|
199
|
+
encrypt(plaintext) {
|
|
200
|
+
if (!this.established || !this.encryptionKey) {
|
|
201
|
+
throw new Error('Session not established');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const nonce = randomBytes(ANNEX_CONFIG.nonceSize);
|
|
205
|
+
const cipher = createCipheriv(
|
|
206
|
+
ANNEX_CONFIG.symmetricAlgorithm,
|
|
207
|
+
this.encryptionKey,
|
|
208
|
+
nonce,
|
|
209
|
+
{ authTagLength: ANNEX_CONFIG.authTagLength }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Include sequence number in AAD for replay protection
|
|
213
|
+
const aad = Buffer.from(`${this.sessionId}:${this.sendSequence}`);
|
|
214
|
+
cipher.setAAD(aad);
|
|
215
|
+
|
|
216
|
+
const data = typeof plaintext === 'string' ? plaintext : JSON.stringify(plaintext);
|
|
217
|
+
const encrypted = Buffer.concat([
|
|
218
|
+
cipher.update(data, 'utf8'),
|
|
219
|
+
cipher.final(),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
this.sendSequence++;
|
|
223
|
+
this.messageCount++;
|
|
224
|
+
this.lastActivity = Date.now();
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
nonce: nonce.toString('hex'),
|
|
228
|
+
ciphertext: encrypted.toString('hex'),
|
|
229
|
+
authTag: cipher.getAuthTag().toString('hex'),
|
|
230
|
+
sequence: this.sendSequence - 1,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Decrypt a message for this session
|
|
236
|
+
*/
|
|
237
|
+
decrypt(encryptedData, expectedSequence) {
|
|
238
|
+
if (!this.established || !this.encryptionKey) {
|
|
239
|
+
throw new Error('Session not established');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Replay protection: sequence must be greater than last received
|
|
243
|
+
if (expectedSequence <= this.recvSequence && this.recvSequence > 0) {
|
|
244
|
+
throw new Error(`Replay detected: sequence ${expectedSequence} <= ${this.recvSequence}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nonce = Buffer.from(encryptedData.nonce, 'hex');
|
|
248
|
+
const ciphertext = Buffer.from(encryptedData.ciphertext, 'hex');
|
|
249
|
+
const authTag = Buffer.from(encryptedData.authTag, 'hex');
|
|
250
|
+
|
|
251
|
+
const decipher = createDecipheriv(
|
|
252
|
+
ANNEX_CONFIG.symmetricAlgorithm,
|
|
253
|
+
this.encryptionKey,
|
|
254
|
+
nonce,
|
|
255
|
+
{ authTagLength: ANNEX_CONFIG.authTagLength }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Verify AAD
|
|
259
|
+
const aad = Buffer.from(`${this.sessionId}:${expectedSequence}`);
|
|
260
|
+
decipher.setAAD(aad);
|
|
261
|
+
decipher.setAuthTag(authTag);
|
|
262
|
+
|
|
263
|
+
const decrypted = Buffer.concat([
|
|
264
|
+
decipher.update(ciphertext),
|
|
265
|
+
decipher.final(),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
this.recvSequence = expectedSequence;
|
|
269
|
+
this.lastActivity = Date.now();
|
|
270
|
+
|
|
271
|
+
return decrypted.toString('utf8');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if session needs re-keying for perfect forward secrecy
|
|
276
|
+
*/
|
|
277
|
+
needsRekey() {
|
|
278
|
+
if (!this.established) return false;
|
|
279
|
+
|
|
280
|
+
const timeSinceRekey = Date.now() - (this.lastRekey || this.createdAt);
|
|
281
|
+
return (
|
|
282
|
+
timeSinceRekey > ANNEX_CONFIG.rekeyInterval ||
|
|
283
|
+
this.messageCount >= ANNEX_CONFIG.maxMessagesPerKey
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Check if session has expired
|
|
289
|
+
*/
|
|
290
|
+
isExpired() {
|
|
291
|
+
return Date.now() - this.lastActivity > ANNEX_CONFIG.sessionTimeout;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Derive symmetric encryption key from shared secret
|
|
296
|
+
*/
|
|
297
|
+
_deriveEncryptionKey() {
|
|
298
|
+
return createHash('sha256')
|
|
299
|
+
.update(this.sharedSecret)
|
|
300
|
+
.update(ANNEX_CONFIG.keyDerivationSalt)
|
|
301
|
+
.update(this.sessionId)
|
|
302
|
+
.update(this.localNodeId)
|
|
303
|
+
.update(this.remoteNodeId)
|
|
304
|
+
.digest();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* ANNEX - Autonomous Network Negotiated Encrypted eXchange
|
|
310
|
+
*
|
|
311
|
+
* Manages encrypted point-to-point communication channels
|
|
312
|
+
* "Annex your own sovereign data territory"
|
|
313
|
+
*/
|
|
314
|
+
export class Annex {
|
|
315
|
+
constructor(options) {
|
|
316
|
+
this.identity = options.identity;
|
|
317
|
+
this.mesh = options.mesh;
|
|
318
|
+
this.sessions = new Map(); // remoteNodeId -> AnnexSession
|
|
319
|
+
this.pendingHandshakes = new Map();
|
|
320
|
+
this.messageHandlers = new Map();
|
|
321
|
+
|
|
322
|
+
// Stats
|
|
323
|
+
this.stats = {
|
|
324
|
+
sessionsCreated: 0,
|
|
325
|
+
messagesEncrypted: 0,
|
|
326
|
+
messagesDecrypted: 0,
|
|
327
|
+
handshakesFailed: 0,
|
|
328
|
+
replaysBlocked: 0,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Register mesh handler for ANNEX messages
|
|
332
|
+
if (this.mesh) {
|
|
333
|
+
this._registerMeshHandlers();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Initialize or get secure session with a peer (annex territory)
|
|
339
|
+
*/
|
|
340
|
+
async openChannel(remoteNodeId) {
|
|
341
|
+
// Check for existing session
|
|
342
|
+
let session = this.sessions.get(remoteNodeId);
|
|
343
|
+
if (session && session.established && !session.isExpired()) {
|
|
344
|
+
return session;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Create new session
|
|
348
|
+
session = new AnnexSession({
|
|
349
|
+
localNodeId: this.identity.identity.nodeId,
|
|
350
|
+
remoteNodeId,
|
|
351
|
+
initiator: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Generate our key pair
|
|
355
|
+
const ourPublicKey = session.generateKeyPair();
|
|
356
|
+
|
|
357
|
+
// Store pending handshake
|
|
358
|
+
this.pendingHandshakes.set(remoteNodeId, session);
|
|
359
|
+
|
|
360
|
+
// Send key exchange request
|
|
361
|
+
const envelope = new AnnexEnvelope({
|
|
362
|
+
type: ANNEX_CONFIG.messageTypes.KEY_EXCHANGE,
|
|
363
|
+
senderId: this.identity.identity.nodeId,
|
|
364
|
+
recipientId: remoteNodeId,
|
|
365
|
+
sessionId: session.sessionId,
|
|
366
|
+
kemPublicKey: ourPublicKey,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Sign the envelope
|
|
370
|
+
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
371
|
+
|
|
372
|
+
// Send via mesh
|
|
373
|
+
await this._sendToMesh(remoteNodeId, envelope);
|
|
374
|
+
|
|
375
|
+
// Wait for response (with timeout)
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const timeout = setTimeout(() => {
|
|
378
|
+
this.pendingHandshakes.delete(remoteNodeId);
|
|
379
|
+
this.stats.handshakesFailed++;
|
|
380
|
+
reject(new Error('ANNEX handshake timeout'));
|
|
381
|
+
}, 30000);
|
|
382
|
+
|
|
383
|
+
session._resolveHandshake = (establishedSession) => {
|
|
384
|
+
clearTimeout(timeout);
|
|
385
|
+
resolve(establishedSession);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
session._rejectHandshake = (error) => {
|
|
389
|
+
clearTimeout(timeout);
|
|
390
|
+
this.stats.handshakesFailed++;
|
|
391
|
+
reject(error);
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Send an encrypted message through the ANNEX channel
|
|
398
|
+
*/
|
|
399
|
+
async send(remoteNodeId, payload, options = {}) {
|
|
400
|
+
// Ensure we have a session
|
|
401
|
+
let session = this.sessions.get(remoteNodeId);
|
|
402
|
+
if (!session || !session.established || session.isExpired()) {
|
|
403
|
+
session = await this.openChannel(remoteNodeId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Check for re-key need
|
|
407
|
+
if (session.needsRekey()) {
|
|
408
|
+
await this._rekey(session);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Encrypt the payload
|
|
412
|
+
const encrypted = session.encrypt(payload);
|
|
413
|
+
|
|
414
|
+
// Create envelope
|
|
415
|
+
const envelope = new AnnexEnvelope({
|
|
416
|
+
type: ANNEX_CONFIG.messageTypes.ENCRYPTED,
|
|
417
|
+
senderId: this.identity.identity.nodeId,
|
|
418
|
+
recipientId: remoteNodeId,
|
|
419
|
+
sessionId: session.sessionId,
|
|
420
|
+
sequence: encrypted.sequence,
|
|
421
|
+
nonce: encrypted.nonce,
|
|
422
|
+
ciphertext: encrypted.ciphertext,
|
|
423
|
+
authTag: encrypted.authTag,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Sign
|
|
427
|
+
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
428
|
+
|
|
429
|
+
// Send
|
|
430
|
+
await this._sendToMesh(remoteNodeId, envelope);
|
|
431
|
+
this.stats.messagesEncrypted++;
|
|
432
|
+
|
|
433
|
+
return { sent: true, sessionId: session.sessionId, sequence: encrypted.sequence };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Register handler for decrypted messages
|
|
438
|
+
*/
|
|
439
|
+
onMessage(handler) {
|
|
440
|
+
const id = bytesToHex(randomBytes(8));
|
|
441
|
+
this.messageHandlers.set(id, handler);
|
|
442
|
+
return id;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Remove message handler
|
|
447
|
+
*/
|
|
448
|
+
offMessage(handlerId) {
|
|
449
|
+
this.messageHandlers.delete(handlerId);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Close ANNEX channel with peer (release territory)
|
|
454
|
+
*/
|
|
455
|
+
async closeChannel(remoteNodeId) {
|
|
456
|
+
const session = this.sessions.get(remoteNodeId);
|
|
457
|
+
if (!session) return;
|
|
458
|
+
|
|
459
|
+
// Send close notification
|
|
460
|
+
const envelope = new AnnexEnvelope({
|
|
461
|
+
type: ANNEX_CONFIG.messageTypes.CLOSE,
|
|
462
|
+
senderId: this.identity.identity.nodeId,
|
|
463
|
+
recipientId: remoteNodeId,
|
|
464
|
+
sessionId: session.sessionId,
|
|
465
|
+
});
|
|
466
|
+
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
467
|
+
|
|
468
|
+
await this._sendToMesh(remoteNodeId, envelope);
|
|
469
|
+
this.sessions.delete(remoteNodeId);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get session info
|
|
474
|
+
*/
|
|
475
|
+
getSessionInfo(remoteNodeId) {
|
|
476
|
+
const session = this.sessions.get(remoteNodeId);
|
|
477
|
+
if (!session) return null;
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
sessionId: session.sessionId,
|
|
481
|
+
established: session.established,
|
|
482
|
+
createdAt: session.createdAt,
|
|
483
|
+
lastActivity: session.lastActivity,
|
|
484
|
+
messageCount: session.messageCount,
|
|
485
|
+
needsRekey: session.needsRekey(),
|
|
486
|
+
isExpired: session.isExpired(),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* List all active annexes (sessions)
|
|
492
|
+
*/
|
|
493
|
+
listAnnexes() {
|
|
494
|
+
const annexes = [];
|
|
495
|
+
for (const [nodeId, session] of this.sessions) {
|
|
496
|
+
annexes.push({
|
|
497
|
+
nodeId,
|
|
498
|
+
sessionId: session.sessionId,
|
|
499
|
+
established: session.established,
|
|
500
|
+
createdAt: session.createdAt,
|
|
501
|
+
lastActivity: session.lastActivity,
|
|
502
|
+
messageCount: session.messageCount,
|
|
503
|
+
isExpired: session.isExpired(),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return annexes;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get stats
|
|
511
|
+
*/
|
|
512
|
+
getStats() {
|
|
513
|
+
return {
|
|
514
|
+
...this.stats,
|
|
515
|
+
activeSessions: this.sessions.size,
|
|
516
|
+
pendingHandshakes: this.pendingHandshakes.size,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// === Private Methods ===
|
|
521
|
+
|
|
522
|
+
_registerMeshHandlers() {
|
|
523
|
+
// Handle incoming ANNEX messages
|
|
524
|
+
this.mesh.on('annex', async (data, origin) => {
|
|
525
|
+
await this._handleAnnexMessage(data, origin);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async _handleAnnexMessage(envelope, origin) {
|
|
530
|
+
try {
|
|
531
|
+
// Verify signature
|
|
532
|
+
const sigPayload = AnnexEnvelope.fromJSON(envelope).getSigningPayload();
|
|
533
|
+
const peerPublicKey = this._getPeerPublicKey(envelope.senderId);
|
|
534
|
+
|
|
535
|
+
if (peerPublicKey && !this.identity.verify(sigPayload, envelope.signature, peerPublicKey)) {
|
|
536
|
+
console.warn(`β οΈ ANNEX: Invalid signature from ${envelope.senderId?.slice(0, 16)}...`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
switch (envelope.type) {
|
|
541
|
+
case ANNEX_CONFIG.messageTypes.KEY_EXCHANGE:
|
|
542
|
+
await this._handleKeyExchange(envelope);
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
case ANNEX_CONFIG.messageTypes.KEY_RESPONSE:
|
|
546
|
+
await this._handleKeyResponse(envelope);
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case ANNEX_CONFIG.messageTypes.ENCRYPTED:
|
|
550
|
+
await this._handleEncrypted(envelope);
|
|
551
|
+
break;
|
|
552
|
+
|
|
553
|
+
case ANNEX_CONFIG.messageTypes.REKEY:
|
|
554
|
+
await this._handleRekey(envelope);
|
|
555
|
+
break;
|
|
556
|
+
|
|
557
|
+
case ANNEX_CONFIG.messageTypes.CLOSE:
|
|
558
|
+
this.sessions.delete(envelope.senderId);
|
|
559
|
+
console.log(`π³οΈ ANNEX: Channel closed by ${envelope.senderId?.slice(0, 16)}...`);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.error('ANNEX error:', err.message);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async _handleKeyExchange(envelope) {
|
|
568
|
+
console.log(`π€ ANNEX: Key exchange from ${envelope.senderId?.slice(0, 16)}...`);
|
|
569
|
+
|
|
570
|
+
// Create responding session
|
|
571
|
+
const session = new AnnexSession({
|
|
572
|
+
sessionId: envelope.sessionId,
|
|
573
|
+
localNodeId: this.identity.identity.nodeId,
|
|
574
|
+
remoteNodeId: envelope.senderId,
|
|
575
|
+
initiator: false,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Generate our key pair and encapsulate with peer's public key
|
|
579
|
+
session.generateKeyPair();
|
|
580
|
+
const kemCiphertext = session.encapsulate(envelope.kemPublicKey);
|
|
581
|
+
|
|
582
|
+
// Store session
|
|
583
|
+
this.sessions.set(envelope.senderId, session);
|
|
584
|
+
this.stats.sessionsCreated++;
|
|
585
|
+
|
|
586
|
+
// Send response with our public key and the KEM ciphertext
|
|
587
|
+
const response = new AnnexEnvelope({
|
|
588
|
+
type: ANNEX_CONFIG.messageTypes.KEY_RESPONSE,
|
|
589
|
+
senderId: this.identity.identity.nodeId,
|
|
590
|
+
recipientId: envelope.senderId,
|
|
591
|
+
sessionId: session.sessionId,
|
|
592
|
+
kemPublicKey: bytesToHex(session.kemKeyPair.publicKey),
|
|
593
|
+
kemCiphertext: kemCiphertext,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
response.signature = this.identity.sign(response.getSigningPayload());
|
|
597
|
+
await this._sendToMesh(envelope.senderId, response);
|
|
598
|
+
|
|
599
|
+
console.log(`π ANNEX: Channel established with ${envelope.senderId?.slice(0, 16)}...`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async _handleKeyResponse(envelope) {
|
|
603
|
+
const session = this.pendingHandshakes.get(envelope.senderId);
|
|
604
|
+
if (!session) {
|
|
605
|
+
console.warn('ANNEX: Unexpected key response');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Decapsulate to get shared secret
|
|
610
|
+
session.decapsulate(envelope.kemCiphertext);
|
|
611
|
+
|
|
612
|
+
// Move to active sessions
|
|
613
|
+
this.pendingHandshakes.delete(envelope.senderId);
|
|
614
|
+
this.sessions.set(envelope.senderId, session);
|
|
615
|
+
this.stats.sessionsCreated++;
|
|
616
|
+
|
|
617
|
+
console.log(`π ANNEX: Channel established with ${envelope.senderId?.slice(0, 16)}...`);
|
|
618
|
+
|
|
619
|
+
// Resolve the handshake promise
|
|
620
|
+
if (session._resolveHandshake) {
|
|
621
|
+
session._resolveHandshake(session);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async _handleEncrypted(envelope) {
|
|
626
|
+
const session = this.sessions.get(envelope.senderId);
|
|
627
|
+
if (!session || !session.established) {
|
|
628
|
+
console.warn('ANNEX: No session for encrypted message');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
// Decrypt
|
|
634
|
+
const plaintext = session.decrypt(
|
|
635
|
+
{
|
|
636
|
+
nonce: envelope.nonce,
|
|
637
|
+
ciphertext: envelope.ciphertext,
|
|
638
|
+
authTag: envelope.authTag,
|
|
639
|
+
},
|
|
640
|
+
envelope.sequence
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
this.stats.messagesDecrypted++;
|
|
644
|
+
|
|
645
|
+
// Parse and dispatch to handlers
|
|
646
|
+
let payload;
|
|
647
|
+
try {
|
|
648
|
+
payload = JSON.parse(plaintext);
|
|
649
|
+
} catch {
|
|
650
|
+
payload = plaintext;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Dispatch to handlers
|
|
654
|
+
for (const handler of this.messageHandlers.values()) {
|
|
655
|
+
try {
|
|
656
|
+
await handler({
|
|
657
|
+
from: envelope.senderId,
|
|
658
|
+
sessionId: envelope.sessionId,
|
|
659
|
+
payload,
|
|
660
|
+
timestamp: envelope.timestamp,
|
|
661
|
+
});
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error('ANNEX handler error:', err.message);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
if (err.message.includes('Replay')) {
|
|
668
|
+
this.stats.replaysBlocked++;
|
|
669
|
+
console.warn(`β οΈ ANNEX: ${err.message}`);
|
|
670
|
+
} else {
|
|
671
|
+
throw err;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async _handleRekey(envelope) {
|
|
677
|
+
const session = this.sessions.get(envelope.senderId);
|
|
678
|
+
if (!session) return;
|
|
679
|
+
|
|
680
|
+
console.log(`π ANNEX: Re-keying with ${envelope.senderId?.slice(0, 16)}...`);
|
|
681
|
+
|
|
682
|
+
// Respond to re-key with new key exchange
|
|
683
|
+
session.generateKeyPair();
|
|
684
|
+
const kemCiphertext = session.encapsulate(envelope.kemPublicKey);
|
|
685
|
+
session.messageCount = 0;
|
|
686
|
+
|
|
687
|
+
const response = new AnnexEnvelope({
|
|
688
|
+
type: ANNEX_CONFIG.messageTypes.KEY_RESPONSE,
|
|
689
|
+
senderId: this.identity.identity.nodeId,
|
|
690
|
+
recipientId: envelope.senderId,
|
|
691
|
+
sessionId: session.sessionId,
|
|
692
|
+
kemPublicKey: bytesToHex(session.kemKeyPair.publicKey),
|
|
693
|
+
kemCiphertext,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
response.signature = this.identity.sign(response.getSigningPayload());
|
|
697
|
+
await this._sendToMesh(envelope.senderId, response);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async _rekey(session) {
|
|
701
|
+
console.log(`π ANNEX: Initiating re-key with ${session.remoteNodeId?.slice(0, 16)}...`);
|
|
702
|
+
|
|
703
|
+
// Generate new ephemeral keys
|
|
704
|
+
const newPublicKey = session.generateKeyPair();
|
|
705
|
+
session.messageCount = 0;
|
|
706
|
+
|
|
707
|
+
const envelope = new AnnexEnvelope({
|
|
708
|
+
type: ANNEX_CONFIG.messageTypes.REKEY,
|
|
709
|
+
senderId: this.identity.identity.nodeId,
|
|
710
|
+
recipientId: session.remoteNodeId,
|
|
711
|
+
sessionId: session.sessionId,
|
|
712
|
+
kemPublicKey: newPublicKey,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
716
|
+
await this._sendToMesh(session.remoteNodeId, envelope);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async _sendToMesh(remoteNodeId, envelope) {
|
|
720
|
+
if (!this.mesh) {
|
|
721
|
+
throw new Error('No mesh connection');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
this.mesh.sendTo(remoteNodeId, {
|
|
725
|
+
type: 'annex',
|
|
726
|
+
annex: envelope.toJSON(),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
_getPeerPublicKey(nodeId) {
|
|
731
|
+
// Get from mesh peer info
|
|
732
|
+
if (this.mesh && this.mesh.peers) {
|
|
733
|
+
const peer = this.mesh.peers.get(nodeId);
|
|
734
|
+
return peer?.identity?.publicKey || null;
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Export config for external use
|
|
741
|
+
export { ANNEX_CONFIG, AnnexEnvelope, AnnexSession };
|
|
742
|
+
|
|
743
|
+
export default Annex;
|