Encryption model
seekrit uses envelope encryption. There are three layers of keys, and the plaintext of your secrets — along with private keys and passphrases — never reaches the server.
The key hierarchy
environment DEK (AES-256) one per environment
├── encrypts every secret in that environment
└── wrapped separately for each principal:
├── user A (ECDH to A's public key)
├── user B (ECDH to B's public key)
└── token T (ECDH to T's public key)
Data encryption key (DEK)
Every environment has its own random 256-bit data encryption key. Secrets in that
environment are encrypted with it using AES-256-GCM. The ciphertext is bound to its location
with additional authenticated data (AAD) of environmentId/SECRET_NAME, so a blob cannot be
silently moved to a different secret or environment.
Principal keypairs
Every principal — a user or a service token — has a P-256 (ECDH) keypair. The public key is stored by the server; the private key is not (for tokens it lives inside the token string; for users it is passphrase-encrypted, see below).
Key wrapping
To give a principal access to an environment, its DEK is wrapped to that principal's public key. Wrapping uses an ephemeral ECDH exchange plus HKDF-SHA256 to derive a one-time AES-256-GCM key — an ECIES-style construction. Only the holder of the matching private key can unwrap the DEK. Each grant is an independent wrapped copy of the same DEK.
Possession of a wrapped-DEK grant and the matching private key is exactly what "having access" means. There is no server-side switch that grants plaintext — access is cryptographic.
Protecting user private keys
A user needs their private key on every device they sign in from, but the server must never see it. So the private key is encrypted client-side with a key derived from the user's passphrase using PBKDF2-HMAC-SHA256 (600,000 iterations), and the encrypted blob is stored server-side.
- On any device, the client fetches the encrypted blob and decrypts it locally with the passphrase.
- The passphrase and the plaintext private key never leave the client.
- A wrong passphrase surfaces as an authentication failure — the blob simply won't decrypt.
There is intentionally no passphrase reset: losing it means the encrypted private key is unrecoverable. That is the cost of the server never holding it.
Service tokens
Service tokens are self-contained principals for machines. The token string itself carries the private key. The server stores only:
- a SHA-256 hash of the full token (to authenticate requests), and
- the token's public key (to wrap DEK grants to it).
So a machine holding the token can unwrap any environment DEK granted to it, entirely offline, without the server ever having its private key. See Service tokens.
What a secret write looks like
- The client fetches its wrapped DEK for the environment and unwraps it with its private key.
- It encrypts the new value with the DEK (AES-256-GCM, AAD =
envId/NAME). - It uploads only the resulting ciphertext blob.
The reverse — fetch ciphertext, unwrap DEK, decrypt — happens on read. The API is never a party to any of the cryptography.
Algorithms at a glance
| Purpose | Algorithm |
|---|---|
| Secret encryption | AES-256-GCM (per-secret AAD) |
| DEK wrapping | Ephemeral ECDH P-256 + HKDF-SHA256 → AES-256-GCM |
| Passphrase key derivation | PBKDF2-HMAC-SHA256, 600k iterations |
| Service token hashing | SHA-256 |
All of it runs on the Web Crypto API, so the exact same code runs in the browser, the Workers
runtime, and Node for the CLI. Every ciphertext blob is versioned (e.g. sc1., wd1., pk1.)
so algorithms can be migrated without breaking existing data.