> ## Documentation Index
> Fetch the complete documentation index at: https://usenaive.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Vault encryption

> Envelope encryption with a managed KMS and per-tenant EncryptionContext binding.

The Credential Vault stores per-user secrets using **envelope encryption backed by a managed KMS**.

## How it works

* A per-entry **data key (DEK)** is generated by `KMS GenerateDataKey`.
* The value is encrypted locally with **AES-256-GCM** (Node `crypto`, never hand-rolled).
  Blob layout: `nonce(12) || tag(16) || ciphertext`. **No AAD.**
* KMS returns the DEK already **wrapped** by the shared customer-managed key (CMK). We
  store `ciphertext`, `wrapped_dek`, and `kek_id` (per row, so rotation knows what
  wrapped what). The plaintext DEK is zeroized immediately and never persisted.

## Per-tenant isolation via EncryptionContext

Every KMS call passes `EncryptionContext = { company_id, tenant_user_id }`. KMS verifies
it on `Decrypt`, so a `wrapped_dek` stolen from one tenant's row cannot decrypt another
tenant's data — even under the same CMK.

`key` (the vault entry name) is **deliberately not** in the context, so vault keys stay
renameable via a plain `UPDATE`. And there is **no AES-GCM AAD** — KMS context is the
sole crypto binding, avoiding a `JSON.stringify` canonicalization footgun that would
otherwise brick every row on a refactor.

## Rotation — two distinct operations

* **Key rotation** (`POST .../rotate`): `KMS ReEncrypt` re-wraps the DEK under the
  current CMK version. The value ciphertext is unchanged. Cheap.
* **Value rotation**: a normal `PUT` with a new value (fresh DEK + ciphertext).
* `?regenerate_dek=true` does a full DEK regeneration + value re-encryption.

Automatic key rotation is enabled on the CMK.

## IAM — least privilege

```hcl theme={"theme":"css-variables"}
Action   = ["kms:GenerateDataKey", "kms:Decrypt", "kms:ReEncrypt*"]
Resource = aws_kms_key.naive_vault.arn   # this one key only
Condition = {
  "ForAnyValue:StringEquals" = {
    "kms:EncryptionContextKeys" = ["company_id", "tenant_user_id"]
  }
}
```

No `kms:*`, no `Resource: "*"`. A request that omits the tenant context is rejected by
IAM before it reaches KMS.

## Configuration

| Env var                       | Purpose                                    |
| ----------------------------- | ------------------------------------------ |
| `NAIVE_VAULT_KEK_ID`          | The CMK ARN that wraps every DEK.          |
| `NAIVE_KMS_REGION`            | Region of the CMK (default `us-east-1`).   |
| `NAIVE_KMS_ACCESS_KEY_ID`     | Access key for the dedicated KMS IAM user. |
| `NAIVE_KMS_SECRET_ACCESS_KEY` | Secret for that IAM user.                  |

<Warning>
  KMS uses its **own** credentials — **not** the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
  `AWS_REGION` env vars. On the hosting platform those are **object-storage** credentials
  with region `auto`; reusing them would route KMS calls to the object store and fail every Vault
  operation. The dedicated `NAIVE_KMS_*` trio keeps the two credential sets isolated. When
  `NAIVE_KMS_ACCESS_KEY_ID`/`NAIVE_KMS_SECRET_ACCESS_KEY` are unset (e.g. local dev with a real
  cloud profile), the client falls back to the default credential chain.
</Warning>

## Secrets never travel in URLs

Reveal is a **POST** (`/vault/:key/reveal`) — the value comes back in the body, never a
query string. List masks values.
