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/jhilke.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JHILKE — Just Hidden In-band Legitimate Key Exchange
|
|
3
|
+
* झिल्के (jhilke) — the sound of crickets
|
|
4
|
+
*
|
|
5
|
+
* Like D-Day cricket clickers: a signal meaningful only to those who know.
|
|
6
|
+
* Like Navajo codetalkers: a language outsiders can't parse.
|
|
7
|
+
*
|
|
8
|
+
* JHILKE provides two critical functions:
|
|
9
|
+
*
|
|
10
|
+
* 1. DETERMINISTIC BOOTSTRAP: Both nodes derive the same initial symmetric
|
|
11
|
+
* key from their shared code hash (verification phrase anchor). This
|
|
12
|
+
* eliminates plaintext KEM exchange — traffic is encrypted from message #1.
|
|
13
|
+
*
|
|
14
|
+
* 2. STEGANOGRAPHIC REKEY: Ongoing key rotations are coordinated via hidden
|
|
15
|
+
* signals embedded in mesh_entropy messages. No explicit REKEY messages
|
|
16
|
+
* are ever sent — an observer sees only entropy exchange.
|
|
17
|
+
*
|
|
18
|
+
* Security properties:
|
|
19
|
+
* - Bootstrap key is derived from code hash + node IDs (deterministic)
|
|
20
|
+
* - Bootstrap key is NOT a long-term secret (upgrades to KEM immediately)
|
|
21
|
+
* - Cricket signals use HKDF dialect derived from codebase hash
|
|
22
|
+
* - Only nodes with identical codebase can encode/decode signals
|
|
23
|
+
* - SST Fibonacci 24-cycle modulates signal encoding (rotational variety)
|
|
24
|
+
* - Ternary state machine: PREPARE (+1) → READY (0) → SWITCH (-1)
|
|
25
|
+
*
|
|
26
|
+
* @module mesh/jhilke
|
|
27
|
+
* @license MIT
|
|
28
|
+
* @copyright 2026 YAKMESH™ Contributors
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { randomBytes } from 'crypto';
|
|
32
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
33
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
34
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
35
|
+
import { createLogger } from '../utils/logger.js';
|
|
36
|
+
|
|
37
|
+
// TRIBHUJ ternary primitives
|
|
38
|
+
import { POSITIVE, NEUTRAL, NEGATIVE } from '../oracle/tribhuj.js';
|
|
39
|
+
|
|
40
|
+
// SST Fibonacci 24-cycle for signal modulation
|
|
41
|
+
import { fibonacciRoot } from '../oracle/sst.js';
|
|
42
|
+
|
|
43
|
+
const log = createLogger('mesh:jhilke');
|
|
44
|
+
|
|
45
|
+
// ═══ JHILKE Configuration ═══
|
|
46
|
+
const JHILKE_CONFIG = {
|
|
47
|
+
// Bootstrap — deterministic key from verification phrase anchor
|
|
48
|
+
bootstrapSalt: 'YAKMESH-JHILKE-BOOTSTRAP-2026',
|
|
49
|
+
bootstrapInfo: 'yakmesh-jhilke-bootstrap-key-v1',
|
|
50
|
+
|
|
51
|
+
// Rekey — deterministic key rotation (no KEM round-trip)
|
|
52
|
+
rekeySalt: 'YAKMESH-JHILKE-REKEY-2026',
|
|
53
|
+
rekeyInfo: 'yakmesh-jhilke-rekey-v1',
|
|
54
|
+
|
|
55
|
+
// Dialect — steganographic signal encoding
|
|
56
|
+
dialectSalt: 'jhilke-cricket-salt-2026',
|
|
57
|
+
dialectInfo: 'yakmesh-jhilke-dialect-v1',
|
|
58
|
+
|
|
59
|
+
// Signal parameters
|
|
60
|
+
signalInfo: 'jhilke-signal-v1',
|
|
61
|
+
signalSize: 8, // 8 bytes of HKDF output per signal
|
|
62
|
+
paddingMin: 16, // minimum random padding bytes
|
|
63
|
+
paddingMax: 64, // maximum random padding bytes
|
|
64
|
+
|
|
65
|
+
// Timing
|
|
66
|
+
tickInterval: 1000, // 1 second ticks
|
|
67
|
+
preparePhase: 3, // ticks in PREPARE before moving to READY
|
|
68
|
+
switchDelay: 3, // ticks of SWITCH signaling before executing (must receive peer SWITCH too)
|
|
69
|
+
retryAfterTicks: 24, // "tag, you're it" retry timeout
|
|
70
|
+
abortAfterCycles: 3, // abort after 3 retry cycles (72 ticks total)
|
|
71
|
+
tickTolerance: 1, // ±1 tick tolerance for signal decoding
|
|
72
|
+
|
|
73
|
+
// Ternary intents (TRIBHUJ trits)
|
|
74
|
+
INTENT_PREPARE: POSITIVE, // +1: I'm preparing new keys
|
|
75
|
+
INTENT_READY: NEUTRAL, // 0: My new key is ready
|
|
76
|
+
INTENT_SWITCH: NEGATIVE, // -1: Switching to new key now
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Per-peer coordination state.
|
|
81
|
+
* Tracks where we are in the cricket chirp dance with each peer.
|
|
82
|
+
*/
|
|
83
|
+
class JhilkeSession {
|
|
84
|
+
constructor(peerId) {
|
|
85
|
+
this.peerId = peerId;
|
|
86
|
+
this.state = 'idle'; // idle | prepare | ready | switch | exchanging
|
|
87
|
+
this.tick = 0; // tick when this coordination started
|
|
88
|
+
this.switchTick = 0; // tick when we entered switch state
|
|
89
|
+
this.retryCount = 0; // how many retry cycles
|
|
90
|
+
this.initiator = false; // did WE request this rekey?
|
|
91
|
+
this.peerReady = false; // has peer signaled READY?
|
|
92
|
+
this.ourReady = false; // have WE signaled READY?
|
|
93
|
+
this.peerSwitchReceived = false; // has peer signaled SWITCH?
|
|
94
|
+
this.startedAt = null;
|
|
95
|
+
this.lastSignalSent = null;
|
|
96
|
+
this.lastSignalReceived = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
reset() {
|
|
100
|
+
this.state = 'idle';
|
|
101
|
+
this.tick = 0;
|
|
102
|
+
this.switchTick = 0;
|
|
103
|
+
this.retryCount = 0;
|
|
104
|
+
this.initiator = false;
|
|
105
|
+
this.peerReady = false;
|
|
106
|
+
this.ourReady = false;
|
|
107
|
+
this.peerSwitchReceived = false;
|
|
108
|
+
this.startedAt = null;
|
|
109
|
+
this.lastSignalSent = null;
|
|
110
|
+
this.lastSignalReceived = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
116
|
+
// JHILKE Coordinator — the cricket chorus conductor
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
export class JhilkeCoordinator {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.codeHash = options.codeHash; // Oracle code hash (same for all valid nodes)
|
|
122
|
+
this.nodeId = options.nodeId; // Our node ID
|
|
123
|
+
this.annex = options.annex; // ANNEX instance
|
|
124
|
+
this.mesh = options.mesh; // Mesh instance (for sendTo)
|
|
125
|
+
|
|
126
|
+
// Derived seeds (deterministic from code hash — same for all nodes)
|
|
127
|
+
this.dialectSeed = this._deriveDialectSeed();
|
|
128
|
+
|
|
129
|
+
// Per-peer sessions
|
|
130
|
+
this.sessions = new Map(); // peerId -> JhilkeSession
|
|
131
|
+
|
|
132
|
+
// Tick timer
|
|
133
|
+
this._tickTimer = null;
|
|
134
|
+
this._globalTick = 0;
|
|
135
|
+
|
|
136
|
+
// Stats
|
|
137
|
+
this.stats = {
|
|
138
|
+
bootstrapKeysDerived: 0,
|
|
139
|
+
signalsSent: 0,
|
|
140
|
+
signalsReceived: 0,
|
|
141
|
+
signalsDecoded: 0,
|
|
142
|
+
rekeyCoordinations: 0,
|
|
143
|
+
rekeySuccesses: 0,
|
|
144
|
+
rekeyAborts: 0,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
log.info('JHILKE coordinator initialized', {
|
|
148
|
+
dialectFingerprint: bytesToHex(this.dialectSeed).slice(0, 16) + '...',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shared time reference for steganographic signal encoding/decoding.
|
|
154
|
+
* Uses wall-clock seconds (Unix time) so both nodes agree on the tick
|
|
155
|
+
* regardless of when they started. GPS-synchronized via MA-902 Stratum 1.
|
|
156
|
+
* The per-node _globalTick is still used for session age tracking.
|
|
157
|
+
*/
|
|
158
|
+
_sharedTick() {
|
|
159
|
+
return Math.floor(Date.now() / 1000);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ══════════════════════════════════════════════════════════════════
|
|
163
|
+
// BOOTSTRAP: Deterministic initial key from shared code hash
|
|
164
|
+
// "Both nodes arrive at the same conclusion because of the
|
|
165
|
+
// anchoring verification phrase" — the code hash IS the anchor.
|
|
166
|
+
// ══════════════════════════════════════════════════════════════════
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Derive a deterministic bootstrap encryption key for a peer.
|
|
170
|
+
* Both nodes independently compute the SAME key because they share
|
|
171
|
+
* the same code hash (enforced by the Validation Oracle + network
|
|
172
|
+
* fingerprint check in HELLO/WELCOME).
|
|
173
|
+
*
|
|
174
|
+
* Key = HKDF(sha3_256, codeHash, sort(nodeA, nodeB), bootstrapInfo, 32)
|
|
175
|
+
*
|
|
176
|
+
* This key is NOT a long-term secret — anyone with source code + both
|
|
177
|
+
* node IDs could theoretically compute it. It exists to:
|
|
178
|
+
* 1. Eliminate plaintext KEM exchange messages entirely
|
|
179
|
+
* 2. Buy seconds for JHILKE to coordinate a proper KEM upgrade
|
|
180
|
+
* 3. Make ALL traffic encrypted from the very first post-handshake message
|
|
181
|
+
*
|
|
182
|
+
* The bootstrap key is replaced by a proper KEM key almost immediately.
|
|
183
|
+
*/
|
|
184
|
+
deriveBootstrapKey(peerId) {
|
|
185
|
+
const hashBytes = hexToBytes(this.codeHash);
|
|
186
|
+
|
|
187
|
+
// Sort node IDs so both sides derive the same key (order-independent)
|
|
188
|
+
const [first, second] = [this.nodeId, peerId].sort();
|
|
189
|
+
const salt = utf8ToBytes(`${JHILKE_CONFIG.bootstrapSalt}:${first}:${second}`);
|
|
190
|
+
const info = utf8ToBytes(JHILKE_CONFIG.bootstrapInfo);
|
|
191
|
+
|
|
192
|
+
const key = hkdf(sha3_256, hashBytes, salt, info, 32);
|
|
193
|
+
|
|
194
|
+
this.stats.bootstrapKeysDerived++;
|
|
195
|
+
log.debug('Bootstrap key derived (verification phrase anchor)', {
|
|
196
|
+
peer: peerId.slice(0, 16),
|
|
197
|
+
keyFingerprint: bytesToHex(key).slice(0, 8) + '...',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return Buffer.from(key);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ══════════════════════════════════════════════════════════════════
|
|
204
|
+
// REKEY: Deterministic key rotation — both nodes derive the SAME
|
|
205
|
+
// new key independently, no KEM round-trip, no race condition.
|
|
206
|
+
// ══════════════════════════════════════════════════════════════════
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Derive a deterministic rekey encryption key for a peer.
|
|
210
|
+
* Both nodes independently compute the SAME new key because they share:
|
|
211
|
+
* - codeHash (verified by Oracle)
|
|
212
|
+
* - currentKey (established via initial KEM exchange)
|
|
213
|
+
* - epoch (incremented together after cricket coordination)
|
|
214
|
+
*
|
|
215
|
+
* Key = HKDF(sha3_256, SHA3(codeHash || currentKey), salt(nodeIds + epoch), rekeyInfo, 32)
|
|
216
|
+
*
|
|
217
|
+
* Security properties:
|
|
218
|
+
* - PFS: depends on currentKey (random from initial KEM), which is discarded after
|
|
219
|
+
* - Forward secure: each epoch is a one-way derivation from the previous
|
|
220
|
+
* - Not publicly derivable: requires knowledge of currentKey
|
|
221
|
+
* - Deterministic: no network round-trip, both sides compute simultaneously
|
|
222
|
+
*/
|
|
223
|
+
deriveRekeyKey(peerId, epoch, currentKey) {
|
|
224
|
+
// IKM = SHA3(codeHash || currentKey) — binds network identity to session secret
|
|
225
|
+
const ikm = sha3_256.create()
|
|
226
|
+
.update(hexToBytes(this.codeHash))
|
|
227
|
+
.update(currentKey)
|
|
228
|
+
.digest();
|
|
229
|
+
|
|
230
|
+
// Sort node IDs so both sides derive the same key (order-independent)
|
|
231
|
+
const [first, second] = [this.nodeId, peerId].sort();
|
|
232
|
+
const salt = utf8ToBytes(`${JHILKE_CONFIG.rekeySalt}:${first}:${second}:${epoch}`);
|
|
233
|
+
const info = utf8ToBytes(JHILKE_CONFIG.rekeyInfo);
|
|
234
|
+
|
|
235
|
+
const key = hkdf(sha3_256, ikm, salt, info, 32);
|
|
236
|
+
|
|
237
|
+
log.debug('Deterministic rekey derived', {
|
|
238
|
+
peer: peerId.slice(0, 16),
|
|
239
|
+
epoch,
|
|
240
|
+
keyFingerprint: bytesToHex(key).slice(0, 8) + '...',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return Buffer.from(key);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ══════════════════════════════════════════════════════════════════
|
|
247
|
+
// DIALECT: Steganographic signal encoding
|
|
248
|
+
// Only nodes with the same codebase can speak this language.
|
|
249
|
+
// ══════════════════════════════════════════════════════════════════
|
|
250
|
+
|
|
251
|
+
_deriveDialectSeed() {
|
|
252
|
+
const hashBytes = hexToBytes(this.codeHash);
|
|
253
|
+
const salt = utf8ToBytes(JHILKE_CONFIG.dialectSalt);
|
|
254
|
+
const info = utf8ToBytes(JHILKE_CONFIG.dialectInfo);
|
|
255
|
+
return hkdf(sha3_256, hashBytes, salt, info, 32);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate a steganographic signal encoding a specific intent.
|
|
260
|
+
* The signal is an 8-byte HKDF output that encodes:
|
|
261
|
+
* - both node IDs (order-sorted for determinism)
|
|
262
|
+
* - current tick
|
|
263
|
+
* - SST Fibonacci 24-cycle position (rotational modulation)
|
|
264
|
+
* - the ternary intent (+1, 0, -1)
|
|
265
|
+
*
|
|
266
|
+
* Only nodes sharing the dialect seed can produce or decode this signal.
|
|
267
|
+
*/
|
|
268
|
+
_generateSignal(peerId, tick, intent) {
|
|
269
|
+
const [first, second] = [this.nodeId, peerId].sort();
|
|
270
|
+
|
|
271
|
+
// SST modulation: Fibonacci 24-cycle digital root at this tick
|
|
272
|
+
const fibPos = tick % 24;
|
|
273
|
+
const fibRoot = fibonacciRoot(fibPos);
|
|
274
|
+
|
|
275
|
+
const context = utf8ToBytes(
|
|
276
|
+
`${first}:${second}:${tick}:${fibPos}:${fibRoot}:${intent}`
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return hkdf(sha3_256, this.dialectSeed, context,
|
|
280
|
+
utf8ToBytes(JHILKE_CONFIG.signalInfo), JHILKE_CONFIG.signalSize);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Decode an incoming signal by brute-forcing all 3 intents
|
|
285
|
+
* across ±1 tick tolerance (9 total attempts).
|
|
286
|
+
* Trivial for nodes sharing the dialect, impossible without it.
|
|
287
|
+
*/
|
|
288
|
+
_decodeSignal(peerId, signalBytes, currentTick) {
|
|
289
|
+
const intents = [
|
|
290
|
+
JHILKE_CONFIG.INTENT_PREPARE,
|
|
291
|
+
JHILKE_CONFIG.INTENT_READY,
|
|
292
|
+
JHILKE_CONFIG.INTENT_SWITCH,
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const signalHex = bytesToHex(signalBytes);
|
|
296
|
+
|
|
297
|
+
for (let offset = -JHILKE_CONFIG.tickTolerance; offset <= JHILKE_CONFIG.tickTolerance; offset++) {
|
|
298
|
+
const testTick = currentTick + offset;
|
|
299
|
+
if (testTick < 0) continue;
|
|
300
|
+
|
|
301
|
+
for (const intent of intents) {
|
|
302
|
+
const expected = this._generateSignal(peerId, testTick, intent);
|
|
303
|
+
if (bytesToHex(expected) === signalHex) {
|
|
304
|
+
return { intent, tick: testTick, offset };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null; // Not a JHILKE signal (genuine entropy)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
// ══════════════════════════════════════════════════════════════════
|
|
314
|
+
// COORDINATION: State machine + tick loop
|
|
315
|
+
// ══════════════════════════════════════════════════════════════════
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Start the tick loop (1-second heartbeat for cricket coordination)
|
|
319
|
+
*/
|
|
320
|
+
start() {
|
|
321
|
+
if (this._tickTimer) return;
|
|
322
|
+
this._tickTimer = setInterval(() => this._tick(), JHILKE_CONFIG.tickInterval);
|
|
323
|
+
log.info('JHILKE tick loop started (crickets chirping)');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Stop the tick loop and clean up all sessions
|
|
328
|
+
*/
|
|
329
|
+
stop() {
|
|
330
|
+
if (this._tickTimer) {
|
|
331
|
+
clearInterval(this._tickTimer);
|
|
332
|
+
this._tickTimer = null;
|
|
333
|
+
}
|
|
334
|
+
this.sessions.clear();
|
|
335
|
+
log.info('JHILKE tick loop stopped');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Request a rekey for a specific peer.
|
|
340
|
+
* Called by ANNEX when needsRekey() returns true.
|
|
341
|
+
* Instead of sending a plaintext REKEY, we begin the cricket dance.
|
|
342
|
+
*/
|
|
343
|
+
initiateRekey(peerId) {
|
|
344
|
+
let session = this.sessions.get(peerId);
|
|
345
|
+
if (session && session.state !== 'idle') {
|
|
346
|
+
log.debug('JHILKE rekey already in progress', { peer: peerId.slice(0, 16) });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!session) {
|
|
351
|
+
session = new JhilkeSession(peerId);
|
|
352
|
+
this.sessions.set(peerId, session);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
session.state = 'prepare';
|
|
356
|
+
session.initiator = true;
|
|
357
|
+
session.startedAt = Date.now();
|
|
358
|
+
session.tick = this._globalTick;
|
|
359
|
+
|
|
360
|
+
this.stats.rekeyCoordinations++;
|
|
361
|
+
log.info('JHILKE rekey initiated (cricket chirping begins)', {
|
|
362
|
+
peer: peerId.slice(0, 16),
|
|
363
|
+
tick: this._globalTick,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Handle incoming mesh_entropy message — check for hidden cricket signals
|
|
369
|
+
*/
|
|
370
|
+
handleIncoming(fromPeerId, message) {
|
|
371
|
+
if (!message.jhilke) return;
|
|
372
|
+
|
|
373
|
+
this.stats.signalsReceived++;
|
|
374
|
+
|
|
375
|
+
let signalBytes;
|
|
376
|
+
try {
|
|
377
|
+
signalBytes = hexToBytes(message.jhilke);
|
|
378
|
+
} catch {
|
|
379
|
+
return; // Malformed hex, ignore
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const decoded = this._decodeSignal(fromPeerId, signalBytes, this._sharedTick());
|
|
383
|
+
if (!decoded) return; // Not a valid signal for us
|
|
384
|
+
|
|
385
|
+
this.stats.signalsDecoded++;
|
|
386
|
+
|
|
387
|
+
log.debug('JHILKE cricket decoded', {
|
|
388
|
+
peer: fromPeerId.slice(0, 16),
|
|
389
|
+
intent: decoded.intent === JHILKE_CONFIG.INTENT_PREPARE ? 'PREPARE' :
|
|
390
|
+
decoded.intent === JHILKE_CONFIG.INTENT_READY ? 'READY' : 'SWITCH',
|
|
391
|
+
tick: decoded.tick,
|
|
392
|
+
offset: decoded.offset,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
this._processSignal(fromPeerId, decoded);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Process a decoded signal through the ternary state machine
|
|
400
|
+
*/
|
|
401
|
+
_processSignal(peerId, decoded) {
|
|
402
|
+
let session = this.sessions.get(peerId);
|
|
403
|
+
|
|
404
|
+
if (!session) {
|
|
405
|
+
session = new JhilkeSession(peerId);
|
|
406
|
+
this.sessions.set(peerId, session);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
session.lastSignalReceived = Date.now();
|
|
410
|
+
const { intent } = decoded;
|
|
411
|
+
|
|
412
|
+
switch (intent) {
|
|
413
|
+
case JHILKE_CONFIG.INTENT_PREPARE:
|
|
414
|
+
// Peer is preparing for rekey
|
|
415
|
+
if (session.state === 'idle') {
|
|
416
|
+
// They initiated — we join the coordination
|
|
417
|
+
session.state = 'prepare';
|
|
418
|
+
session.initiator = false;
|
|
419
|
+
session.startedAt = Date.now();
|
|
420
|
+
session.tick = this._globalTick;
|
|
421
|
+
this.stats.rekeyCoordinations++;
|
|
422
|
+
log.info('JHILKE: peer initiated rekey, joining dance', {
|
|
423
|
+
peer: peerId.slice(0, 16),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case JHILKE_CONFIG.INTENT_READY:
|
|
429
|
+
// Peer's new key material is ready
|
|
430
|
+
if (session.state === 'prepare' || session.state === 'ready') {
|
|
431
|
+
session.peerReady = true;
|
|
432
|
+
log.debug('JHILKE: peer ready', { peer: peerId.slice(0, 16) });
|
|
433
|
+
|
|
434
|
+
// Both sides ready → move to switch phase
|
|
435
|
+
if (session.ourReady && session.peerReady) {
|
|
436
|
+
session.state = 'switch';
|
|
437
|
+
session.switchTick = this._globalTick; // Track when we entered switch state
|
|
438
|
+
log.info('JHILKE: both ready, entering switch phase', { peer: peerId.slice(0, 16) });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case JHILKE_CONFIG.INTENT_SWITCH:
|
|
444
|
+
// Peer is switching — mark peer switch received
|
|
445
|
+
session.peerSwitchReceived = true;
|
|
446
|
+
// Don't execute here — let _tick() handle it after switchDelay
|
|
447
|
+
if (session.state === 'ready' && session.ourReady && session.peerReady) {
|
|
448
|
+
session.state = 'switch';
|
|
449
|
+
session.switchTick = this._globalTick; // Track when we entered switch state
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Main tick handler — drives the state machine forward.
|
|
458
|
+
* Called every 1 second. Sends cricket signals and manages transitions.
|
|
459
|
+
*/
|
|
460
|
+
_tick() {
|
|
461
|
+
this._globalTick++;
|
|
462
|
+
|
|
463
|
+
for (const [peerId, session] of this.sessions) {
|
|
464
|
+
if (session.state === 'idle') continue;
|
|
465
|
+
|
|
466
|
+
const sessionAge = this._globalTick - session.tick;
|
|
467
|
+
|
|
468
|
+
switch (session.state) {
|
|
469
|
+
case 'prepare':
|
|
470
|
+
// Send PREPARE chirp
|
|
471
|
+
this._sendSignal(peerId, JHILKE_CONFIG.INTENT_PREPARE);
|
|
472
|
+
|
|
473
|
+
// After preparePhase ticks, our key material is "ready"
|
|
474
|
+
if (sessionAge >= JHILKE_CONFIG.preparePhase) {
|
|
475
|
+
session.ourReady = true;
|
|
476
|
+
session.state = 'ready';
|
|
477
|
+
log.debug('JHILKE: our key ready, signaling READY', {
|
|
478
|
+
peer: peerId.slice(0, 16),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
|
|
483
|
+
case 'ready':
|
|
484
|
+
// Send READY chirp
|
|
485
|
+
this._sendSignal(peerId, JHILKE_CONFIG.INTENT_READY);
|
|
486
|
+
|
|
487
|
+
// Check if both sides are ready
|
|
488
|
+
if (session.ourReady && session.peerReady) {
|
|
489
|
+
session.state = 'switch';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Retry timeout: "tag, you're it"
|
|
493
|
+
if (sessionAge > JHILKE_CONFIG.retryAfterTicks) {
|
|
494
|
+
session.retryCount++;
|
|
495
|
+
if (session.retryCount >= JHILKE_CONFIG.abortAfterCycles) {
|
|
496
|
+
log.warn('JHILKE: rekey coordination timed out', {
|
|
497
|
+
peer: peerId.slice(0, 16),
|
|
498
|
+
retries: session.retryCount,
|
|
499
|
+
});
|
|
500
|
+
session.reset();
|
|
501
|
+
this.stats.rekeyAborts++;
|
|
502
|
+
} else {
|
|
503
|
+
// Reset and retry
|
|
504
|
+
session.tick = this._globalTick;
|
|
505
|
+
session.peerReady = false;
|
|
506
|
+
session.ourReady = false;
|
|
507
|
+
session.state = 'prepare';
|
|
508
|
+
log.debug('JHILKE: retry cycle', {
|
|
509
|
+
peer: peerId.slice(0, 16),
|
|
510
|
+
retry: session.retryCount,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
|
|
516
|
+
case 'switch':
|
|
517
|
+
// Send SWITCH chirp, then execute after switchDelay ticks
|
|
518
|
+
this._sendSignal(peerId, JHILKE_CONFIG.INTENT_SWITCH);
|
|
519
|
+
|
|
520
|
+
// Execute ONLY after:
|
|
521
|
+
// 1. We've been in switch state for switchDelay ticks
|
|
522
|
+
// 2. AND we received peer's SWITCH signal
|
|
523
|
+
const switchAge = this._globalTick - (session.switchTick || session.tick);
|
|
524
|
+
if (switchAge >= JHILKE_CONFIG.switchDelay && session.peerSwitchReceived) {
|
|
525
|
+
this._executeSwitch(peerId, session);
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case 'exchanging':
|
|
530
|
+
// KEM exchange in progress via ANNEX internals — nothing to do
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Periodic check: scan ANNEX sessions for rekey needs
|
|
536
|
+
// (every 30 ticks = 30 seconds, matches ANNEX ping interval)
|
|
537
|
+
if (this._globalTick % 30 === 0) {
|
|
538
|
+
this.checkAnnexRekeys();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Send a steganographic signal (cricket chirp) to a peer
|
|
544
|
+
*/
|
|
545
|
+
_sendSignal(peerId, intent) {
|
|
546
|
+
const signal = this._generateSignal(peerId, this._sharedTick(), intent);
|
|
547
|
+
|
|
548
|
+
// Random padding to vary message size (camouflage)
|
|
549
|
+
const paddingSize = JHILKE_CONFIG.paddingMin +
|
|
550
|
+
Math.floor(Math.random() * (JHILKE_CONFIG.paddingMax - JHILKE_CONFIG.paddingMin));
|
|
551
|
+
const padding = bytesToHex(randomBytes(paddingSize));
|
|
552
|
+
|
|
553
|
+
// Send as mesh_entropy — blends with normal entropy exchange traffic
|
|
554
|
+
this.mesh.sendTo(peerId, {
|
|
555
|
+
type: 'mesh_entropy',
|
|
556
|
+
entropy: bytesToHex(randomBytes(32)), // Genuine entropy contribution
|
|
557
|
+
jhilke: bytesToHex(signal), // Hidden cricket signal
|
|
558
|
+
pad: padding, // Variable-size camouflage
|
|
559
|
+
t: Date.now(),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
this.stats.signalsSent++;
|
|
563
|
+
|
|
564
|
+
const session = this.sessions.get(peerId);
|
|
565
|
+
if (session) session.lastSignalSent = Date.now();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Execute the actual key switch after both sides confirmed ready.
|
|
570
|
+
*
|
|
571
|
+
* DETERMINISTIC: Both nodes derive the SAME new key independently.
|
|
572
|
+
* No KEM round-trip, no initiator/responder asymmetry, no race condition.
|
|
573
|
+
* The cricket dance (PREPARE → READY → SWITCH) ensures both nodes
|
|
574
|
+
* execute this at the same moment — then both compute the same key.
|
|
575
|
+
*/
|
|
576
|
+
async _executeSwitch(peerId, session) {
|
|
577
|
+
if (session.state === 'exchanging') return;
|
|
578
|
+
session.state = 'exchanging';
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const annexSession = this.annex.sessions.get(peerId);
|
|
582
|
+
if (!annexSession || !annexSession.encryptionKey) {
|
|
583
|
+
log.warn('JHILKE: no ANNEX session for rekey switch', {
|
|
584
|
+
peer: peerId.slice(0, 16),
|
|
585
|
+
});
|
|
586
|
+
this.stats.rekeyAborts++;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const epoch = annexSession.rekeyEpoch + 1;
|
|
591
|
+
const newKey = this.deriveRekeyKey(peerId, epoch, annexSession.encryptionKey);
|
|
592
|
+
|
|
593
|
+
// Both nodes arrive here simultaneously — switch key, no round-trip.
|
|
594
|
+
annexSession.deterministicRekey(newKey, epoch);
|
|
595
|
+
|
|
596
|
+
this.stats.rekeySuccesses++;
|
|
597
|
+
log.info('JHILKE: deterministic rekey complete', {
|
|
598
|
+
peer: peerId.slice(0, 16),
|
|
599
|
+
epoch,
|
|
600
|
+
ticks: this._globalTick - session.tick,
|
|
601
|
+
});
|
|
602
|
+
} catch (err) {
|
|
603
|
+
log.error('JHILKE: rekey execution failed', {
|
|
604
|
+
peer: peerId.slice(0, 16),
|
|
605
|
+
error: err.message,
|
|
606
|
+
});
|
|
607
|
+
this.stats.rekeyAborts++;
|
|
608
|
+
} finally {
|
|
609
|
+
session.reset();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Check all ANNEX sessions for rekey needs.
|
|
615
|
+
* Called periodically by the tick loop.
|
|
616
|
+
*/
|
|
617
|
+
checkAnnexRekeys() {
|
|
618
|
+
if (!this.annex) return;
|
|
619
|
+
|
|
620
|
+
for (const [peerId, annexSession] of this.annex.sessions) {
|
|
621
|
+
if (annexSession.needsRekey()) {
|
|
622
|
+
this.initiateRekey(peerId);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Clean up session for a disconnected peer
|
|
629
|
+
*/
|
|
630
|
+
removePeer(peerId) {
|
|
631
|
+
this.sessions.delete(peerId);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Get JHILKE stats
|
|
636
|
+
*/
|
|
637
|
+
getStats() {
|
|
638
|
+
return {
|
|
639
|
+
...this.stats,
|
|
640
|
+
activeSessions: this.sessions.size,
|
|
641
|
+
activeCoordinations: [...this.sessions.values()].filter(s => s.state !== 'idle').length,
|
|
642
|
+
globalTick: this._globalTick,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Export config for testing
|
|
648
|
+
export { JHILKE_CONFIG };
|
|
649
|
+
|
|
650
|
+
export default JhilkeCoordinator;
|
|
651
|
+
|