← All posts
2 min readThe nvault team

Defense in depth: the second encryption layer

The browser already encrypts your files. So why does the server encrypt them again before writing to Postgres? Because a database dump should be worth nothing.

securityarchitecture

nvault's client-side encryption means the ciphertext that reaches the API is already unreadable without your passphrase. We could store that blob directly. We don't. Every blob is encrypted a second time on the server before it touches the database.

The layer

client ciphertext
   ──▶ AES-256-GCM( key = STORAGE_ENCRYPTION_KEY, AAD = storage key )
   ──▶ [version:1][iv:12][gcmTag:16][ciphertext]  ──▶ storage_objects.data

STORAGE_ENCRYPTION_KEY is a 32-byte key that lives only in the deployment's environment — a secrets manager, a platform environment variable — and never in the database. The blob format is versioned so the scheme can evolve without making old data unreadable.

What it defends against

  • A stolen database dump. The most common real-world exposure. On its own it now contains only doubly-encrypted blobs and metadata — the server key is not in it, and neither is your passphrase.
  • A read-only SQL injection or backup misconfiguration. Same result: no usable data.
  • Accidental replication to the wrong place. A copied database is inert without the environment key.

What it does not do

This layer is not a substitute for client-side encryption, and we are careful not to describe it as one. If an attacker has both the database and the running server's environment, this layer is defeated — but they still only have your client ciphertext, which is useless without your passphrase. The two layers protect against different attackers:

  • Client layer → protects against us, and against anyone who compromises the server.
  • Server layer → protects against anyone who gets the data but not the live environment.
Layers are valuable when each one fails independently. A database leak and an environment-variable leak are different incidents with different causes.

Key rotation

Rotating STORAGE_ENCRYPTION_KEY naively would make existing blobs unreadable, so it is treated as a permanent per-environment secret. A safe rotation path re-encrypts stored objects under the new key as a migration; the versioned blob header exists precisely so that can be done incrementally.

The short version: your data is encrypted by you, and then encrypted again by us, with keys that live in different places and leak for different reasons.

Your development environment, available anywhere.

Create a vault, push your first project, and restore it anywhere.

Related reading