@xiotbox/e2e-channel 0.1.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/lib/crypto.js ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * XiotBox E2E Secure Channel — generic cryptographic primitives.
3
+ *
4
+ * Application-agnostic building blocks for the `xiotbox-e2e` protocol:
5
+ * X25519 key agreement, HKDF-SHA256 session-key derivation, AES-256-GCM
6
+ * frame sealing, Ed25519 identity signing, and per-device key persistence.
7
+ *
8
+ * Nothing here knows about DSH, OpenClaw, tunnels or chat: any agent channel
9
+ * that relays opaque bytes through the XiotBox Gateway can reuse this module.
10
+ * The wire protocol is documented in `docs/e2e-secure-channel.md`.
11
+ */
12
+ import crypto from 'node:crypto';
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import os from 'node:os';
15
+ import { join } from 'node:path';
16
+ export const E2E_PROTOCOL = 'xiotbox-e2e';
17
+ export const E2E_VERSION = 1;
18
+ export const E2E_MODE_STREAM = 'stream';
19
+ /** Ed25519 SPKI DER prefix: SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING } */
20
+ export const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
21
+ function x25519ToJwk(raw, isPrivate) {
22
+ return {
23
+ kty: 'OKP',
24
+ crv: 'X25519',
25
+ x: raw.toString('base64url'),
26
+ ...(isPrivate ? { d: raw.toString('base64url') } : {}),
27
+ };
28
+ }
29
+ export function x25519Keypair() {
30
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519');
31
+ const pubJwk = publicKey.export({ format: 'jwk' });
32
+ const privJwk = privateKey.export({ format: 'jwk' });
33
+ const pub = Buffer.from(pubJwk.x ?? '', 'base64url');
34
+ const priv = Buffer.from(privJwk.d ?? '', 'base64url');
35
+ if (priv.length !== 32 || pub.length !== 32) {
36
+ throw new Error('x25519 keypair export failed');
37
+ }
38
+ return { priv, pub };
39
+ }
40
+ export function x25519Shared(priv, pub) {
41
+ if (priv.length !== 32 || pub.length !== 32) {
42
+ throw new Error('invalid_x25519_key_len');
43
+ }
44
+ const privateKey = crypto.createPrivateKey({ key: x25519ToJwk(priv, true), format: 'jwk' });
45
+ const publicKey = crypto.createPublicKey({ key: x25519ToJwk(pub, false), format: 'jwk' });
46
+ return crypto.diffieHellman({ privateKey, publicKey });
47
+ }
48
+ export function b64e(value) {
49
+ return Buffer.from(value).toString('base64');
50
+ }
51
+ export function b64d(value) {
52
+ if (typeof value !== 'string' || value.length === 0)
53
+ return undefined;
54
+ try {
55
+ return Buffer.from(value, 'base64');
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ /** Key id: first 16 hex chars of SHA-256(public key) — matches OpenClaw / client. */
62
+ export function computeKeyId(pubRaw) {
63
+ return crypto.createHash('sha256').update(pubRaw).digest('hex').slice(0, 16);
64
+ }
65
+ /** Identity fingerprint: full SHA-256 hex of the SPKI DER public key. */
66
+ export function computeIdentityFingerprint(pubDer) {
67
+ return crypto.createHash('sha256').update(pubDer).digest('hex');
68
+ }
69
+ /**
70
+ * Human-readable fingerprint for out-of-band verification: uppercase hex in
71
+ * groups of 4, e.g. `A1B2 C3D4 E5F6 …`. Host logs and client UIs must format
72
+ * identically so a user can compare them.
73
+ */
74
+ export function formatFingerprint(fingerprint) {
75
+ const hex = (fingerprint ?? '').toLowerCase().replace(/[^0-9a-f]/g, '');
76
+ const groups = [];
77
+ for (let i = 0; i < hex.length; i += 4) {
78
+ groups.push(hex.slice(i, i + 4).toUpperCase());
79
+ }
80
+ return groups.join(' ');
81
+ }
82
+ /** Extract the raw 32-byte Ed25519 public key from SPKI DER. */
83
+ export function ed25519RawFromDer(pubDer) {
84
+ if (pubDer.length !== 44 || !pubDer.subarray(0, 12).equals(ED25519_SPKI_PREFIX)) {
85
+ throw new Error('invalid_ed25519_spki_der');
86
+ }
87
+ return pubDer.subarray(12);
88
+ }
89
+ /** Normalize a peer Ed25519 public key (raw 32B or SPKI DER 44B) to raw 32B. */
90
+ export function normalizeEd25519Pub(value) {
91
+ if (value.length === 32)
92
+ return value;
93
+ if (value.length === 44) {
94
+ try {
95
+ return ed25519RawFromDer(value);
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Session keys & AEAD
105
+ // ---------------------------------------------------------------------------
106
+ /**
107
+ * Derive the per-channel session key.
108
+ * session_key = HKDF-SHA256(ECDH(eph), salt=channel_id, info='xiotbox-e2e|v=1|stream')
109
+ */
110
+ export function deriveSessionKey(shared, channelId) {
111
+ const info = Buffer.from(`${E2E_PROTOCOL}|v=${E2E_VERSION}|${E2E_MODE_STREAM}`, 'utf8');
112
+ const derived = crypto.hkdfSync('sha256', shared, Buffer.from(channelId, 'utf8'), info, 32);
113
+ return Buffer.from(derived);
114
+ }
115
+ /**
116
+ * AAD bound to every encrypted frame:
117
+ * 'xiotbox-e2e|v=1|stream|{channel_id}|{dir}|{seq}'
118
+ * `dir` is the direction the frame travels ('c2p' client→plugin, 'p2c' plugin→client).
119
+ */
120
+ export function buildFrameAad(channelId, dir, seq) {
121
+ return Buffer.from(`${E2E_PROTOCOL}|v=${E2E_VERSION}|${E2E_MODE_STREAM}|${channelId}|${dir}|${seq}`, 'utf8');
122
+ }
123
+ /**
124
+ * Handshake transcript signed by each side's Ed25519 identity key.
125
+ *
126
+ * Fields use a canonical role-agnostic order (`c_` = client/initiator side,
127
+ * `p_` = plugin/responder side) so both peers compute the identical string
128
+ * for the same `dir`, regardless of which side is signing.
129
+ */
130
+ export function buildHandshakeTranscript(opts) {
131
+ return Buffer.from(`${E2E_PROTOCOL}|v=${E2E_VERSION}|channel=${opts.channelId}|dir=${opts.dir}|` +
132
+ `c_eph=${opts.cEphPubB64}|p_eph=${opts.pEphPubB64}|c_kid=${opts.cKid}|` +
133
+ `p_kid=${opts.pKid}|c_nonce=${opts.cNonce}|p_nonce=${opts.pNonce}`, 'utf8');
134
+ }
135
+ /**
136
+ * Registration identity claim, mirroring the OpenClaw `ocid|...` canonical
137
+ * format so Lite/clients that already verify bot identity claims accept the
138
+ * same signature scheme.
139
+ */
140
+ export function buildRegistrationSigPayload(opts) {
141
+ const parts = [
142
+ 'v=1',
143
+ `device=${opts.deviceId}`,
144
+ `peer_pub=${opts.peerPubB64}`,
145
+ `peer_key_id=${opts.keyId}`,
146
+ `ts=${opts.ts}`,
147
+ `nonce=${opts.nonce}`,
148
+ ];
149
+ return Buffer.from(`ocid|${parts.join('|')}`, 'utf-8');
150
+ }
151
+ /** AES-256-GCM seal: returns { iv, tag, ct } all base64 for wire transport. */
152
+ export function sealFrame(key, plaintext, aad) {
153
+ const iv = crypto.randomBytes(12);
154
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
155
+ cipher.setAAD(aad);
156
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
157
+ const tag = cipher.getAuthTag();
158
+ return { iv: iv.toString('base64'), tag: tag.toString('base64'), ct: ct.toString('base64') };
159
+ }
160
+ /** AES-256-GCM open: returns the plaintext Buffer, or undefined on auth failure. */
161
+ export function openFrame(key, frame, aad) {
162
+ const iv = b64d(frame.iv);
163
+ const tag = b64d(frame.tag);
164
+ const ct = b64d(frame.ct);
165
+ if (iv === undefined || iv.length !== 12 || tag === undefined || tag.length !== 16 || ct === undefined) {
166
+ return undefined;
167
+ }
168
+ try {
169
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
170
+ decipher.setAAD(aad);
171
+ decipher.setAuthTag(tag);
172
+ return Buffer.concat([decipher.update(ct), decipher.final()]);
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // Ed25519 identity
180
+ // ---------------------------------------------------------------------------
181
+ export function generateEd25519Identity() {
182
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
183
+ const pubDer = publicKey.export({ format: 'der', type: 'spki' });
184
+ return {
185
+ privKey: privateKey,
186
+ pubRaw: ed25519RawFromDer(pubDer),
187
+ pubDer,
188
+ fingerprint: computeIdentityFingerprint(pubDer),
189
+ };
190
+ }
191
+ export function signMessage(privKey, message) {
192
+ return crypto.sign(null, message, privKey);
193
+ }
194
+ /** Verify an Ed25519 signature against a raw (32B) or SPKI DER (44B) public key. */
195
+ export function verifyMessage(pubKey, message, signature) {
196
+ if (pubKey.length !== 32 && pubKey.length !== 44)
197
+ return false;
198
+ try {
199
+ const der = pubKey.length === 32
200
+ ? Buffer.concat([ED25519_SPKI_PREFIX, pubKey])
201
+ : pubKey;
202
+ const key = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
203
+ return crypto.verify(null, message, key, signature);
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ // ---------------------------------------------------------------------------
210
+ // Per-device key persistence
211
+ // ---------------------------------------------------------------------------
212
+ /** Default key directory: `~/.xiotbox/e2e`. Consumers may pass their own. */
213
+ export function defaultE2EKeyDir() {
214
+ return join(os.homedir(), '.xiotbox', 'e2e');
215
+ }
216
+ export function deviceKeyPath(keyDir, deviceId) {
217
+ const safe = String(deviceId).replace(/[^A-Za-z0-9_.-]/g, '_');
218
+ return join(keyDir, `keys_${safe}.json`);
219
+ }
220
+ /** Load persisted keys for a device, creating them on first run. */
221
+ export function loadOrCreateDeviceKeys(deviceId, keyDir, log) {
222
+ const dir = keyDir ?? defaultE2EKeyDir();
223
+ const path = deviceKeyPath(dir, deviceId);
224
+ let stored;
225
+ if (existsSync(path)) {
226
+ try {
227
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
228
+ if (raw?.v === 1 && raw.x25519?.priv_b64 && raw.x25519?.pub_b64 && raw.identity?.priv_der_b64) {
229
+ stored = raw;
230
+ }
231
+ }
232
+ catch (error) {
233
+ log?.('warn', `[E2E] ignoring corrupt key file ${path}: ${error instanceof Error ? error.message : String(error)}`);
234
+ }
235
+ }
236
+ if (stored === undefined) {
237
+ const keypair = x25519Keypair();
238
+ const identity = generateEd25519Identity();
239
+ stored = {
240
+ v: 1,
241
+ x25519: {
242
+ priv_b64: keypair.priv.toString('base64'),
243
+ pub_b64: keypair.pub.toString('base64'),
244
+ key_id: computeKeyId(keypair.pub),
245
+ },
246
+ identity: {
247
+ priv_der_b64: identity.privKey.export({ format: 'der', type: 'pkcs8' }).toString('base64'),
248
+ pub_der_b64: identity.pubDer.toString('base64'),
249
+ fingerprint: identity.fingerprint,
250
+ },
251
+ };
252
+ try {
253
+ mkdirSync(dir, { recursive: true });
254
+ writeFileSync(path, JSON.stringify(stored, null, 2));
255
+ chmodSync(path, 0o600);
256
+ }
257
+ catch (error) {
258
+ log?.('warn', `[E2E] key persistence failed (keys live in memory only): ${error instanceof Error ? error.message : String(error)}`);
259
+ }
260
+ log?.('info', `[E2E] generated fresh device keys device=${deviceId} keyId=${stored.x25519.key_id} fp=${stored.identity.fingerprint.slice(0, 12)}…`);
261
+ return {
262
+ keypair: {
263
+ priv: Buffer.from(stored.x25519.priv_b64, 'base64'),
264
+ pub: Buffer.from(stored.x25519.pub_b64, 'base64'),
265
+ keyId: stored.x25519.key_id,
266
+ },
267
+ identity,
268
+ };
269
+ }
270
+ const keypair = {
271
+ priv: Buffer.from(stored.x25519.priv_b64, 'base64'),
272
+ pub: Buffer.from(stored.x25519.pub_b64, 'base64'),
273
+ keyId: stored.x25519.key_id,
274
+ };
275
+ const identity = {
276
+ privKey: crypto.createPrivateKey({ key: Buffer.from(stored.identity.priv_der_b64, 'base64'), format: 'der', type: 'pkcs8' }),
277
+ pubRaw: ed25519RawFromDer(Buffer.from(stored.identity.pub_der_b64, 'base64')),
278
+ pubDer: Buffer.from(stored.identity.pub_der_b64, 'base64'),
279
+ fingerprint: stored.identity.fingerprint,
280
+ };
281
+ return { keypair, identity };
282
+ }
283
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * XiotBox E2E Secure Channel — public entry point.
3
+ *
4
+ * Generic end-to-end encrypted relay protocol for the XiotBox Gateway:
5
+ * any agent channel (DSH, OpenClaw, future Codex/Claude Code runtimes)
6
+ * relays opaque frames through the Gateway and wraps them with this layer.
7
+ *
8
+ * See `docs/e2e-secure-channel.md` for the wire protocol specification and
9
+ * cross-language (Dart / Python) implementation guidance.
10
+ */
11
+ export * from './crypto.js';
12
+ export * from './secure-channel.js';
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * XiotBox E2E Secure Channel — public entry point.
3
+ *
4
+ * Generic end-to-end encrypted relay protocol for the XiotBox Gateway:
5
+ * any agent channel (DSH, OpenClaw, future Codex/Claude Code runtimes)
6
+ * relays opaque frames through the Gateway and wraps them with this layer.
7
+ *
8
+ * See `docs/e2e-secure-channel.md` for the wire protocol specification and
9
+ * cross-language (Dart / Python) implementation guidance.
10
+ */
11
+ export * from './crypto.js';
12
+ export * from './secure-channel.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * XiotBox E2E Secure Channel — generic end-to-end encrypted channel.
3
+ *
4
+ * Sits between an application protocol and the XiotBox Gateway relay: any
5
+ * channel that relays opaque JSON frames (DSH web tunnel, agent RPC, PTY
6
+ * streams, …) can wrap them with this layer and get:
7
+ *
8
+ * - confidentiality — the Gateway only ever sees ciphertext frames
9
+ * - integrity — AES-256-GCM AEAD rejects any modified frame
10
+ * - anti-replay — per-direction sequence counters bound in the AAD
11
+ * - forward secrecy — fresh ephemeral X25519 key pair per channel
12
+ * - peer authentication — Ed25519 identity signatures over the handshake
13
+ * transcript, pinned TOFU by the application via `peerIdentityPolicy`
14
+ *
15
+ * Wire format (all frames are JSON, base64-encoded by the relay):
16
+ * handshake: {t:'hello', v:1, pub, kid, nonce}
17
+ * {t:'hello_ack', v:1, pub, kid, nonce}
18
+ * {t:'ident', v:1, id, fp, sig} (identity claim, signed)
19
+ * {t:'ident_ack', v:1, id, fp, sig}
20
+ * data: {t:'enc', v:1, iv, tag, ct} (ct = encrypted app frame)
21
+ *
22
+ * Application keepalives (e.g. the tunnel's ping/pong) stay outside this
23
+ * layer and are relayed plaintext; they carry no sensitive content.
24
+ *
25
+ * See `docs/e2e-secure-channel.md` for the full protocol specification.
26
+ */
27
+ import { type Ed25519Identity, type X25519Keypair } from './crypto.js';
28
+ export type ChannelDirection = 'c2p' | 'p2c';
29
+ export interface PeerIdentityResult {
30
+ ok: boolean;
31
+ reason?: string;
32
+ }
33
+ /**
34
+ * Application-provided trust policy for the peer's Ed25519 identity.
35
+ * The protocol always verifies the signature; this decides whether the
36
+ * claimed public key is *trusted* as the peer (TOFU pinning, registered
37
+ * identity comparison, …).
38
+ */
39
+ export type PeerIdentityPolicy = (peerIdRaw: Buffer, fingerprint: string, transcript: Buffer, signature: Buffer) => PeerIdentityResult;
40
+ export interface SecureChannelOptions {
41
+ channelId: string;
42
+ /** Client = 'initiator' (sends hello), host/plugin = 'responder'. */
43
+ role: 'initiator' | 'responder';
44
+ /** Long-term X25519 keypair of this side (kid in hello/hello_ack). */
45
+ keypair: X25519Keypair;
46
+ /** Ed25519 identity of this side; required to complete the handshake. */
47
+ identity: Ed25519Identity;
48
+ /** Registered key id of the peer; verified when set (mismatch → reject). */
49
+ peerKid?: string;
50
+ /** Trust policy for the peer's identity claim. Default: accept (TOFU). */
51
+ peerIdentityPolicy?: PeerIdentityPolicy;
52
+ /** Emit a frame onto the relay. Must return false when the channel is gone. */
53
+ send: (frame: Record<string, unknown>) => boolean;
54
+ /** Called once when the channel closes (timeout, security failure, …) so
55
+ * the transport can tear down the relay channel. */
56
+ onClose?: (reason: string) => void;
57
+ log?: (level: 'debug' | 'info' | 'warn', message: string) => void;
58
+ handshakeTimeoutMs?: number;
59
+ }
60
+ /**
61
+ * One E2E session bound to one relay channel. Created per channel; the
62
+ * session key lives only in memory and is dropped on close.
63
+ */
64
+ export declare class SecureChannel {
65
+ private readonly opts;
66
+ private state;
67
+ private readonly eph;
68
+ private readonly myNonce;
69
+ private readonly myKid;
70
+ private peerEphPubB64;
71
+ private peerKid;
72
+ private peerNonce;
73
+ private peerIdentityFp;
74
+ private sessionKey;
75
+ private sendSeq;
76
+ private recvSeq;
77
+ private timeout;
78
+ private closedReason;
79
+ constructor(options: SecureChannelOptions);
80
+ get ready(): boolean;
81
+ get channelId(): string;
82
+ get direction(): ChannelDirection;
83
+ /** Initiate (initiator) or arm the handshake timeout (responder). */
84
+ start(): void;
85
+ /**
86
+ * Feed an inbound relay frame. Returns:
87
+ * { status: 'ok', app?: Record } — app frame decrypted (when ready)
88
+ * { status: 'ok' } — handshake/control frame consumed
89
+ * { status: 'closed', reason } — protocol/security failure
90
+ */
91
+ receive(frame: Record<string, unknown>): {
92
+ status: 'ok';
93
+ app?: Record<string, unknown>;
94
+ } | {
95
+ status: 'closed';
96
+ reason: string;
97
+ };
98
+ /** Wrap an application frame into an encrypted relay frame. */
99
+ wrap(app: Record<string, unknown>): Record<string, unknown>;
100
+ /** Close the channel; clears the session key and timers. */
101
+ close(reason?: string): void;
102
+ private handleHello;
103
+ private handleHelloAck;
104
+ private handleIdent;
105
+ private handleIdentAck;
106
+ private verifyIdentityClaim;
107
+ private buildTranscript;
108
+ private handleEnc;
109
+ private armTimeout;
110
+ private clearTimeout;
111
+ }