/** * Sealed content v1 (#654 design §8.1, §8.2, §8.6, §9.1). * * A member seals the fields one category moves out of a payload. The result is * a header the Host may read — version, suite, category, scope, chain head, * sender, salt, nonce, padded length and content MAC — and the ciphertext * bytes. Whether the ciphertext travels inline or by reference is the wire * schema's rule, not this module's. * * The plaintext is the canonical JSON of the moved fields. It is framed before * encryption: `u32be(contentLength) || content || zeros`, padded in 256-byte * steps up to 16 KiB and with Padmé above. The frame is inside the AEAD, so the * length and the padding are authenticated, and one content has exactly one * legal padded length. * * The AAD binds every identity the Host could otherwise swap: tenant, scope, * epoch, key id, chain head, sender, message family, direction, the write's * identity, target device, Agent, the task's sealing categories, padded length * and content MAC. Envelope `id`, `seq` and `ts` stay out, so an envelope that * a dead-letter replay re-encodes still opens. * * `contentMac` is a keyed MAC over category, family, identity and plaintext. The * Host compares two submissions of one identity by it, without a key. * * A blob field is sealed by the same frame and key rule, before its outer * content. Its AAD is the outer AAD without `paddedLength` and `contentMac`, * plus its own `paddedLength` and the JSON Pointer `fieldPath` to its BlobRef. */ import { z } from 'zod'; import { type JsonObject } from '../attestation'; import { type Bytes } from './primitives'; export declare const SEALED_CONTENT_VERSION = 1; /** The only suite v1 admits. A new suite is a new constant and a new capability, never a parser branch. */ export declare const SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm"; /** C1–C4, in their canonical order. A task's sealing categories are listed in this order. */ export declare const SEALED_CATEGORIES: readonly ['task-input', 'agent-message', 'task-output', 'memory-projection']; export type SealedCategory = (typeof SEALED_CATEGORIES)[number]; export declare const SEALED_DIRECTIONS: readonly ['server-to-daemon', 'daemon-to-server']; export type SealedDirection = (typeof SEALED_DIRECTIONS)[number]; export declare const SEALED_NONCE_BYTES = 12; /** * The one legal padded length of a frame whose unpadded length is * `frameLength` (`4 + contentLength`). Up to 16 KiB: the next multiple of 256. * Above: Padmé — `E = floor(log2 n)`, `S = floor(log2 E) + 1`, rounded up to a * multiple of `2^(E − S)`, which adds less than 6.25%. */ export declare function sealedPaddedLength(frameLength: number): number; /** `u32be(contentLength) || content || zeros`, as long as {@link sealedPaddedLength} says. */ export declare function encodeSealedFrame(content: Uint8Array): Bytes; /** * The content of a decrypted frame, or `undefined` when one of the five * receiver checks fails: frame length equals `paddedLength`; the content fits; * `paddedLength` is the padded length of exactly this content; every padding * byte is zero; the content is within the message type's plaintext limit. */ export declare function decodeSealedFrame(frame: Uint8Array, paddedLength: number, maxContentLength: number): Uint8Array | undefined; /** What the Host may read of one sealed content. The ciphertext travels next to it. */ export declare const SealedContentHeaderV1Schema: z.ZodObject<{ v: z.ZodLiteral<1>; suite: z.ZodLiteral<"byok-sealed-v1.hkdf-sha256.aes-256-gcm">; category: z.ZodEnum<{ "agent-message": "agent-message"; "memory-projection": "memory-projection"; "task-input": "task-input"; "task-output": "task-output"; }>; scope: z.ZodObject<{ id: z.ZodString; epoch: z.ZodNumber; keyId: z.ZodString; }, z.core.$strict>; chainHead: z.ZodObject<{ seq: z.ZodNumber; hash: z.ZodString; }, z.core.$strict>; sender: z.ZodString; salt: z.ZodString; nonce: z.ZodString; paddedLength: z.ZodNumber; contentMac: z.ZodString; }, z.core.$strict>; export type SealedContentHeaderV1 = z.infer; /** * What the receiver knows from its own context and the payload's plaintext * fields. The receiver builds it; the sealed bytes never supply it. */ export interface SealedBinding { readonly tenantId: string; /** Message family, e.g. `task.offer_for_agent`, or `routine.template`. */ readonly family: string; readonly direction: SealedDirection; /** The write's identity: `{ taskId }`, `{ messageId }`, … or `{ routineId, definitionVersion }`. */ readonly identity: { readonly [field: string]: string | number; }; /** Target device. `null` only for the `routine.template` family, which binds no device. */ readonly deviceId: string | null; readonly agentRef: { readonly agentId: string; readonly profileRevision: string; } | null; /** The task's `sealing.categories`, unique and in {@link SEALED_CATEGORIES} order. */ readonly categories: readonly SealedCategory[]; /** A `routine.template`'s expiry; `null` for every other family. */ readonly notAfter: string | null; } /** The canonical AAD bytes of a header under a binding (§8.2). */ export declare function sealedContentAad(header: SealedContentHeaderV1, binding: SealedBinding): Uint8Array; /** The content MAC of one plaintext under one scope key (§9.1). */ export declare function sealedContentMac(input: { readonly scopeKey: Uint8Array; readonly category: SealedCategory; readonly binding: SealedBinding; readonly plaintext: JsonObject; }): Promise; export interface SealContentInput { readonly scopeKey: Uint8Array; readonly scope: { readonly id: string; readonly epoch: number; }; /** The member chain head the sender verified before sealing. */ readonly chainHead: { readonly seq: number; readonly hash: string; }; /** The sender's member id. */ readonly sender: string; readonly category: SealedCategory; readonly binding: SealedBinding; /** The fields this category moves out of the payload. */ readonly plaintext: JsonObject; /** The plaintext limit of this message type, in UTF-8 bytes of canonical JSON. */ readonly maxContentLength: number; } export interface SealedContent { readonly header: SealedContentHeaderV1; /** AES-256-GCM output: the padded frame followed by the 16-byte tag. */ readonly ciphertext: Uint8Array; } /** Seal one category of a payload once. A retry must resend these bytes, never seal again (§9.2). */ export declare function sealContent(input: SealContentInput): Promise; /** The receiver failure codes of design §8.3. */ export type SealedContentOpenFailure = 'sealed_key_unavailable' | 'sealed_open_failed' | 'sealed_content_mac_mismatch' | 'sealing_policy_mismatch'; /** * An opened content, with the header that the AEAD authenticated. The caller * reads `chainHead` and `sender` from it for the sender-view check (§7.3). */ export type SealedContentOpenResult = { readonly ok: true; readonly plaintext: JsonObject; readonly header: SealedContentHeaderV1; } | { readonly ok: false; readonly reason: SealedContentOpenFailure; }; /** The task's frozen `sealing` scope (§6.2). Its `categories` are {@link SealedBinding.categories}. */ export interface SealedScopePolicy { readonly scopeId: string; /** The lowest epoch the task accepts: the epoch of the offer's C1 content. */ readonly epoch: number; } export interface OpenSealedContentInput { /** The header as received. It is untrusted. */ readonly header: unknown; readonly ciphertext: Uint8Array; /** The scope key the receiver holds for `header.scope`. */ readonly scopeKey: Uint8Array; /** Partly written by the Host (`sealing.categories`, `notAfter`), so it is untrusted too. */ readonly binding: SealedBinding; /** From the task's `sealing`, which the Host writes. Untrusted. */ readonly sealing: SealedScopePolicy; readonly maxContentLength: number; } /** * Open one sealed content, fail closed. In order: a binding and a task scope * that parse (`sealing_policy_mismatch`); header shape (`sealed_open_failed`); * the category within the task's categories, and the header scope equal to * the task's scope at its epoch or later (both `sealing_policy_mismatch`); key * id of the held scope key (`sealed_key_unavailable`); ciphertext length; AEAD * under the rebuilt AAD; the five frame checks; canonical JSON of a plain * object (all `sealed_open_failed`); and the content MAC * (`sealed_content_mac_mismatch`). * There is no plaintext fallback, and nothing here throws for hostile input. */ export declare function openSealedContent(input: OpenSealedContentInput): Promise;