seekrit

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.

note

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

  1. The client fetches its wrapped DEK for the environment and unwraps it with its private key.
  2. It encrypts the new value with the DEK (AES-256-GCM, AAD = envId/NAME).
  3. 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

PurposeAlgorithm
Secret encryptionAES-256-GCM (per-secret AAD)
DEK wrappingEphemeral ECDH P-256 + HKDF-SHA256 → AES-256-GCM
Passphrase key derivationPBKDF2-HMAC-SHA256, 600k iterations
Service token hashingSHA-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.