How Meritonis Encryption Works
A complete walkthrough of the multi-layer encryption system — from your passphrase to the last ciphertext byte stored in the database. Designed to be understandable by everyone, with enough depth for security engineers.
Architecture Overview
Meritonis uses a four-layer key hierarchy. Each layer protects the one below it. Losing your passphrase means losing access — by design there is no recovery path, no key escrow, and no server-side copy of your keys.
Step 1 — Passphrase Key Derivation (PBKDF2)
When you enter your passphrase, the browser runs PBKDF2 (Password-Based Key Derivation Function 2) to produce a 256-bit AES key. This key is called the wrapping key because it wraps (encrypts) your private key.
// Pseudocode const salt = SHA-256(userId).slice(0, 16) // 16 bytes const rawKey = PBKDF2(passphrase, salt, 200_000, 32, "SHA-256") const wrapKey = importKey(rawKey, "AES-GCM")
Why is the salt user-specific?
The salt is derived deterministically from your user ID: SHA-256(userId).slice(0, 16). This means:
- Two users with the same passphrase produce different wrapping keys.
- You can re-derive your wrapping key any time from passphrase + userId — no stored state needed.
- Pre-built rainbow tables cannot be reused across users.
Step 2 — ECDH P-256 Keypair
Each user gets an ECDH P-256 keypair generated entirely in the browser on first login. ECDH (Elliptic Curve Diffie-Hellman) enables two parties to agree on a shared secret using only each other's public keys — no secret is ever transmitted.
// Private key storage format (base64 encoded) // [12-byte IV] [encrypted PKCS8 private key] const encryptedPrivKey = AES-GCM.encrypt(wrappingKey, PKCS8(privateKey), randomIV) stored = base64(IV || encryptedPrivKey)
Step 3 — Organisation Key Distribution (ECDH Key Exchange)
Organisations share data (accounts, proxies, IMAP credentials) through a single random 32-byte organisation key. Rather than transmitting this key in the clear, it is wrapped individually for each member via an ephemeral ECDH key exchange.
// Recipient decrypts using their private key + the stored ephemeral public key const sharedSecret = ECDH(privateKey, ephPubKey) const orgKey = AES-GCM.decrypt(sharedSecret, IV, ciphertext)
Adding a new team member
When a new user joins an organisation, an existing member (who already has the org key) re-wraps it for the newcomer using the newcomer's public key and a fresh ephemeral keypair. The org key itself never changes; only a new wrapped copy is added to the database.
Step 4 — Field Encryption (AES-GCM-256)
With the organisation key recovered, the browser can encrypt and decrypt individual data fields. Every sensitive value — password, email credential, proxy URL, IMAP password — is encrypted independently with AES-GCM-256 using a fresh random IV.
// Encrypt a plaintext field
const iv = crypto.getRandomValues(new Uint8Array(12))
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, orgKey, plaintext)
const stored = base64(iv || new Uint8Array(ciphertext)) // IV prepended
// Decrypt
const iv_ = stored.slice(0, 12)
const plaintext_ = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv_ }, orgKey, stored.slice(12))Security Properties
The combined design provides the following provable security properties:
Algorithm Reference
All algorithms are standard primitives available in the W3C WebCrypto API. No custom cryptography is used.
| Algorithm | Parameters | Usage |
|---|---|---|
| PBKDF2 | SHA-256, 200,000 iterations, 32-byte output | Derive wrapping key from passphrase |
| SHA-256 | — | Derive deterministic user salt from userId |
| AES-GCM | 256-bit key, 12-byte random IV, 128-bit auth tag | Wrap private key; encrypt org key; encrypt fields |
| ECDH | P-256 (secp256r1), extractable | Key agreement for org-key distribution |
| getRandomValues | WebCrypto CSPRNG | IV generation, org key generation, API key tokens |
Ready to get started?
All encryption happens automatically in your browser. Sign in to start managing your accounts and credentials with end-to-end encryption.
Sign In with Discord