Add the TypeScript twin of the sealed box, proven by the Go package's fixtures (issue 650)
mesh/merge-gate head a7997fc857ae merged into trunk c579f4de7511 — pass: the change touches no module of the mesh's graph
mesh/repo-check head a7997fc857ae merged into trunk c579f4de7511 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered

(Also replaces the placeholder package.json the previous commit carried by mistake.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 23:48:02 +00:00
co-authored by Claude Opus 5.5
parent adfb9c84b5
commit a7997fc857
5 changed files with 221 additions and 1 deletions
+30 -1
View File
@@ -1 +1,30 @@
PKGJSON
{
"name": "@novox/mesh-sdk",
"version": "0.1.14",
"description": "The stable spine a Novox Mesh module's own code builds against.",
"type": "module",
"exports": {
".": "./dist/index.js",
"./contracts": "./dist/contracts/index.js",
"./provisioner": "./dist/provisioner/index.js",
"./tools": "./dist/tools/index.js",
"./messaging": "./dist/messaging/index.js",
"./events": "./dist/events/index.js",
"./primitives": "./dist/primitives/index.js",
"./stdio": "./dist/stdio/index.js",
"./state": "./dist/state/index.js",
"./seal": "./dist/seal/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"test": "node --test --experimental-strip-types 'test/*.test.ts'",
"prepare": "tsc"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}
+1
View File
@@ -11,3 +11,4 @@ export * as messaging from "./messaging/index.js";
export * as events from "./events/index.js";
export * as primitives from "./primitives/index.js";
export * as state from "./state/index.js";
export * as seal from "./seal/index.js";
+90
View File
@@ -0,0 +1,90 @@
// The sealed box itself (novox/hq ADR 0183, ADR 0206), with its two sources of randomness as arguments so
// the tests can prove it byte for byte against the Go package's vectors. Modules import `./index.js`.
import {
createCipheriv,
createDecipheriv,
createPrivateKey,
createPublicKey,
diffieHellman,
hkdfSync,
type KeyObject,
} from "node:crypto";
/** A value sealed to one recipient, as it travels in JSON; every field but `v` is standard base64. */
export interface SealedBox {
readonly v: number;
/** The one-time public key, SPKI DER. */
readonly eph: string;
readonly iv: string;
readonly tag: string;
readonly ct: string;
}
/** The only box version `open` opens. */
export const VERSION = 1;
const INFO = "novox-mesh sealed box v1";
const IV_BYTES = 12;
const TAG_BYTES = 16;
function x25519(key: KeyObject, what: string): KeyObject {
if (key.asymmetricKeyType !== "x25519") throw new Error(`not an X25519 ${what} key`);
return key;
}
export function publicFromPem(pem: string): KeyObject {
return x25519(createPublicKey({ key: pem, format: "pem" }), "public");
}
export function privateFromPem(pem: string): KeyObject {
return x25519(createPrivateKey({ key: pem, format: "pem" }), "private");
}
/** The recipient's raw 32-byte public key: the last 32 bytes of its SPKI DER. */
function raw(pub: KeyObject): Buffer {
const der = pub.export({ format: "der", type: "spki" });
return der.subarray(der.length - 32);
}
function boxKey(secret: Buffer, ephDer: Buffer, recipientRaw: Buffer): Buffer {
return Buffer.from(hkdfSync("sha256", secret, Buffer.concat([ephDer, recipientRaw]), INFO, 32));
}
/** `seal` with the one-time private key and the iv given. */
export function sealWith(plaintext: string, recipientPem: string, eph: KeyObject, iv: Buffer): SealedBox {
const recipient = publicFromPem(recipientPem);
x25519(eph, "one-time");
if (iv.length !== IV_BYTES) throw new Error(`an iv of ${iv.length} bytes, not ${IV_BYTES}`);
const secret = diffieHellman({ privateKey: eph, publicKey: recipient });
const ephDer = createPublicKey(eph).export({ format: "der", type: "spki" });
const cipher = createCipheriv("aes-256-gcm", boxKey(secret, ephDer, raw(recipient)), iv);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
return {
v: VERSION,
eph: ephDer.toString("base64"),
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
ct: ct.toString("base64"),
};
}
/** Open a box with the recipient's PEM private key; throws for a box to another key, changed, or of another version. */
export function open(box: SealedBox, privatePem: string): string {
if (box.v !== VERSION) throw new Error(`a sealed box of version ${box.v}, not ${VERSION}`);
const priv = privateFromPem(privatePem);
const ephDer = Buffer.from(box.eph, "base64");
const eph = x25519(createPublicKey({ key: ephDer, format: "der", type: "spki" }), "one-time");
const iv = Buffer.from(box.iv, "base64");
const tag = Buffer.from(box.tag, "base64");
// node accepts a shorter tag than 16 bytes; the box never has one, so a short one is refused here.
if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) throw new Error("the box does not open with this key");
const secret = diffieHellman({ privateKey: priv, publicKey: eph });
const decipher = createDecipheriv("aes-256-gcm", boxKey(secret, ephDer, raw(createPublicKey(priv))), iv);
decipher.setAuthTag(tag);
try {
return Buffer.concat([decipher.update(Buffer.from(box.ct, "base64")), decipher.final()]).toString("utf8");
} catch {
throw new Error("the box does not open with this key");
}
}
+29
View File
@@ -0,0 +1,29 @@
// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): X25519 for the agreement, HKDF-SHA256 for the
// key, AES-256-GCM for the box. The TypeScript twin of the Go package `go/seal`, byte for byte: the same
// fixtures prove both. A box is `{ v: 1, eph, iv, tag, ct }`; keys travel as PEM (SPKI public, PKCS #8
// private).
import { generateKeyPairSync, randomBytes } from "node:crypto";
import { sealWith, open, VERSION, type SealedBox } from "./box.js";
export { open, VERSION, type SealedBox };
/** A recipient's keypair as the two PEM strings it is kept and sent as. */
export interface KeyPair {
readonly publicKey: string;
readonly privateKey: string;
}
/** A fresh X25519 keypair, PEM-encoded. */
export function generateKeyPair(): KeyPair {
const { publicKey, privateKey } = generateKeyPairSync("x25519");
return {
publicKey: publicKey.export({ format: "pem", type: "spki" }) as string,
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
};
}
/** Seal plaintext to the recipient's PEM public key, with a fresh one-time key and iv. */
export function seal(plaintext: string, recipientPem: string): SealedBox {
return sealWith(plaintext, recipientPem, generateKeyPairSync("x25519").privateKey, randomBytes(12));
}
+71
View File
@@ -0,0 +1,71 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createPrivateKey } from "node:crypto";
import { seal, open, generateKeyPair, type SealedBox } from "../dist/seal/index.js";
import { sealWith } from "../dist/seal/box.js";
// The fixtures the Go package proves itself against: boxes the licence manager's own seal.go made, and
// the one the agent module's TypeScript made. One format, three implementations.
const fixtures = new URL("../go/seal/testdata/", import.meta.url);
const lm = JSON.parse(readFileSync(new URL("sealed-by-licence-manager.json", fixtures), "utf8")) as {
recipient: { publicKey: string; privateKey: string };
sealed: { plaintext: string; box: SealedBox }[];
vectors: { plaintext: string; ephPrivate: string; iv: string; box: SealedBox }[];
};
// PKCS #8 for an X25519 private key is this prefix and the 32 raw bytes.
const pkcs8 = (raw: Buffer) =>
createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b656e04220420", "hex"), raw]), format: "der", type: "pkcs8" });
test("seal: every box the licence manager sealed opens to its plaintext", () => {
assert.ok(lm.sealed.length > 0);
for (const s of lm.sealed) assert.equal(open(s.box, lm.recipient.privateKey), s.plaintext);
});
test("seal: given the same one-time key and iv, the box is byte for byte the licence manager's", () => {
assert.ok(lm.vectors.length > 0);
for (const v of lm.vectors) {
const box = sealWith(v.plaintext, lm.recipient.publicKey, pkcs8(Buffer.from(v.ephPrivate, "base64")), Buffer.from(v.iv, "base64"));
assert.deepEqual(box, v.box);
assert.equal(open(box, lm.recipient.privateKey), v.plaintext);
}
});
test("seal: the agent module's TypeScript box opens", () => {
const f = JSON.parse(readFileSync(new URL("sealed-by-typescript.json", fixtures), "utf8"));
assert.equal(open(f.box, f.privateKey), f.plaintext);
});
test("seal: a box opens for its recipient only, and two boxes share no one-time key or iv", () => {
const a = generateKeyPair();
const b = generateKeyPair();
const box = seal("a token", a.publicKey);
assert.equal(open(box, a.privateKey), "a token");
assert.throws(() => open(box, b.privateKey));
const again = seal("a token", a.publicKey);
assert.notEqual(again.eph, box.eph);
assert.notEqual(again.iv, box.iv);
});
test("seal: a box changed in any field, or of another version, does not open", () => {
const good = lm.sealed[1].box;
const flip = (s: string) => {
const b = Buffer.from(s, "base64");
b[b.length - 1] ^= 1;
return b.toString("base64");
};
for (const box of [
{ ...good, ct: flip(good.ct) },
{ ...good, tag: flip(good.tag) },
{ ...good, iv: flip(good.iv) },
{ ...good, eph: flip(good.eph) },
{ ...good, v: 2 },
]) assert.throws(() => open(box as SealedBox, lm.recipient.privateKey));
});
test("seal: a key that is not X25519 PEM is refused", () => {
assert.throws(() => seal("x", "not a key"));
assert.throws(() => open(lm.sealed[0].box, lm.recipient.publicKey));
});