@robota-sdk/agent-keyring 0.0.0-stage → 3.1.1

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +661 -0
  3. package/README.md +91 -2
  4. package/dist/node/index.cjs +1 -0
  5. package/dist/node/index.d.cts +470 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +470 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +2 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +74 -3
  12. package/src/__tests__/authorship-constant-time.test.ts +46 -0
  13. package/src/__tests__/authorship.test.ts +65 -0
  14. package/src/__tests__/crypto-failures.test.ts +89 -0
  15. package/src/__tests__/fixtures/keyring-v1.json +25 -0
  16. package/src/__tests__/fixtures/sealed-v1.json +8 -0
  17. package/src/__tests__/helpers.ts +130 -0
  18. package/src/__tests__/kdf-bounds.test.ts +154 -0
  19. package/src/__tests__/keyring-document.test.ts +251 -0
  20. package/src/__tests__/keyring.test.ts +329 -0
  21. package/src/__tests__/keyslot.test.ts +98 -0
  22. package/src/__tests__/known-answers.test.ts +110 -0
  23. package/src/__tests__/link-helpers.ts +228 -0
  24. package/src/__tests__/owner-handover-derivation.test.ts +50 -0
  25. package/src/__tests__/owner-handover-dispose.test.ts +64 -0
  26. package/src/__tests__/owner-handover.test.ts +145 -0
  27. package/src/__tests__/owner-link-adversarial.test.ts +712 -0
  28. package/src/__tests__/owner-link-grant-wipe.test.ts +75 -0
  29. package/src/__tests__/owner-link-known-answers.test.ts +117 -0
  30. package/src/__tests__/owner-link-secret-wipe.test.ts +116 -0
  31. package/src/__tests__/owner-link.test.ts +229 -0
  32. package/src/__tests__/read-keys.test.ts +67 -0
  33. package/src/__tests__/recovery-key.test.ts +76 -0
  34. package/src/__tests__/runtime-identity.test.ts +171 -0
  35. package/src/__tests__/sealed-format-vectors.test.ts +91 -0
  36. package/src/__tests__/sealing.test.ts +376 -0
  37. package/src/__tests__/spawner-exchange.test.ts +127 -0
  38. package/src/__tests__/transient-key-pair.test.ts +36 -0
  39. package/src/aead.ts +62 -0
  40. package/src/authorship.ts +61 -0
  41. package/src/bytes.ts +67 -0
  42. package/src/errors.ts +106 -0
  43. package/src/fingerprint.ts +32 -0
  44. package/src/index.ts +77 -0
  45. package/src/kdf.ts +111 -0
  46. package/src/key-pairs.ts +148 -0
  47. package/src/keyring-document.ts +168 -0
  48. package/src/keyring-types.ts +51 -0
  49. package/src/keyring.ts +307 -0
  50. package/src/keyslot.ts +66 -0
  51. package/src/link-entropy.ts +24 -0
  52. package/src/link-frames.ts +271 -0
  53. package/src/link-primitives.ts +77 -0
  54. package/src/owner-handover.ts +155 -0
  55. package/src/owner-link.ts +508 -0
  56. package/src/read-keys.ts +78 -0
  57. package/src/recovery-key.ts +79 -0
  58. package/src/runtime-certificate.ts +129 -0
  59. package/src/runtime-identity.ts +166 -0
  60. package/src/sealing.ts +281 -0
  61. package/src/signatures.ts +45 -0
  62. package/src/spawner-exchange.ts +155 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["LENGTH_PREFIX_BYTES","requireBytes","#material","#require","SECRETS","#sealingKeyMatches","#secrets","#require","#instance","#entropy","#spawnerKey","#trust","#exchanged","#role","#instancePublicKey","#sendKey","#receiveKey","#requireOpen","#sent","#received","#handover","#auth","#entropy","#expectedInstance","#state","#identity","#binding","#step","#pending"],"sources":["../../src/errors.ts","../../src/keyring-types.ts","../../src/bytes.ts","../../src/aead.ts","../../src/kdf.ts","../../src/keyring-document.ts","../../src/authorship.ts","../../src/key-pairs.ts","../../src/keyslot.ts","../../src/recovery-key.ts","../../src/read-keys.ts","../../src/keyring.ts","../../src/fingerprint.ts","../../src/signatures.ts","../../src/runtime-certificate.ts","../../src/owner-handover.ts","../../src/link-entropy.ts","../../src/link-primitives.ts","../../src/spawner-exchange.ts","../../src/runtime-identity.ts","../../src/link-frames.ts","../../src/owner-link.ts","../../src/sealing.ts"],"sourcesContent":["/**\n * Why an operation was refused. The union is the whole error vocabulary: a caller decides on `code`,\n * never on the message.\n *\n * - `wrong-credential` — the passphrase or recovery key did not open the keyslot. A mistyped\n * credential and a tampered slot are indistinguishable by design (both fail authentication).\n * - `malformed-keyring` / `unsupported-version` — the keyring document is not a valid version-1\n * document, or names a version this build does not know. `unsupported-version` also covers an\n * unknown version byte on a sealed key, record or blob.\n * - `kdf-parameters-out-of-bounds` — a slot asks for a scrypt cost outside the accepted range; raised\n * before any key derivation runs. `kdf-failed` means a key derivation or key generation could not\n * run on this host (for example memory, or a crypto build without the curve), with inputs that\n * were in range.\n * - `empty-passphrase` / `invalid-recovery-key` — the credential has the wrong form; no key\n * derivation ran.\n * - `slot-not-found` — the keyring has no slot of the type the operation needs.\n * - `malformed-ciphertext` — a sealed key, record or blob is structurally impossible (too short).\n * - `decryption-failed` — authentication failed: wrong key, wrong AAD, or modified bytes.\n * - `invalid-argument` — a key or buffer argument has the wrong type or length, or a key the\n * cipher or key import refuses.\n * - `keys-locked` — the unlocked key set was locked and no longer hands out keys.\n * - `link-refused` — an owner-link step was refused; the error is a {@link LinkRefusedError}, whose\n * `reason` names the check that failed. The other end of a link is never told the reason.\n * - `link-closed` — the link was closed, by `close()` or by a frame that did not open, and protects\n * nothing more.\n */\nexport type TKeyringErrorCode =\n | 'wrong-credential'\n | 'malformed-keyring'\n | 'unsupported-version'\n | 'kdf-parameters-out-of-bounds'\n | 'kdf-failed'\n | 'empty-passphrase'\n | 'invalid-recovery-key'\n | 'slot-not-found'\n | 'malformed-ciphertext'\n | 'decryption-failed'\n | 'invalid-argument'\n | 'keys-locked'\n | 'link-refused'\n | 'link-closed';\n\n/**\n * The only error this package throws on purpose. Messages are fixed text that names a field or a\n * rule; no message or property ever carries a passphrase, a key, a recovery key or any value derived\n * from them, and the original `node:crypto` error is deliberately not chained.\n */\nexport class KeyringError extends Error {\n readonly code: TKeyringErrorCode;\n\n constructor(code: TKeyringErrorCode, message: string) {\n super(message);\n this.name = 'KeyringError';\n this.code = code;\n }\n}\n\n/**\n * Why an owner-link step was refused. Each names the one check that failed, for logs and for the\n * caller's fixed wording; none says anything about a key.\n *\n * - `malformed` — a message or value of the wrong length, version, mode or encoding.\n * - `out-of-order` — a step taken twice, or before its turn.\n * - `spawner-unavailable` — spawner standing was asked of a runtime without a spawner key: it never\n * ran its exchange, or its spawner key was forgotten when its trust root was installed.\n * - `not-certified` — owner mode was asked of a runtime with no trust root.\n * - `already-certified` — a second trust root.\n * - `certificate-mismatch` — the certificate is not the attach key's signature over this instance\n * and binding: made by another keyring, or for another runtime or start.\n * - `instance-mismatch` — the runtime answered with an instance key other than its exchange's.\n * - `bad-runtime-proof` — the runtime's proof of the transcript did not verify.\n * - `bad-proof` — the initiator's proof of the transcript did not verify.\n */\nexport type TLinkRefusalReason =\n | 'malformed'\n | 'out-of-order'\n | 'spawner-unavailable'\n | 'not-certified'\n | 'already-certified'\n | 'certificate-mismatch'\n | 'instance-mismatch'\n | 'bad-runtime-proof'\n | 'bad-proof';\n\nconst LINK_REFUSAL_MESSAGES: Readonly<Record<TLinkRefusalReason, string>> = {\n malformed: 'The owner link refused a malformed message.',\n 'out-of-order': 'The owner link refused a step out of order.',\n 'spawner-unavailable': 'The runtime holds no spawner key.',\n 'not-certified': 'The runtime holds no trust root.',\n 'already-certified': 'The runtime already holds a trust root.',\n 'certificate-mismatch': 'The certificate does not certify this runtime for this owner.',\n 'instance-mismatch': 'The runtime answered with another instance key.',\n 'bad-runtime-proof': 'The runtime did not prove the transcript.',\n 'bad-proof': 'The initiator did not prove the transcript.',\n};\n\n/** An owner-link refusal: a `KeyringError` with code `link-refused` and the failed check as `reason`. */\nexport class LinkRefusedError extends KeyringError {\n readonly reason: TLinkRefusalReason;\n\n constructor(reason: TLinkRefusalReason) {\n super('link-refused', LINK_REFUSAL_MESSAGES[reason]);\n this.name = 'LinkRefusedError';\n this.reason = reason;\n }\n}\n","/** The only keyring document version this build reads and writes. */\nexport const KEYRING_VERSION = 1;\n\n/**\n * How a passphrase slot stretches the passphrase. All fields are stored in the slot so a keyring\n * written at one cost still unlocks after the default cost changes.\n */\nexport interface IScryptKdfParameters {\n readonly algorithm: 'scrypt';\n readonly N: number;\n readonly r: number;\n readonly p: number;\n /** base64url, 16 bytes. */\n readonly salt: string;\n}\n\ninterface IKeyslotBase {\n /** base64url, 16 random bytes; bound into the wrapping AAD. */\n readonly id: string;\n /** base64url, 12 bytes. */\n readonly iv: string;\n /** base64url, the AES-256-GCM-wrapped master key plus its 16-byte tag (48 bytes). */\n readonly wrappedKey: string;\n}\n\nexport interface IPassphraseKeyslot extends IKeyslotBase {\n readonly type: 'passphrase';\n readonly kdf: IScryptKdfParameters;\n}\n\nexport interface IRecoveryKeyslot extends IKeyslotBase {\n readonly type: 'recovery';\n}\n\n/**\n * A keyslot is a tagged union on `type`: a future way to unlock (a device secret, for example) adds\n * a member here and a branch in the parser, without touching the existing slots.\n */\nexport type TKeyslot = IPassphraseKeyslot | IRecoveryKeyslot;\nexport type TKeyslotType = TKeyslot['type'];\n\n/**\n * The persisted keyring document. There is deliberately no field for the attach public key: the\n * file lets anyone seal to the owner, but nothing in it lets a program claim to be the owner.\n */\nexport interface IKeyring {\n readonly version: typeof KEYRING_VERSION;\n /** base64url, the raw 32-byte X25519 sealing public key. */\n readonly sealingPublicKey: string;\n readonly slots: readonly TKeyslot[];\n}\n","import { Buffer } from 'node:buffer';\n\nimport { KeyringError } from './errors.js';\n\nconst BASE64URL_PATTERN = /^[A-Za-z0-9_-]*$/;\nconst LENGTH_PREFIX_BYTES = 2;\nconst MAX_PART_BYTES = 0xffff;\n\n/** A byte array from any realm (a `Buffer` qualifies); `instanceof` fails across realms. */\nexport function isBytes(value: unknown): value is Uint8Array {\n return Object.prototype.toString.call(value) === '[object Uint8Array]';\n}\n\nexport function toBase64Url(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64url');\n}\n\n/**\n * Decodes canonical, unpadded base64url of exactly `expectedLength` bytes, or returns `undefined`.\n * `Buffer` alone is lenient (it skips junk characters and accepts non-canonical trailing bits), so a\n * document could carry two spellings of one value; the round-trip check keeps the encoding unique.\n */\nexport function fromBase64Url(text: unknown, expectedLength: number): Buffer | undefined {\n if (typeof text !== 'string' || !BASE64URL_PATTERN.test(text)) return undefined;\n const decoded = Buffer.from(text, 'base64url');\n if (decoded.length !== expectedLength) return undefined;\n return decoded.toString('base64url') === text ? decoded : undefined;\n}\n\n/** {@link fromBase64Url} for keyring fields, where a bad encoding means a malformed document. */\nexport function decodeKeyringField(text: unknown, expectedLength: number): Buffer {\n const decoded = fromBase64Url(text, expectedLength);\n if (!decoded) {\n throw new KeyringError(\n 'malformed-keyring',\n 'The keyring is malformed: a field is not decodable.',\n );\n }\n return decoded;\n}\n\nexport function utf8(text: string): Buffer {\n return Buffer.from(text, 'utf8');\n}\n\n/**\n * Joins parts so that no two different part lists produce the same bytes (each part is preceded by\n * its 2-byte big-endian length). Used wherever several values are bound into one AEAD AAD or one\n * HKDF input.\n */\nexport function concatLengthPrefixed(parts: readonly Uint8Array[]): Buffer {\n const chunks: Buffer[] = [];\n for (const part of parts) {\n if (part.length > MAX_PART_BYTES) {\n throw new KeyringError('invalid-argument', 'A value bound into the AAD exceeds 65535 bytes.');\n }\n const prefix = Buffer.alloc(LENGTH_PREFIX_BYTES);\n prefix.writeUInt16BE(part.length);\n chunks.push(prefix, Buffer.from(part.buffer, part.byteOffset, part.byteLength));\n }\n return Buffer.concat(chunks);\n}\n\n/** Zeroes buffers in place. Best effort: it cannot reach copies the JavaScript engine made. */\nexport function wipe(...buffers: ReadonlyArray<Uint8Array | undefined>): void {\n for (const buffer of buffers) buffer?.fill(0);\n}\n","import { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv } from 'node:crypto';\n\nimport { wipe } from './bytes.js';\nimport { KeyringError } from './errors.js';\n\nexport const GCM_IV_BYTES = 12;\nexport const GCM_TAG_BYTES = 16;\nconst AES_256_GCM = 'aes-256-gcm';\n\n/**\n * AES-256-GCM seal. Returns `ciphertext || tag`. The cipher is AES-GCM everywhere because it is the\n * AEAD both Node and Bun implement (ChaCha20-Poly1305 is not available on Bun).\n */\nexport function gcmEncrypt(\n key: Uint8Array,\n iv: Uint8Array,\n plaintext: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n try {\n const cipher = createCipheriv(AES_256_GCM, key, iv, { authTagLength: GCM_TAG_BYTES });\n cipher.setAAD(aad);\n const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n return Buffer.concat([body, cipher.getAuthTag()]);\n } catch {\n // The inputs are checked before they get here; what is left is a key or IV the cipher refused.\n throw new KeyringError('invalid-argument', 'The key or IV cannot be used for encryption.');\n }\n}\n\n/**\n * AES-256-GCM open of `ciphertext || tag`. Plaintext is returned only after the tag verified; any\n * failure — wrong key, wrong AAD, modified or truncated bytes — is one `decryption-failed`.\n */\nexport function gcmDecrypt(\n key: Uint8Array,\n iv: Uint8Array,\n sealed: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n if (sealed.length < GCM_TAG_BYTES) {\n throw new KeyringError('decryption-failed', 'Authentication of the ciphertext failed.');\n }\n const bodyLength = sealed.length - GCM_TAG_BYTES;\n const body = sealed.subarray(0, bodyLength);\n const tag = sealed.subarray(bodyLength);\n const parts: Buffer[] = [];\n try {\n const decipher = createDecipheriv(AES_256_GCM, key, iv, { authTagLength: GCM_TAG_BYTES });\n decipher.setAAD(aad);\n decipher.setAuthTag(tag);\n parts.push(decipher.update(body));\n parts.push(decipher.final());\n return Buffer.concat(parts);\n } catch {\n throw new KeyringError('decryption-failed', 'Authentication of the ciphertext failed.');\n } finally {\n // `update` hands out unauthenticated plaintext before `final` verifies the tag; never keep it.\n wipe(...parts);\n }\n}\n","import { Buffer } from 'node:buffer';\nimport { hkdfSync, scrypt } from 'node:crypto';\n\nimport { utf8 } from './bytes.js';\nimport { KeyringError } from './errors.js';\n\nexport const KEY_BYTES = 32;\nexport const SCRYPT_SALT_BYTES = 16;\n\nexport interface IScryptParameters {\n readonly N: number;\n readonly r: number;\n readonly p: number;\n}\n\n/** The cost a new passphrase slot is written with (OWASP's scrypt baseline, ~128 MiB, ~160 ms). */\nexport const DEFAULT_SCRYPT_PARAMETERS: IScryptParameters = { N: 2 ** 17, r: 8, p: 1 };\n\n/**\n * The cost range an unlock accepts. The floor stops a hostile keyring from downgrading the work\n * factor; the ceiling stops one from making unlock allocate gigabytes or run for minutes. The file\n * is writable by every program of the user, so the memory a keyring may ask for is capped at 512 MiB\n * (four times the default) however N and r are combined. All of it is checked before any key\n * derivation starts.\n */\nexport const SCRYPT_BOUNDS = {\n minN: 2 ** 15,\n maxN: 2 ** 19,\n minR: 8,\n maxR: 16,\n p: 1,\n maxMemoryBytes: 2 ** 29,\n} as const;\n\nconst SCRYPT_BLOCK_BYTES = 128;\nconst SCRYPT_MEMORY_HEADROOM_BYTES = 1024 * 1024;\n\nexport function assertScryptParametersInBounds(parameters: IScryptParameters): void {\n const { N, r, p } = parameters;\n const nInBounds =\n Number.isInteger(N) &&\n N >= SCRYPT_BOUNDS.minN &&\n N <= SCRYPT_BOUNDS.maxN &&\n (N & (N - 1)) === 0;\n const rInBounds = Number.isInteger(r) && r >= SCRYPT_BOUNDS.minR && r <= SCRYPT_BOUNDS.maxR;\n const memoryInBounds = SCRYPT_BLOCK_BYTES * r * N <= SCRYPT_BOUNDS.maxMemoryBytes;\n if (!nInBounds || !rInBounds || !memoryInBounds || p !== SCRYPT_BOUNDS.p) {\n throw new KeyringError(\n 'kdf-parameters-out-of-bounds',\n 'A passphrase slot requests scrypt parameters outside the accepted range.',\n );\n }\n}\n\n/**\n * NFKC then UTF-8, so the same typed text unlocks on every platform and keyboard layout. Only the\n * empty string is refused; length and strength policy belong to the caller. The returned buffer is\n * the caller's to wipe (the JavaScript string it came from cannot be wiped).\n */\nexport function normalizePassphrase(passphrase: unknown): Buffer {\n if (typeof passphrase !== 'string') {\n throw new KeyringError('invalid-argument', 'The passphrase must be a string.');\n }\n const normalized = passphrase.normalize('NFKC');\n if (normalized.length === 0) {\n throw new KeyringError('empty-passphrase', 'The passphrase is empty.');\n }\n return utf8(normalized);\n}\n\n/**\n * Derives the passphrase slot's key-encryption key. Asynchronous (`crypto.scrypt` runs on the\n * libuv thread pool) so ~160 ms of work never blocks the caller's event loop.\n */\nexport function deriveKeyFromPassphrase(\n passphrase: Uint8Array,\n salt: Uint8Array,\n parameters: IScryptParameters,\n): Promise<Buffer> {\n assertScryptParametersInBounds(parameters);\n const { N, r, p } = parameters;\n // scrypt needs ~128*r*(N+p+2) bytes; Node refuses unless maxmem is above that.\n const maxmem = SCRYPT_BLOCK_BYTES * r * (N + p + 2) + SCRYPT_MEMORY_HEADROOM_BYTES;\n return new Promise<Buffer>((resolve, reject) => {\n try {\n scrypt(passphrase, salt, KEY_BYTES, { N, r, p, maxmem }, (error, derivedKey) => {\n if (error) reject(kdfFailed());\n else resolve(derivedKey);\n });\n } catch {\n reject(kdfFailed());\n }\n });\n}\n\nfunction kdfFailed(): KeyringError {\n return new KeyringError('kdf-failed', 'The key derivation could not run on this host.');\n}\n\n/** HKDF-SHA256 to 32 bytes. An empty salt is RFC 5869's \"no salt\" (a string of zero bytes). */\nexport function hkdf32(\n inputKeyMaterial: Uint8Array,\n info: string | Uint8Array,\n salt: Uint8Array = new Uint8Array(0),\n): Buffer {\n try {\n return Buffer.from(hkdfSync('sha256', inputKeyMaterial, salt, info, KEY_BYTES));\n } catch {\n throw kdfFailed();\n }\n}\n","import { Buffer } from 'node:buffer';\n\nimport { fromBase64Url } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { GCM_IV_BYTES, GCM_TAG_BYTES } from './aead.js';\nimport { KEY_BYTES, SCRYPT_SALT_BYTES } from './kdf.js';\nimport {\n KEYRING_VERSION,\n type IKeyring,\n type IPassphraseKeyslot,\n type IRecoveryKeyslot,\n type TKeyslot,\n} from './keyring-types.js';\n\nexport const SLOT_ID_BYTES = 16;\nexport const WRAPPED_KEY_BYTES = KEY_BYTES + GCM_TAG_BYTES;\n/** A keyring is a few hundred bytes; anything near this size is not one. */\nexport const MAX_KEYRING_JSON_CHARS = 16 * 1024;\nexport const MAX_KEYSLOTS = 8;\n\nconst KEYRING_KEYS = ['version', 'sealingPublicKey', 'slots'];\nconst SLOT_COMMON_KEYS = ['id', 'type', 'iv', 'wrappedKey'];\nconst PASSPHRASE_SLOT_KEYS = [...SLOT_COMMON_KEYS, 'kdf'];\nconst KDF_KEYS = ['algorithm', 'N', 'r', 'p', 'salt'];\n\nfunction malformed(detail: string): KeyringError {\n return new KeyringError('malformed-keyring', `The keyring is malformed: ${detail}.`);\n}\n\n/** A plain data object (JSON-shaped), not an array, class instance or null-prototype oddity. */\nfunction asRecord(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw malformed(`${what} must be an object`);\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw malformed(`${what} must be a plain object`);\n }\n return value as Record<string, unknown>;\n}\n\n/** Exactly the allowed own keys: an unknown key (including `__proto__` as data) is refused. */\nfunction requireExactKeys(record: Record<string, unknown>, allowed: string[], what: string): void {\n const keys = Object.keys(record);\n if (keys.length !== allowed.length || keys.some((key) => !allowed.includes(key))) {\n throw malformed(`${what} must have exactly the fields ${allowed.join(', ')}`);\n }\n}\n\nfunction requireBytes(value: unknown, length: number, what: string): string {\n if (fromBase64Url(value, length) === undefined) {\n throw malformed(`${what} must be canonical base64url of ${length} bytes`);\n }\n return value as string;\n}\n\nfunction requirePositiveInteger(value: unknown, what: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {\n throw malformed(`${what} must be a positive integer`);\n }\n return value;\n}\n\nfunction readPassphraseSlot(record: Record<string, unknown>, what: string): IPassphraseKeyslot {\n requireExactKeys(record, PASSPHRASE_SLOT_KEYS, what);\n const kdf = asRecord(record.kdf, `${what}.kdf`);\n requireExactKeys(kdf, KDF_KEYS, `${what}.kdf`);\n if (kdf.algorithm !== 'scrypt') throw malformed(`${what}.kdf.algorithm must be \"scrypt\"`);\n // Built field by field in the document's fixed order, so serialization is deterministic.\n return {\n id: requireBytes(record.id, SLOT_ID_BYTES, `${what}.id`),\n type: 'passphrase',\n kdf: Object.freeze({\n algorithm: 'scrypt',\n N: requirePositiveInteger(kdf.N, `${what}.kdf.N`),\n r: requirePositiveInteger(kdf.r, `${what}.kdf.r`),\n p: requirePositiveInteger(kdf.p, `${what}.kdf.p`),\n salt: requireBytes(kdf.salt, SCRYPT_SALT_BYTES, `${what}.kdf.salt`),\n } as const),\n iv: requireBytes(record.iv, GCM_IV_BYTES, `${what}.iv`),\n wrappedKey: requireBytes(record.wrappedKey, WRAPPED_KEY_BYTES, `${what}.wrappedKey`),\n };\n}\n\nfunction readRecoverySlot(record: Record<string, unknown>, what: string): IRecoveryKeyslot {\n requireExactKeys(record, SLOT_COMMON_KEYS, what);\n return {\n id: requireBytes(record.id, SLOT_ID_BYTES, `${what}.id`),\n type: 'recovery',\n iv: requireBytes(record.iv, GCM_IV_BYTES, `${what}.iv`),\n wrappedKey: requireBytes(record.wrappedKey, WRAPPED_KEY_BYTES, `${what}.wrappedKey`),\n };\n}\n\nfunction readSlot(value: unknown, index: number): TKeyslot {\n const what = `slots[${index}]`;\n const record = asRecord(value, what);\n switch (record.type) {\n case 'passphrase':\n return Object.freeze(readPassphraseSlot(record, what));\n case 'recovery':\n return Object.freeze(readRecoverySlot(record, what));\n default:\n // Not \"ignore and carry on\": dropping an unknown slot on the next write would destroy it.\n throw malformed(`${what}.type is not a known slot type`);\n }\n}\n\n/**\n * Validates a version-1 keyring document and returns a fresh, frozen copy built only from\n * validated primitives — nothing from the input object (and nothing from its prototype chain)\n * is carried over. Used by `parseKeyring` and by every operation that accepts a keyring, so a\n * hand-built object gets the same scrutiny as a parsed file.\n */\nexport function validateKeyring(value: unknown): IKeyring {\n const record = asRecord(value, 'the document');\n // Version first: a future document may legitimately have a different shape.\n if (record.version !== KEYRING_VERSION) {\n if (typeof record.version === 'number' && Number.isSafeInteger(record.version)) {\n throw new KeyringError(\n 'unsupported-version',\n 'The keyring was written by a version this build does not support.',\n );\n }\n throw malformed('version must be an integer');\n }\n requireExactKeys(record, KEYRING_KEYS, 'the document');\n const sealingPublicKey = requireBytes(record.sealingPublicKey, KEY_BYTES, 'sealingPublicKey');\n if (\n !Array.isArray(record.slots) ||\n record.slots.length < 1 ||\n record.slots.length > MAX_KEYSLOTS\n ) {\n throw malformed(`slots must be an array of 1 to ${MAX_KEYSLOTS} entries`);\n }\n const slots = record.slots.map((slot, index) => readSlot(slot, index));\n if (new Set(slots.map((slot) => slot.id)).size !== slots.length) {\n throw malformed('slot ids must be unique');\n }\n return Object.freeze({\n version: KEYRING_VERSION,\n sealingPublicKey,\n slots: Object.freeze(slots),\n });\n}\n\n/** Parses keyring JSON strictly; see {@link validateKeyring}. */\nexport function parseKeyring(json: string): IKeyring {\n if (typeof json !== 'string') throw malformed('the document must be a JSON string');\n if (json.length > MAX_KEYRING_JSON_CHARS) throw malformed('the document is too large');\n let value: unknown;\n try {\n value = JSON.parse(json);\n } catch {\n throw malformed('the document is not valid JSON');\n }\n return validateKeyring(value);\n}\n\n/** The keyring as JSON with a fixed field order, ready to write to disk. */\nexport function serializeKeyring(keyring: IKeyring): string {\n return JSON.stringify(validateKeyring(keyring), null, 2);\n}\n\n/** The raw 32-byte sealing public key — all a locked process needs to seal new artifacts. */\nexport function getSealingPublicKey(keyring: IKeyring): Buffer {\n return Buffer.from(validateKeyring(keyring).sealingPublicKey, 'base64url');\n}\n","import { Buffer } from 'node:buffer';\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport { isBytes } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { hkdf32 } from './kdf.js';\n\n/**\n * The HKDF `info` label of the authorship key, versioned and part of the on-disk contract like the\n * keypairs' labels: a tag written under one label never verifies under another.\n */\nexport const AUTHORSHIP_KEY_LABEL = 'robota/keyring/v1/authorship-hmac';\n\n/** An HMAC-SHA256 tag. */\nexport const AUTHORSHIP_TAG_BYTES = 32;\n\n/**\n * The authorship key: an HMAC-SHA256 key from its own HKDF-SHA256 label (empty salt), so it says\n * nothing about the keypairs and they say nothing about it. Unlike the sealing key it has no public\n * half, so only a process that recovered the master key can make a tag that verifies.\n */\nexport function deriveAuthorshipKey(masterKey: Uint8Array): Buffer {\n return hkdf32(masterKey, AUTHORSHIP_KEY_LABEL);\n}\n\nfunction asBuffer(bytes: Uint8Array): Buffer {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n}\n\n/** HMAC-SHA256 of `message` under `key`; a non-byte argument is a caller bug (`invalid-argument`). */\nexport function authorshipTag(key: Uint8Array, message: Uint8Array): Buffer {\n if (!isBytes(key) || !isBytes(message)) {\n throw new KeyringError('invalid-argument', 'An authorship tag is made over bytes.');\n }\n try {\n return createHmac('sha256', asBuffer(key)).update(asBuffer(message)).digest();\n } catch {\n throw new KeyringError('invalid-argument', 'The authorship tag could not be computed.');\n }\n}\n\n/**\n * Whether `tag` is the authorship tag of `message` under `key`, compared in constant time. A tag of\n * another length is simply not one (the length is public), so it is refused before any comparison.\n */\nexport function verifyAuthorshipTag(\n key: Uint8Array,\n message: Uint8Array,\n tag: Uint8Array,\n): boolean {\n if (!isBytes(tag)) {\n throw new KeyringError('invalid-argument', 'An authorship tag is bytes.');\n }\n const expected = authorshipTag(key, message);\n try {\n if (tag.length !== expected.length) return false;\n return timingSafeEqual(expected, asBuffer(tag));\n } finally {\n expected.fill(0);\n }\n}\n","import { Buffer } from 'node:buffer';\nimport { createPrivateKey, createPublicKey, randomBytes, type KeyObject } from 'node:crypto';\n\nimport { isBytes, wipe } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { hkdf32, KEY_BYTES } from './kdf.js';\n\n/**\n * The HKDF `info` labels. They are versioned and part of the on-disk contract: changing a byte\n * changes every derived key, so the known-answer tests pin the public keys they produce.\n */\nexport const SEALING_KEY_LABEL = 'robota/keyring/v1/sealing-x25519';\nexport const ATTACH_KEY_LABEL = 'robota/keyring/v1/attach-ed25519';\n\n// `node:crypto` has no \"private key from raw seed\" import that Bun and Node both accept, so the raw\n// 32-byte seed is wrapped in the fixed PKCS#8 / SPKI headers of RFC 8410.\nconst PKCS8_X25519_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex');\nconst PKCS8_ED25519_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');\nconst SPKI_X25519_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');\nconst SPKI_ED25519_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');\nconst SPKI_PREFIX_BYTES = SPKI_X25519_PREFIX.length;\n\nexport interface IKeyPair {\n /** The raw 32-byte public key. */\n readonly publicKey: Uint8Array;\n readonly privateKey: KeyObject;\n}\n\nexport interface IDerivedKeyPairs {\n readonly sealing: IKeyPair;\n readonly attach: IKeyPair;\n}\n\n/** The raw 32-byte public key of an X25519 or Ed25519 key object. */\nexport function rawPublicKey(key: KeyObject): Buffer {\n try {\n const spki = (key.type === 'public' ? key : createPublicKey(key)).export({\n format: 'der',\n type: 'spki',\n });\n return Buffer.from(spki.subarray(SPKI_PREFIX_BYTES));\n } catch {\n throw new KeyringError('invalid-argument', 'The key has no exportable public key.');\n }\n}\n\n/** An X25519 public key object from its raw 32 bytes, from any realm (see `isBytes`). */\nexport function x25519PublicKeyFromRaw(raw: Uint8Array): KeyObject {\n if (!isBytes(raw) || raw.length !== KEY_BYTES) {\n throw new KeyringError('invalid-argument', 'A public key must be exactly 32 bytes.');\n }\n try {\n return createPublicKey({\n key: Buffer.concat([SPKI_X25519_PREFIX, raw]),\n format: 'der',\n type: 'spki',\n });\n } catch {\n throw new KeyringError('invalid-argument', 'The public key is not a valid X25519 key.');\n }\n}\n\n/** An Ed25519 public key object from its raw 32 bytes, from any realm (see `isBytes`). */\nexport function ed25519PublicKeyFromRaw(raw: Uint8Array): KeyObject {\n if (!isBytes(raw) || raw.length !== KEY_BYTES) {\n throw new KeyringError('invalid-argument', 'A public key must be exactly 32 bytes.');\n }\n try {\n return createPublicKey({\n key: Buffer.concat([SPKI_ED25519_PREFIX, raw]),\n format: 'der',\n type: 'spki',\n });\n } catch {\n throw new KeyringError('invalid-argument', 'The public key is not a valid Ed25519 key.');\n }\n}\n\n/** A keypair from a derived seed; a host that cannot build one fails as `kdf-failed`, whatever it threw. */\nfunction keyPairFromSeed(prefix: Buffer, seed: Uint8Array): IKeyPair {\n const pkcs8 = Buffer.concat([prefix, seed]);\n try {\n const privateKey = createPrivateKey({ key: pkcs8, format: 'der', type: 'pkcs8' });\n return { publicKey: rawPublicKey(privateKey), privateKey };\n } catch {\n throw new KeyringError('kdf-failed', 'The key derivation could not run on this host.');\n } finally {\n wipe(pkcs8);\n }\n}\n\n/** The X25519 keypair a 32-byte seed makes: what a sealing seed, derived or granted, becomes. */\nexport function x25519KeyPairFromSeed(seed: Uint8Array): IKeyPair {\n return keyPairFromSeed(PKCS8_X25519_PREFIX, seed);\n}\n\n/** The Ed25519 keypair a 32-byte seed makes. */\nexport function ed25519KeyPairFromSeed(seed: Uint8Array): IKeyPair {\n return keyPairFromSeed(PKCS8_ED25519_PREFIX, seed);\n}\n\n/** The sealing keypair's seed; the caller wipes it. */\nexport function deriveSealingSeed(masterKey: Uint8Array): Buffer {\n return hkdf32(masterKey, SEALING_KEY_LABEL);\n}\n\n/** The attach keypair alone, for the one holder that may have it (the owner handover). */\nexport function deriveAttachKeyPair(masterKey: Uint8Array): IKeyPair {\n const attachSeed = hkdf32(masterKey, ATTACH_KEY_LABEL);\n try {\n return keyPairFromSeed(PKCS8_ED25519_PREFIX, attachSeed);\n } finally {\n wipe(attachSeed);\n }\n}\n\n/**\n * Derives the two long-lived keypairs from the master key: an X25519 sealing keypair and an Ed25519\n * attach keypair, each from its own HKDF-SHA256 seed (empty salt, versioned `info` label), so\n * neither key says anything about the other.\n */\nexport function deriveKeyPairs(masterKey: Uint8Array): IDerivedKeyPairs {\n const sealingSeed = hkdf32(masterKey, SEALING_KEY_LABEL);\n const attachSeed = hkdf32(masterKey, ATTACH_KEY_LABEL);\n try {\n return {\n sealing: keyPairFromSeed(PKCS8_X25519_PREFIX, sealingSeed),\n attach: keyPairFromSeed(PKCS8_ED25519_PREFIX, attachSeed),\n };\n } finally {\n wipe(sealingSeed, attachSeed);\n }\n}\n\n/**\n * A new X25519 sealing keypair from a random seed, for a key that lives only in the memory of the\n * process that made it: nothing derives it again, so whatever is sealed to it opens only while that\n * process keeps the private key. Built from the seed as the derived pairs are, so it has their shape\n * on every runtime.\n */\nexport function generateTransientSealingKeyPair(): IKeyPair {\n const seed = randomBytes(KEY_BYTES);\n try {\n return keyPairFromSeed(PKCS8_X25519_PREFIX, seed);\n } finally {\n wipe(seed);\n }\n}\n","import { Buffer } from 'node:buffer';\nimport { randomBytes } from 'node:crypto';\n\nimport { gcmDecrypt, gcmEncrypt, GCM_IV_BYTES } from './aead.js';\nimport { concatLengthPrefixed, decodeKeyringField, toBase64Url, utf8 } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { hkdf32 } from './kdf.js';\nimport { SLOT_ID_BYTES, WRAPPED_KEY_BYTES } from './keyring-document.js';\nimport { KEYRING_VERSION, type TKeyslot, type TKeyslotType } from './keyring-types.js';\n\n/** `info` of the recovery slot's key-encryption key. Versioned like every other HKDF label. */\nexport const RECOVERY_KEK_LABEL = 'robota/keyring/v1/recovery-kek';\n\nexport function newSlotId(): string {\n return toBase64Url(randomBytes(SLOT_ID_BYTES));\n}\n\n/**\n * The key-encryption key of a recovery slot. The recovery key is 160 random bits, so HKDF is all it\n * needs; stretching it with scrypt would add cost and no strength.\n */\nexport function deriveRecoveryKek(recoveryKeyBytes: Uint8Array): Buffer {\n return hkdf32(recoveryKeyBytes, RECOVERY_KEK_LABEL);\n}\n\n/**\n * What the wrapping authenticates: version, slot id and slot type. A slot that is relabelled,\n * re-identified or moved to a document of another version stops unwrapping. The sealing public key\n * is deliberately NOT bound: the product detects a swapped public key at unlock and repairs it, and\n * a binding would turn that repairable tamper into a lockout.\n */\nexport function keyslotAad(slotId: string, slotType: TKeyslotType): Buffer {\n return concatLengthPrefixed([Buffer.from([KEYRING_VERSION]), utf8(slotId), utf8(slotType)]);\n}\n\nexport interface IWrappedMasterKey {\n readonly iv: string;\n readonly wrappedKey: string;\n}\n\n/** AES-256-GCM wrap of the master key under `kek` with a fresh random 12-byte IV. */\nexport function wrapMasterKey(\n kek: Uint8Array,\n masterKey: Uint8Array,\n slotId: string,\n slotType: TKeyslotType,\n): IWrappedMasterKey {\n const iv = randomBytes(GCM_IV_BYTES);\n const wrapped = gcmEncrypt(kek, iv, masterKey, keyslotAad(slotId, slotType));\n return { iv: toBase64Url(iv), wrappedKey: toBase64Url(wrapped) };\n}\n\n/**\n * Unwraps the master key of `slot` with `kek`. Any failure to authenticate is `wrong-credential`:\n * a wrong passphrase, a wrong recovery key and a tampered slot cannot be told apart, and the\n * caller must not be able to either. The caller owns (and wipes) `kek` and the returned key.\n */\nexport function unwrapMasterKey(kek: Uint8Array, slot: TKeyslot): Buffer {\n const iv = decodeKeyringField(slot.iv, GCM_IV_BYTES);\n const wrapped = decodeKeyringField(slot.wrappedKey, WRAPPED_KEY_BYTES);\n try {\n return gcmDecrypt(kek, iv, wrapped, keyslotAad(slot.id, slot.type));\n } catch {\n throw new KeyringError('wrong-credential', 'The credential did not open the keyring.');\n }\n}\n","import { Buffer } from 'node:buffer';\n\nimport { KeyringError } from './errors.js';\n\n/** 160 random bits: far beyond brute force, and exactly 32 base32 symbols with no padding. */\nexport const RECOVERY_KEY_BYTES = 20;\n\nconst CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';\nconst BITS_PER_SYMBOL = 5;\nconst SYMBOL_COUNT = (RECOVERY_KEY_BYTES * 8) / BITS_PER_SYMBOL;\nconst GROUP_SIZE = 4;\nconst GROUP_SEPARATOR = '-';\n/** Far longer than any spelling of a valid key; stops a pathological input before it is scanned. */\nconst MAX_INPUT_LENGTH = 128;\n// ASCII only: `toUpperCase` would otherwise fold characters such as `ß` or `ı` into valid symbols.\nconst TYPED_CHARACTERS = /^[0-9A-Za-z\\s-]*$/;\n\nfunction invalidRecoveryKey(): KeyringError {\n return new KeyringError(\n 'invalid-recovery-key',\n 'The recovery key is not in the expected format.',\n );\n}\n\n/** Crockford base32 of the key bytes in groups of four: `XXXX-XXXX-...` (32 symbols, 8 groups). */\nexport function formatRecoveryKey(bytes: Uint8Array): string {\n if (bytes.length !== RECOVERY_KEY_BYTES) {\n throw new KeyringError('invalid-argument', 'A recovery key is exactly 20 bytes.');\n }\n let symbols = '';\n let accumulator = 0;\n let bits = 0;\n for (const byte of bytes) {\n accumulator = (accumulator << 8) | byte;\n bits += 8;\n while (bits >= BITS_PER_SYMBOL) {\n symbols += CROCKFORD_ALPHABET[(accumulator >>> (bits - BITS_PER_SYMBOL)) & 31];\n bits -= BITS_PER_SYMBOL;\n }\n accumulator &= (1 << bits) - 1;\n }\n const groups: string[] = [];\n for (let index = 0; index < symbols.length; index += GROUP_SIZE) {\n groups.push(symbols.slice(index, index + GROUP_SIZE));\n }\n return groups.join(GROUP_SEPARATOR);\n}\n\n/**\n * Reads a recovery key as a person types it: case-insensitive, with or without the group hyphens\n * or spaces, and with Crockford's look-alike aliases (`O` is `0`; `I` and `L` are `1`).\n */\nexport function parseRecoveryKey(text: unknown): Buffer {\n if (typeof text !== 'string' || text.length > MAX_INPUT_LENGTH || !TYPED_CHARACTERS.test(text)) {\n throw invalidRecoveryKey();\n }\n const symbols = text.toUpperCase().replace(/[\\s-]/g, '').replace(/O/g, '0').replace(/[IL]/g, '1');\n if (symbols.length !== SYMBOL_COUNT) throw invalidRecoveryKey();\n const bytes = Buffer.alloc(RECOVERY_KEY_BYTES);\n let accumulator = 0;\n let bits = 0;\n let written = 0;\n for (const symbol of symbols) {\n const value = CROCKFORD_ALPHABET.indexOf(symbol);\n if (value < 0) {\n bytes.fill(0);\n throw invalidRecoveryKey();\n }\n accumulator = (accumulator << BITS_PER_SYMBOL) | value;\n bits += BITS_PER_SYMBOL;\n if (bits >= 8) {\n bytes[written] = (accumulator >>> (bits - 8)) & 0xff;\n written += 1;\n bits -= 8;\n accumulator &= (1 << bits) - 1;\n }\n }\n return bytes;\n}\n","/**\n * What reading needs, and nothing that proves ownership: the X25519 sealing keypair, which opens\n * sealed artifact keys, and the authorship key, which tags and checks what the owner wrote. An\n * unlock yields exactly this, and so does a read grant on an owner link. Neither carries the master\n * key or the attach keypair, so a holder of read keys can read and vouch but can never prove itself\n * the owner to a verifier, certify a runtime or re-wrap the master key.\n */\nimport { authorshipTag, verifyAuthorshipTag } from './authorship.js';\nimport { KeyringError } from './errors.js';\nimport type { IKeyPair } from './key-pairs.js';\n\n/**\n * `lock()` drops this object's references to the keys, zero-fills the authorship key (plain bytes\n * held only here) and makes every accessor and operation throw `keys-locked`. It does NOT zero the\n * sealing keypair: a `KeyObject` offers no way to overwrite its contents, so the private key stays\n * in memory until the garbage collector frees it, and any reference a caller copied out earlier\n * stays valid. Treat `lock()` as \"stop handing out keys\", not as erasure.\n */\nexport interface IReadKeys {\n readonly locked: boolean;\n /** X25519 keypair that opens sealed artifact keys. */\n readonly sealing: IKeyPair;\n /**\n * The HMAC-SHA256 tag of `message` under the authorship key, which has no public half: what an\n * unlocked process attaches to what it wrote, since a sealed artifact opens for whoever sealed it.\n * The key itself is never handed out.\n */\n tagAuthorship(message: Uint8Array): Uint8Array;\n /** Whether `tag` is {@link tagAuthorship} of `message`, compared in constant time. */\n verifyAuthorship(message: Uint8Array, tag: Uint8Array): boolean;\n lock(): void;\n}\n\ninterface IReadKeyMaterial {\n readonly sealing: IKeyPair;\n readonly authorship: Uint8Array;\n}\n\nclass ReadKeys implements IReadKeys {\n #material: IReadKeyMaterial | undefined;\n\n constructor(sealing: IKeyPair, authorship: Uint8Array) {\n this.#material = { sealing, authorship };\n }\n\n get locked(): boolean {\n return this.#material === undefined;\n }\n\n get sealing(): IKeyPair {\n return this.#require().sealing;\n }\n\n tagAuthorship(message: Uint8Array): Uint8Array {\n return authorshipTag(this.#require().authorship, message);\n }\n\n verifyAuthorship(message: Uint8Array, tag: Uint8Array): boolean {\n return verifyAuthorshipTag(this.#require().authorship, message, tag);\n }\n\n lock(): void {\n this.#material?.authorship.fill(0);\n this.#material = undefined;\n }\n\n #require(): IReadKeyMaterial {\n if (this.#material === undefined) {\n throw new KeyringError('keys-locked', 'The keys are locked.');\n }\n return this.#material;\n }\n}\n\n/** The read keys over `sealing` and `authorship`; the authorship bytes are this object's from now on. */\nexport function createReadKeys(sealing: IKeyPair, authorship: Uint8Array): IReadKeys {\n return new ReadKeys(sealing, authorship);\n}\n","import type { Buffer } from 'node:buffer';\nimport { randomBytes, timingSafeEqual } from 'node:crypto';\n\nimport { deriveAuthorshipKey } from './authorship.js';\nimport { decodeKeyringField, toBase64Url, wipe } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport {\n deriveKeyPairs,\n deriveSealingSeed,\n x25519KeyPairFromSeed,\n type IDerivedKeyPairs,\n} from './key-pairs.js';\nimport {\n assertScryptParametersInBounds,\n DEFAULT_SCRYPT_PARAMETERS,\n deriveKeyFromPassphrase,\n KEY_BYTES,\n normalizePassphrase,\n SCRYPT_SALT_BYTES,\n} from './kdf.js';\nimport { getSealingPublicKey, validateKeyring } from './keyring-document.js';\nimport {\n KEYRING_VERSION,\n type IKeyring,\n type IPassphraseKeyslot,\n type IRecoveryKeyslot,\n type TKeyslot,\n} from './keyring-types.js';\nimport { deriveRecoveryKek, newSlotId, unwrapMasterKey, wrapMasterKey } from './keyslot.js';\nimport { formatRecoveryKey, parseRecoveryKey, RECOVERY_KEY_BYTES } from './recovery-key.js';\nimport { createReadKeys, type IReadKeys } from './read-keys.js';\n\n/** What proves the right to open a keyring. A tagged union so a future credential is one more member. */\nexport type TKeyringCredential =\n | { readonly type: 'passphrase'; readonly passphrase: string }\n | { readonly type: 'recoveryKey'; readonly recoveryKey: string };\n\nexport interface ICreateKeyringOptions {\n /** Any non-empty string; minimum length and strength are the caller's policy. */\n readonly passphrase: string;\n}\n\nexport interface ICreatedKeyring {\n readonly keyring: IKeyring;\n /**\n * The recovery key, formatted for a person to write down. It exists only here: it is not stored\n * anywhere and cannot be shown again, so the caller must present it exactly once.\n */\n readonly recoveryKey: string;\n readonly unlocked: IReadKeys;\n}\n\nexport interface IAssembleKeyringInput {\n readonly masterKey: Uint8Array;\n /** Normalized passphrase bytes (see `normalizePassphrase`). */\n readonly passphrase: Uint8Array;\n readonly recoveryKeyBytes: Uint8Array;\n}\n\nfunction isPassphraseSlot(slot: TKeyslot): slot is IPassphraseKeyslot {\n return slot.type === 'passphrase';\n}\n\nfunction isRecoverySlot(slot: TKeyslot): slot is IRecoveryKeyslot {\n return slot.type === 'recovery';\n}\n\nasync function makePassphraseSlot(\n masterKey: Uint8Array,\n passphrase: Uint8Array,\n slotId: string,\n): Promise<IPassphraseKeyslot> {\n const salt = randomBytes(SCRYPT_SALT_BYTES);\n const kek = await deriveKeyFromPassphrase(passphrase, salt, DEFAULT_SCRYPT_PARAMETERS);\n try {\n const wrapped = wrapMasterKey(kek, masterKey, slotId, 'passphrase');\n return {\n id: slotId,\n type: 'passphrase',\n kdf: { algorithm: 'scrypt', ...DEFAULT_SCRYPT_PARAMETERS, salt: toBase64Url(salt) },\n ...wrapped,\n };\n } finally {\n wipe(kek);\n }\n}\n\nfunction makeRecoverySlot(masterKey: Uint8Array, recoveryKeyBytes: Uint8Array): IRecoveryKeyslot {\n const slotId = newSlotId();\n const kek = deriveRecoveryKek(recoveryKeyBytes);\n try {\n return { id: slotId, type: 'recovery', ...wrapMasterKey(kek, masterKey, slotId, 'recovery') };\n } finally {\n wipe(kek);\n }\n}\n\n/**\n * Wraps `masterKey` into a fresh keyring with one passphrase slot and one recovery slot. Exposed to\n * the package's own tests (a fixed master key makes the known-answer vectors possible); it is not\n * part of the public entry point, whose only way to make a keyring draws the master key at random.\n */\nexport async function assembleKeyring(\n input: IAssembleKeyringInput,\n): Promise<{ keyring: IKeyring; pairs: IDerivedKeyPairs }> {\n const pairs = deriveKeyPairs(input.masterKey);\n const passphraseSlot = await makePassphraseSlot(input.masterKey, input.passphrase, newSlotId());\n const recoverySlot = makeRecoverySlot(input.masterKey, input.recoveryKeyBytes);\n const keyring = validateKeyring({\n version: KEYRING_VERSION,\n sealingPublicKey: toBase64Url(pairs.sealing.publicKey),\n slots: [passphraseSlot, recoverySlot],\n });\n return { keyring, pairs };\n}\n\n/**\n * Creates a keyring around a new random 256-bit master key, protected by the passphrase and by a\n * new recovery key, and returns it already unlocked. The master key is wiped before this returns.\n */\nexport async function createKeyring(options: ICreateKeyringOptions): Promise<ICreatedKeyring> {\n const passphrase = normalizePassphrase(options.passphrase);\n const masterKey = randomBytes(KEY_BYTES);\n const recoveryKeyBytes = randomBytes(RECOVERY_KEY_BYTES);\n try {\n const { keyring, pairs } = await assembleKeyring({ masterKey, passphrase, recoveryKeyBytes });\n return {\n keyring,\n recoveryKey: formatRecoveryKey(recoveryKeyBytes),\n unlocked: createReadKeys(pairs.sealing, deriveAuthorshipKey(masterKey)),\n };\n } finally {\n wipe(passphrase, masterKey, recoveryKeyBytes);\n }\n}\n\nexport interface IRecoveredMasterKey {\n readonly masterKey: Buffer;\n readonly slotId: string;\n}\n\nfunction isWrongCredential(error: unknown): boolean {\n return error instanceof KeyringError && error.code === 'wrong-credential';\n}\n\nasync function recoverWithPassphrase(\n keyring: IKeyring,\n passphraseText: unknown,\n): Promise<IRecoveredMasterKey> {\n const passphrase = normalizePassphrase(passphraseText);\n try {\n const slots = keyring.slots.filter(isPassphraseSlot);\n if (slots.length === 0) {\n throw new KeyringError('slot-not-found', 'The keyring has no passphrase slot.');\n }\n // Every slot's cost is checked before the first derivation, so a hostile keyring cannot make\n // an earlier, in-range slot burn CPU on the way to a refusal.\n for (const slot of slots) assertScryptParametersInBounds(slot.kdf);\n for (const slot of slots) {\n const salt = decodeKeyringField(slot.kdf.salt, SCRYPT_SALT_BYTES);\n const kek = await deriveKeyFromPassphrase(passphrase, salt, slot.kdf);\n try {\n return { masterKey: unwrapMasterKey(kek, slot), slotId: slot.id };\n } catch (error) {\n if (!isWrongCredential(error)) throw error;\n } finally {\n wipe(kek);\n }\n }\n throw new KeyringError('wrong-credential', 'The credential did not open the keyring.');\n } finally {\n wipe(passphrase);\n }\n}\n\nfunction recoverWithRecoveryKey(keyring: IKeyring, recoveryKeyText: unknown): IRecoveredMasterKey {\n const recoveryKeyBytes = parseRecoveryKey(recoveryKeyText);\n try {\n const slots = keyring.slots.filter(isRecoverySlot);\n if (slots.length === 0) {\n throw new KeyringError('slot-not-found', 'The keyring has no recovery slot.');\n }\n for (const slot of slots) {\n const kek = deriveRecoveryKek(recoveryKeyBytes);\n try {\n return { masterKey: unwrapMasterKey(kek, slot), slotId: slot.id };\n } catch (error) {\n if (!isWrongCredential(error)) throw error;\n } finally {\n wipe(kek);\n }\n }\n throw new KeyringError('wrong-credential', 'The credential did not open the keyring.');\n } finally {\n wipe(recoveryKeyBytes);\n }\n}\n\n/**\n * The master key `credential` opens, and the slot it opened. The caller owns and wipes it; nothing\n * outside this package ever sees one.\n */\nexport async function recoverMasterKey(\n keyring: IKeyring,\n credential: TKeyringCredential,\n): Promise<IRecoveredMasterKey> {\n switch (credential?.type) {\n case 'passphrase':\n return recoverWithPassphrase(keyring, credential.passphrase);\n case 'recoveryKey':\n return recoverWithRecoveryKey(keyring, credential.recoveryKey);\n default:\n throw new KeyringError('invalid-argument', 'The credential type is not recognised.');\n }\n}\n\nasync function unlock(keyring: IKeyring, credential: TKeyringCredential): Promise<IReadKeys> {\n const document = validateKeyring(keyring);\n const { masterKey } = await recoverMasterKey(document, credential);\n const sealingSeed = deriveSealingSeed(masterKey);\n try {\n return createReadKeys(x25519KeyPairFromSeed(sealingSeed), deriveAuthorshipKey(masterKey));\n } finally {\n wipe(masterKey, sealingSeed);\n }\n}\n\n/**\n * Unlocks with the passphrase, for reading: the read keys and nothing that proves ownership (see\n * `openOwnerHandover` for that). Costs one scrypt run per passphrase slot (~160 ms at the default\n * cost, off the event loop). The stored sealing public key is NOT consulted — see\n * {@link checkSealingPublicKey}.\n */\nexport function unlockWithPassphrase(keyring: IKeyring, passphrase: string): Promise<IReadKeys> {\n return unlock(keyring, { type: 'passphrase', passphrase });\n}\n\n/** Unlocks with the recovery key shown at creation (case-insensitive, hyphens optional). */\nexport function unlockWithRecoveryKey(keyring: IKeyring, recoveryKey: string): Promise<IReadKeys> {\n return unlock(keyring, { type: 'recoveryKey', recoveryKey });\n}\n\n/**\n * Replaces the passphrase slot with one that wraps the same master key under `newPassphrase`, and\n * returns the new keyring; the input is untouched. Everything encrypted under the keys stays\n * readable because the master key does not change. The caller must prove the right to change it by\n * supplying a working credential — the current passphrase, or the recovery key to reset a forgotten\n * one. The new slot is written at the current default scrypt cost with a fresh salt and IV, and\n * keeps the old slot's id. The stored sealing public key and the recovery slot are carried over\n * as they are.\n */\nexport async function changePassphrase(\n keyring: IKeyring,\n credential: TKeyringCredential,\n newPassphrase: string,\n): Promise<IKeyring> {\n const document = validateKeyring(keyring);\n const replacement = normalizePassphrase(newPassphrase);\n let recovered: IRecoveredMasterKey | undefined;\n try {\n recovered = await recoverMasterKey(document, credential);\n const targetId =\n credential.type === 'passphrase'\n ? recovered.slotId\n : document.slots.find(isPassphraseSlot)?.id;\n if (targetId === undefined) {\n throw new KeyringError('slot-not-found', 'The keyring has no passphrase slot.');\n }\n const slot = await makePassphraseSlot(recovered.masterKey, replacement, targetId);\n return validateKeyring({\n ...document,\n slots: document.slots.map((existing) => (existing.id === targetId ? slot : existing)),\n });\n } finally {\n wipe(replacement, recovered?.masterKey);\n }\n}\n\n/**\n * Whether the sealing public key stored in the keyring is the one the master key derives. A\n * mismatch means the file was altered (an attacker who can write it could substitute their own key\n * so new artifacts are sealed to them); the stored key cannot be authenticated without the master\n * key, so this check is only possible after an unlock.\n */\nexport function checkSealingPublicKey(\n keyring: IKeyring,\n keys: Pick<IReadKeys, 'sealing'>,\n): { matches: boolean } {\n return { matches: sealingPublicKeyMatches(keyring, keys.sealing.publicKey) };\n}\n\n/** Whether the keyring's stored sealing public key is `derived`, compared in constant time. */\nexport function sealingPublicKeyMatches(keyring: IKeyring, derived: Uint8Array): boolean {\n const stored = getSealingPublicKey(keyring);\n return stored.length === derived.length && timingSafeEqual(stored, derived);\n}\n\n/** A copy of the keyring whose stored sealing public key is the one derived from the read keys. */\nexport function repairSealingPublicKey(\n keyring: IKeyring,\n keys: Pick<IReadKeys, 'sealing'>,\n): IKeyring {\n return validateKeyring({\n ...validateKeyring(keyring),\n sealingPublicKey: toBase64Url(keys.sealing.publicKey),\n });\n}\n","import { createHash } from 'node:crypto';\n\nimport { isBytes } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { KEY_BYTES } from './kdf.js';\n\nconst FINGERPRINT_BYTES = 10;\nconst GROUP_HEX_CHARS = 4;\n\n/**\n * A short string two people can read to each other to confirm they hold the same public key: the\n * first 10 bytes of SHA-256 of the raw 32-byte key, as upper-case hex in groups of four\n * (`A1B2-C3D4-E5F6-0718-293A`). 80 bits is for a human comparing out loud or on screen; it is a\n * check against accidents and casual swaps, not a collision-resistant identifier.\n */\nexport function fingerprint(publicKey: Uint8Array): string {\n if (!isBytes(publicKey) || publicKey.length !== KEY_BYTES) {\n throw new KeyringError(\n 'invalid-argument',\n 'A public key must be a Uint8Array of exactly 32 bytes.',\n );\n }\n const hex = createHash('sha256')\n .update(publicKey)\n .digest('hex')\n .slice(0, FINGERPRINT_BYTES * 2);\n const groups: string[] = [];\n for (let index = 0; index < hex.length; index += GROUP_HEX_CHARS) {\n groups.push(hex.slice(index, index + GROUP_HEX_CHARS).toUpperCase());\n }\n return groups.join('-');\n}\n","/**\n * Ed25519 signing and checking, in the one form every link check uses: a check answers yes or no and\n * never throws, so a malformed signature or key from the other end is just a refusal, and a failed\n * signature surfaces as a fixed error that says nothing about the key.\n */\nimport type { Buffer } from 'node:buffer';\nimport { sign, verify, type KeyObject } from 'node:crypto';\n\nimport { isBytes } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { ed25519PublicKeyFromRaw } from './key-pairs.js';\n\n/** An Ed25519 signature. */\nexport const SIGNATURE_BYTES = 64;\n\n/**\n * Ed25519 over `message` (pure Ed25519: the message is signed as it is, with no pre-hash). A host\n * that cannot sign fails as `kdf-failed`, whatever it threw, so nothing about the key surfaces.\n */\nexport function signEd25519(privateKey: KeyObject, message: Uint8Array): Buffer {\n try {\n return sign(null, message, privateKey);\n } catch {\n throw new KeyringError('kdf-failed', 'The key operation could not run on this host.');\n }\n}\n\n/**\n * Whether `signature` is the Ed25519 signature of `message` under the raw 32-byte `publicKey`. A\n * signature of another length, a public key that is not one, or a host that refuses the check are\n * all simply \"not a valid signature\": a verifier decides on this answer alone.\n */\nexport function verifyEd25519(\n publicKey: Uint8Array,\n message: Uint8Array,\n signature: Uint8Array,\n): boolean {\n if (!isBytes(signature) || signature.length !== SIGNATURE_BYTES || !isBytes(message))\n return false;\n try {\n return verify(null, message, ed25519PublicKeyFromRaw(publicKey), signature);\n } catch {\n return false;\n }\n}\n","/**\n * What a runtime is, as its owner vouches for it: an instance key bound to one runtime start.\n *\n * A runtime cannot prove it belongs to the owner by anything it can show on its own, because every\n * same-user program can show the same things: a file, a socket path, a registration. So the owner\n * signs, with the attach key only the passphrase reaches, the runtime's memory-only instance key\n * together with the start it belongs to (the binding). A client that later checks this certificate\n * under the attach key it derived itself knows the runtime is one its owner certified, for exactly\n * this start; a certificate made by any other keyring, or for any other instance or start, fails.\n */\nimport { Buffer } from 'node:buffer';\n\nimport { concatLengthPrefixed, isBytes, utf8 } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { KEY_BYTES } from './kdf.js';\nimport { SIGNATURE_BYTES, verifyEd25519 } from './signatures.js';\n\n/** The runtime start a link and a certificate are bound to. Public values. */\nexport interface ILinkBinding {\n readonly runtimeId: string;\n readonly generation: string;\n}\n\n/**\n * What a runtime is told about its owner by its spawner: the owner's attach public key and the\n * certificate of the runtime's own instance key. Public values, held only in runtime memory: a\n * same-user-writable file is never a trust anchor.\n */\nexport interface ITrustRoot {\n readonly attachPublicKey: Uint8Array;\n /** Ed25519 under the attach key over the certificate message (see {@link runtimeCertificateMessage}). */\n readonly certificate: Uint8Array;\n}\n\n/** Versioned like every derivation label: a certificate of one version never verifies as another. */\nexport const RUNTIME_CERTIFICATE_LABEL = 'robota/link/v1/runtime-cert';\n\n/** The longest runtime id or generation a binding carries. */\nexport const MAX_BINDING_PART_LENGTH = 128;\n\n// Printable ASCII without separators: what a registration's ids are made of, and nothing a\n// terminal, a log line or a length prefix could read twice.\nconst BINDING_PART_PATTERN = /^[A-Za-z0-9._:-]+$/u;\nconst BINDING_KEYS = ['generation', 'runtimeId'];\n\nfunction isBindingPart(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= MAX_BINDING_PART_LENGTH &&\n BINDING_PART_PATTERN.test(value)\n );\n}\n\n/**\n * A fresh binding built from `value`, or `invalid-argument`. Exactly the two fields, each 1 to\n * {@link MAX_BINDING_PART_LENGTH} characters of `[A-Za-z0-9._:-]`, so every binding has one byte\n * spelling and a decoder never meets a value it would have to reinterpret.\n */\nexport function checkLinkBinding(value: unknown): ILinkBinding {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new KeyringError('invalid-argument', 'A link binding is a runtime id and a generation.');\n }\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n if (\n keys.length !== BINDING_KEYS.length ||\n keys.some((key, index) => key !== BINDING_KEYS[index]) ||\n !isBindingPart(record['runtimeId']) ||\n !isBindingPart(record['generation'])\n ) {\n throw new KeyringError(\n 'invalid-argument',\n `A link binding's runtime id and generation are 1-${MAX_BINDING_PART_LENGTH} characters of [A-Za-z0-9._:-].`,\n );\n }\n return { runtimeId: record['runtimeId'], generation: record['generation'] };\n}\n\n/** The bytes of a binding part as it is decoded from the wire, or `undefined` when it is not one. */\nexport function decodeBindingPart(bytes: Uint8Array): string | undefined {\n if (bytes.length === 0 || bytes.length > MAX_BINDING_PART_LENGTH) return undefined;\n // ASCII only, so latin1 maps each byte to exactly the character the pattern then checks.\n const text = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('latin1');\n return isBindingPart(text) ? text : undefined;\n}\n\nexport function requireInstanceKey(value: unknown): Uint8Array {\n if (!isBytes(value) || value.length !== KEY_BYTES) {\n throw new KeyringError('invalid-argument', 'An instance public key is exactly 32 bytes.');\n }\n return value;\n}\n\n/** `label | instance key | runtime id | generation`, each part length-prefixed. */\nexport function runtimeCertificateMessage(\n instancePublicKey: Uint8Array,\n binding: ILinkBinding,\n): Buffer {\n const instance = requireInstanceKey(instancePublicKey);\n const checked = checkLinkBinding(binding);\n return concatLengthPrefixed([\n utf8(RUNTIME_CERTIFICATE_LABEL),\n instance,\n utf8(checked.runtimeId),\n utf8(checked.generation),\n ]);\n}\n\n/** Whether `root` certifies `instancePublicKey` for `binding` under the attach key `root` names. */\nexport function certifies(\n root: ITrustRoot,\n instancePublicKey: Uint8Array,\n binding: ILinkBinding,\n): boolean {\n if (\n !isBytes(root.attachPublicKey) ||\n root.attachPublicKey.length !== KEY_BYTES ||\n !isBytes(root.certificate) ||\n root.certificate.length !== SIGNATURE_BYTES\n ) {\n return false;\n }\n return verifyEd25519(\n root.attachPublicKey,\n runtimeCertificateMessage(instancePublicKey, binding),\n root.certificate,\n );\n}\n","/**\n * The owner, present for a moment.\n *\n * Reading needs only the read keys; proving ownership needs the attach key, and that is the one key\n * a runtime must never hold, since whoever holds it can certify impostors and present as the owner to\n * any runtime. So the attach key has one home: a handover, opened from the keyring with a credential\n * a person just typed, held for the seconds a command needs it, and disposed. A handover hands out\n * operations and public values, never a key. Certifying a runtime instance is its one public\n * operation; signing an owner proof and sealing a read grant happen inside the link initiator,\n * through an accessor this package does not export.\n */\nimport type { Buffer } from 'node:buffer';\n\nimport { deriveAuthorshipKey } from './authorship.js';\nimport { wipe } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { fingerprint } from './fingerprint.js';\nimport {\n deriveAttachKeyPair,\n deriveSealingSeed,\n x25519KeyPairFromSeed,\n type IKeyPair,\n} from './key-pairs.js';\nimport { validateKeyring } from './keyring-document.js';\nimport { recoverMasterKey, sealingPublicKeyMatches, type TKeyringCredential } from './keyring.js';\nimport type { IKeyring } from './keyring-types.js';\nimport {\n runtimeCertificateMessage,\n type ILinkBinding,\n type ITrustRoot,\n} from './runtime-certificate.js';\nimport { signEd25519 } from './signatures.js';\n\nexport interface IOwnerHandover {\n /**\n * Whether the keyring file holds the sealing public key the credential derives. `false` means the\n * file was altered: a runtime given these keys seals to the derived key, and the file is repaired\n * by `secure verify`, not here.\n */\n readonly sealingKeyMatches: boolean;\n /** The derived sealing public key's fingerprint, shown so a replaced keyring file is noticed. */\n readonly fingerprint: string;\n readonly disposed: boolean;\n /**\n * The trust root that tells a runtime its owner: the attach public key and the certificate of\n * `instancePublicKey` for `binding`. Give it only for an instance key and binding learned over an\n * authenticated link, since the certificate vouches for exactly them.\n */\n certify(instancePublicKey: Uint8Array, binding: ILinkBinding): ITrustRoot;\n /** Zero-fills what it can and drops every key; later calls throw `keys-locked`. Idempotent. */\n dispose(): void;\n}\n\n/** What only the link initiator reaches: everything an owner proof and a read grant are made of. */\nexport interface IOwnerHandoverSecrets {\n readonly attach: IKeyPair;\n /** The 32-byte seed of the sealing keypair: what a read grant carries, since it rebuilds the pair. */\n readonly sealingSeed: Buffer;\n readonly sealingPublicKey: Uint8Array;\n readonly authorship: Buffer;\n}\n\nconst SECRETS = new WeakMap<IOwnerHandover, () => IOwnerHandoverSecrets>();\n\nclass OwnerHandover implements IOwnerHandover {\n #secrets: IOwnerHandoverSecrets | undefined;\n readonly #sealingKeyMatches: boolean;\n\n constructor(secrets: IOwnerHandoverSecrets, sealingKeyMatches: boolean) {\n this.#secrets = secrets;\n this.#sealingKeyMatches = sealingKeyMatches;\n SECRETS.set(this, () => this.#require());\n }\n\n get sealingKeyMatches(): boolean {\n this.#require();\n return this.#sealingKeyMatches;\n }\n\n get fingerprint(): string {\n return fingerprint(this.#require().sealingPublicKey);\n }\n\n get disposed(): boolean {\n return this.#secrets === undefined;\n }\n\n certify(instancePublicKey: Uint8Array, binding: ILinkBinding): ITrustRoot {\n const { attach } = this.#require();\n const message = runtimeCertificateMessage(instancePublicKey, binding);\n return {\n attachPublicKey: Uint8Array.from(attach.publicKey),\n certificate: Uint8Array.from(signEd25519(attach.privateKey, message)),\n };\n }\n\n dispose(): void {\n const secrets = this.#secrets;\n if (secrets === undefined) return;\n this.#secrets = undefined;\n wipe(secrets.sealingSeed, secrets.authorship);\n }\n\n #require(): IOwnerHandoverSecrets {\n if (this.#secrets === undefined) {\n throw new KeyringError('keys-locked', 'The owner handover was disposed.');\n }\n return this.#secrets;\n }\n}\n\n/**\n * Package-private: the owner's keys behind a handover, for the link initiator alone. It is not\n * exported from the entry point, and it answers only for a handover this package opened.\n */\nexport function ownerHandoverSecrets(handover: IOwnerHandover): IOwnerHandoverSecrets {\n const secrets = SECRETS.get(handover);\n if (secrets === undefined) {\n throw new KeyringError(\n 'invalid-argument',\n 'This is not an owner handover this package opened.',\n );\n }\n return secrets();\n}\n\n/**\n * Opens the owner handover with `credential`: one key derivation (one scrypt run per passphrase\n * slot, as an unlock), from whose master key the attach keypair and the read keys are derived; the\n * master key is zero-filled before this returns. A wrong credential and a tampered slot fail alike.\n */\nexport async function openOwnerHandover(\n keyring: IKeyring,\n credential: TKeyringCredential,\n): Promise<IOwnerHandover> {\n const document = validateKeyring(keyring);\n const { masterKey } = await recoverMasterKey(document, credential);\n let sealingSeed: Buffer | undefined;\n let authorship: Buffer | undefined;\n try {\n sealingSeed = deriveSealingSeed(masterKey);\n authorship = deriveAuthorshipKey(masterKey);\n const attach = deriveAttachKeyPair(masterKey);\n const sealingPublicKey = x25519KeyPairFromSeed(sealingSeed).publicKey;\n return new OwnerHandover(\n { attach, sealingSeed, sealingPublicKey, authorship },\n sealingPublicKeyMatches(document, sealingPublicKey),\n );\n } catch (error) {\n wipe(sealingSeed, authorship);\n throw error;\n } finally {\n wipe(masterKey);\n }\n}\n","/**\n * Where the owner link's randomness comes from. Every instance key, ephemeral key and nonce of the\n * spawner exchange and the handshake is drawn here, so the package's known-answer tests can replay a\n * whole exchange or handshake from fixed values. The entry point only ever draws from `randomBytes`;\n * nothing a caller of the package can reach takes other entropy.\n */\nimport { randomBytes } from 'node:crypto';\n\nimport type { Buffer } from 'node:buffer';\n\nexport const LINK_SEED_BYTES = 32;\nexport const LINK_NONCE_BYTES = 32;\n\nexport interface ILinkEntropy {\n /** A fresh 32-byte private-key seed. The caller wipes it once the key exists. */\n seed(): Buffer;\n /** A fresh 32-byte nonce. */\n nonce(): Buffer;\n}\n\nexport const SYSTEM_LINK_ENTROPY: ILinkEntropy = {\n seed: () => randomBytes(LINK_SEED_BYTES),\n nonce: () => randomBytes(LINK_NONCE_BYTES),\n};\n","/**\n * The few operations the spawner exchange and the handshake are built from, each in the one form\n * they use it: a key agreement that refuses a point with no usable secret, transcript hashing and\n * MACs over length-prefixed parts, and HKDF to any length. Each wraps a `node:crypto` primitive the\n * keyring already uses, so the link adds nothing a supported runtime lacks.\n */\nimport { Buffer } from 'node:buffer';\nimport {\n createHash,\n createHmac,\n diffieHellman,\n hkdfSync,\n timingSafeEqual,\n type KeyObject,\n} from 'node:crypto';\n\nimport { concatLengthPrefixed, isBytes, wipe } from './bytes.js';\nimport { KeyringError, LinkRefusedError } from './errors.js';\nimport { x25519KeyPairFromSeed, x25519PublicKeyFromRaw, type IKeyPair } from './key-pairs.js';\nimport { KEY_BYTES } from './kdf.js';\nimport type { ILinkEntropy } from './link-entropy.js';\n\n/** A fresh X25519 keypair from the entropy's seed; the seed is wiped once the pair exists. */\nexport function ephemeralKeyPair(entropy: ILinkEntropy): IKeyPair {\n const seed = entropy.seed();\n try {\n return x25519KeyPairFromSeed(seed);\n } finally {\n wipe(seed);\n }\n}\n\n/**\n * The X25519 shared secret with the peer's raw public key. A value that is not a 32-byte key, or a\n * low-order point with no usable secret, is the peer's malformed message: refused, never retried.\n */\nexport function agree(privateKey: KeyObject, peerPublicKey: Uint8Array): Buffer {\n if (!isBytes(peerPublicKey) || peerPublicKey.length !== KEY_BYTES) {\n throw new LinkRefusedError('malformed');\n }\n try {\n return diffieHellman({ privateKey, publicKey: x25519PublicKeyFromRaw(peerPublicKey) });\n } catch {\n throw new LinkRefusedError('malformed');\n }\n}\n\n/** SHA-256 over length-prefixed parts, so no two part lists hash alike. */\nexport function hashParts(parts: readonly Uint8Array[]): Buffer {\n return createHash('sha256').update(concatLengthPrefixed(parts)).digest();\n}\n\n/** HMAC-SHA256 of the concatenation of `parts` under `key`. */\nexport function hmac(key: Uint8Array, ...parts: readonly Uint8Array[]): Buffer {\n const mac = createHmac('sha256', key);\n for (const part of parts) mac.update(part);\n return mac.digest();\n}\n\n/** Whether two byte strings are equal, in constant time once their (public) lengths match. */\nexport function sameBytes(left: Uint8Array, right: Uint8Array): boolean {\n return left.length === right.length && timingSafeEqual(left, right);\n}\n\n/** HKDF-SHA256 to `length` bytes; a host that cannot run it fails as `kdf-failed`. */\nexport function hkdf(\n inputKeyMaterial: Uint8Array,\n salt: Uint8Array,\n info: string,\n length: number,\n): Buffer {\n try {\n return Buffer.from(hkdfSync('sha256', inputKeyMaterial, salt, info, length));\n } catch {\n throw new KeyringError('kdf-failed', 'The key derivation could not run on this host.');\n }\n}\n","/**\n * The spawner exchange: how a runtime and the process that started it come to share a key that no\n * other program holds.\n *\n * It runs once, over the socket pair the runtime inherited from its spawner and before the runtime\n * starts any child, so the two ends are known by construction. The spawner writes its X25519\n * ephemeral first; the runtime answers with its own ephemeral and its instance public key; each\n * side derives the spawner key from the shared secret, salted with a hash of all three public values\n * so the key is bound to this runtime's instance key. The spawner learns the instance key here, over\n * the one channel no other program reaches, and later certifies exactly that key.\n *\n * The spawner key has no lifetime of its own. The runtime forgets it when its trust root is\n * installed, and the spawner wipes it once it has certified the runtime, so it is never an owner\n * credential: it admits only the process that started this runtime, and only until certification.\n */\nimport { Buffer } from 'node:buffer';\n\nimport { isBytes, utf8, wipe } from './bytes.js';\nimport { KeyringError, LinkRefusedError } from './errors.js';\nimport type { IKeyPair } from './key-pairs.js';\nimport { KEY_BYTES } from './kdf.js';\nimport { SYSTEM_LINK_ENTROPY, type ILinkEntropy } from './link-entropy.js';\nimport { agree, ephemeralKeyPair, hashParts, hkdf } from './link-primitives.js';\n\n/** Bound into the salt, so a key from this exchange is never one of any other derivation. */\nexport const SPAWNER_EXCHANGE_LABEL = 'robota/link/v1/spawner-exchange';\n/** The HKDF `info` of the spawner key. */\nexport const SPAWNER_KEY_LABEL = 'robota/link/v1/spawner';\n/** The spawner's message: its ephemeral public key. */\nexport const SPAWNER_EXCHANGE_MESSAGE_BYTES = KEY_BYTES;\n/** The runtime's reply: its ephemeral public key, then its instance public key. */\nexport const SPAWNER_EXCHANGE_REPLY_BYTES = 2 * KEY_BYTES;\n\nconst SPAWNER_KEYS = new WeakMap<SpawnerKey, Buffer>();\n/** Only this module holds it, so only a completed exchange can make a spawner key. */\nconst ISSUED = Symbol('spawner-key');\n\n/**\n * The key a spawner shares with the runtime it started. Opaque: it offers only `wipe()`, and the link\n * initiator reads it through an accessor this package does not export. It has no expiry; it is\n * wiped by whoever holds it once it is no longer needed.\n */\nexport class SpawnerKey {\n constructor(issued: typeof ISSUED, bytes: Buffer) {\n if (issued !== ISSUED) {\n throw new KeyringError('invalid-argument', 'A spawner key comes only from an exchange.');\n }\n SPAWNER_KEYS.set(this, bytes);\n }\n\n get wiped(): boolean {\n return !SPAWNER_KEYS.has(this);\n }\n\n /** Zero-fills the key and forgets it. Idempotent. */\n wipe(): void {\n SPAWNER_KEYS.get(this)?.fill(0);\n SPAWNER_KEYS.delete(this);\n }\n}\n\n/** Package-private: the bytes of a spawner key, for the link initiator alone. */\nexport function spawnerKeyBytes(key: SpawnerKey): Buffer {\n const bytes = SPAWNER_KEYS.get(key);\n if (bytes === undefined) throw new KeyringError('keys-locked', 'The spawner key was wiped.');\n return bytes;\n}\n\nexport interface ISpawnerExchangeResult {\n readonly spawnerKey: SpawnerKey;\n /** The runtime's instance public key, as it sent it over the inherited pair. */\n readonly instancePublicKey: Uint8Array;\n}\n\n/** The spawner's side of one exchange. */\nexport interface ISpawnerExchange {\n /** The first message to write to the runtime: the spawner's ephemeral public key. */\n readonly message: Uint8Array;\n /** Derives the spawner key from the runtime's reply. Once: the ephemeral is dropped by the first call. */\n complete(reply: Uint8Array): ISpawnerExchangeResult;\n}\n\n/** The spawner key both sides derive from the shared secret and the three public values. */\nexport function deriveSpawnerKey(\n sharedSecret: Uint8Array,\n spawnerEphemeral: Uint8Array,\n runtimeEphemeral: Uint8Array,\n instancePublicKey: Uint8Array,\n): Buffer {\n const salt = hashParts([\n utf8(SPAWNER_EXCHANGE_LABEL),\n spawnerEphemeral,\n runtimeEphemeral,\n instancePublicKey,\n ]);\n return hkdf(sharedSecret, salt, SPAWNER_KEY_LABEL, KEY_BYTES);\n}\n\n/** {@link startSpawnerExchange} drawing from `entropy`; the package's own tests pass fixed values. */\nexport function startSpawnerExchangeWith(entropy: ILinkEntropy): ISpawnerExchange {\n let ephemeral: IKeyPair | undefined = ephemeralKeyPair(entropy);\n const message = Uint8Array.from(ephemeral.publicKey);\n return {\n message,\n complete(reply: Uint8Array): ISpawnerExchangeResult {\n const own = ephemeral;\n ephemeral = undefined;\n if (own === undefined) throw new LinkRefusedError('out-of-order');\n if (!isBytes(reply) || reply.length !== SPAWNER_EXCHANGE_REPLY_BYTES) {\n throw new LinkRefusedError('malformed');\n }\n const runtimeEphemeral = reply.subarray(0, KEY_BYTES);\n const instancePublicKey = Uint8Array.from(reply.subarray(KEY_BYTES));\n const shared = agree(own.privateKey, runtimeEphemeral);\n try {\n const key = deriveSpawnerKey(shared, own.publicKey, runtimeEphemeral, instancePublicKey);\n return { spawnerKey: new SpawnerKey(ISSUED, key), instancePublicKey };\n } finally {\n wipe(shared);\n }\n },\n };\n}\n\n/**\n * Starts the spawner's side of the exchange with a fresh ephemeral. Write `message` to the runtime\n * over the inherited pair, read its 64-byte reply, and `complete` it.\n */\nexport function startSpawnerExchange(): ISpawnerExchange {\n return startSpawnerExchangeWith(SYSTEM_LINK_ENTROPY);\n}\n\n/**\n * The runtime's side: its reply to the spawner's `message` and the spawner key, for its instance key.\n * The caller keeps the key; the ephemeral exists only inside this call.\n */\nexport function answerSpawnerExchange(\n message: Uint8Array,\n instancePublicKey: Uint8Array,\n entropy: ILinkEntropy,\n): { readonly reply: Buffer; readonly spawnerKey: Buffer } {\n if (!isBytes(message) || message.length !== SPAWNER_EXCHANGE_MESSAGE_BYTES) {\n throw new LinkRefusedError('malformed');\n }\n const ephemeral = ephemeralKeyPair(entropy);\n const shared = agree(ephemeral.privateKey, message);\n try {\n return {\n reply: Buffer.concat([ephemeral.publicKey, instancePublicKey]),\n spawnerKey: deriveSpawnerKey(shared, message, ephemeral.publicKey, instancePublicKey),\n };\n } finally {\n wipe(shared);\n }\n}\n","/**\n * A runtime's identity on the owner link: the crypto state a served runtime holds, and nothing else.\n *\n * - An Ed25519 instance key made at start, in memory only, so no file or other process can stand in\n * for this runtime; its public half is what the owner certifies.\n * - The spawner key from the one exchange over the inherited pair: spawner standing, until the trust\n * root is installed.\n * - The trust root, once its spawner installed it: the owner's attach public key and the certificate\n * of this instance for this runtime start. From then on spawner standing is gone for good, and only\n * a client that proves the attach key is admitted.\n *\n * Installing a root checks that it certifies this instance for the given binding, under the attach\n * key it names; which link may install it, and when, is the runtime custody's decision.\n */\nimport type { Buffer } from 'node:buffer';\n\nimport { isBytes, wipe } from './bytes.js';\nimport { KeyringError, LinkRefusedError } from './errors.js';\nimport { ed25519KeyPairFromSeed, type IKeyPair } from './key-pairs.js';\nimport { KEY_BYTES } from './kdf.js';\nimport { SYSTEM_LINK_ENTROPY, type ILinkEntropy } from './link-entropy.js';\nimport {\n certifies,\n checkLinkBinding,\n type ILinkBinding,\n type ITrustRoot,\n} from './runtime-certificate.js';\nimport { SIGNATURE_BYTES } from './signatures.js';\nimport { answerSpawnerExchange } from './spawner-exchange.js';\n\nexport interface IRuntimeIdentity {\n /** The instance public key, the value the owner certifies. */\n readonly instancePublicKey: Uint8Array;\n /** Whether the spawner exchange ran and its key is still held. */\n readonly spawnerKeyHeld: boolean;\n /** Whether a trust root is installed. */\n readonly certified: boolean;\n /**\n * Answers the spawner's first message with this runtime's ephemeral and instance public key, and\n * keeps the spawner key. Once: a second call is refused, even after a refused first one.\n */\n answerSpawnerExchange(message: Uint8Array): Uint8Array;\n /**\n * Keeps `root` when it certifies this instance for `binding`, and forgets the spawner key. Once,\n * and only while the spawner key is held. A refused root changes nothing.\n */\n installTrustRoot(root: ITrustRoot, binding: ILinkBinding): void;\n}\n\n/** The root a runtime was certified with, and the binding it certifies. */\nexport interface IRuntimeTrust {\n readonly root: ITrustRoot;\n readonly binding: ILinkBinding;\n}\n\n/** What only the link responder reaches. */\nexport interface IRuntimeIdentitySecrets {\n readonly instance: IKeyPair;\n /** Where the responder draws its ephemerals and nonces: the identity's own source. */\n readonly entropy: ILinkEntropy;\n spawnerKey(): Buffer | undefined;\n trust(): IRuntimeTrust | undefined;\n}\n\nconst SECRETS = new WeakMap<IRuntimeIdentity, IRuntimeIdentitySecrets>();\n\nfunction checkTrustRoot(value: unknown): ITrustRoot {\n const root = value as Partial<ITrustRoot> | null;\n if (\n typeof root !== 'object' ||\n root === null ||\n !isBytes(root.attachPublicKey) ||\n root.attachPublicKey.length !== KEY_BYTES ||\n !isBytes(root.certificate) ||\n root.certificate.length !== SIGNATURE_BYTES\n ) {\n throw new KeyringError(\n 'invalid-argument',\n 'A trust root is a 32-byte attach public key and a 64-byte certificate.',\n );\n }\n return {\n attachPublicKey: Uint8Array.from(root.attachPublicKey),\n certificate: Uint8Array.from(root.certificate),\n };\n}\n\nclass RuntimeIdentity implements IRuntimeIdentity {\n readonly #instance: IKeyPair;\n readonly #entropy: ILinkEntropy;\n #exchanged = false;\n #spawnerKey: Buffer | undefined;\n #trust: IRuntimeTrust | undefined;\n\n constructor(entropy: ILinkEntropy) {\n this.#entropy = entropy;\n const seed = entropy.seed();\n try {\n this.#instance = ed25519KeyPairFromSeed(seed);\n } finally {\n wipe(seed);\n }\n SECRETS.set(this, {\n instance: this.#instance,\n entropy,\n spawnerKey: () => this.#spawnerKey,\n trust: () => this.#trust,\n });\n }\n\n get instancePublicKey(): Uint8Array {\n return Uint8Array.from(this.#instance.publicKey);\n }\n\n get spawnerKeyHeld(): boolean {\n return this.#spawnerKey !== undefined;\n }\n\n get certified(): boolean {\n return this.#trust !== undefined;\n }\n\n answerSpawnerExchange(message: Uint8Array): Uint8Array {\n if (this.#exchanged) throw new LinkRefusedError('out-of-order');\n this.#exchanged = true;\n const { reply, spawnerKey } = answerSpawnerExchange(\n message,\n this.#instance.publicKey,\n this.#entropy,\n );\n this.#spawnerKey = spawnerKey;\n return reply;\n }\n\n installTrustRoot(root: ITrustRoot, binding: ILinkBinding): void {\n const checkedRoot = checkTrustRoot(root);\n const checkedBinding = checkLinkBinding(binding);\n if (this.#trust !== undefined) throw new LinkRefusedError('already-certified');\n if (this.#spawnerKey === undefined) throw new LinkRefusedError('spawner-unavailable');\n if (!certifies(checkedRoot, this.#instance.publicKey, checkedBinding)) {\n throw new LinkRefusedError('certificate-mismatch');\n }\n this.#trust = { root: checkedRoot, binding: checkedBinding };\n wipe(this.#spawnerKey);\n this.#spawnerKey = undefined;\n }\n}\n\n/** {@link createRuntimeIdentity} drawing from `entropy`; the package's own tests pass fixed values. */\nexport function createRuntimeIdentityWith(entropy: ILinkEntropy): IRuntimeIdentity {\n return new RuntimeIdentity(entropy);\n}\n\n/** A new runtime identity with a fresh instance key, uncertified and before its spawner exchange. */\nexport function createRuntimeIdentity(): IRuntimeIdentity {\n return createRuntimeIdentityWith(SYSTEM_LINK_ENTROPY);\n}\n\n/** Package-private: what the link responder needs of an identity this package made. */\nexport function runtimeIdentitySecrets(identity: IRuntimeIdentity): IRuntimeIdentitySecrets {\n const secrets = SECRETS.get(identity);\n if (secrets === undefined) {\n throw new KeyringError('invalid-argument', 'This is not a runtime identity this package made.');\n }\n return secrets;\n}\n","/**\n * Frame protection once a link is established.\n *\n * Every frame after the handshake is AES-256-GCM under the key of its direction, with a 96-bit\n * big-endian counter as the nonce and the direction and frame kind as the AAD. The counter is never\n * sent: each end expects the next one, so a replayed, reordered, dropped or misdirected frame fails\n * authentication like a tampered one, and the first frame that fails closes the link. A relay\n * between the two ends can only carry or drop frames.\n *\n * There are three kinds. `session` and `owner` frames carry bytes the carrier decodes. A `grant`\n * frame carries the owner's read keys and never passes through JSON: it is sealed from and opened\n * into key objects and byte buffers this module zero-fills. A grant travels only on an owner link,\n * from the handover that opened it, because whoever can send one decides which key the runtime\n * seals to.\n */\nimport { Buffer } from 'node:buffer';\n\nimport { gcmDecrypt, gcmEncrypt, GCM_IV_BYTES, GCM_TAG_BYTES } from './aead.js';\nimport { isBytes, wipe } from './bytes.js';\nimport { KeyringError, LinkRefusedError } from './errors.js';\nimport { x25519KeyPairFromSeed } from './key-pairs.js';\nimport { KEY_BYTES } from './kdf.js';\nimport { ownerHandoverSecrets, type IOwnerHandover } from './owner-handover.js';\nimport { createReadKeys, type IReadKeys } from './read-keys.js';\nimport type { ILinkBinding } from './runtime-certificate.js';\n\n/** How the initiator was admitted, which is the standing its link has. */\nexport type TLinkMode = 'spawner' | 'owner';\nexport type TLinkFrameKind = 'session' | 'owner' | 'grant';\n/** The kinds a caller seals and opens as bytes; a grant has its own two operations. */\nexport type TLinkDataFrameKind = Exclude<TLinkFrameKind, 'grant'>;\n\nconst KIND_BYTES: Readonly<Record<TLinkFrameKind, number>> = { session: 1, owner: 2, grant: 3 };\nconst KINDS_BY_BYTE: ReadonlyMap<number, TLinkFrameKind> = new Map(\n Object.entries(KIND_BYTES).map(([kind, byte]) => [byte, kind as TLinkFrameKind]),\n);\n/** The direction byte of the AAD names the sending end. */\nconst DIRECTION_BYTES = { initiator: 1, responder: 2 } as const;\ntype TLinkRole = keyof typeof DIRECTION_BYTES;\n\n/** The kind byte and the GCM tag around every frame's ciphertext. */\nexport const LINK_FRAME_OVERHEAD_BYTES = 1 + GCM_TAG_BYTES;\n/** The most one frame carries; a carrier bounds its frames below this. */\nexport const LINK_FRAME_MAX_PLAINTEXT_BYTES = 16 * 1024 * 1024;\nconst GRANT_VERSION = 1;\n/** version | sealing key seed | authorship key */\nconst GRANT_PLAINTEXT_BYTES = 1 + KEY_BYTES + KEY_BYTES;\n/** A sealed read grant is always exactly this long. */\nexport const LINK_GRANT_FRAME_BYTES = GRANT_PLAINTEXT_BYTES + LINK_FRAME_OVERHEAD_BYTES;\n\nexport interface ILinkFrameKeys {\n readonly initiatorToResponder: Buffer;\n readonly responderToInitiator: Buffer;\n}\n\n/** The kind a frame says it is. Unauthenticated until it opens, which checks the kind too. */\nexport function linkFrameKind(frame: Uint8Array): TLinkFrameKind {\n const kind =\n isBytes(frame) && frame.length >= LINK_FRAME_OVERHEAD_BYTES\n ? KINDS_BY_BYTE.get(frame[0]!)\n : undefined;\n if (kind === undefined) throw new LinkRefusedError('malformed');\n return kind;\n}\n\nfunction nonceFor(counter: number): Buffer {\n const nonce = Buffer.alloc(GCM_IV_BYTES);\n nonce.writeBigUInt64BE(BigInt(counter), GCM_IV_BYTES - 8);\n return nonce;\n}\n\n/** One end of an established link. */\nexport interface IEstablishedLink {\n readonly standing: TLinkMode;\n /** The runtime start the link is bound to: authenticated by the handshake on both ends. */\n readonly binding: ILinkBinding;\n /** The runtime's instance key: checked by the initiator, its own at the responder. */\n readonly instancePublicKey: Uint8Array;\n readonly closed: boolean;\n seal(kind: TLinkDataFrameKind, plaintext: Uint8Array): Uint8Array;\n /** The next frame from the other end; throws, and closes the link, unless it is exactly that. */\n open(frame: Uint8Array): { readonly kind: TLinkDataFrameKind; readonly plaintext: Buffer };\n /** Zero-fills both frame keys. Idempotent. */\n close(): void;\n}\n\nexport interface IInitiatorLink extends IEstablishedLink {\n /**\n * The owner's read keys (the sealing key and the authorship key, never the master key or the\n * attach key) as one grant frame. Only on an owner link, and only from the handover that opened it.\n */\n sealReadGrant(handover: IOwnerHandover): Uint8Array;\n}\n\nexport interface IResponderLink extends IEstablishedLink {\n /** The read keys a grant frame carries. Only on an owner-standing link; anything else closes it. */\n openReadGrant(frame: Uint8Array): IReadKeys;\n}\n\nabstract class LinkEnd implements IEstablishedLink {\n readonly standing: TLinkMode;\n readonly binding: ILinkBinding;\n readonly #role: TLinkRole;\n readonly #instancePublicKey: Uint8Array;\n #sendKey: Buffer | undefined;\n #receiveKey: Buffer | undefined;\n #sent = 0;\n #received = 0;\n\n constructor(\n role: TLinkRole,\n standing: TLinkMode,\n binding: ILinkBinding,\n instancePublicKey: Uint8Array,\n keys: ILinkFrameKeys,\n ) {\n this.#role = role;\n this.standing = standing;\n this.binding = Object.freeze({ runtimeId: binding.runtimeId, generation: binding.generation });\n this.#instancePublicKey = Uint8Array.from(instancePublicKey);\n this.#sendKey = role === 'initiator' ? keys.initiatorToResponder : keys.responderToInitiator;\n this.#receiveKey = role === 'initiator' ? keys.responderToInitiator : keys.initiatorToResponder;\n }\n\n get instancePublicKey(): Uint8Array {\n return Uint8Array.from(this.#instancePublicKey);\n }\n\n get closed(): boolean {\n return this.#sendKey === undefined;\n }\n\n seal(kind: TLinkDataFrameKind, plaintext: Uint8Array): Uint8Array {\n if (kind !== 'session' && kind !== 'owner') {\n throw new KeyringError('invalid-argument', 'A frame is a session or an owner frame.');\n }\n return this.sealKind(kind, plaintext);\n }\n\n open(frame: Uint8Array): { readonly kind: TLinkDataFrameKind; readonly plaintext: Buffer } {\n const kind = this.kindOf(frame);\n if (kind === 'grant') {\n this.close();\n throw new KeyringError('invalid-argument', 'A read grant opens only as a grant.');\n }\n return { kind, plaintext: this.openKind(frame, kind) };\n }\n\n close(): void {\n wipe(this.#sendKey, this.#receiveKey);\n this.#sendKey = undefined;\n this.#receiveKey = undefined;\n }\n\n protected sealKind(kind: TLinkFrameKind, plaintext: Uint8Array): Uint8Array {\n if (!isBytes(plaintext) || plaintext.length > LINK_FRAME_MAX_PLAINTEXT_BYTES) {\n throw new KeyringError(\n 'invalid-argument',\n `A frame carries 0-${LINK_FRAME_MAX_PLAINTEXT_BYTES} bytes.`,\n );\n }\n const key = this.#requireOpen(this.#sendKey);\n if (!Number.isSafeInteger(this.#sent + 1)) {\n this.close();\n throw new KeyringError('link-closed', 'The link has sent all the frames it can.');\n }\n const aad = Buffer.from([DIRECTION_BYTES[this.#role], KIND_BYTES[kind]]);\n const sealed = gcmEncrypt(key, nonceFor(this.#sent), plaintext, aad);\n this.#sent += 1;\n return Buffer.concat([Buffer.from([KIND_BYTES[kind]]), sealed]);\n }\n\n /** The frame's kind; a frame that is not one closes the link. */\n protected kindOf(frame: Uint8Array): TLinkFrameKind {\n this.#requireOpen(this.#receiveKey);\n try {\n return linkFrameKind(frame);\n } catch (error) {\n this.close();\n throw error;\n }\n }\n\n /** The next frame's plaintext, which the caller owns; any failure closes the link. */\n protected openKind(frame: Uint8Array, kind: TLinkFrameKind): Buffer {\n const key = this.#requireOpen(this.#receiveKey);\n const peer: TLinkRole = this.#role === 'initiator' ? 'responder' : 'initiator';\n const aad = Buffer.from([DIRECTION_BYTES[peer], KIND_BYTES[kind]]);\n let plaintext: Buffer;\n try {\n plaintext = gcmDecrypt(key, nonceFor(this.#received), frame.subarray(1), aad);\n } catch (error) {\n this.close();\n throw error;\n }\n this.#received += 1;\n return plaintext;\n }\n\n #requireOpen(key: Buffer | undefined): Buffer {\n if (key === undefined) throw new KeyringError('link-closed', 'The link is closed.');\n return key;\n }\n}\n\nexport class InitiatorLink extends LinkEnd implements IInitiatorLink {\n readonly #handover: IOwnerHandover | undefined;\n\n constructor(\n standing: TLinkMode,\n binding: ILinkBinding,\n instancePublicKey: Uint8Array,\n keys: ILinkFrameKeys,\n handover: IOwnerHandover | undefined,\n ) {\n super('initiator', standing, binding, instancePublicKey, keys);\n this.#handover = handover;\n }\n\n sealReadGrant(handover: IOwnerHandover): Uint8Array {\n if (this.standing !== 'owner' || handover !== this.#handover) {\n throw new KeyringError(\n 'invalid-argument',\n 'A read grant goes only over an owner link, from the handover that opened it.',\n );\n }\n const { sealingSeed, authorship } = ownerHandoverSecrets(handover);\n const plaintext = Buffer.alloc(GRANT_PLAINTEXT_BYTES);\n try {\n plaintext[0] = GRANT_VERSION;\n sealingSeed.copy(plaintext, 1);\n authorship.copy(plaintext, 1 + KEY_BYTES);\n return this.sealKind('grant', plaintext);\n } finally {\n wipe(plaintext);\n }\n }\n}\n\nexport class ResponderLink extends LinkEnd implements IResponderLink {\n constructor(\n standing: TLinkMode,\n binding: ILinkBinding,\n instancePublicKey: Uint8Array,\n keys: ILinkFrameKeys,\n ) {\n super('responder', standing, binding, instancePublicKey, keys);\n }\n\n openReadGrant(frame: Uint8Array): IReadKeys {\n if (this.standing !== 'owner') {\n this.close();\n throw new KeyringError('invalid-argument', 'A read grant is accepted only on an owner link.');\n }\n if (this.kindOf(frame) !== 'grant') {\n this.close();\n throw new KeyringError('invalid-argument', 'Only a read grant opens as a grant.');\n }\n const plaintext = this.openKind(frame, 'grant');\n try {\n if (plaintext.length !== GRANT_PLAINTEXT_BYTES || plaintext[0] !== GRANT_VERSION) {\n this.close();\n throw new LinkRefusedError('malformed');\n }\n const sealing = x25519KeyPairFromSeed(plaintext.subarray(1, 1 + KEY_BYTES));\n return createReadKeys(sealing, Buffer.from(plaintext.subarray(1 + KEY_BYTES)));\n } finally {\n wipe(plaintext);\n }\n }\n}\n","/**\n * The owner link handshake: how a client and a served runtime decide that each is who it must be,\n * before anything that depends on a secret crosses between them.\n *\n * I → R Hello v=1 | mode | e_i | n_i\n * R → I Challenge e_r | n_r | inst | runtime id | generation | cert? | auth_r\n * I → R Proof auth_i\n *\n * th = SHA-256 of the length-prefixed label, mode, e_i, n_i, e_r, n_r, inst, runtime id,\n * generation and certificate (empty in spawner mode)\n * spawner: auth_r = HMAC(K_s, \"r\" | th) | Sig_inst(\"r\" | th); auth_i = HMAC(K_s, \"i\" | th)\n * owner: auth_r = Sig_inst(\"r\" | th), with the certificate; auth_i = Sig_attach(\"i\" | th)\n * frame keys = HKDF(X25519(e_i, e_r), salt = th, \"robota/link/v1/frames\"), one per direction\n *\n * The runtime answers first, and the initiator checks the whole answer before it produces its\n * proof. A spawner checks that the instance key is the one its exchange carried, then both halves of\n * `auth_r`: only the child it started holds the spawner key and that instance key. An owner checks\n * the certificate under the attach key it derived itself, for exactly the instance and binding the\n * answer names, then the instance signature. So a program on a taken-over socket, a runtime nobody\n * certified and a runtime another keyring certified are all refused having received only a hello,\n * which is fresh randomness. The mode, the binding and both ephemerals are in `th`, so no answer can\n * be moved to another mode, another link or another runtime start.\n *\n * Every refusal is a `LinkRefusedError` naming the failed check for the caller's logs; the carrier\n * closes the stream and tells the other end nothing.\n */\nimport { Buffer } from 'node:buffer';\n\nimport { concatLengthPrefixed, isBytes, utf8, wipe } from './bytes.js';\nimport { KeyringError, LinkRefusedError } from './errors.js';\nimport type { IKeyPair } from './key-pairs.js';\nimport { KEY_BYTES } from './kdf.js';\nimport { SYSTEM_LINK_ENTROPY, LINK_NONCE_BYTES, type ILinkEntropy } from './link-entropy.js';\nimport {\n InitiatorLink,\n ResponderLink,\n type IInitiatorLink,\n type ILinkFrameKeys,\n type IResponderLink,\n type TLinkMode,\n} from './link-frames.js';\nimport { agree, ephemeralKeyPair, hashParts, hkdf, hmac, sameBytes } from './link-primitives.js';\nimport { ownerHandoverSecrets, type IOwnerHandover } from './owner-handover.js';\nimport {\n certifies,\n checkLinkBinding,\n decodeBindingPart,\n MAX_BINDING_PART_LENGTH,\n requireInstanceKey,\n type ILinkBinding,\n} from './runtime-certificate.js';\nimport { runtimeIdentitySecrets, type IRuntimeIdentity } from './runtime-identity.js';\nimport { SIGNATURE_BYTES, signEd25519, verifyEd25519 } from './signatures.js';\nimport { spawnerKeyBytes, type SpawnerKey } from './spawner-exchange.js';\n\nexport const LINK_PROTOCOL_VERSION = 1;\nexport const LINK_HANDSHAKE_LABEL = 'robota/link/v1/handshake';\nexport const LINK_FRAME_KEYS_LABEL = 'robota/link/v1/frames';\n\nconst MODE_BYTES: Readonly<Record<TLinkMode, number>> = { spawner: 1, owner: 2 };\nconst HMAC_BYTES = 32;\nconst CERTIFICATE_BYTES: Readonly<Record<TLinkMode, number>> = {\n spawner: 0,\n owner: SIGNATURE_BYTES,\n};\nconst RUNTIME_PROOF_BYTES: Readonly<Record<TLinkMode, number>> = {\n spawner: HMAC_BYTES + SIGNATURE_BYTES,\n owner: SIGNATURE_BYTES,\n};\nconst PROOF_BYTES: Readonly<Record<TLinkMode, number>> = {\n spawner: HMAC_BYTES,\n owner: SIGNATURE_BYTES,\n};\nconst LENGTH_PREFIX_BYTES = 2;\nconst RESPONDER_TAG = utf8('r');\nconst INITIATOR_TAG = utf8('i');\n\n/** version | mode | e_i | n_i */\nexport const LINK_HELLO_BYTES = 2 + KEY_BYTES + LINK_NONCE_BYTES;\n/** The longest challenge either mode can send. */\nexport const LINK_CHALLENGE_MAX_BYTES =\n KEY_BYTES +\n LINK_NONCE_BYTES +\n KEY_BYTES +\n 3 * LENGTH_PREFIX_BYTES +\n 2 * MAX_BINDING_PART_LENGTH +\n Math.max(...Object.values(CERTIFICATE_BYTES)) +\n Math.max(...Object.values(RUNTIME_PROOF_BYTES));\n/** The longest proof either mode can send. */\nexport const LINK_PROOF_MAX_BYTES = Math.max(...Object.values(PROOF_BYTES));\n\n/** How the initiator proves itself: the spawner by its spawner key, the owner by its handover. */\nexport type TLinkInitiatorAuth =\n | {\n readonly mode: 'spawner';\n readonly spawnerKey: SpawnerKey;\n /** The instance key the spawner exchange carried. */\n readonly instancePublicKey: Uint8Array;\n }\n | { readonly mode: 'owner'; readonly handover: IOwnerHandover };\n\n/** Everything the transcript hash covers. */\nexport interface ILinkTranscript {\n readonly mode: TLinkMode;\n readonly initiatorEphemeral: Uint8Array;\n readonly initiatorNonce: Uint8Array;\n readonly responderEphemeral: Uint8Array;\n readonly responderNonce: Uint8Array;\n readonly instancePublicKey: Uint8Array;\n readonly binding: ILinkBinding;\n /** Empty in spawner mode. */\n readonly certificate: Uint8Array;\n}\n\nexport function transcriptHash(transcript: ILinkTranscript): Buffer {\n return hashParts([\n utf8(LINK_HANDSHAKE_LABEL),\n Buffer.from([MODE_BYTES[transcript.mode]]),\n transcript.initiatorEphemeral,\n transcript.initiatorNonce,\n transcript.responderEphemeral,\n transcript.responderNonce,\n transcript.instancePublicKey,\n utf8(transcript.binding.runtimeId),\n utf8(transcript.binding.generation),\n transcript.certificate,\n ]);\n}\n\n/** One key per direction from the handshake's shared secret, salted with the transcript hash. */\nexport function deriveFrameKeys(sharedSecret: Uint8Array, th: Uint8Array): ILinkFrameKeys {\n const keys = hkdf(sharedSecret, th, LINK_FRAME_KEYS_LABEL, 2 * KEY_BYTES);\n try {\n return {\n initiatorToResponder: Buffer.from(keys.subarray(0, KEY_BYTES)),\n responderToInitiator: Buffer.from(keys.subarray(KEY_BYTES)),\n };\n } finally {\n wipe(keys);\n }\n}\n\nfunction frameKeysFor(\n ephemeral: IKeyPair,\n peerEphemeral: Uint8Array,\n th: Uint8Array,\n): ILinkFrameKeys {\n const shared = agree(ephemeral.privateKey, peerEphemeral);\n try {\n return deriveFrameKeys(shared, th);\n } finally {\n wipe(shared);\n }\n}\n\ninterface IChallenge {\n readonly responderEphemeral: Uint8Array;\n readonly responderNonce: Uint8Array;\n readonly instancePublicKey: Uint8Array;\n readonly binding: ILinkBinding;\n readonly certificate: Uint8Array;\n readonly runtimeProof: Uint8Array;\n}\n\nfunction encodeChallenge(challenge: IChallenge): Buffer {\n return Buffer.concat([\n challenge.responderEphemeral,\n challenge.responderNonce,\n challenge.instancePublicKey,\n concatLengthPrefixed([\n utf8(challenge.binding.runtimeId),\n utf8(challenge.binding.generation),\n challenge.certificate,\n ]),\n challenge.runtimeProof,\n ]);\n}\n\n/** The challenge `bytes` encodes for an initiator in `mode`, or `malformed`: every length is exact. */\nfunction decodeChallenge(bytes: unknown, mode: TLinkMode): IChallenge {\n if (!isBytes(bytes) || bytes.length > LINK_CHALLENGE_MAX_BYTES)\n throw new LinkRefusedError('malformed');\n let offset = 0;\n const take = (length: number): Uint8Array => {\n if (offset + length > bytes.length) throw new LinkRefusedError('malformed');\n const part = Uint8Array.from(bytes.subarray(offset, offset + length));\n offset += length;\n return part;\n };\n const takePrefixed = (): Uint8Array => {\n const prefix = take(LENGTH_PREFIX_BYTES);\n return take((prefix[0]! << 8) | prefix[1]!);\n };\n const responderEphemeral = take(KEY_BYTES);\n const responderNonce = take(LINK_NONCE_BYTES);\n const instancePublicKey = take(KEY_BYTES);\n const runtimeId = decodeBindingPart(takePrefixed());\n const generation = decodeBindingPart(takePrefixed());\n const certificate = takePrefixed();\n const runtimeProof = take(bytes.length - offset);\n if (\n runtimeId === undefined ||\n generation === undefined ||\n certificate.length !== CERTIFICATE_BYTES[mode] ||\n runtimeProof.length !== RUNTIME_PROOF_BYTES[mode]\n ) {\n throw new LinkRefusedError('malformed');\n }\n return {\n responderEphemeral,\n responderNonce,\n instancePublicKey,\n binding: { runtimeId, generation },\n certificate,\n runtimeProof,\n };\n}\n\nfunction decodeHello(bytes: unknown): {\n readonly mode: TLinkMode;\n readonly ephemeral: Uint8Array;\n readonly nonce: Uint8Array;\n} {\n if (!isBytes(bytes) || bytes.length !== LINK_HELLO_BYTES || bytes[0] !== LINK_PROTOCOL_VERSION) {\n throw new LinkRefusedError('malformed');\n }\n const mode =\n bytes[1] === MODE_BYTES.spawner\n ? 'spawner'\n : bytes[1] === MODE_BYTES.owner\n ? 'owner'\n : undefined;\n if (mode === undefined) throw new LinkRefusedError('malformed');\n return {\n mode,\n ephemeral: Uint8Array.from(bytes.subarray(2, 2 + KEY_BYTES)),\n nonce: Uint8Array.from(bytes.subarray(2 + KEY_BYTES)),\n };\n}\n\n/** The client side of one handshake. */\nexport interface ILinkInitiator {\n readonly mode: TLinkMode;\n /** The first message: the version, the mode, a fresh ephemeral and a fresh nonce. Once. */\n hello(): Uint8Array;\n /**\n * Checks the runtime's challenge and, only if every check passed, returns the proof to send and\n * the established link. Throws `LinkRefusedError` otherwise. Once, whatever the outcome.\n */\n answer(challenge: Uint8Array): { readonly proof: Uint8Array; readonly link: IInitiatorLink };\n}\n\ntype TInitiatorState =\n | { readonly step: 'new' }\n | {\n readonly step: 'waiting';\n readonly ephemeral: IKeyPair;\n readonly nonce: Uint8Array;\n }\n | { readonly step: 'spent' };\n\nclass LinkInitiator implements ILinkInitiator {\n readonly mode: TLinkMode;\n readonly #auth: TLinkInitiatorAuth;\n readonly #entropy: ILinkEntropy;\n readonly #expectedInstance: Uint8Array | undefined;\n #state: TInitiatorState = { step: 'new' };\n\n constructor(auth: TLinkInitiatorAuth, entropy: ILinkEntropy) {\n if (auth?.mode === 'spawner') {\n spawnerKeyBytes(auth.spawnerKey);\n this.#expectedInstance = Uint8Array.from(requireInstanceKey(auth.instancePublicKey));\n } else if (auth?.mode === 'owner') {\n ownerHandoverSecrets(auth.handover);\n } else {\n throw new KeyringError('invalid-argument', 'A link initiator is a spawner or an owner.');\n }\n this.mode = auth.mode;\n this.#auth = auth;\n this.#entropy = entropy;\n }\n\n hello(): Uint8Array {\n if (this.#state.step !== 'new') throw new LinkRefusedError('out-of-order');\n const ephemeral = ephemeralKeyPair(this.#entropy);\n const nonce = this.#entropy.nonce();\n this.#state = { step: 'waiting', ephemeral, nonce };\n return Buffer.concat([\n Buffer.from([LINK_PROTOCOL_VERSION, MODE_BYTES[this.mode]]),\n ephemeral.publicKey,\n nonce,\n ]);\n }\n\n answer(challenge: Uint8Array): { readonly proof: Uint8Array; readonly link: IInitiatorLink } {\n const state = this.#state;\n if (state.step !== 'waiting') throw new LinkRefusedError('out-of-order');\n this.#state = { step: 'spent' };\n const answer = decodeChallenge(challenge, this.mode);\n if (\n this.#auth.mode === 'spawner' &&\n !sameBytes(answer.instancePublicKey, this.#expectedInstance!)\n ) {\n throw new LinkRefusedError('instance-mismatch');\n }\n if (this.#auth.mode === 'owner') {\n const { attach } = ownerHandoverSecrets(this.#auth.handover);\n const root = { attachPublicKey: attach.publicKey, certificate: answer.certificate };\n if (!certifies(root, answer.instancePublicKey, answer.binding)) {\n throw new LinkRefusedError('certificate-mismatch');\n }\n }\n const th = transcriptHash({\n mode: this.mode,\n initiatorEphemeral: state.ephemeral.publicKey,\n initiatorNonce: state.nonce,\n responderEphemeral: answer.responderEphemeral,\n responderNonce: answer.responderNonce,\n instancePublicKey: answer.instancePublicKey,\n binding: answer.binding,\n certificate: answer.certificate,\n });\n const signed = Buffer.concat([RESPONDER_TAG, th]);\n if (this.#auth.mode === 'spawner') {\n const spawnerKey = spawnerKeyBytes(this.#auth.spawnerKey);\n const macMatches = sameBytes(\n hmac(spawnerKey, RESPONDER_TAG, th),\n answer.runtimeProof.subarray(0, HMAC_BYTES),\n );\n const signatureMatches = verifyEd25519(\n answer.instancePublicKey,\n signed,\n answer.runtimeProof.subarray(HMAC_BYTES),\n );\n if (!macMatches || !signatureMatches) throw new LinkRefusedError('bad-runtime-proof');\n } else if (!verifyEd25519(answer.instancePublicKey, signed, answer.runtimeProof)) {\n throw new LinkRefusedError('bad-runtime-proof');\n }\n // Every check passed: from here on the initiator may produce what depends on its secret.\n const keys = frameKeysFor(state.ephemeral, answer.responderEphemeral, th);\n const proof =\n this.#auth.mode === 'spawner'\n ? hmac(spawnerKeyBytes(this.#auth.spawnerKey), INITIATOR_TAG, th)\n : signEd25519(\n ownerHandoverSecrets(this.#auth.handover).attach.privateKey,\n Buffer.concat([INITIATOR_TAG, th]),\n );\n const handover = this.#auth.mode === 'owner' ? this.#auth.handover : undefined;\n return {\n proof,\n link: new InitiatorLink(this.mode, answer.binding, answer.instancePublicKey, keys, handover),\n };\n }\n}\n\n/** {@link createLinkInitiator} drawing from `entropy`; the package's own tests pass fixed values. */\nexport function createLinkInitiatorWith(\n auth: TLinkInitiatorAuth,\n entropy: ILinkEntropy,\n): ILinkInitiator {\n return new LinkInitiator(auth, entropy);\n}\n\n/**\n * The client side of a handshake. A wiped spawner key or a disposed handover is refused here, before\n * any message exists.\n */\nexport function createLinkInitiator(auth: TLinkInitiatorAuth): ILinkInitiator {\n return createLinkInitiatorWith(auth, SYSTEM_LINK_ENTROPY);\n}\n\n/** The runtime side of one handshake. */\nexport interface ILinkResponder {\n /**\n * Answers a hello, or throws `LinkRefusedError`: a malformed hello, spawner mode without the\n * spawner key, or owner mode without a trust root for this binding. Once.\n */\n challenge(hello: Uint8Array): Uint8Array;\n /** Checks the proof; the established link and the standing it earned. Once. */\n accept(proof: Uint8Array): { readonly standing: TLinkMode; readonly link: IResponderLink };\n}\n\ninterface IPendingResponse {\n readonly mode: TLinkMode;\n readonly th: Buffer;\n readonly keys: ILinkFrameKeys;\n /** Spawner mode: the proof only a spawner-key holder can make. */\n readonly expectedProof?: Buffer;\n /** Owner mode: the attach key the proof must verify under. */\n readonly attachPublicKey?: Uint8Array;\n}\n\nclass LinkResponder implements ILinkResponder {\n readonly #identity: IRuntimeIdentity;\n readonly #binding: ILinkBinding;\n #step: 'new' | 'waiting' | 'spent' = 'new';\n #pending: IPendingResponse | undefined;\n\n constructor(identity: IRuntimeIdentity, binding: ILinkBinding) {\n runtimeIdentitySecrets(identity);\n this.#identity = identity;\n this.#binding = checkLinkBinding(binding);\n }\n\n challenge(hello: Uint8Array): Uint8Array {\n if (this.#step !== 'new') throw new LinkRefusedError('out-of-order');\n this.#step = 'spent';\n const { mode, ephemeral: initiatorEphemeral, nonce: initiatorNonce } = decodeHello(hello);\n const secrets = runtimeIdentitySecrets(this.#identity);\n const spawnerKey = secrets.spawnerKey();\n const trust = secrets.trust();\n if (mode === 'spawner' && spawnerKey === undefined) {\n throw new LinkRefusedError('spawner-unavailable');\n }\n if (\n mode === 'owner' &&\n (trust === undefined ||\n trust.binding.runtimeId !== this.#binding.runtimeId ||\n trust.binding.generation !== this.#binding.generation)\n ) {\n throw new LinkRefusedError('not-certified');\n }\n const ephemeral = ephemeralKeyPair(secrets.entropy);\n const responderNonce = secrets.entropy.nonce();\n const certificate = mode === 'owner' ? trust!.root.certificate : new Uint8Array(0);\n const th = transcriptHash({\n mode,\n initiatorEphemeral,\n initiatorNonce,\n responderEphemeral: ephemeral.publicKey,\n responderNonce,\n instancePublicKey: secrets.instance.publicKey,\n binding: this.#binding,\n certificate,\n });\n const keys = frameKeysFor(ephemeral, initiatorEphemeral, th);\n const signature = signEd25519(secrets.instance.privateKey, Buffer.concat([RESPONDER_TAG, th]));\n const runtimeProof =\n mode === 'spawner'\n ? Buffer.concat([hmac(spawnerKey!, RESPONDER_TAG, th), signature])\n : signature;\n this.#pending = {\n mode,\n th,\n keys,\n ...(mode === 'spawner'\n ? { expectedProof: hmac(spawnerKey!, INITIATOR_TAG, th) }\n : { attachPublicKey: Uint8Array.from(trust!.root.attachPublicKey) }),\n };\n this.#step = 'waiting';\n return encodeChallenge({\n responderEphemeral: ephemeral.publicKey,\n responderNonce,\n instancePublicKey: secrets.instance.publicKey,\n binding: this.#binding,\n certificate,\n runtimeProof,\n });\n }\n\n accept(proof: Uint8Array): { readonly standing: TLinkMode; readonly link: IResponderLink } {\n const pending = this.#pending;\n if (this.#step !== 'waiting' || pending === undefined)\n throw new LinkRefusedError('out-of-order');\n this.#pending = undefined;\n this.#step = 'spent';\n try {\n if (!isBytes(proof) || proof.length !== PROOF_BYTES[pending.mode]) {\n throw new LinkRefusedError('malformed');\n }\n if (pending.mode === 'spawner') {\n // The spawner key may have been forgotten since the challenge: certification ends spawner standing.\n if (runtimeIdentitySecrets(this.#identity).spawnerKey() === undefined) {\n throw new LinkRefusedError('spawner-unavailable');\n }\n if (!sameBytes(pending.expectedProof!, proof)) throw new LinkRefusedError('bad-proof');\n } else if (\n !verifyEd25519(pending.attachPublicKey!, Buffer.concat([INITIATOR_TAG, pending.th]), proof)\n ) {\n throw new LinkRefusedError('bad-proof');\n }\n } catch (error) {\n wipe(\n pending.keys.initiatorToResponder,\n pending.keys.responderToInitiator,\n pending.expectedProof,\n );\n throw error;\n }\n wipe(pending.expectedProof);\n const instancePublicKey = runtimeIdentitySecrets(this.#identity).instance.publicKey;\n return {\n standing: pending.mode,\n link: new ResponderLink(pending.mode, this.#binding, instancePublicKey, pending.keys),\n };\n }\n}\n\n/**\n * The runtime side of a handshake, for `identity` at the runtime start `binding` names. Which\n * standing may then do what is the runtime custody's decision, not this module's.\n */\nexport function createLinkResponder(\n identity: IRuntimeIdentity,\n binding: ILinkBinding,\n): ILinkResponder {\n return new LinkResponder(identity, binding);\n}\n","import { Buffer } from 'node:buffer';\nimport { diffieHellman, generateKeyPairSync, randomBytes, type KeyObject } from 'node:crypto';\n\nimport { gcmDecrypt, gcmEncrypt, GCM_IV_BYTES, GCM_TAG_BYTES } from './aead.js';\nimport { concatLengthPrefixed, isBytes, wipe } from './bytes.js';\nimport { KeyringError } from './errors.js';\nimport { rawPublicKey, x25519PublicKeyFromRaw } from './key-pairs.js';\nimport { hkdf32, KEY_BYTES } from './kdf.js';\n\n/** The first byte of every sealed key, record and blob. Authenticated, so it cannot be downgraded. */\nexport const SEALED_FORMAT_VERSION = 1;\n\n/** `info` of the HKDF that turns the ECDH secret into the key that wraps an artifact key. */\nexport const ARTIFACT_KEY_WRAP_LABEL = 'robota/keyring/v1/dek-wrap';\n\nconst VERSION_BYTES = 1;\n/** version | ephemeral X25519 public key | IV | wrapped key + tag */\nexport const SEALED_KEY_BYTES =\n VERSION_BYTES + KEY_BYTES + GCM_IV_BYTES + KEY_BYTES + GCM_TAG_BYTES;\n/** version | nonce | tag, around the ciphertext */\nexport const RECORD_OVERHEAD_BYTES = VERSION_BYTES + GCM_IV_BYTES + GCM_TAG_BYTES;\n\nfunction requireBytes(value: unknown, what: string, length?: number): Uint8Array {\n if (!isBytes(value) || (length !== undefined && value.length !== length)) {\n throw new KeyringError(\n 'invalid-argument',\n length === undefined\n ? `The ${what} must be a Uint8Array.`\n : `The ${what} must be a Uint8Array of exactly ${length} bytes.`,\n );\n }\n return value;\n}\n\nfunction requireSealingPrivateKey(value: unknown): KeyObject {\n const key = value as KeyObject | undefined;\n if (\n typeof key !== 'object' ||\n key === null ||\n key.type !== 'private' ||\n key.asymmetricKeyType !== 'x25519'\n ) {\n throw new KeyringError(\n 'invalid-argument',\n 'The sealing private key must be an X25519 private KeyObject.',\n );\n }\n return key;\n}\n\n/** The AAD the AEAD actually authenticates: the format version plus the caller's context. */\nfunction boundAad(callerAad: unknown): Buffer {\n return concatLengthPrefixed([\n Buffer.from([SEALED_FORMAT_VERSION]),\n requireBytes(callerAad, 'aad'),\n ]);\n}\n\n/**\n * Both ends of the ECDH mix both public keys into the HKDF salt, so the wrapping key is bound to\n * this exact sender/recipient pair and not only to the shared secret.\n */\nfunction deriveWrapKey(\n sharedSecret: Buffer,\n ephemeralPublicKey: Uint8Array,\n recipientPublicKey: Uint8Array,\n): Buffer {\n const salt = Buffer.concat([ephemeralPublicKey, recipientPublicKey]);\n try {\n return hkdf32(sharedSecret, ARTIFACT_KEY_WRAP_LABEL, salt);\n } finally {\n wipe(sharedSecret);\n }\n}\n\n/** A fresh ephemeral X25519 keypair; a host that cannot make one fails as `kdf-failed`. */\nfunction generateEphemeralKeyPair(): {\n readonly privateKey: KeyObject;\n readonly publicKey: Buffer;\n} {\n try {\n const ephemeral = generateKeyPairSync('x25519');\n return { privateKey: ephemeral.privateKey, publicKey: rawPublicKey(ephemeral.publicKey) };\n } catch {\n throw new KeyringError('kdf-failed', 'The key derivation could not run on this host.');\n }\n}\n\n/** A fresh random 256-bit data-encryption key (DEK) for one artifact. The caller wipes it after use. */\nexport function generateArtifactKey(): Buffer {\n return randomBytes(KEY_BYTES);\n}\n\n/**\n * Seals `artifactKey` to the sealing PUBLIC key (ECIES: fresh ephemeral X25519 key, ECDH, HKDF-SHA256,\n * AES-256-GCM). Only the public key is needed, so a process that never unlocked the keyring can\n * still produce artifacts that only the owner can open. Every call uses a new ephemeral key and IV,\n * so sealing the same key twice yields different bytes.\n *\n * `aad` is authenticated, not stored: the caller must present the same bytes to open it. Put the\n * artifact id in it, so a sealed key cannot be moved to another artifact.\n */\nexport function sealArtifactKey(\n sealingPublicKey: Uint8Array,\n artifactKey: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n const recipientRaw = requireBytes(sealingPublicKey, 'sealing public key', KEY_BYTES);\n requireBytes(artifactKey, 'artifact key', KEY_BYTES);\n const bound = boundAad(aad);\n const recipient = x25519PublicKeyFromRaw(recipientRaw);\n const ephemeral = generateEphemeralKeyPair();\n const ephemeralPublic = ephemeral.publicKey;\n let sharedSecret: Buffer;\n try {\n sharedSecret = diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient });\n } catch {\n // A low-order point (for example an all-zero key written into the file) has no usable secret.\n throw new KeyringError(\n 'invalid-argument',\n 'The sealing public key cannot be used for key agreement.',\n );\n }\n const wrapKey = deriveWrapKey(sharedSecret, ephemeralPublic, recipientRaw);\n try {\n const iv = randomBytes(GCM_IV_BYTES);\n const wrapped = gcmEncrypt(wrapKey, iv, artifactKey, bound);\n return Buffer.concat([Buffer.from([SEALED_FORMAT_VERSION]), ephemeralPublic, iv, wrapped]);\n } finally {\n wipe(wrapKey);\n }\n}\n\nfunction requireSealedVersion(bytes: Uint8Array): void {\n if (bytes.length === 0) {\n throw new KeyringError('malformed-ciphertext', 'The sealed data is empty.');\n }\n if (bytes[0] !== SEALED_FORMAT_VERSION) {\n throw new KeyringError(\n 'unsupported-version',\n 'The sealed data uses a format this build does not support.',\n );\n }\n}\n\n/**\n * Opens a key produced by {@link sealArtifactKey}. Needs the unlocked sealing private key. The\n * caller owns and wipes the returned key.\n */\nexport function openArtifactKey(\n sealingPrivateKey: KeyObject,\n sealed: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n const privateKey = requireSealingPrivateKey(sealingPrivateKey);\n requireBytes(sealed, 'sealed key');\n const bound = boundAad(aad);\n requireSealedVersion(sealed);\n if (sealed.length !== SEALED_KEY_BYTES) {\n throw new KeyringError('malformed-ciphertext', 'The sealed key has the wrong length.');\n }\n const ephemeralPublic = sealed.subarray(VERSION_BYTES, VERSION_BYTES + KEY_BYTES);\n const ivStart = VERSION_BYTES + KEY_BYTES;\n const iv = sealed.subarray(ivStart, ivStart + GCM_IV_BYTES);\n const wrapped = sealed.subarray(ivStart + GCM_IV_BYTES);\n let sharedSecret: Buffer;\n try {\n sharedSecret = diffieHellman({\n privateKey,\n publicKey: x25519PublicKeyFromRaw(ephemeralPublic),\n });\n } catch {\n throw new KeyringError('decryption-failed', 'Authentication of the ciphertext failed.');\n }\n const wrapKey = deriveWrapKey(sharedSecret, ephemeralPublic, rawPublicKey(privateKey));\n try {\n return gcmDecrypt(wrapKey, iv, wrapped, bound);\n } finally {\n wipe(wrapKey);\n }\n}\n\n/**\n * One content record: `version | 12-byte random nonce | ciphertext | 16-byte tag`, AES-256-GCM\n * under the artifact key. Use it as often as needed with one key — a JSONL log appends one record\n * per batch of lines, a whole-file blob is one record — but keep a key's record count far below\n * 2^32, the safe limit for random 96-bit nonces.\n *\n * `aad` is authenticated, not stored. Put the artifact id and the record's sequence number in it:\n * that is what stops records from being swapped between files or reordered. Detecting that\n * TRAILING records were cut off is not something one record can know — a log truncated at a record\n * boundary is a valid log — so the caller must keep the expected record count (or a final marker\n * record) somewhere it trusts.\n */\nexport function encryptRecord(\n artifactKey: Uint8Array,\n plaintext: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n requireBytes(artifactKey, 'artifact key', KEY_BYTES);\n requireBytes(plaintext, 'plaintext');\n const bound = boundAad(aad);\n const nonce = randomBytes(GCM_IV_BYTES);\n const sealed = gcmEncrypt(artifactKey, nonce, plaintext, bound);\n return Buffer.concat([Buffer.from([SEALED_FORMAT_VERSION]), nonce, sealed]);\n}\n\n/** Opens a record from {@link encryptRecord}; returns plaintext only if the whole record authenticates. */\nexport function decryptRecord(\n artifactKey: Uint8Array,\n record: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n requireBytes(artifactKey, 'artifact key', KEY_BYTES);\n requireBytes(record, 'record');\n const bound = boundAad(aad);\n requireSealedVersion(record);\n if (record.length < RECORD_OVERHEAD_BYTES) {\n throw new KeyringError(\n 'malformed-ciphertext',\n 'The record is shorter than its fixed overhead.',\n );\n }\n const nonceEnd = VERSION_BYTES + GCM_IV_BYTES;\n return gcmDecrypt(\n artifactKey,\n record.subarray(VERSION_BYTES, nonceEnd),\n record.subarray(nonceEnd),\n bound,\n );\n}\n\n/**\n * Seals a whole value in one step: a sealed key header followed by one record under that key.\n * Like {@link sealArtifactKey} it needs only the sealing public key.\n */\nexport function sealBlob(\n sealingPublicKey: Uint8Array,\n plaintext: Uint8Array,\n aad: Uint8Array,\n): Buffer {\n const artifactKey = generateArtifactKey();\n try {\n return Buffer.concat([\n sealArtifactKey(sealingPublicKey, artifactKey, aad),\n encryptRecord(artifactKey, plaintext, aad),\n ]);\n } finally {\n wipe(artifactKey);\n }\n}\n\n/**\n * The two halves of a blob from {@link sealBlob}: the sealed key and the one record under it, as\n * views into `blob`. A holder that keeps opened keys by sealed key splits with this, so the blob\n * layout stays in this package. Checks the layout only; nothing is authenticated until each half is\n * opened.\n */\nexport function splitSealedBlob(blob: Uint8Array): {\n readonly sealedKey: Uint8Array;\n readonly record: Uint8Array;\n} {\n requireBytes(blob, 'blob');\n requireSealedVersion(blob);\n if (blob.length < SEALED_KEY_BYTES + RECORD_OVERHEAD_BYTES) {\n throw new KeyringError('malformed-ciphertext', 'The blob is shorter than its fixed overhead.');\n }\n return { sealedKey: blob.subarray(0, SEALED_KEY_BYTES), record: blob.subarray(SEALED_KEY_BYTES) };\n}\n\n/** Opens a blob from {@link sealBlob}; any truncation, extension or edit fails as a whole. */\nexport function openBlob(sealingPrivateKey: KeyObject, blob: Uint8Array, aad: Uint8Array): Buffer {\n requireSealingPrivateKey(sealingPrivateKey);\n const { sealedKey, record } = splitSealedBlob(blob);\n const artifactKey = openArtifactKey(sealingPrivateKey, sealedKey, aad);\n try {\n return decryptRecord(artifactKey, record, aad);\n } finally {\n wipe(artifactKey);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-keyring",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "3.1.1",
4
+ "description": "Passphrase and recovery-key custody of a master key, derived sealing and attach keypairs, and public-key sealing of artifact keys, on node:crypto only",
5
+ "type": "module",
6
+ "main": "dist/node/index.js",
7
+ "types": "dist/node/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "source": "./src/index.ts",
11
+ "browser": null,
12
+ "node": {
13
+ "import": {
14
+ "types": "./dist/node/index.d.ts",
15
+ "default": "./dist/node/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/node/index.d.cts",
19
+ "default": "./dist/node/index.cjs"
20
+ }
21
+ },
22
+ "default": {
23
+ "import": {
24
+ "types": "./dist/node/index.d.ts",
25
+ "default": "./dist/node/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/node/index.d.cts",
29
+ "default": "./dist/node/index.cjs"
30
+ }
31
+ }
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "engines": {
36
+ "node": ">=22.12.0"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "CHANGELOG.md"
42
+ ],
43
+ "dependencies": {},
44
+ "devDependencies": {
45
+ "@types/node": "^20.19.43",
46
+ "rimraf": "^5.0.10",
47
+ "tsdown": "^0.22.14",
48
+ "vitest": "^3.2.6"
49
+ },
50
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "volta": {
55
+ "extends": "../../package.json"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/woojubb/robota"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/woojubb/robota/issues"
63
+ },
64
+ "homepage": "https://robota.io/",
65
+ "scripts": {
66
+ "build": "rimraf dist && tsdown",
67
+ "build:js": "pnpm run build",
68
+ "build:types": "pnpm run build",
69
+ "test": "vitest run --passWithNoTests",
70
+ "test:coverage": "vitest run --coverage --passWithNoTests",
71
+ "typecheck": "tsgo --noEmit",
72
+ "lint": "eslint src/ --ext .ts",
73
+ "clean": "rimraf dist",
74
+ "smoke": "node scripts/smoke.mjs",
75
+ "smoke:bun": "bun scripts/smoke.mjs"
76
+ }
6
77
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * An authorship tag is compared in constant time: a comparison that stopped at the first differing
3
+ * byte would let a program that can submit forged records and time the check learn a valid tag byte
4
+ * by byte. Kept apart because it replaces `node:crypto` for the whole file.
5
+ */
6
+ import { afterEach, describe, expect, it, vi } from 'vitest';
7
+
8
+ const compared = vi.hoisted(() => [] as Array<{ readonly left: string; readonly right: string }>);
9
+
10
+ vi.mock('node:crypto', async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import('node:crypto')>();
12
+ return {
13
+ ...actual,
14
+ timingSafeEqual: (left: NodeJS.ArrayBufferView, right: NodeJS.ArrayBufferView) => {
15
+ const hex = (view: NodeJS.ArrayBufferView): string =>
16
+ Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString('hex');
17
+ compared.push({ left: hex(left), right: hex(right) });
18
+ return actual.timingSafeEqual(left, right);
19
+ },
20
+ };
21
+ });
22
+
23
+ const { authorshipTag, deriveAuthorshipKey, verifyAuthorshipTag } =
24
+ await import('../authorship.js');
25
+ const { FIXED_MASTER_KEY, bytesOf, tamper } = await import('./helpers.js');
26
+
27
+ afterEach(() => {
28
+ compared.length = 0;
29
+ });
30
+
31
+ describe('verifying an authorship tag', () => {
32
+ it('compares the whole tag with timingSafeEqual, whether it matches or not', () => {
33
+ const key = deriveAuthorshipKey(FIXED_MASTER_KEY);
34
+ const message = bytesOf('robota/authorship/v1|session-record|sess_a|digest');
35
+ const tag = authorshipTag(key, message);
36
+ const forged = tamper(tag, 0);
37
+
38
+ expect(verifyAuthorshipTag(key, message, tag)).toBe(true);
39
+ expect(verifyAuthorshipTag(key, message, forged)).toBe(false);
40
+
41
+ expect(compared).toEqual([
42
+ { left: Buffer.from(tag).toString('hex'), right: Buffer.from(tag).toString('hex') },
43
+ { left: Buffer.from(tag).toString('hex'), right: forged.toString('hex') },
44
+ ]);
45
+ });
46
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The authorship key: what only an unlocked keyring can do, since sealing needs only the public key
3
+ * and so proves nothing about who wrote an artifact. A tag made under it verifies only for the exact
4
+ * message it was made for, and the unlocked keys stop tagging and verifying once they are locked.
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ import { createKeyring, unlockWithRecoveryKey } from '../index.js';
9
+ import { authorshipTag, deriveAuthorshipKey, verifyAuthorshipTag } from '../authorship.js';
10
+ import { FIXED_MASTER_KEY, bytesOf, codeOf, tamper } from './helpers.js';
11
+
12
+ const message = bytesOf('robota/authorship/v1|session-record|sess_a|digest');
13
+
14
+ describe('authorship tags', () => {
15
+ it('verify for the message they were made for, and for nothing else', () => {
16
+ const key = deriveAuthorshipKey(FIXED_MASTER_KEY);
17
+ const tag = authorshipTag(key, message);
18
+
19
+ expect(tag).toHaveLength(32);
20
+ expect(verifyAuthorshipTag(key, message, tag)).toBe(true);
21
+ expect(
22
+ verifyAuthorshipTag(key, bytesOf('robota/authorship/v1|session-record|sess_b|digest'), tag),
23
+ ).toBe(false);
24
+ expect(verifyAuthorshipTag(key, message, tamper(tag, 31))).toBe(false);
25
+ expect(verifyAuthorshipTag(key, message, tag.subarray(0, 31))).toBe(false);
26
+ expect(verifyAuthorshipTag(key, message, new Uint8Array(0))).toBe(false);
27
+ });
28
+
29
+ it('do not verify under another master key', () => {
30
+ const other = deriveAuthorshipKey(tamper(FIXED_MASTER_KEY, 0));
31
+ const tag = authorshipTag(deriveAuthorshipKey(FIXED_MASTER_KEY), message);
32
+ expect(verifyAuthorshipTag(other, message, tag)).toBe(false);
33
+ });
34
+
35
+ it('refuse a value that is not bytes as a caller bug', () => {
36
+ const key = deriveAuthorshipKey(FIXED_MASTER_KEY);
37
+ expect(codeOf(() => authorshipTag(key, 'text' as unknown as Uint8Array))).toBe(
38
+ 'invalid-argument',
39
+ );
40
+ expect(codeOf(() => verifyAuthorshipTag(key, message, 'tag' as unknown as Uint8Array))).toBe(
41
+ 'invalid-argument',
42
+ );
43
+ });
44
+ });
45
+
46
+ describe('the unlocked keys', () => {
47
+ it('tag and verify with the key the master key derives, from creation on', async () => {
48
+ const { keyring, recoveryKey, unlocked } = await createKeyring({ passphrase: 'a passphrase' });
49
+ const tag = unlocked.tagAuthorship(message);
50
+
51
+ expect(unlocked.verifyAuthorship(message, tag)).toBe(true);
52
+ const again = await unlockWithRecoveryKey(keyring, recoveryKey);
53
+ expect(again.verifyAuthorship(message, tag)).toBe(true);
54
+ expect(Buffer.from(again.tagAuthorship(message)).equals(Buffer.from(tag))).toBe(true);
55
+ });
56
+
57
+ it('stop tagging and verifying once locked', async () => {
58
+ const { unlocked } = await createKeyring({ passphrase: 'a passphrase' });
59
+ const tag = unlocked.tagAuthorship(message);
60
+ unlocked.lock();
61
+
62
+ expect(codeOf(() => unlocked.tagAuthorship(message))).toBe('keys-locked');
63
+ expect(codeOf(() => unlocked.verifyAuthorship(message, tag))).toBe('keys-locked');
64
+ });
65
+ });