/* RFC-0921: Shared Ed25519 key management — generation, loading, saving, and encoding helpers. Does not implement signing or verification — those live in sign.ts. Does not manage key publication or key rotation — domain-specific concerns belong in consumers. RFC-0921: initial signing key management module. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared */ import path from "node:path"; import { createDefaultIO, type WorkspaceIO } from "../kernel/workspace-io.ts"; import { createPrivateKey, createPublicKey } from "node:crypto"; import * as ed from "@noble/ed25519"; import type { KeyEncoding, SigningKeyPair } from "./types.ts"; export async function generateKeyPair(): Promise { const privateKey = ed.utils.randomSecretKey(); const publicKey = await getPublicKey(privateKey); return { privateKey, publicKey }; } export async function getPublicKey(privateKey: Uint8Array): Promise { return ed.getPublicKeyAsync(privateKey); } export async function loadPrivateKey( source: { pem: string } | { hex: string } | { filePath: string; encoding: KeyEncoding }, io: WorkspaceIO = createDefaultIO().io, ): Promise { if ("pem" in source) { return fromPem(source.pem); } if ("hex" in source) { return fromHex(source.hex); } const raw = (await io.readFile(source.filePath)).trim(); if (source.encoding === "pem") { return fromPem(raw); } return fromHex(raw); } export async function loadPublicKey( source: { pem: string } | { hex: string } | { filePath: string; encoding: KeyEncoding }, io: WorkspaceIO = createDefaultIO().io, ): Promise { if ("pem" in source) { return fromPem(source.pem); } if ("hex" in source) { return fromHex(source.hex); } const raw = (await io.readFile(source.filePath)).trim(); if (source.encoding === "pem") { return fromPem(raw); } return fromHex(raw); } export async function saveKeyPair( keyPair: SigningKeyPair, outputDir: string, encoding: KeyEncoding, io: WorkspaceIO = createDefaultIO().io, ): Promise<{ privateKeyPath: string; publicKeyPath: string }> { await io.mkdir(outputDir); const privateKeyPath = path.join(outputDir, `signing.key${encoding === "pem" ? ".pem" : ""}`); const publicKeyPath = path.join(outputDir, `signing.key.pub${encoding === "pem" ? ".pem" : ""}`); if (encoding === "pem") { await io.writeFile(privateKeyPath, toPem(keyPair.privateKey, "private")); await io.writeFile(publicKeyPath, toPem(keyPair.publicKey, "public")); } else { await io.writeFile(privateKeyPath, toHex(keyPair.privateKey)); await io.writeFile(publicKeyPath, toHex(keyPair.publicKey)); } return { privateKeyPath, publicKeyPath }; } export function toHex(bytes: Uint8Array): string { return Buffer.from(bytes).toString("hex"); } export function fromHex(hex: string): Uint8Array { return new Uint8Array(Buffer.from(hex, "hex")); } export function toPem(bytes: Uint8Array, type: "private" | "public"): string { const label = type === "private" ? "PRIVATE KEY" : "PUBLIC KEY"; const base64 = Buffer.from(bytes).toString("base64"); const lines = base64.match(/.{1,64}/g) ?? [base64]; return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`; } export function fromPem(pem: string): Uint8Array { const base64 = pem .replace(/-----BEGIN [A-Z ]+-----/g, "") .replace(/-----END [A-Z ]+-----/g, "") .replace(/\s+/g, ""); return new Uint8Array(Buffer.from(base64, "base64")); } export function privateKeyPemToBytes(pem: string): Uint8Array { const key = createPrivateKey(pem); if (key.asymmetricKeyType !== "ed25519") { throw new Error( `Wrong private key type: ${key.asymmetricKeyType ?? "unknown"}. Expected ed25519.`, ); } const jwk = key.export({ format: "jwk" }) as { d: string }; return new Uint8Array(Buffer.from(jwk.d, "base64url")); } export function publicKeyPemToBytes(pem: string): Uint8Array { const key = createPublicKey(pem); if (key.asymmetricKeyType !== "ed25519") { throw new Error( `Wrong public key type: ${key.asymmetricKeyType ?? "unknown"}. Expected ed25519.`, ); } const jwk = key.export({ format: "jwk" }) as { x: string }; return new Uint8Array(Buffer.from(jwk.x, "base64url")); } export async function keyExists( filePath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { return io.exists(filePath); }