# Encryption

How Shelve protects secrets at rest and in transit.

Shelve treats every secret value as encrypted data until the very moment you need it. This page describes the encryption scheme, the key hierarchy, and what actually hits the database.

## [Two-tier envelope encryption](/content/docs/core-features/encryption#two-tier-envelope-encryption/index.html)

Secret values are never encrypted directly with the global server key. Instead Shelve uses an envelope scheme:

```text
value ──seal──▶ ciphertext     (key = DEK, per-project)
DEK   ──seal──▶ encryptedDek   (key = KEK, platform-wide)
```

- **KEK** — _Key Encryption Key_. A single secret sourced from `NUXT_PRIVATE_ENCRYPTION_KEY` at boot. It never touches stored data directly; it is only used to seal and unseal DEKs.
- **DEK** — _Data Encryption Key_. Generated server-side on a project's first write (32 random bytes, base64-encoded), sealed with the KEK, and persisted in `projects.encryptedDek`. From then on every variable on that project is encrypted with that DEK.

The sealing primitive underneath is [`iron-webcrypto`](https://github.com/brc-dd/iron-webcrypto), configured with authenticated encryption (`aes-256-gcm`). A tampered ciphertext fails to decrypt — there is no silent downgrade.

## [Why envelope encryption?](/content/docs/core-features/encryption#why-envelope-encryption/index.html)

- Fast key rotation
- Scoped blast radius
- BYOK-ready

## [Backward compatibility](/content/docs/core-features/encryption#backward-compatibility/index.html)

Projects created before the envelope upgrade have no `encryptedDek` column value. Their variables keep decrypting directly with the KEK and the first new write provisions a DEK automatically. Reads tolerate mixed-state data: the service tries the project DEK first, then falls back to the KEK so no variable is left unreadable during the transition.

## [What is stored in the database](/content/docs/core-features/encryption#what-is-stored-in-the-database/index.html)

| Column                       | What it holds                                                      |
|------------------------------|------------------------------------------------------------------|
| `variables.encryptedValue`   | Sealed ciphertext of the secret value. Never plaintext.          |
| `projects.encryptedDek`      | Project DEK, sealed by the KEK. `null` for pre-envelope projects.|
| `tokens.hash`                | `sha256(token)` as hex. The plaintext token is never stored.     |
| `tokens.prefix`              | Non-secret 12-char prefix used for display and audit logs.      |

## [API tokens](/content/docs/core-features/encryption#api-tokens/index.html)

API tokens follow a different (but compatible) model: they are hashed, not encrypted. See [API Tokens](/content/docs/core-features/tokens/index.html) for the full story.

## [In transit](/content/docs/core-features/encryption#in-transit/index.html)

Traffic to `app.shelve.cloud` uses TLS 1.3 end-to-end. The CLI pins the same endpoint and refuses downgrade. For self-hosted instances, configure HTTPS at your reverse proxy or platform.

## [Self-host checklist](/content/docs/core-features/encryption#self-host-checklist/index.html)

1. Generate a strong KEK: `openssl rand -base64 48`.
2. Set it as `NUXT_PRIVATE_ENCRYPTION_KEY` on your platform.
3. **Never** rotate the KEK in-place without re-sealing existing DEKs — existing data becomes unreadable. A safe rotation procedure is on the [roadmap](https://github.com/HugoRCD/shelve/issues).
4. Back up `projects.encryptedDek` alongside `variables.encryptedValue`; losing either makes the corresponding data unrecoverable.
