/** * Recovery profile persistence + dispatch. * * Wires the **paper** profile end-to-end through * `@noy-db/on-recovery`. The other three profiles (Shamir, * multi-channel, admin-mediated) ship the API surface and throw * {@link RecoveryProfileNotImplementedError} during use; per-profile * dispatch lands in follow-up issues. * * Storage layout: * * ``` * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`. * _meta/recovery-shamir — reserved * _meta/recovery-multi — reserved * _meta/recovery-admin — reserved * ``` * * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON * with empty `_iv` — the recovery-code wrapping is what protects the * KEK; the entries themselves are inert without the user's code. * * @module */ import type { NoydbStore } from '../../kernel/types.js'; import type { EnclaveKey } from '../../kernel/enclave/index.js'; import { type WrappedDeksBlob } from './wrapped-deks.js'; import type { ShamirRecoveryProvider } from './shamir-recovery-provider.js'; /** * One paper recovery code as persisted in `_meta/recovery-paper`. * * The hub's KEK is intentionally non-extractable (see `crypto.ts`), * so the recovery entry can't AES-KW-wrap the KEK directly. Instead * we wrap a serialized DEK set: the entry holds the AES-GCM * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery * deserializes the DEK set, then mints a fresh KEK from the new * passphrase and rewraps the DEKs under it. * * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick * resume — the cryptographic guarantee is identical (AES-GCM with a * PBKDF2-derived key), and it sidesteps the non-extractable-KEK * constraint cleanly. * * Type-level composition: `PaperRecoveryEntry extends * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`, * `wrappedDeks`) come from the shared primitive; `codeId` and * `enrolledAt` are paper-recovery's own metadata. Wire format * unchanged. */ export interface PaperRecoveryEntry extends WrappedDeksBlob { readonly codeId: string; readonly enrolledAt: string; } export interface PaperRecoveryDoc { readonly _noydb_recovery: 1; readonly profile: 'paper'; readonly entries: ReadonlyArray; } /** Read the paper-recovery entries. Returns empty array when absent. */ export declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise>; /** Replace the paper-recovery entries (used after burn-on-recovery). */ export declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray): Promise; /** Drop a single paper-recovery entry (burn-on-use). */ export declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise; /** Whether at least one recovery profile has any enrolled entries. */ export declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise; /** * Whether at least one **strong** recovery profile is enrolled. * * "Strong" excludes paper-alone — under managed-passphrase mode the * user has no memorized passphrase, so a stolen/lost paper sheet * would be a single point of total loss. Strong profiles today: * * - `shamir` (k-of-n threshold; survives loss of up to n-k shares) * - `multi-channel` (when shipped — follow-up slice) * - `admin-mediated` (when shipped — follow-up slice) * * Managed mode requires this check to pass before `openVault` returns. */ export declare function hasStrongRecoveryEnrolled(store: NoydbStore, vault: string): Promise; /** * One Shamir-recovery entry as persisted in `_meta/recovery-shamir`. * * Like {@link PaperRecoveryEntry}, the entry composes * {@link WrappedDeksBlob} (DEKs wrapped under a fresh ephemeral * recovery secret) with profile-specific metadata. Unlike paper, the * "credential" was never visible to the user — it was 32 random * bytes split into N Shamir shares at enrollment. The shares ARE * the credential; the user holds them, the hub never sees them * again after `enrollRecovery` returns. * * The recovery secret is base64-encoded and * passed as the `credential` arg to * {@link mintWrappedDeksBlob} / {@link unwrapDeksFromBlob}. The * PBKDF2 round over high-entropy input is harmless overhead — it * keeps the shared primitive unchanged while letting Shamir reuse * the same wrapping pipeline as paper. */ export interface ShamirRecoveryEntry extends WrappedDeksBlob { /** Stable id for this entry. Allows multiple Shamir splits to coexist. */ readonly entryId: string; /** Threshold — minimum shares to reconstruct. */ readonly k: number; /** Total shares minted at enrollment. */ readonly n: number; /** x-coordinates of the n minted shares. Informational. Omitted as of 0.2 * (string-level provider doesn't expose share x-coords); kept optional so * pre-0.2 entries still read. */ readonly xCoords?: ReadonlyArray; /** ISO timestamp. */ readonly enrolledAt: string; /** Optional caller-supplied label (e.g., "2-of-3 board escrow"). */ readonly label?: string; } export interface ShamirRecoveryDoc { readonly _noydb_recovery: 1; readonly profile: 'shamir'; readonly entries: ReadonlyArray; } /** Read the Shamir-recovery entries. Returns empty array when absent. */ export declare function loadShamirRecoveryEntries(store: NoydbStore, vault: string): Promise>; /** Replace the Shamir-recovery entries (used by enrollment and rotation). */ export declare function saveShamirRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray): Promise; /** * Mint a fresh Shamir recovery entry from a DEK set. * * 1. Generates a 32-byte recovery secret. * 2. Wraps the DEK set under that secret via * {@link mintWrappedDeksBlob} (the recovery secret is base64- * encoded as the credential string — PBKDF2 over high-entropy * input is harmless overhead). * 3. Splits the recovery secret via Shamir into `n` shares with * threshold `k`. * 4. Zeros the in-memory recovery secret after wrapping + splitting. * * Returns: * - `entry` — the {@link ShamirRecoveryEntry} to persist. * - `shareStrings` — the `n` Base32-encoded share strings to * return to the caller. The HUB MUST NOT PERSIST THESE; once * returned they are the user's responsibility. * * @param deks - DEK set to wrap. * @param entryId - Stable id for this entry (caller-supplied or * hub-generated). * @param k - Threshold (>= 2). * @param n - Total shares (k <= n <= 255). * @param label - Optional caller label. */ export declare function mintShamirRecoveryEntry(provider: ShamirRecoveryProvider, deks: Map, entryId: string, k: number, n: number, label?: string): Promise<{ entry: ShamirRecoveryEntry; shareStrings: string[]; }>; /** * Decrypt a Shamir recovery entry to recover the raw DEK set. * * Combines K or more `shares`, reconstructs the recovery secret, * unwraps the DEKs via {@link unwrapDeksFromBlob}. * * Throws (AES-GCM auth-tag mismatch) when the shares don't combine * to the secret originally used to mint the entry — typically * because they came from a different enrollment or were tampered * with. Callers iterating multiple entries should catch. */ export declare function unwrapDeksFromShamirEntry(provider: ShamirRecoveryProvider, entry: ShamirRecoveryEntry, shareStrings: readonly string[]): Promise>; /** * Generate one paper-recovery entry from an unlocked DEK set. * * Returns the serializable entry (persisted via * {@link savePaperRecoveryEntries}). The recovery flow unwraps the * DEK set, then mints a fresh KEK from the user's new passphrase. * * Thin wrapper over {@link mintWrappedDeksBlob} — the crypto * lives in the shared primitive; this function just adds paper- * recovery's own metadata (`codeId`, `enrolledAt`). * * @param deks Map of collection-name → DEK (extractable). * @param code The plaintext recovery code (caller-supplied; * pair this with `@noy-db/on-recovery`'s code * generator/parser if available). * @param codeId Stable id used by `burnPaperRecoveryEntry`. */ export declare function mintPaperRecoveryEntry(deks: Map, code: string, codeId: string): Promise; /** * Decrypt a recovery entry to recover the raw DEK set. Used by the * `recoverPassphrase` flow after the user's code has been parsed. * * Thin wrapper over {@link unwrapDeksFromBlob}. * * @throws when the code does not match the entry (AES-GCM auth tag fail). */ export declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise>;