# vault field encryption _(no submodules)_ ## Purpose `field-encryption.ts` implements per-item, per-field XChaCha20-Poly1305 AEAD encryption for vault items' concealed fields (passwords, PINs, secure notes). It is the **defence-in-depth** layer that lives **inside** the SQLite container: even if the drive-level AEAD is broken (which by construction it cannot be), every concealed field remains ciphertext until lazily decrypted on `vault.items.get`. The module derives a per-item key from the vault key via `@suaveplan/crypto-vault`'s `deriveItemKey(vaultKey, itemId)`, encrypts the plaintext with XChaCha20-Poly1305, binds the AEAD AAD to the `(itemId, fieldName)` pair so a swapped envelope (e.g. moving a password between items) fails authentication, and zeroizes the per-item key and plaintext buffer immediately after the operation. ## Features - `encryptField(options) → EncryptedField` — encrypt a single named field value. - `decryptField(options) → string` — decrypt a single envelope; throws `VAULT.DECRYPT_FAILED` on auth failure or malformed encoding. - `encryptConcealedFields(vaultKey, itemId, concealed, rng?) → Record` — bulk-encrypt every field in a record. - `decryptConcealedFields(vaultKey, itemId, encrypted) → Record` — bulk-decrypt every envelope. - AAD-binding to `(itemId, fieldName)` prevents envelope-swapping attacks. - Per-item key + plaintext zeroization after every operation. ## Usage: Basic Example ```ts import { encryptField, decryptField } from "@suaveplan/vault/encryption/field-encryption"; const envelope = encryptField({ vaultKey, itemId: "abc-123", fieldName: "password", value: "p@ssw0rd", }); const recovered = decryptField({ vaultKey, itemId: "abc-123", fieldName: "password", envelope, }); // recovered === "p@ssw0rd" ``` ## Usage: Advanced Example ```ts import { encryptConcealedFields, decryptConcealedFields, } from "@suaveplan/vault/encryption/field-encryption"; // Encrypt every concealed field in one call before persisting. const concealed = { password: "p@ssw0rd", pin: "1234" }; const envelopes = encryptConcealedFields(vaultKey, itemId, concealed); // Persist `envelopes` as JSON inside the SQLite row. // On read: const decrypted = decryptConcealedFields(vaultKey, itemId, envelopes); // decrypted.password === "p@ssw0rd" ``` ## API Reference - `interface EncryptedField` — `{ algorithm: "xchacha20-poly1305", nonce: base64, ciphertext: base64 }`. - `encryptField(options: EncryptFieldOptions): EncryptedField`. - `decryptField(options: DecryptFieldOptions): string`. - throws `VaultError(VAULT.DECRYPT_FAILED)` on bad algorithm / encoding / authentication. - `encryptConcealedFields(vaultKey, itemId, concealed, rng?): Record`. - `decryptConcealedFields(vaultKey, itemId, encrypted): Record`. ## Implementation Notes - The AEAD AAD is `"vault/v1/item//field/"`. The `v1` literal prefix lets future versions bump without colliding with old envelopes; consumers that try to decrypt a v1 envelope under a hypothetical v2 AAD will get `VAULT.DECRYPT_FAILED`. - The 24-byte XChaCha20-Poly1305 nonce is sampled from the injected `rng` (or `crypto.getRandomValues` by default) per-encryption. The 192-bit nonce space lets nonce reuse remain birthday-bound at ~2⁸⁰ messages — well beyond any vault's lifetime. - Per-item keys are derived deterministically from `(vaultKey, itemId)`. They are zeroized via `zeroize(itemKey)` in a `finally` block after every encrypt or decrypt — this minimises the wall-clock window during which a key is reachable in heap memory. - The plaintext buffer (`TEXT_ENCODER.encode(value)`) is also zeroized after encryption. Strings, by contrast, are immutable in JS and cannot be cleared from heap; callers that want full lifecycle hygiene must source plaintexts from `Uint8Array` and overwrite their own buffers. - A failure of `xchacha20poly1305.decrypt` is mapped to `VAULT.DECRYPT_FAILED` with an `{ reason: "auth" }` context tag. Encoding failures (bad base64) are mapped to the same code with `{ reason: "encoding" }` so callers can branch on the failure mode without exposing implementation details.