/** * Managed-passphrase mode — rubber-hose-resistant vaults. * * A vault mode where the passphrase is machine-generated and never * exposed to the user, sealed under a developer-provided * {@link SealingKeyProvider} (macOS Keychain, Windows Credential * Manager, libsecret, AWS KMS, …). The user has no secret to give * up to coercion — they can't reveal what they don't know. * * ## Components in this file * * - {@link SealingKeyProvider} — the interface concrete providers * implement. Provider implementations live OUTSIDE hub (per- * platform packages). * - {@link MemorySealingKeyProvider} — in-memory test provider; uses * a deterministic per-instance "key" so two providers with * different ids cannot unseal each other's outputs. * - {@link RecipientHint} — public material a sender uses to seal * plaintext for a specific recipient; published by * {@link RecipientSealer.publishRecipientHint} and transported * out-of-band to the sender before bundle writes. * - {@link RecipientSealer} — interface for asymmetric/granted * providers that support recipient-target sealing (RSA-OAEP, * cloud-KMS asymmetric, etc.); distinct from self-only * {@link SealingKeyProvider} (macOS Keychain, WebAuthn-PRF). * - {@link MemoryRecipientSealer} — in-process reference * implementation of both `RecipientSealer` and * `SealingKeyProvider` using real WebCrypto RSA-OAEP + AES-GCM; * safe for tests and same-process sender/recipient scenarios. * - {@link loadSealedPassphrase} / {@link saveSealedPassphrase} — * plaintext envelope storage at `_meta/sealed-passphrase`. * Mirrors the `_meta/handle` and `_meta/public-envelope` AES- * GCM-bypassed patterns. The sealing layer (provider's job) * is the security boundary; hub doesn't have a key to encrypt * with at this layer — that's the whole point of the design. * - {@link resolveManagedSecret} — orchestrates the "generate + * seal + persist on first open; unseal on reopen" flow. * Returns the plaintext passphrase string that the rest of the * `createNoydb` keyring path consumes. * * Deferred to follow-ups: * - Block `rotate-passphrase` policy gate under managed mode. * - Mandatory strong-recovery enforcement. * - Recovery flow under managed mode (generates fresh sealed phrase). * * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/session-tiers.md → Managed-passphrase mode * * @module */ import type { NoydbStore, RecipientSealer } from '../../kernel/types.js'; /** * The contract concrete providers (per-platform key stores) implement * to seal and unseal a hub-generated random passphrase. The plaintext * passphrase NEVER leaves hub-controlled memory in unsealed form — * the provider receives the bytes, returns opaque sealed bytes, and * later reverses the operation. Hub treats the sealed bytes as * fully opaque. * * Implementations live OUTSIDE `@noy-db/hub` (separate packages * per the issue's "Concrete providers (live outside hub)" note): * * | Platform | Package (TBD) | Backing | * |---|---|---| * | macOS | `@noy-db/seal-macos-keychain` | Security.framework | * | Windows | `@noy-db/seal-wincred` | Credential Manager | * | Linux | `@noy-db/seal-libsecret` | libsecret / secret-service | * | Cloud / server | `@noy-db/seal-aws-kms` | AWS KMS Decrypt | */ export interface SealingKeyProvider { /** * Non-sensitive identifier disclosed in the persisted envelope. * Surfaced to consumers via `loadSealedPassphrase().providerId` so * a vault opened with the wrong provider class can detect the * mismatch and surface a clear error. NOT secret — fine to log. * * Suggested format: `:` — e.g. `macos-keychain:com.acme.app`, * `aws-kms:arn:aws:kms:us-east-1:123:key/abc`. The hub never * parses this; it's purely audit metadata. */ readonly id: string; /** Seal raw passphrase bytes. Output bytes are opaque to hub. */ seal(passphrase: Uint8Array): Promise; /** * Reverse {@link seal}. MUST throw on tamper, wrong-provider, or * any other failure — hub treats a thrown error as "this provider * cannot unlock this vault" and surfaces it to the caller. */ unseal(sealed: Uint8Array): Promise; } /** * In-memory test provider. NOT secure — uses a deterministic * per-instance "key" (16-byte SHA-256 of `id`) XOR'd over the * passphrase plus a 4-byte provider-id fingerprint prefix. The XOR is * sufficient to make different `id` values produce mutually-unsealable * outputs (the contract tests for that), but offers ZERO real * confidentiality — never use outside tests. * * Replace with a real platform provider in production. */ export declare class MemorySealingKeyProvider implements SealingKeyProvider { readonly id: string; private readonly fingerprint; private readonly keyBytes; constructor(opts: { id: string; }); seal(passphrase: Uint8Array): Promise; unseal(sealed: Uint8Array): Promise; } /** * Public material a sender uses to seal-for-this-recipient. Published by * a recipient's RecipientSealer; transported to the sender out-of-band * (email, S3, in-app message). The sender obtains the hint, supplies it * to writePod's sealedCredentials.perUser[userId].hint, and the * hub seals each user's credential against it. Per foundation §11.4. */ export type RecipientHint = { readonly v: 1; /** Recipient's provider id; matches the SealedAutoUnlockEntry.pid they'll unseal under. */ readonly pid: string; /** Algorithm the sender uses to produce the seal. Slice 1 ships RSA-OAEP-SHA256 only. */ readonly alg: 'rsa-oaep-sha256'; /** Public material — alg-specific. For 'rsa-oaep-sha256': { publicKeyPem: string }. */ readonly material: Readonly>; }; export type { RecipientSealer } from '../../kernel/types.js'; /** * Shared RSA-OAEP-SHA256 + AES-GCM seal in the canonical recipient-target * TLV wire format. Mints a fresh 32-byte CEK, AES-GCM-encrypts `plaintext` * under it, RSA-OAEP-SHA256-wraps the CEK to `publicKeyPem`, and packs: * * byte 0 : version (0x01) * bytes 1..256 : RSA-OAEP-wrapped CEK (fixed 256 bytes at RSA-2048) * bytes 257..268: AES-GCM IV (12 bytes) * bytes 269.. : AES-GCM ciphertext ‖ 16-byte tag * * This is the single source of truth for the wire format — both * {@link MemoryRecipientSealer} and external sealers (e.g. `@noy-db/at-aws-kms`'s * asymmetric-KMS recipient sealer) call it so a blob sealed by one unseals * by the other. WebCrypto RSA-OAEP/SHA-256 here is wire-compatible with * AWS KMS `RSAES_OAEP_SHA_256` (both RSAES-OAEP, SHA-256 hash, MGF1-SHA256, * empty label). * * @public — re-exported from the hub barrel for external sealer packages. */ export declare function sealRsaOaepTlv(plaintext: Uint8Array, publicKeyPem: string): Promise; /** * Parse a {@link sealRsaOaepTlv} blob into its three segments without * decrypting. The `wrapped` CEK is RSA-OAEP-SHA256 ciphertext over a * 32-byte CEK — the unwrap step is pluggable: {@link MemoryRecipientSealer} * decrypts it with a local RSA private key; `@noy-db/at-aws-kms` hands it to * KMS `Decrypt`. After unwrapping, pass the CEK + `iv` + `ct` to * {@link aesGcmOpen}. * * @public — re-exported from the hub barrel for external sealer packages. */ export declare function parseRsaOaepTlv(bytes: Uint8Array): { wrapped: Uint8Array; iv: Uint8Array; ct: Uint8Array; }; /** * AES-GCM-decrypt the `ct` segment of a {@link parseRsaOaepTlv} result under * the unwrapped 32-byte CEK and its `iv`. Throws on a bad tag (tamper) — the * same authenticated-decryption guarantee the TLV relies on. * * @public — re-exported from the hub barrel for external sealer packages. */ export declare function aesGcmOpen(cekBytes: Uint8Array, iv: Uint8Array, ct: Uint8Array): Promise; /** * Reference implementation of `RecipientSealer` + `SealingKeyProvider`. * Uses WebCrypto RSA-OAEP-SHA256 (2048-bit) to wrap a fresh 32-byte * AES-GCM CEK, AES-GCM-encrypts plaintext under it, and packs the * result into a self-describing TLV: * * byte 0 : version (0x01) * bytes 1..256 : RSA-OAEP-wrapped CEK (fixed 256 bytes at RSA-2048) * bytes 257..268: AES-GCM IV (12 bytes) * bytes 269.. : AES-GCM ciphertext ‖ 16-byte tag * * Implements BOTH interfaces. `seal(plaintext)` (self-target) is just * `sealForRecipient(plaintext, this own hint)` — same TLV. Convenient * for tests where one provider plays both ends. Real cloud providers * (`at-aws-kms`, etc.) will pick their own internal layouts; the only * contract is round-trip identity. * * SAFE for production within its scope — the cryptography is real * (RSA-OAEP + AES-GCM via WebCrypto), but the keypair lives in-process * and is regenerated on every construction. Not suitable as a managed * keychain; use it for tests and for shipping bundles where the * recipient instance lives in the same process as the sender (rare). */ export declare class MemoryRecipientSealer implements SealingKeyProvider, RecipientSealer { readonly id: string; private readonly keypair; constructor(opts: { id: string; }); publishRecipientHint(): Promise; sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise; seal(plaintext: Uint8Array): Promise; unseal(bytes: Uint8Array): Promise; } /** Reserved id for the managed-passphrase envelope under `_meta`. */ export declare const SEALED_PASSPHRASE_RECORD_ID: "sealed-passphrase"; /** Plaintext payload stored inside the `_meta/sealed-passphrase` envelope. */ export interface SealedPassphrase { readonly _noydb_sealed: 1; readonly providerId: string; /** Sealed bytes. Base64-encoded on the wire; decoded on load. */ readonly sealed: Uint8Array; } /** * Wire-format envelope persisted at `_meta/sealed-passphrase` for * managed-mode vaults. The provider produces raw sealed bytes via * {@link SealingKeyProvider.seal}; this wrapper carries the dispatch * metadata hub needs to pick the right provider on the unseal path. * * Stability boundary: once shipped, the wire format only grows by * adding optional fields. See the at-* sealing dimension foundation * doc, §11.9.1. * * v1 shape (this release): `{ v: 1, _noydb_sealed: 1, pid, payload }`. * * Legacy shape (earlier releases): `{ _noydb_sealed: 1, providerId, sealed }` * — accepted on read for backwards compatibility; never produced on * write going forward. */ export interface SealedEnvelope { /** Envelope schema version. v1 is the current shape. */ readonly v: 1; /** Magic marker for forensics + legacy-shape detection. */ readonly _noydb_sealed: 1; /** Matches the producing provider's `.id`. Dispatch key on unseal. */ readonly pid: string; /** Sealed bytes from the provider, base64-encoded on the wire. */ readonly payload: string; } /** * Parse a `_meta/sealed-passphrase` `_data` JSON string into the * in-memory {@link SealedPassphrase} representation. Accepts both: * * 1. v1 wire format `{ v: 1, _noydb_sealed: 1, pid, payload }` — * the current shape. * 2. Legacy wire format `{ _noydb_sealed: 1, providerId, sealed }` — * read-only; never written * going forward. * * Returns `undefined` for any input that doesn't match either shape, * so callers can fall back to "no managed-mode envelope present." * * @internal — exported only for the migration safety-net test suite. */ export declare function parseSealedEnvelope(raw: unknown): SealedPassphrase | undefined; export declare function saveSealedPassphrase(store: NoydbStore, vault: string, payload: { readonly providerId: string; readonly sealed: Uint8Array; }): Promise; export declare function loadSealedPassphrase(store: NoydbStore, vault: string): Promise; /** * Resolve the effective plaintext passphrase string for a managed-mode * vault. Two paths: * * 1. **First open (no envelope persisted):** generate a 256-bit random * via `crypto.getRandomValues`, base64-encode for use as a * passphrase string, seal the underlying bytes under the * provider, persist `_meta/sealed-passphrase`, return the * base64 string. * * 2. **Reopen (envelope exists):** read + unseal + decode → return. * A different provider whose `seal` output disagrees on the * stored bytes throws here, surfaced as a clear error. * * The returned string is the same shape that `secret:` would take in * standard mode — the rest of the keyring path consumes it * unchanged. * * @internal — called from `createNoydb` / `getKeyringInternal`. */ export declare function resolveManagedSecret(store: NoydbStore, vault: string, provider: SealingKeyProvider): Promise;