yakmesh 2.8.2 → 3.0.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 +637 -0
- package/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +334 -74
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +28 -13
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
package/mesh/annex.js
CHANGED
|
@@ -27,10 +27,32 @@
|
|
|
27
27
|
|
|
28
28
|
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
29
29
|
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
30
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
30
|
+
import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
|
|
31
31
|
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
32
32
|
import { createLogger } from '../utils/logger.js';
|
|
33
33
|
|
|
34
|
+
// ACCEL: Hardware-accelerated crypto (native SHA3, native KEM via liboqs/AVX-512)
|
|
35
|
+
import { sha3_256, mlKem768Keygen, mlKem768Encapsulate, mlKem768Decapsulate } from '../utils/accel.js';
|
|
36
|
+
|
|
37
|
+
// STEADYWATCH: Quantum-hardware-validated entropy seeds (Hurwitz quaternion, IBM Quantum)
|
|
38
|
+
import { getHybridSeed, seedStore as steadywatchStore } from '../security/steadywatch.js';
|
|
39
|
+
|
|
40
|
+
// ═══ TRIBHUJ — Balanced ternary for channel lifecycle ═══
|
|
41
|
+
// POSITIVE: ESTABLISHED (secure channel active)
|
|
42
|
+
// NEUTRAL: NEGOTIATING (key exchange in progress)
|
|
43
|
+
// NEGATIVE: CLOSED (session terminated or expired)
|
|
44
|
+
import { POSITIVE, NEUTRAL, NEGATIVE } from '../oracle/tribhuj.js';
|
|
45
|
+
|
|
46
|
+
/** ANNEX channel lifecycle states (TRIBHUJ trits) */
|
|
47
|
+
export const ChannelState = Object.freeze({
|
|
48
|
+
CLOSED: NEGATIVE, // -1: Session terminated or expired
|
|
49
|
+
NEGOTIATING: NEUTRAL, // 0: Key exchange in progress
|
|
50
|
+
ESTABLISHED: POSITIVE, // +1: Secure channel active
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** Extract unique peer suffix from nodeId (e.g. 'node-net-name-pq-kEEU' → 'kEEU') */
|
|
54
|
+
const peerTag = (id) => id?.split('-pq-').pop() || id?.slice?.(-8) || String(id);
|
|
55
|
+
|
|
34
56
|
const log = createLogger('mesh:annex');
|
|
35
57
|
|
|
36
58
|
const ANNEX_CONFIG = {
|
|
@@ -144,36 +166,77 @@ class AnnexSession {
|
|
|
144
166
|
this.kemKeyPair = null; // Our ephemeral KEM key pair
|
|
145
167
|
this.sharedSecret = null; // Derived shared secret
|
|
146
168
|
this.encryptionKey = null; // Current symmetric key
|
|
169
|
+
this.pendingEncryptionKey = null; // Future key awaiting implicit ack (bootstrap→KEM upgrade only)
|
|
170
|
+
this.bootstrapped = false; // JHILKE: true if using deterministic bootstrap key (not yet KEM-backed)
|
|
171
|
+
this.rekeyEpoch = 0; // JHILKE: deterministic rekey epoch (incremented on each cricket-coordinated switch)
|
|
147
172
|
this.sendSequence = 0; // Outbound message counter
|
|
148
|
-
this.recvSequence =
|
|
173
|
+
this.recvSequence = -1; // Inbound message counter (-1 so first msg seq 0 passes)
|
|
149
174
|
this.messageCount = 0; // Total messages with current key
|
|
150
175
|
|
|
151
176
|
// State
|
|
152
177
|
this.established = false;
|
|
178
|
+
this.channelState = ChannelState.NEGOTIATING; // TRIBHUJ trit lifecycle
|
|
153
179
|
this.createdAt = Date.now();
|
|
154
180
|
this.lastActivity = Date.now();
|
|
155
181
|
this.lastRekey = null;
|
|
156
182
|
}
|
|
157
183
|
|
|
158
184
|
/**
|
|
159
|
-
*
|
|
185
|
+
* JHILKE deterministic rekey — both nodes derive the same key simultaneously.
|
|
186
|
+
* No KEM round-trip, no pending key, no race condition.
|
|
187
|
+
* Called by JhilkeCoordinator._executeSwitch() after cricket coordination.
|
|
188
|
+
*/
|
|
189
|
+
deterministicRekey(newKey, epoch) {
|
|
190
|
+
this.encryptionKey = newKey;
|
|
191
|
+
this.rekeyEpoch = epoch;
|
|
192
|
+
this.messageCount = 0;
|
|
193
|
+
this.lastRekey = Date.now();
|
|
194
|
+
this.lastActivity = Date.now();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate ephemeral KEM key pair for this session.
|
|
199
|
+
*
|
|
200
|
+
* ACCEL: Routes through mlKem768Keygen() for native liboqs AVX-512 NTT
|
|
201
|
+
* acceleration when available (previously called noble directly — bypassed ACCEL).
|
|
202
|
+
*
|
|
203
|
+
* STEADYWATCH: If quantum satellite seeds are loaded, uses hybrid entropy:
|
|
204
|
+
* hybridSeed = SHA3(satelliteSeed || EXPAND) ⊕ randomBytes(64)
|
|
205
|
+
* Two-source extractor: even if one source is compromised, keys are safe.
|
|
160
206
|
*/
|
|
161
|
-
generateKeyPair() {
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
async generateKeyPair() {
|
|
208
|
+
// STEADYWATCH hybrid seed (quantum + CSPRNG) or pure CSPRNG fallback
|
|
209
|
+
const seed = steadywatchStore.initialized
|
|
210
|
+
? getHybridSeed()
|
|
211
|
+
: randomBytes(64);
|
|
212
|
+
|
|
213
|
+
// Route through ACCEL for native liboqs/AVX-512 acceleration
|
|
214
|
+
this.kemKeyPair = await mlKem768Keygen(seed);
|
|
164
215
|
return bytesToHex(this.kemKeyPair.publicKey);
|
|
165
216
|
}
|
|
166
217
|
|
|
167
218
|
/**
|
|
168
219
|
* Complete key exchange as initiator (encapsulate with peer's public key)
|
|
169
220
|
*/
|
|
170
|
-
encapsulate(peerPublicKey) {
|
|
221
|
+
encapsulate(peerPublicKey, { defer = false } = {}) {
|
|
171
222
|
const publicKeyBytes = hexToBytes(peerPublicKey);
|
|
172
|
-
const result =
|
|
223
|
+
const result = mlKem768Encapsulate(publicKeyBytes);
|
|
173
224
|
|
|
174
225
|
this.sharedSecret = result.sharedSecret;
|
|
175
|
-
|
|
226
|
+
const newKey = this._deriveEncryptionKey();
|
|
227
|
+
|
|
228
|
+
if (defer && this.encryptionKey) {
|
|
229
|
+
// Rekey responder: store new key as pending, keep current active.
|
|
230
|
+
// PFS-safe: we only hold the FUTURE key, never the past key.
|
|
231
|
+
// Activation happens implicitly when we receive a message encrypted
|
|
232
|
+
// with the new key (see decrypt()).
|
|
233
|
+
this.pendingEncryptionKey = newKey;
|
|
234
|
+
} else {
|
|
235
|
+
// Initial handshake or initiator: switch immediately
|
|
236
|
+
this.encryptionKey = newKey;
|
|
237
|
+
}
|
|
176
238
|
this.established = true;
|
|
239
|
+
this.channelState = ChannelState.ESTABLISHED;
|
|
177
240
|
this.lastRekey = Date.now();
|
|
178
241
|
|
|
179
242
|
return bytesToHex(result.cipherText);
|
|
@@ -188,9 +251,34 @@ class AnnexSession {
|
|
|
188
251
|
}
|
|
189
252
|
|
|
190
253
|
const ciphertextBytes = hexToBytes(ciphertext);
|
|
191
|
-
this.sharedSecret =
|
|
254
|
+
this.sharedSecret = mlKem768Decapsulate(ciphertextBytes, this.kemKeyPair.secretKey);
|
|
255
|
+
|
|
256
|
+
// Zero ephemeral KEM secret key — shared secret is extracted, secret key is
|
|
257
|
+
// no longer needed. Minimizes exposure window if memory is later compromised.
|
|
258
|
+
if (this.kemKeyPair.secretKey instanceof Uint8Array) {
|
|
259
|
+
this.kemKeyPair.secretKey.fill(0);
|
|
260
|
+
}
|
|
261
|
+
this.kemKeyPair = null; // Release reference entirely
|
|
262
|
+
|
|
263
|
+
// Bootstrap→KEM upgrade bridge: briefly retain old key for in-flight messages.
|
|
264
|
+
// The responder still encrypts with bootstrap key until implicit ack arrives.
|
|
265
|
+
// Without this bridge, those in-flight messages cause AEAD auth failures.
|
|
266
|
+
// Auto-expires after 5s — NOT a permanent "previous key" (PFS preserved).
|
|
267
|
+
if (this.encryptionKey) {
|
|
268
|
+
this._transitionKey = this.encryptionKey;
|
|
269
|
+
this._transitionKeyTimer = setTimeout(() => {
|
|
270
|
+
this._transitionKey = null;
|
|
271
|
+
this._transitionKeyTimer = null;
|
|
272
|
+
}, 5000);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Initiator receiving KEY_RESPONSE: switch immediately to KEM key.
|
|
276
|
+
// The initiator is always "first mover" — its next message triggers
|
|
277
|
+
// the responder to promote pendingEncryptionKey.
|
|
192
278
|
this.encryptionKey = this._deriveEncryptionKey();
|
|
279
|
+
this.pendingEncryptionKey = null; // Clear any pending state
|
|
193
280
|
this.established = true;
|
|
281
|
+
this.channelState = ChannelState.ESTABLISHED;
|
|
194
282
|
this.lastRekey = Date.now();
|
|
195
283
|
|
|
196
284
|
return true;
|
|
@@ -237,28 +325,21 @@ class AnnexSession {
|
|
|
237
325
|
/**
|
|
238
326
|
* Decrypt a message for this session
|
|
239
327
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// Replay protection: sequence must be greater than last received
|
|
246
|
-
if (expectedSequence <= this.recvSequence && this.recvSequence > 0) {
|
|
247
|
-
throw new Error(`Replay detected: sequence ${expectedSequence} <= ${this.recvSequence}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Decrypt with a specific key (internal helper)
|
|
330
|
+
*/
|
|
331
|
+
_decryptWithKey(key, encryptedData, expectedSequence) {
|
|
250
332
|
const nonce = Buffer.from(encryptedData.nonce, 'hex');
|
|
251
333
|
const ciphertext = Buffer.from(encryptedData.ciphertext, 'hex');
|
|
252
334
|
const authTag = Buffer.from(encryptedData.authTag, 'hex');
|
|
253
335
|
|
|
254
336
|
const decipher = createDecipheriv(
|
|
255
337
|
ANNEX_CONFIG.symmetricAlgorithm,
|
|
256
|
-
|
|
338
|
+
key,
|
|
257
339
|
nonce,
|
|
258
340
|
{ authTagLength: ANNEX_CONFIG.authTagLength }
|
|
259
341
|
);
|
|
260
342
|
|
|
261
|
-
// Verify AAD
|
|
262
343
|
const aad = Buffer.from(`${this.sessionId}:${expectedSequence}`);
|
|
263
344
|
decipher.setAAD(aad);
|
|
264
345
|
decipher.setAuthTag(authTag);
|
|
@@ -268,12 +349,66 @@ class AnnexSession {
|
|
|
268
349
|
decipher.final(),
|
|
269
350
|
]);
|
|
270
351
|
|
|
271
|
-
this.recvSequence = expectedSequence;
|
|
272
|
-
this.lastActivity = Date.now();
|
|
273
|
-
|
|
274
352
|
return decrypted.toString('utf8');
|
|
275
353
|
}
|
|
276
354
|
|
|
355
|
+
decrypt(encryptedData, expectedSequence) {
|
|
356
|
+
if (!this.established || !this.encryptionKey) {
|
|
357
|
+
throw new Error('Session not established');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Replay protection: sequence must be greater than last received
|
|
361
|
+
if (typeof expectedSequence !== 'number' || expectedSequence <= this.recvSequence) {
|
|
362
|
+
throw new Error(`Replay detected: sequence ${expectedSequence} <= ${this.recvSequence}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
// Try current key first
|
|
367
|
+
const result = this._decryptWithKey(this.encryptionKey, encryptedData, expectedSequence);
|
|
368
|
+
this.recvSequence = expectedSequence;
|
|
369
|
+
this.lastActivity = Date.now();
|
|
370
|
+
return result;
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// During rekey transition, the initiator has switched to the new key
|
|
373
|
+
// but the responder is still on the old key. Try the PENDING (future)
|
|
374
|
+
// key — if it works, promote it to current. This is the implicit ack.
|
|
375
|
+
//
|
|
376
|
+
// Security note: we only ever store the FUTURE key as fallback, never
|
|
377
|
+
// the PAST key. An attacker who dumps memory gets a key they'd have
|
|
378
|
+
// gotten anyway once activated. PFS of past messages is never at risk.
|
|
379
|
+
if (this.pendingEncryptionKey) {
|
|
380
|
+
try {
|
|
381
|
+
const result = this._decryptWithKey(this.pendingEncryptionKey, encryptedData, expectedSequence);
|
|
382
|
+
// Implicit ack: promote pending → current, zero old key
|
|
383
|
+
this.encryptionKey = this.pendingEncryptionKey;
|
|
384
|
+
this.pendingEncryptionKey = null;
|
|
385
|
+
this.recvSequence = expectedSequence;
|
|
386
|
+
this.lastActivity = Date.now();
|
|
387
|
+
log.info('Rekey activated via implicit ack', { sessionId: this.sessionId?.slice(0, 16) });
|
|
388
|
+
return result;
|
|
389
|
+
} catch {
|
|
390
|
+
// pending key also failed — fall through to transition key
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Bootstrap→KEM upgrade bridge: the responder may still send messages
|
|
395
|
+
// encrypted with the bootstrap key between our KEM switch and their
|
|
396
|
+
// implicit-ack promotion. Try the briefly-retained old key.
|
|
397
|
+
// NO promotion — this key is being phased out (auto-expires via timer).
|
|
398
|
+
if (this._transitionKey) {
|
|
399
|
+
const result = this._decryptWithKey(this._transitionKey, encryptedData, expectedSequence);
|
|
400
|
+
this.recvSequence = expectedSequence;
|
|
401
|
+
this.lastActivity = Date.now();
|
|
402
|
+
log.info('Bootstrap→KEM transition: decoded in-flight message with old key', {
|
|
403
|
+
sessionId: this.sessionId?.slice(0, 16),
|
|
404
|
+
});
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
277
412
|
/**
|
|
278
413
|
* Check if session needs re-keying for perfect forward secrecy
|
|
279
414
|
*/
|
|
@@ -298,12 +433,15 @@ class AnnexSession {
|
|
|
298
433
|
* Derive symmetric encryption key from shared secret
|
|
299
434
|
*/
|
|
300
435
|
_deriveEncryptionKey() {
|
|
436
|
+
// Sort nodeIds so both sides derive the same key
|
|
437
|
+
// (localNodeId and remoteNodeId are swapped between initiator/responder)
|
|
438
|
+
const [first, second] = [this.localNodeId, this.remoteNodeId].sort();
|
|
301
439
|
return createHash('sha3-256')
|
|
302
440
|
.update(this.sharedSecret)
|
|
303
441
|
.update(ANNEX_CONFIG.keyDerivationSalt)
|
|
304
442
|
.update(this.sessionId)
|
|
305
|
-
.update(
|
|
306
|
-
.update(
|
|
443
|
+
.update(first)
|
|
444
|
+
.update(second)
|
|
307
445
|
.digest();
|
|
308
446
|
}
|
|
309
447
|
}
|
|
@@ -322,6 +460,9 @@ export class Annex {
|
|
|
322
460
|
this.pendingHandshakes = new Map();
|
|
323
461
|
this.messageHandlers = new Map();
|
|
324
462
|
|
|
463
|
+
// JHILKE coordinator reference (set by network.js after initialization)
|
|
464
|
+
this.jhilke = null;
|
|
465
|
+
|
|
325
466
|
// Stats
|
|
326
467
|
this.stats = {
|
|
327
468
|
sessionsCreated: 0,
|
|
@@ -331,31 +472,90 @@ export class Annex {
|
|
|
331
472
|
replaysBlocked: 0,
|
|
332
473
|
};
|
|
333
474
|
|
|
475
|
+
// Deferred message queue — buffer ANNEX messages from peers whose
|
|
476
|
+
// public key hasn't arrived yet (HELLO/WELCOME still in flight).
|
|
477
|
+
// Max 10 senders, 1 message per sender, 3s timeout.
|
|
478
|
+
this._deferredMessages = new Map(); // senderId -> { envelope, origin, timer }
|
|
479
|
+
this._maxDeferredSenders = 10;
|
|
480
|
+
this._deferTimeoutMs = 3000;
|
|
481
|
+
|
|
334
482
|
// Register mesh handler for ANNEX messages
|
|
335
483
|
if (this.mesh) {
|
|
336
484
|
this._registerMeshHandlers();
|
|
337
485
|
}
|
|
338
486
|
}
|
|
339
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Create a bootstrap session with a deterministic key.
|
|
490
|
+
* JHILKE derives this key from the shared code hash + both node IDs.
|
|
491
|
+
* Both sides compute the same key independently.
|
|
492
|
+
*
|
|
493
|
+
* The bootstrap session enables encrypted communication from message #1,
|
|
494
|
+
* eliminating plaintext KEM exchange. It is immediately upgraded to a
|
|
495
|
+
* proper KEM-backed session with full PFS.
|
|
496
|
+
*/
|
|
497
|
+
bootstrapSession(peerId, bootstrapKey) {
|
|
498
|
+
// Deterministic sessionId — both sides MUST agree on AES-GCM AAD.
|
|
499
|
+
// AAD = "${sessionId}:${sequence}", so random sessionId = instant auth failure.
|
|
500
|
+
const localNodeId = this.identity.identity.nodeId;
|
|
501
|
+
const [first, second] = [localNodeId, peerId].sort();
|
|
502
|
+
const deterministicSessionId = createHash('sha3-256')
|
|
503
|
+
.update(`yakmesh-annex-bootstrap-session:${first}:${second}`)
|
|
504
|
+
.digest('hex')
|
|
505
|
+
.slice(0, 32); // same length as bytesToHex(randomBytes(16))
|
|
506
|
+
|
|
507
|
+
const session = new AnnexSession({
|
|
508
|
+
sessionId: deterministicSessionId,
|
|
509
|
+
localNodeId,
|
|
510
|
+
remoteNodeId: peerId,
|
|
511
|
+
});
|
|
512
|
+
session.encryptionKey = bootstrapKey;
|
|
513
|
+
session.established = true;
|
|
514
|
+
session.bootstrapped = true;
|
|
515
|
+
session.channelState = ChannelState.ESTABLISHED;
|
|
516
|
+
session.lastRekey = Date.now();
|
|
517
|
+
|
|
518
|
+
this.sessions.set(peerId, session);
|
|
519
|
+
log.info('JHILKE bootstrap session created (encrypted from message #1)', {
|
|
520
|
+
peerId: peerTag(peerId),
|
|
521
|
+
sessionId: deterministicSessionId.slice(0, 8) + '...',
|
|
522
|
+
});
|
|
523
|
+
return session;
|
|
524
|
+
}
|
|
525
|
+
|
|
340
526
|
/**
|
|
341
527
|
* Initialize or get secure session with a peer (annex territory)
|
|
528
|
+
*
|
|
529
|
+
* JHILKE integration: If a bootstrap session exists, this method
|
|
530
|
+
* upgrades it to a KEM-backed session by performing the key exchange
|
|
531
|
+
* THROUGH the bootstrap-encrypted channel (no plaintext KEM).
|
|
342
532
|
*/
|
|
343
533
|
async openChannel(remoteNodeId) {
|
|
344
534
|
// Check for existing session
|
|
345
535
|
let session = this.sessions.get(remoteNodeId);
|
|
346
|
-
|
|
536
|
+
|
|
537
|
+
// Return existing FULL (non-bootstrap) session
|
|
538
|
+
if (session && session.established && !session.isExpired() && !session.bootstrapped) {
|
|
347
539
|
return session;
|
|
348
540
|
}
|
|
349
541
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
initiator
|
|
355
|
-
|
|
542
|
+
// Bootstrap upgrade: reuse existing session, add KEM negotiation
|
|
543
|
+
const isBootstrapUpgrade = session?.bootstrapped;
|
|
544
|
+
|
|
545
|
+
if (isBootstrapUpgrade) {
|
|
546
|
+
session.initiator = true;
|
|
547
|
+
log.info('JHILKE: upgrading bootstrap → KEM', { peer: peerTag(remoteNodeId) });
|
|
548
|
+
} else {
|
|
549
|
+
// Create new session
|
|
550
|
+
session = new AnnexSession({
|
|
551
|
+
localNodeId: this.identity.identity.nodeId,
|
|
552
|
+
remoteNodeId,
|
|
553
|
+
initiator: true,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
356
556
|
|
|
357
|
-
// Generate our key pair
|
|
358
|
-
const ourPublicKey = session.generateKeyPair();
|
|
557
|
+
// Generate our key pair (ACCEL: native liboqs/AVX-512, STEADYWATCH: quantum seed)
|
|
558
|
+
const ourPublicKey = await session.generateKeyPair();
|
|
359
559
|
|
|
360
560
|
// Store pending handshake
|
|
361
561
|
this.pendingHandshakes.set(remoteNodeId, session);
|
|
@@ -372,8 +572,8 @@ export class Annex {
|
|
|
372
572
|
// Sign the envelope
|
|
373
573
|
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
374
574
|
|
|
375
|
-
// Send via
|
|
376
|
-
await this.
|
|
575
|
+
// JHILKE: Send via secure channel if bootstrap exists, else raw
|
|
576
|
+
await this._sendControlSecure(remoteNodeId, envelope);
|
|
377
577
|
|
|
378
578
|
// Wait for response (with timeout)
|
|
379
579
|
return new Promise((resolve, reject) => {
|
|
@@ -406,9 +606,14 @@ export class Annex {
|
|
|
406
606
|
session = await this.openChannel(remoteNodeId);
|
|
407
607
|
}
|
|
408
608
|
|
|
409
|
-
// Check for re-key need
|
|
410
|
-
|
|
411
|
-
|
|
609
|
+
// Check for re-key need — JHILKE handles all rekeys deterministically.
|
|
610
|
+
// Both sides derive the same key after cricket coordination (no KEM round-trip).
|
|
611
|
+
if (!options._skipRekeyCheck && session.needsRekey()) {
|
|
612
|
+
if (this.jhilke) {
|
|
613
|
+
this.jhilke.initiateRekey(remoteNodeId);
|
|
614
|
+
}
|
|
615
|
+
// No fallback — JHILKE is the only rekey path. If unavailable,
|
|
616
|
+
// the session continues until timeout/reconnect.
|
|
412
617
|
}
|
|
413
618
|
|
|
414
619
|
// Encrypt the payload
|
|
@@ -469,7 +674,18 @@ export class Annex {
|
|
|
469
674
|
envelope.signature = this.identity.sign(envelope.getSigningPayload());
|
|
470
675
|
|
|
471
676
|
await this._sendToMesh(remoteNodeId, envelope);
|
|
677
|
+
session.channelState = ChannelState.CLOSED;
|
|
472
678
|
this.sessions.delete(remoteNodeId);
|
|
679
|
+
|
|
680
|
+
// Clean up any deferred messages from this peer
|
|
681
|
+
const deferred = this._deferredMessages?.get(remoteNodeId);
|
|
682
|
+
if (deferred) {
|
|
683
|
+
clearTimeout(deferred.timer);
|
|
684
|
+
if (deferred.onRegistered) {
|
|
685
|
+
this.mesh.off('peer-registered', deferred.onRegistered);
|
|
686
|
+
}
|
|
687
|
+
this._deferredMessages.delete(remoteNodeId);
|
|
688
|
+
}
|
|
473
689
|
}
|
|
474
690
|
|
|
475
691
|
/**
|
|
@@ -482,6 +698,7 @@ export class Annex {
|
|
|
482
698
|
return {
|
|
483
699
|
sessionId: session.sessionId,
|
|
484
700
|
established: session.established,
|
|
701
|
+
channelState: session.channelState, // TRIBHUJ trit: ESTABLISHED/NEGOTIATING/CLOSED
|
|
485
702
|
createdAt: session.createdAt,
|
|
486
703
|
lastActivity: session.lastActivity,
|
|
487
704
|
messageCount: session.messageCount,
|
|
@@ -500,6 +717,7 @@ export class Annex {
|
|
|
500
717
|
nodeId,
|
|
501
718
|
sessionId: session.sessionId,
|
|
502
719
|
established: session.established,
|
|
720
|
+
channelState: session.channelState, // TRIBHUJ trit
|
|
503
721
|
createdAt: session.createdAt,
|
|
504
722
|
lastActivity: session.lastActivity,
|
|
505
723
|
messageCount: session.messageCount,
|
|
@@ -531,12 +749,21 @@ export class Annex {
|
|
|
531
749
|
|
|
532
750
|
async _handleAnnexMessage(envelope, origin) {
|
|
533
751
|
try {
|
|
534
|
-
// Verify signature
|
|
535
|
-
|
|
752
|
+
// Verify ML-DSA-65 signature — MANDATORY for all ANNEX messages.
|
|
753
|
+
// "Changes pass through math alone" — no key, no entry.
|
|
536
754
|
const peerPublicKey = this._getPeerPublicKey(envelope.senderId);
|
|
537
755
|
|
|
538
|
-
if (
|
|
539
|
-
|
|
756
|
+
if (!peerPublicKey) {
|
|
757
|
+
// Peer's public key isn't registered yet — their HELLO/WELCOME
|
|
758
|
+
// may still be in flight. Defer the message and replay it once
|
|
759
|
+
// the mesh emits 'peer-registered' for this sender.
|
|
760
|
+
this._deferMessage(envelope, origin);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const sigPayload = AnnexEnvelope.fromJSON(envelope).getSigningPayload();
|
|
765
|
+
if (!this.identity.verify(sigPayload, envelope.signature, peerPublicKey)) {
|
|
766
|
+
log.warn('Invalid ML-DSA-65 signature from peer', { peerId: peerTag(envelope.senderId) });
|
|
540
767
|
return;
|
|
541
768
|
}
|
|
542
769
|
|
|
@@ -553,14 +780,13 @@ export class Annex {
|
|
|
553
780
|
await this._handleEncrypted(envelope);
|
|
554
781
|
break;
|
|
555
782
|
|
|
556
|
-
case ANNEX_CONFIG.messageTypes.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
case ANNEX_CONFIG.messageTypes.CLOSE:
|
|
783
|
+
case ANNEX_CONFIG.messageTypes.CLOSE: {
|
|
784
|
+
const closedSession = this.sessions.get(envelope.senderId);
|
|
785
|
+
if (closedSession) closedSession.channelState = ChannelState.CLOSED;
|
|
561
786
|
this.sessions.delete(envelope.senderId);
|
|
562
|
-
log.info('Channel closed by peer', { peerId: envelope.senderId
|
|
787
|
+
log.info('Channel closed by peer', { peerId: peerTag(envelope.senderId) });
|
|
563
788
|
break;
|
|
789
|
+
}
|
|
564
790
|
}
|
|
565
791
|
} catch (err) {
|
|
566
792
|
log.error('Error handling ANNEX message', { error: err.message });
|
|
@@ -568,23 +794,40 @@ export class Annex {
|
|
|
568
794
|
}
|
|
569
795
|
|
|
570
796
|
async _handleKeyExchange(envelope) {
|
|
571
|
-
log.info('Key exchange from peer', { peerId: envelope.senderId
|
|
797
|
+
log.info('Key exchange from peer', { peerId: peerTag(envelope.senderId) });
|
|
572
798
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
799
|
+
// JHILKE: Check for existing bootstrap session to upgrade
|
|
800
|
+
let session = this.sessions.get(envelope.senderId);
|
|
801
|
+
const isBootstrapUpgrade = session?.bootstrapped;
|
|
802
|
+
|
|
803
|
+
if (isBootstrapUpgrade) {
|
|
804
|
+
// Upgrade existing bootstrap session — keep bootstrap key as current
|
|
805
|
+
session.sessionId = envelope.sessionId;
|
|
806
|
+
session.initiator = false;
|
|
807
|
+
log.info('JHILKE: upgrading bootstrap → KEM (responder)', { peerId: peerTag(envelope.senderId) });
|
|
808
|
+
} else {
|
|
809
|
+
// Create responding session
|
|
810
|
+
session = new AnnexSession({
|
|
811
|
+
sessionId: envelope.sessionId,
|
|
812
|
+
localNodeId: this.identity.identity.nodeId,
|
|
813
|
+
remoteNodeId: envelope.senderId,
|
|
814
|
+
initiator: false,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
580
817
|
|
|
581
818
|
// Generate our key pair and encapsulate with peer's public key
|
|
582
|
-
|
|
583
|
-
|
|
819
|
+
// ACCEL: native liboqs/AVX-512, STEADYWATCH: quantum seed
|
|
820
|
+
await session.generateKeyPair();
|
|
821
|
+
|
|
822
|
+
// CRITICAL: For bootstrap upgrades, DEFER the KEM key.
|
|
823
|
+
// Keep bootstrap key active for the KEY_RESPONSE message.
|
|
824
|
+
// The KEM key activates via implicit ack when we receive
|
|
825
|
+
// a message encrypted with it from the initiator.
|
|
826
|
+
const kemCiphertext = session.encapsulate(envelope.kemPublicKey, { defer: isBootstrapUpgrade });
|
|
584
827
|
|
|
585
828
|
// Store session
|
|
586
829
|
this.sessions.set(envelope.senderId, session);
|
|
587
|
-
this.stats.sessionsCreated++;
|
|
830
|
+
if (!isBootstrapUpgrade) this.stats.sessionsCreated++;
|
|
588
831
|
|
|
589
832
|
// Send response with our public key and the KEM ciphertext
|
|
590
833
|
const response = new AnnexEnvelope({
|
|
@@ -597,27 +840,34 @@ export class Annex {
|
|
|
597
840
|
});
|
|
598
841
|
|
|
599
842
|
response.signature = this.identity.sign(response.getSigningPayload());
|
|
600
|
-
await this._sendToMesh(envelope.senderId, response);
|
|
601
843
|
|
|
602
|
-
|
|
844
|
+
// JHILKE: Send via secure channel (encrypted through bootstrap or current key)
|
|
845
|
+
await this._sendControlSecure(envelope.senderId, response);
|
|
846
|
+
|
|
847
|
+
log.info('Channel established with peer', { peerId: peerTag(envelope.senderId) });
|
|
603
848
|
}
|
|
604
849
|
|
|
605
850
|
async _handleKeyResponse(envelope) {
|
|
851
|
+
// KEY_RESPONSE is only used during initial handshake or bootstrap→KEM upgrade.
|
|
852
|
+
// All subsequent rekeys are deterministic via JHILKE (no KEM round-trip).
|
|
606
853
|
const session = this.pendingHandshakes.get(envelope.senderId);
|
|
854
|
+
|
|
607
855
|
if (!session) {
|
|
608
|
-
log.warn('Unexpected key response', { peerId: envelope.senderId
|
|
856
|
+
log.warn('Unexpected key response (no pending handshake)', { peerId: peerTag(envelope.senderId) });
|
|
609
857
|
return;
|
|
610
858
|
}
|
|
611
859
|
|
|
612
860
|
// Decapsulate to get shared secret
|
|
613
861
|
session.decapsulate(envelope.kemCiphertext);
|
|
614
862
|
|
|
615
|
-
//
|
|
863
|
+
// JHILKE: Clear bootstrap flag — now KEM-backed with full PFS
|
|
864
|
+
session.bootstrapped = false;
|
|
865
|
+
|
|
866
|
+
// Move from pending to active
|
|
616
867
|
this.pendingHandshakes.delete(envelope.senderId);
|
|
617
868
|
this.sessions.set(envelope.senderId, session);
|
|
618
869
|
this.stats.sessionsCreated++;
|
|
619
|
-
|
|
620
|
-
log.info('Channel established with peer', { peerId: envelope.senderId?.slice(0, 16) });
|
|
870
|
+
log.info('Channel established with peer', { peerId: peerTag(envelope.senderId) });
|
|
621
871
|
|
|
622
872
|
// Resolve the handshake promise
|
|
623
873
|
if (session._resolveHandshake) {
|
|
@@ -628,7 +878,7 @@ export class Annex {
|
|
|
628
878
|
async _handleEncrypted(envelope) {
|
|
629
879
|
const session = this.sessions.get(envelope.senderId);
|
|
630
880
|
if (!session || !session.established) {
|
|
631
|
-
log.warn('No session for encrypted message', { peerId: envelope.senderId
|
|
881
|
+
log.warn('No session for encrypted message', { peerId: peerTag(envelope.senderId) });
|
|
632
882
|
return;
|
|
633
883
|
}
|
|
634
884
|
|
|
@@ -653,6 +903,13 @@ export class Annex {
|
|
|
653
903
|
payload = plaintext;
|
|
654
904
|
}
|
|
655
905
|
|
|
906
|
+
// JHILKE: Intercept secure ANNEX control messages routed through
|
|
907
|
+
// the encrypted channel (bootstrap upgrade, encrypted rekey, etc.)
|
|
908
|
+
if (payload && payload._annexControl) {
|
|
909
|
+
await this._handleAnnexControl(payload._annexControl, envelope.senderId);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
656
913
|
// Dispatch to handlers
|
|
657
914
|
for (const handler of this.messageHandlers.values()) {
|
|
658
915
|
try {
|
|
@@ -675,48 +932,58 @@ export class Annex {
|
|
|
675
932
|
}
|
|
676
933
|
}
|
|
677
934
|
}
|
|
935
|
+
|
|
678
936
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
937
|
+
// KEM-based _handleRekey and _rekey REMOVED — JHILKE handles all rekeys
|
|
938
|
+
// deterministically via deriveRekeyKey(). Both nodes compute the same key
|
|
939
|
+
// after cricket coordination. No encapsulate/decapsulate dance needed.
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Send an ANNEX control message securely through the existing encrypted channel.
|
|
943
|
+
* When a session exists, wraps the control message inside an encrypted payload.
|
|
944
|
+
* Falls back to raw transport only when no session exists (pre-bootstrap).
|
|
945
|
+
*
|
|
946
|
+
* This eliminates plaintext KEM exchange after bootstrap — all ANNEX control
|
|
947
|
+
* messages (KEY_EXCHANGE, KEY_RESPONSE, REKEY) are encrypted on the wire.
|
|
948
|
+
*/
|
|
949
|
+
async _sendControlSecure(remoteNodeId, controlEnvelope) {
|
|
950
|
+
const session = this.sessions.get(remoteNodeId);
|
|
951
|
+
if (session?.established) {
|
|
952
|
+
// Wrap control message inside encrypted ANNEX payload
|
|
953
|
+
// _skipRekeyCheck prevents recursive rekey triggers
|
|
954
|
+
await this.send(remoteNodeId, { _annexControl: controlEnvelope.toJSON() }, { _skipRekeyCheck: true });
|
|
955
|
+
} else {
|
|
956
|
+
// No session yet — raw transport (only during pre-JHILKE or HELLO/WELCOME phase)
|
|
957
|
+
await this._sendToMesh(remoteNodeId, controlEnvelope);
|
|
958
|
+
}
|
|
701
959
|
}
|
|
702
960
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
senderId: this.identity.identity.nodeId,
|
|
713
|
-
recipientId: session.remoteNodeId,
|
|
714
|
-
sessionId: session.sessionId,
|
|
715
|
-
kemPublicKey: newPublicKey,
|
|
961
|
+
/**
|
|
962
|
+
* Handle a secure ANNEX control message received through the encrypted channel.
|
|
963
|
+
* The outer AEAD encryption guarantees authenticity — no separate signature
|
|
964
|
+
* check needed (only the session peer could have encrypted it).
|
|
965
|
+
*/
|
|
966
|
+
async _handleAnnexControl(controlData, senderId) {
|
|
967
|
+
log.debug('Processing secure ANNEX control', {
|
|
968
|
+
type: controlData.type,
|
|
969
|
+
from: peerTag(senderId),
|
|
716
970
|
});
|
|
717
971
|
|
|
718
|
-
|
|
719
|
-
|
|
972
|
+
switch (controlData.type) {
|
|
973
|
+
case ANNEX_CONFIG.messageTypes.KEY_EXCHANGE:
|
|
974
|
+
await this._handleKeyExchange(controlData);
|
|
975
|
+
break;
|
|
976
|
+
case ANNEX_CONFIG.messageTypes.KEY_RESPONSE:
|
|
977
|
+
await this._handleKeyResponse(controlData);
|
|
978
|
+
break;
|
|
979
|
+
case ANNEX_CONFIG.messageTypes.CLOSE: {
|
|
980
|
+
const closedSession = this.sessions.get(controlData.senderId);
|
|
981
|
+
if (closedSession) closedSession.channelState = ChannelState.CLOSED;
|
|
982
|
+
this.sessions.delete(controlData.senderId);
|
|
983
|
+
log.info('Channel closed by peer (secure)', { peerId: peerTag(controlData.senderId) });
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
720
987
|
}
|
|
721
988
|
|
|
722
989
|
async _sendToMesh(remoteNodeId, envelope) {
|
|
@@ -730,11 +997,98 @@ export class Annex {
|
|
|
730
997
|
});
|
|
731
998
|
}
|
|
732
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* Buffer an ANNEX message whose sender key hasn't arrived yet.
|
|
1002
|
+
* Replays automatically when 'peer-registered' fires, or discards
|
|
1003
|
+
* after _deferTimeoutMs (preventing memory leaks from spoofed senderIds).
|
|
1004
|
+
*/
|
|
1005
|
+
_deferMessage(envelope, origin) {
|
|
1006
|
+
const senderId = envelope.senderId;
|
|
1007
|
+
|
|
1008
|
+
// Already deferring a message from this sender — discard the older one
|
|
1009
|
+
if (this._deferredMessages.has(senderId)) {
|
|
1010
|
+
const existing = this._deferredMessages.get(senderId);
|
|
1011
|
+
clearTimeout(existing.timer);
|
|
1012
|
+
if (existing.onRegistered) {
|
|
1013
|
+
this.mesh.off('peer-registered', existing.onRegistered);
|
|
1014
|
+
}
|
|
1015
|
+
this._deferredMessages.delete(senderId);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Cap total deferred senders to prevent memory abuse
|
|
1019
|
+
if (this._deferredMessages.size >= this._maxDeferredSenders) {
|
|
1020
|
+
log.debug('Deferred ANNEX queue full, dropping message from unknown peer', {
|
|
1021
|
+
peerId: peerTag(senderId),
|
|
1022
|
+
type: envelope.type,
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
log.debug('Deferring ANNEX message until peer key arrives', {
|
|
1028
|
+
peerId: peerTag(senderId),
|
|
1029
|
+
type: envelope.type,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// Event listener: replay when peer registers
|
|
1033
|
+
const onRegistered = (registeredNodeId) => {
|
|
1034
|
+
if (registeredNodeId === senderId) {
|
|
1035
|
+
this._replayDeferred(senderId);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// Safety timeout: if key never arrives, discard silently
|
|
1040
|
+
const timer = setTimeout(() => {
|
|
1041
|
+
if (this._deferredMessages.has(senderId)) {
|
|
1042
|
+
this.mesh.off('peer-registered', onRegistered);
|
|
1043
|
+
this._deferredMessages.delete(senderId);
|
|
1044
|
+
log.debug('Deferred ANNEX message expired (peer key never arrived)', {
|
|
1045
|
+
peerId: peerTag(senderId),
|
|
1046
|
+
type: envelope.type,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}, this._deferTimeoutMs);
|
|
1050
|
+
|
|
1051
|
+
this._deferredMessages.set(senderId, { envelope, origin, timer, onRegistered });
|
|
1052
|
+
this.mesh.on('peer-registered', onRegistered);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Replay a deferred ANNEX message now that the sender's key is available.
|
|
1057
|
+
*/
|
|
1058
|
+
_replayDeferred(senderId) {
|
|
1059
|
+
const deferred = this._deferredMessages.get(senderId);
|
|
1060
|
+
if (!deferred) return;
|
|
1061
|
+
|
|
1062
|
+
clearTimeout(deferred.timer);
|
|
1063
|
+
this.mesh.off('peer-registered', deferred.onRegistered);
|
|
1064
|
+
this._deferredMessages.delete(senderId);
|
|
1065
|
+
|
|
1066
|
+
log.debug('Replaying deferred ANNEX message (peer key arrived)', {
|
|
1067
|
+
peerId: peerTag(senderId),
|
|
1068
|
+
type: deferred.envelope.type,
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// Re-enter the handler — this time _getPeerPublicKey should succeed
|
|
1072
|
+
this._handleAnnexMessage(deferred.envelope, deferred.origin).catch(err => {
|
|
1073
|
+
log.warn('Deferred ANNEX replay failed', { peerId: peerTag(senderId), error: err.message });
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
733
1077
|
_getPeerPublicKey(nodeId) {
|
|
734
|
-
// Get from
|
|
1078
|
+
// Get from WS peer info first
|
|
735
1079
|
if (this.mesh && this.mesh.peers) {
|
|
736
1080
|
const peer = this.mesh.peers.get(nodeId);
|
|
737
|
-
|
|
1081
|
+
if (peer?.identity?.publicKey) return peer.identity.publicKey;
|
|
1082
|
+
}
|
|
1083
|
+
// Fallback: relay peer keys stored during signed registration
|
|
1084
|
+
if (this.mesh && this.mesh._relayPeerKeys) {
|
|
1085
|
+
const key = this.mesh._relayPeerKeys.get(nodeId);
|
|
1086
|
+
if (key) return key;
|
|
1087
|
+
}
|
|
1088
|
+
// Fallback: SHERPA registry (populated during relay registration)
|
|
1089
|
+
if (this.mesh && this.mesh.sherpa?.registry) {
|
|
1090
|
+
const regPeer = this.mesh.sherpa.registry.get(nodeId);
|
|
1091
|
+
if (regPeer?.publicKey) return regPeer.publicKey;
|
|
738
1092
|
}
|
|
739
1093
|
return null;
|
|
740
1094
|
}
|