first commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# vault field encryption
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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<string, EncryptedField>` — bulk-encrypt every field in a record.
|
||||
- `decryptConcealedFields(vaultKey, itemId, encrypted) → Record<string, string>` — 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<string, EncryptedField>`.
|
||||
- `decryptConcealedFields(vaultKey, itemId, encrypted): Record<string, string>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The AEAD AAD is `"vault/v1/item/<itemId>/field/<fieldName>"`. 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.
|
||||
Reference in New Issue
Block a user