/** * Sealed-slot sub-step — the "seal these declared fields into their own * `iv:data` slots" mechanism, extracted byte-parity from `RecordCodec` * (#629 Task 1; see `.superpowers/sdd/seam-map-classified-blobs.md` §2). * * This is the ONE genuinely separable sub-step `RecordCodec.encryptRecord`/ * `decryptRecord` perform (the digest-only `_vdig`/`_bidx` mechanism is NOT * separable the same way — it needs write-history `prev` context, see the * seam map's separability verdict). `sealFields`/`unsealFields` are the * natural shape of a `via` `encodeAtRest`/`decodeAtRest` hook for a "seal * this field" via-feature, and `makeSealedSlotCapability`/ * `makeReservedEnvelopes` (below) are the `ViaCryptoCtx` capability * factories built on top of them. * * The capability factories live here (rather than a sibling * `kernel/via-crypto.ts`) so they can call `deriveSealedFieldKey`/ * `deriveSealedFieldKeyFromCek`/`encrypt`/`decrypt` directly — this file IS * kernel enclave code (C3), so it is exempt from `enclave-barrel-only` the * same way `record-codec.ts`/`sealing.ts` are. A file outside * `kernel/enclave/**` would have had to go through the enclave barrel * (`kernel/enclave/index.ts`), which does not (yet) export these symbols. */ import { type EnclaveKey } from '../crypto.js'; import { SealedHandle } from '../../types.js'; import type { ViaCryptoCtx } from '../../via/index.js'; /** * Key material for one collection's sealed-field derivation. `cek`, when * supplied, is preferred (record-scoped); `getDEK` is the fallback (and, * for legacy/no-CEK collections, the only key) — mirrors the dual-read * `RecordCodec` has always done. */ export interface SealKeyMaterial { readonly collection: string; readonly cek?: EnclaveKey; getDEK(): Promise; } export interface SealFieldsResult { readonly openRecord: Record; readonly sealed: Record | undefined; } /** * Peel declared `sensitiveFields` out of `record` BEFORE building `_data`, * sealing each into its own `iv:data` slot. Extracted byte-parity from * `RecordCodec.encryptRecord` step 2 (pre-#629 `record-codec.ts:261-288`). * Returns `record` untouched (and `sealed: undefined`) when no declared * field is present with a defined value — the envelope stays byte-identical * to legacy output, exactly as before. */ export declare function sealFields(record: Record, sensitiveFields: ReadonlySet, keyMaterial: SealKeyMaterial): Promise; /** * Unseal one `_sealed[field]` slot to its plaintext value: dual-read (try * the CEK-derived key, fall back to the DEK-derived key for legacy * records), AES-GCM-decrypt the `iv:data` blob, and JSON-parse the result. * Extracted byte-parity from `RecordCodec.unsealField` * (pre-#629 `record-codec.ts:485-493`). */ export declare function unsealOneField(field: string, blob: string, keyMaterial: SealKeyMaterial): Promise; /** * Build a non-leaking {@link SealedHandle} producer over sealed key * material: each call captures only the ciphertext `blob` and a closure to * {@link unsealOneField} — the plaintext is never stored on the handle, and * (crucially) the DEK is fetched lazily on `reveal()`, never at handle- * construction time. Extracted byte-parity from * `RecordCodec.makeSealedHandle` (pre-#629 `record-codec.ts:552-554`). */ export declare function makeHandleProducer(keyMaterial: SealKeyMaterial): (field: string, blob: string) => SealedHandle; /** * Restore every `[field]: blob` entry in `sealed` onto `record` (mutated in * place, matching the original's mutate-then-return shape): eagerly unseal * to plaintext (`opts.asHandles` falsy) or wrap each in a * {@link SealedHandle} (`opts.asHandles: true`) so the plaintext is never * materialised. Extracted byte-parity from `RecordCodec.decryptRecord`'s * sealed-handle block (pre-#629 `record-codec.ts:615-623`). */ export declare function unsealFields(record: Record, sealed: Record, keyMaterial: SealKeyMaterial, opts?: { asHandles?: boolean; }): Promise>; /** What {@link makeSealedSlotCapability} needs — a subset of `RecordCodecContext`. */ export interface SealedSlotCapabilityCtx { readonly name: string; getDEK(): Promise; } /** * Build a `ViaCryptoCtx.sealedSlots` capability pre-bound to one * `(collection, recordId)`. The key material (`cek`/DEK resolver) is closed * over and never placed on the returned object — `Object.keys`/ * `Object.values` of the capability expose only the three methods, never a * key (zero-knowledge: capabilities never expose keys). * * `delete(field)` is **advisory and capability-instance-local, not real * erasure** — it has no store to reach and never will by itself: it just * adds `field` to a `Set` closed over by THIS ONE capability object, so a * later `unseal` call on that SAME object refuses. No I/O happens. A fresh * capability for the same record (e.g. `seal()`'d again, or a new * `makeSealedSlotCapability(...)` call) has an empty set and sees no * deletion at all — `seal()` on a given field also clears any prior mark * for it. Do not build production logic on `delete()` persisting anything. * The real erasure guarantee for a forgotten record is `vault.forget()`'s * unconditional `_writeTombstone` call, which replaces the whole live * envelope with a fresh, `_sealed`-free tombstone — `_sealed` disappears * from the store regardless of what any capability's `delete()` marked. * (#629 Task 10 traced this end-to-end; see task-10-report.md "sealedSlots * .delete()" section. Removing/changing this method's shape is a phase-E * decision, not implied by this comment.) */ export declare function makeSealedSlotCapability(ctx: SealedSlotCapabilityCtx, recordId: string, cek?: EnclaveKey): ViaCryptoCtx['sealedSlots']; /** Resolve the DEK for an arbitrary (possibly synthetic, e.g. `_dict_en`) collection name. */ export type ReservedEnvelopeDekResolver = (collection: string) => Promise; /** * Build a `ViaCryptoCtx.reservedEnvelopes` capability: a whole-envelope * encrypt/decrypt door scoped to collection names under a declared prefix * (e.g. `_dict_`) — the shape `via/i18n/dictionary.ts`'s * `DictionaryHandle` needs (seam map Part 3): build JSON → encrypt under a * DEK → wrap in an `EncryptedEnvelope`; decrypt → JSON text. No per-record * CEK — reserved envelopes are whole-DEK-encrypted, same as the dictionary * grandfather. * * `reservedEnvelopes(prefix)` throws `ValidationError` immediately when * `prefix` is not in `declaredPrefixes`; each `encrypt`/`decrypt` call * throws `ValidationError` when its `collection` argument does not start * with `prefix`. */ export declare function makeReservedEnvelopes(dekResolver: ReservedEnvelopeDekResolver, declaredPrefixes: readonly string[]): ViaCryptoCtx['reservedEnvelopes'];