Skip to content
Shortspan

Search documentation

Find a page by title, section, or summary.

Encryption

Envelope encryption, the per-organization master key, the root key that wraps it, and how rotation works.

Envelope encryption

Three layers. Each exists so that compromising one thing does not compromise everything.

text
SHORTSPAN_MASTER_KEY  (root key - environment, never the database)
        |  wraps
        v
  master key (MEK)    one per workspace, stored wrapped in encryption_keys
        |  wraps
        v
  data key (DEK)      one per secret, stored wrapped beside the ciphertext
        |  encrypts
        v
  the secret value
Algorithm

libsodium's crypto_secretbox - XSalsa20 for confidentiality, Poly1305 for authentication. Authenticated encryption throughout, so tampered ciphertext fails to decrypt rather than decrypting to garbage.

Key size

32 bytes at every layer. Nonces are 24 bytes, randomly generated per operation and stored alongside the ciphertext.

Key material in memory

Zeroed with sodium_memzero immediately after use, so a core dump taken a moment later does not contain it.

Why three layers

Each layer answers a specific question.

A key per secret

Nothing about one secret's ciphertext tells you anything about another's, and a single leaked data key is one secret rather than all of them.

A key per workspace

Tenants share no key material. A flaw exposing one workspace's master key exposes nothing about any other, and deleting a workspace means destroying one key rather than sweeping a table.

One root key

Rotating it rewrites one row per workspace, not one row per secret - so rotation stays cheap no matter how much ciphertext exists.

The root key

Configured as SHORTSPAN_MASTER_KEY, and deliberately not APP_KEY.

APP_KEY signs cookies and sessions. It is handled by the web tier, it is leak-prone, and it is expected to be rotatable. A key that roots the secret store can be none of those things. Keeping them separate means a leaked session key is not a total compromise, and APP_KEY can be rotated normally without destroying customer data.

bash
php artisan encryption:generate-key

In production an unset root key is a hard failure rather than a silent downgrade. Outside production it falls back to APP_KEY so development and CI need no extra setup.

The rewrap migration

Master keys were originally wrapped by APP_KEY. Rows record which key wrapped them - local for the legacy scheme, local-v2 for the dedicated root key - so both resolve correctly and there was no flag day.

bash
php artisan encryption:rewrap

Rewrites one row per workspace. It never touches secrets, is idempotent and resumable, and reads each row back after writing it - a rewrap that cannot be undone by reading it back is worse than no rewrap at all.

Rotation

Rotating a workspace master key

A new master key is minted and secrets are re-encrypted under it as a background job. Versions are tracked, so which key protects which secret stays answerable, and rotation runs without downtime.

Rotating the root key

Set the new key, run encryption:rewrap, and keep the old key available until it completes. One row per workspace is rewritten; no ciphertext moves.

Rotating a stored credential

That is a product operation, not a cryptographic one - see Secrets and groups. Set the new value, let it propagate, then revoke the old credential at its source.

Verifying it works

The operations health check actually unwraps a key rather than counting rows. A deployment carrying the wrong root key otherwise looks entirely healthy right up until a customer tries to read a secret - which is the worst possible moment to find out.

It also reports how many keys remain on the legacy wrapping, so an unfinished migration is visible rather than silently blocking APP_KEY rotation forever.

KMS and HSM

The code has the seam: an unrecognised wrapping identifier raises rather than guessing, so a KMS-backed root can be added without a migration of existing rows. The implementation is deferred until there is a compliance requirement for it.

Until then, the root key is an environment variable, and the security of your secret store is the security of your secret manager.

Something here wrong or missing? Tell us.