← All posts
2 min readThe nvault team

Zero-knowledge encryption, explained for developers

What "the server can't read your data" actually means: key derivation, key wrapping, AEAD, and the specific hierarchy nvault uses to make it true.

securitycryptography

"Zero-knowledge" is an overloaded phrase. In nvault's case it has a precise, checkable meaning: the server never holds the information needed to decrypt your files. Here is how that is arranged.

Passwords are not keys

A passphrase is low-entropy and human-chosen; an encryption key needs to be high-entropy and uniform. You bridge the gap with a key derivation function. nvault uses PBKDF2-HMAC-SHA256 with a large iteration count and a random 128-bit salt per user. The iteration count is stored per user, so it can be raised over time without breaking existing vaults — re-derivation just happens on the next unlock.

vault passphrase
   │  PBKDF2-HMAC-SHA256 (many iterations, random salt)
   ▼
Key Encryption Key (KEK)   — derived fresh on every unlock, never stored

Key wrapping: a chain, not a single key

If the passphrase directly encrypted every file, changing it would mean re-encrypting everything. Instead the KEK only ever encrypts ("wraps") the next key down:

KEK  ──wrap──▶  Master Key      (one per account, random 256-bit)
Master Key  ──wrap──▶  Project Key  (one per project, random 256-bit)
Project Key  ──encrypt──▶  file version ciphertext

The server stores the master key and project keys only in wrapped form (wrappedMasterKey, wrappedProjectKey). Without the KEK — which requires the passphrase — they are meaningless bytes. Changing your passphrase re-wraps one key, not your whole history.

AEAD and bound identifiers

Every encryption step uses AES-256-GCM, an authenticated cipher: decryption fails loudly if the ciphertext was tampered with. nvault also passes an identifier as *additional authenticated data* at each step — "master-key", "project:<id>", "<contentId>". The identifier is not secret, but it is covered by the authentication tag, so a blob encrypted for one record cannot be silently swapped onto another; the tag check fails.

What this buys you

  • A database dump reveals metadata and doubly-encrypted blobs — no secrets. (There is a second server-side layer on top of this.)
  • A malicious or compelled operator cannot produce plaintext they never had the keys for.
  • A bug that logs a request body logs ciphertext, not secrets.

The honest cost

There is no "forgot passphrase" link, because there is nothing on our side to reset. If the passphrase is lost, the data wrapped under it is unrecoverable. Use a password manager, and keep offline backups of anything you truly cannot lose. This is the deliberate trade for data this sensitive — see Our threat model.

Your development environment, available anywhere.

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

Related reading

  • Why we encrypt on the client, not the server

    Server-side encryption is easier to build and easier to use. We still chose client-side. Here is the threat model that made the decision for us.

  • How nvault works, end to end

    Follow a single .env file from your editor to storage and back: what the browser does, what the API sees, and why the server can hold your secrets without being able to read them.

  • 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.