“Zero-knowledge” has gone soft
Stuck onto everything and anything, the term zero-knowledge has lost its edge. Plenty of products advertise it while still holding a server-derivable key, or while encrypting only at rest with a key the vendor can recover at will.
At Servor we took the strict reading: we must not be able to read your credentials, even if we wanted to. Not by policy. By maths. Here is how we did it, and why each brick is the brick it is.
The cast: KEK, DEK, and the browser
Three keys meet in Servor's cryptographic pipeline:
- KEK (key encryption key): derived from your passphrase with Argon2id. It never leaves your browser.
- User private key: an X25519 pair generated when your account is initialised. The private half is encrypted under the KEK before it is ever transmitted.
- DEK (data encryption key): one AES-256 key per team, and the key that actually encrypts credentials. It is wrapped for each member over X25519.
The payoff of that indirection: adding someone to a team does not mean sharing your passphrase. You wrap the DEK to the new member's public key, and you are done.
Argon2id: why those parameters
Argon2id is the de facto standard for deriving a key from a human secret. The devil is in the parameters. Servor's:
- memory = 64 MB: enough to make a massively parallel GPU attack expensive, not so much that unlocking becomes painful on a modest machine.
- iterations = 3: the trade-off between attacker cost and the latency you feel at unlock time.
- parallelism = 4: uses multi-threading without needlessly fragmenting the memory footprint.
These sit above the common public recommendations for interactive use. They are a dial, not a guarantee: as hardware gets cheaper they will have to go up, and this is the kind of parameter you revisit periodically rather than carve in stone.
X25519: wrapping the DEK
Once the KEK is derived, it decrypts the X25519 private key that the server stores in encrypted form. That private key then unwraps the team's DEK.
Why X25519 rather than RSA or P-256? Three reasons:
- speed: an X25519 wrap/unwrap is an order of magnitude faster than the RSA-2048 equivalent, which matters when the work happens inside a browser tab;
- simplicity: no tortured DER encoding, no curve parameters to validate, so a far smaller surface for implementation mistakes;
- side-channel resistance: Curve25519 is designed so that a correct implementation is naturally constant-time.
The envelope we derive is an HKDF(X25519(ephemeral private key, recipient public key)) producing the AES key that wraps the DEK. A fresh ephemeral key per operation: no nonce is ever reused.
AES-256-GCM: the application-level encryption
The DEK is what actually encrypts credentials, with AES-256-GCM. Every entry carries its own random 12-byte IV and 16-byte authentication tag. The AAD always includes the logical identifier of the entity— the server ID, for instance. The consequence: you cannot lift one server's ciphertext and have it decrypt in another server's context.
Every buffer holding a secret or a key is wiped after use (Uint8Array.fill(0)), even though the garbage collector would get around to it eventually. Belt and suspenders.
Ed25519: signing execution, not just encrypting storage
Encrypting credentials is not enough on its own. If the control plane could manufacture a command, an attacker who owned it would not need to read anything — running commands would be plenty.
So the vault key also derives an Ed25519 pair. Every command is signed in the browser, the API relays it untouched, and the agent verifies the signature on the target machine before executing. A per-server option makes that signature mandatory. The operational corollary: the server can never wrap a signed command in a sudo or a cd — the signature would no longer cover what actually runs.
BIP39: the recovery phrase
What happens if you forget your passphrase? At sign-up you were handed a 24-word BIP39 recovery phrase, to print out or stash in 1Password, Bitwarden or a physical safe. Those 24 words derive a deterministic seed from which your private key can be rebuilt, and with it the DEKs wrapped in your name.
Conversely, losing both — passphrase and recovery phrase — means losing access to the encrypted data, permanently. By design.That is the accepted price of real zero-knowledge: no back door, no support agent who can “take a look inside your vault”, no small secret quietly kept server-side.
What Servor sees, and what it does not
Concretely, if our database leaked:
- what comes out: ciphertext, with its IV and tag, for every secret. Without the user's KEK it is noise, indistinguishable from random.
- what also comes out: metadata — team names, hostnames, email addresses. That is not nothing, and it deserves to be said.
- what does not come out: no password, no SSH private key, no token the user stored.
Put another way, and without the theatrics: a fully compromised control plane can neither read your credentials nor forge an execution. That is not a promise of invulnerability — it is a reduction in what a compromise is worth.
The audit ledger is security too
Something worth repeating: encryption is only part of the defence. Servor's hash-chained audit log records every vault access and every execution. An attacker who got past authentication would find their actions immediately traceable — and the append-only PostgreSQL trigger stops them from tidying up after themselves. Two-factor authentication is mandatory, with re-verification required on sensitive operations.
Related reading: Servor's Plan-Execute-Verify discipline, which covers how this ledger fits into the rest of the operational runtime.
For the sceptics
Any change touching cryptography or the execution path is reviewed by at least two people, and nothing that looks like a secret, a key, an IV or an encrypted payload is allowed anywhere near the logs. If you want to challenge the choices, run an audit, or simply understand an implementation detail, write to contact@benode.fr. We always answer.