/** * Cryptographic primitives — thin wrappers around the Web Crypto API. * * ## Design principle * * **Zero npm crypto dependencies.** Every operation uses `globalThis.crypto.subtle`, * which is available natively in Node.js ≥ 18, all modern browsers, and * Deno/Bun. This avoids supply-chain risk from third-party crypto packages and * ensures the library stays auditable. * * ## Algorithms * * | Use case | Algorithm | Parameters | * |----------|-----------|------------| * | Key derivation | PBKDF2-SHA256 | 600,000 iterations, 32-byte salt | * | Record encryption | AES-256-GCM | 12-byte random IV per operation | * | DEK wrapping | AES-KW (RFC 3394) | 256-bit KEK | * | Binary encrypt | AES-256-GCM | same as record encryption | * | Integrity | HMAC-SHA256 | for presence channels | * | Content hash | SHA-256 | for ledger and bundle integrity | * * ## Key lifecycle * * ``` * passphrase + salt * └─► deriveKey() → KEK (CryptoKey, extractable: false) * └─► wrapKey() → wrapped DEK bytes [stored in keyring] * └─► unwrapKey() → DEK (CryptoKey) [memory only during session] * └─► encrypt() / decrypt() → ciphertext / plaintext * ``` * * IVs are generated fresh by {@link generateIV} on every encrypt call. * Reusing an IV with the same key would break GCM's authentication guarantee — * this function should be the only place IVs are produced. * * @module */ /** * **EnclaveKey** — the opaque key type at the enclave seam. * * In noy-db's reference enclave this is `CryptoKey` (the Web Crypto API's * key handle). A fork's enclave redefines this alias to its own key * representation (e.g. `type EnclaveKey = null` for a keyless HSM-backed * fork) — outside `kernel/enclave/**`, every consumer must treat it as * opaque: never construct, inspect, or serialize it directly, only pass it * between barrel functions (`encrypt`/`decrypt`/`wrapKey`/`unwrapKey`/…). */ export type EnclaveKey = CryptoKey; /** Derive a KEK from a passphrase and salt using PBKDF2-SHA256. */ export declare function deriveKey(passphrase: string, salt: Uint8Array): Promise; /** * Which AES algorithm/usage a {@link derivePassphraseKey} result is for: * `'aes-kw'` mints a key-wrapping key (`wrapKey`/`unwrapKey`, e.g. for * KEK derivation); `'aes-gcm'` mints a direct encrypt/decrypt key (e.g. * for the wrap-DEKs primitive in `with-party/team/wrapped-deks.ts`). */ export type PassphraseKeyUsage = 'aes-kw' | 'aes-gcm'; /** * Derive a non-extractable AES key from a passphrase/credential and salt * via PBKDF2-SHA256, generalizing {@link deriveKey}'s AES-KW derivation to * also cover AES-GCM-usage keys. Callers pin their own `iterations` and * `keyUsage` so an existing call site's exact parameters (and thus its * derived-key bytes) are preserved verbatim when migrated onto this * shared primitive — this is call-site consolidation, not a KDF change. */ export declare function derivePassphraseKey(passphrase: string, salt: Uint8Array, params: { iterations: number; keyUsage: PassphraseKeyUsage; }): Promise; /** Generate a random AES-256-GCM data encryption key. */ export declare function generateDEK(): Promise; /** Wrap (encrypt) a DEK with a KEK using AES-KW. Returns base64 string. */ export declare function wrapKey(dek: CryptoKey, kek: CryptoKey): Promise; /** Unwrap (decrypt) a DEK from base64 string using a KEK. */ export declare function unwrapKey(wrappedBase64: string, kek: CryptoKey): Promise; /** * AES-KW-wrap a per-record CEK under a collection/tier DEK. Returns base64. * Deterministic over `(cek, dek)`. */ export declare function wrapCek(cek: CryptoKey, dek: CryptoKey): Promise; /** * Unwrap a per-record CEK from base64 under a collection/tier DEK. Throws * `InvalidKeyError` if the wrapped bytes do not authenticate under the DEK. */ export declare function unwrapCek(wrappedBase64: string, dek: CryptoKey): Promise; /** * Import a raw 32-byte AES-256-GCM record CEK (e.g. from a sealed-CEK * delivery binding). Non-extractable, decrypt-only — the imported key is * used solely to open the record's `_iv`/`_data` body under `decrypt()`. */ export declare function importCek(rawKey: Uint8Array): Promise; export interface EncryptResult { iv: string; data: string; } /** Encrypt plaintext JSON string with AES-256-GCM. Fresh IV per call. */ export declare function encrypt(plaintext: string, dek: CryptoKey): Promise; /** Decrypt AES-256-GCM ciphertext. Throws on wrong key or tampered data. */ export declare function decrypt(ivBase64: string, dataBase64: string, dek: CryptoKey): Promise; /** * Encrypt raw bytes with AES-256-GCM using a fresh random IV. * Used by the attachment store so binary blobs avoid double base64 encoding * (the existing `encrypt()` function calls `TextEncoder` on a string — here * we pass the `Uint8Array` directly to `subtle.encrypt`). */ export declare function encryptBytes(data: Uint8Array, dek: CryptoKey): Promise; /** * Decrypt AES-256-GCM ciphertext back to raw bytes. * Counterpart to `encryptBytes`. Throws `TamperedError` on auth-tag failure. */ export declare function decryptBytes(ivBase64: string, dataBase64: string, dek: CryptoKey): Promise; /** * SHA-256 hex digest of raw bytes. Used to derive content-addressed * eTags for blob deduplication. Computed on plaintext bytes * before compression and encryption so the eTag identifies content, not * ciphertext, and survives re-encryption (key rotation, re-upload). */ export declare function sha256Hex(data: Uint8Array): Promise; /** * Compute HMAC-SHA-256(key, data) and return hex string. * * Used to derive content-addressed eTags that are opaque to the store: * ``` * eTag = hmacSha256Hex(blobDEK, plaintext) * ``` * * Unlike a plain SHA-256, the HMAC is keyed by the vault-shared `_blob` DEK, * so an attacker with store access cannot pre-compute eTags for known files. * Deduplication still works within a vault (same key + same content = same eTag). */ export declare function hmacSha256Hex(key: CryptoKey, data: Uint8Array): Promise; /** * Encrypt raw bytes with AES-256-GCM using Additional Authenticated Data. * * The AAD binds each chunk to its parent blob and position, preventing * chunk reorder, substitution, and truncation attacks: * ``` * AAD = UTF-8("{eTag}:{chunkIndex}:{chunkCount}") * ``` * * The AAD is NOT stored — the reader reconstructs it from `BlobObject` * metadata and passes it to `decryptBytesWithAAD`. */ export declare function encryptBytesWithAAD(data: Uint8Array, dek: CryptoKey, aad: Uint8Array): Promise; /** * Decrypt AES-256-GCM ciphertext with AAD verification. * * If the AAD does not match the one used at encryption time (e.g. because * a chunk was reordered or substituted from another blob), the GCM auth * tag fails and this throws `TamperedError`. */ export declare function decryptBytesWithAAD(ivBase64: string, dataBase64: string, dek: CryptoKey, aad: Uint8Array): Promise; /** * Derive an AES-256-GCM presence key from a collection DEK using HKDF-SHA256. * * The presence key is domain-separated from the data DEK by the fixed salt * `'noydb-presence'` and the `info` = collection name. This means: * - The adapter never sees the presence key. * - Presence payloads rotate automatically when the collection DEK is rotated. * - Revoked users cannot derive the new presence key after a DEK rotation. * * @param dek The collection's AES-256-GCM DEK (extractable). * @param collectionName Used as the HKDF `info` parameter for domain separation. * @returns A non-extractable AES-256-GCM key suitable for presence payload encryption. */ export declare function derivePresenceKey(dek: CryptoKey, collectionName: string): Promise; /** * Derive a **per-field** AES-256-GCM key from a collection DEK using * HKDF-SHA256, used to encrypt a single sensitive field into its own * `_sealed[field]` envelope slot (structural group-encryption). * * Domain-separated from the data DEK (and from the presence/deterministic * channels) by the fixed salt `'noydb-sealed'` and an `info` of * `JSON.stringify(['noydb-sealed', collectionName, field])`. The JSON-array * encoding is injective — each element is separately quoted/escaped — so * collection `a/sealed/b` + field `c` cannot collide with collection `a` + * field `b/sealed/c`. Each sensitive field gets a **distinct** key, so a * `_sealed[a]` ciphertext does not authenticate under field `b`'s key — * sealed fields are cryptographically isolated from one another. The * collection name keeps the same field name in two collections from sharing a * key, mirroring how `derivePresenceKey` / `encryptDeterministic` domain-separate. * * @param dek The collection's AES-256-GCM DEK (extractable). * @param collectionName Part of the HKDF `info` for domain separation. * @param field Sensitive field name — the rest of the `info`. * @returns A non-extractable AES-256-GCM key for that field's sealed slot. */ export declare function deriveSealedFieldKey(dek: CryptoKey, collectionName: string, field: string): Promise; /** * Derive a per-record sealed-field key from the record's **per-record CEK** * rather than the collection DEK. Same HKDF construction as * {@link deriveSealedFieldKey} but with salt `'noydb-sealed-cek'` (distinct * from `'noydb-sealed'`), so a CEK-derived key can never collide with a * DEK-derived one for the same `{collection, field}`. * * The point: the sealed ciphertext's only key now flows from the record's CEK. * Dropping the record's wrapped `_cek` (crypto-shred / `forget`) makes the key * — and thus `_sealed[field]` — unrecoverable, giving sealed fields the same * erasure guarantee `_data` already has. The CEK must be extractable (it is — * `unwrapCek`/`generateDEK` mint extractable keys). * * @param cek The record's AES-256-GCM CEK (extractable). * @param collectionName Part of the HKDF `info` for domain separation. * @param field Sensitive field name — the rest of the `info`. * @returns A non-extractable AES-256-GCM key for that field's sealed slot. */ export declare function deriveSealedFieldKeyFromCek(cek: CryptoKey, collectionName: string, field: string): Promise; /** * Derive the collection's **dedicated deterministic-index key** from its DEK * via HKDF-SHA256 (L-1, #554). * * `_det` slots previously encrypted under the raw collection DEK — the same * key `_data` uses with a *randomized*-IV regime while `_det` uses a * *deterministic*-IV regime. Two IV regimes on one key is avoidable hygiene * debt (~2^-96 theoretical collision), so the deterministic index now gets its * own HKDF-separated key: salt `'noydb-det'`, info * `JSON.stringify(['noydb-det'])` (the injective JSON-array domain tag, same * convention as {@link deriveSealedFieldKey}). Per-field separation still * comes from the `'/'` context inside * {@link encryptDeterministic}, exactly as before. * * The derived key stays **DEK-derived (collection-level)** on purpose: `_det` * is carried verbatim through per-record CEK rotation (see * `record-keys/sealing.ts`), so its key must not depend on the CEK. It rotates * with the collection DEK, like the presence key. * * The key is minted **extractable** — {@link encryptDeterministic} derives its * per-value IV via HKDF over the raw key bytes (an `exportKey` under the * hood), which a non-extractable key would forbid. Same in-memory posture as * the DEK itself (`generateDEK` mints extractable keys). * * @param dek The collection's AES-256-GCM DEK (extractable). * @returns A dedicated AES-256-GCM key for the `_det` deterministic index. */ export declare function deriveDeterministicKey(dek: CryptoKey): Promise; /** * Encrypt a plaintext string with AES-256-GCM and a deterministic, * HKDF-derived IV. * * The same `{ dek, context, plaintext }` triple always produces the * same `{ iv, data }` — call this twice and you can string-compare the * ciphertexts to check equality of the inputs without decrypting them. * * @param context Domain-separation string — by convention * `'/'`. Different contexts encrypt * the same plaintext to different ciphertexts, so * `email` in collection `users` does not collide with * `email` in collection `customers`. */ export declare function encryptDeterministic(plaintext: string, dek: CryptoKey, context: string): Promise; /** * Counterpart to {@link encryptDeterministic}. The IV is stored * alongside the ciphertext (exactly like the randomized path), so * decrypt uses the stored IV and verifies the GCM auth tag — a tampered * ciphertext throws `TamperedError` just like randomized AES-GCM. */ export declare function decryptDeterministic(ivBase64: string, dataBase64: string, dek: CryptoKey): Promise; /** Generate a random 12-byte IV for AES-GCM. */ export declare function generateIV(): Uint8Array; /** Generate a random 32-byte salt for PBKDF2. */ export declare function generateSalt(): Uint8Array; export declare function bufferToBase64(buffer: ArrayBuffer | Uint8Array): string; export declare function base64ToBuffer(base64: string): Uint8Array;