Security model

A technical explanation of how SealedKeys protects your secrets. We believe in transparency over security-by-obscurity.

Core principle: zero-knowledge

SealedKeys's servers never receive, store, or can compute your plaintext secrets. Every secret is encrypted on your device before being sent. Even if our database were stolen in its entirety, the attacker would have only ciphertext — mathematically useless without your master password.

Encryption flow

1

Key derivation (client-only)

Your master password is combined with your email address and passed through PBKDF2-SHA256 with 600,000 iterations. This produces your vault key — a 256-bit AES key. This computation happens entirely in your browser using the Web Crypto API.

vaultKey = PBKDF2(masterPassword, email + "sealedkeys_v1", 600000, SHA-256, AES-256-GCM)
2

Encryption (client-only)

Each secret's sensitive fields are serialised to JSON and encrypted using AES-256-GCM with a random 12-byte IV (nonce). The output is iv + ciphertext, base64url-encoded.

encryptedData = base64url(iv[12] || AES-GCM(vaultKey, JSON(secretFields)))
3

What the server receives

Only the encrypted blob, the item name (for search), the URL (for favicon matching), and metadata tags. The server can never reconstruct the secret — it lacks the vault key.

Server stores: { name, url, tags, encryptedData } — no plaintext secrets
4

Decryption (client-only)

When you open your vault, the encrypted blobs are fetched from the server. Your browser derives the vault key again from your master password and decrypts each item locally.

secrets = JSON.parse(AES-GCM-decrypt(vaultKey, base64url-decode(encryptedData)))

Authentication

Your master password is hashed server-side using bcrypt (cost factor 12) solely to verify your identity at login. This hash cannot be reversed to derive your vault key — they are cryptographically independent.

MVP limitation

Currently the master password is transmitted to the server over HTTPS for bcrypt verification. A production implementation should use SRP (Secure Remote Password Protocol) to eliminate any server involvement in the authentication credential entirely. This is on our roadmap.

What we store vs. what we don't

✓ What we store

  • ·Your email address
  • ·bcrypt(masterPassword) — for auth only
  • ·Item names and URLs — unencrypted, for search
  • ·Tags — unencrypted metadata
  • ·AES-256-GCM ciphertext of all secret values
  • ·Audit event metadata (timestamps, actions)

✗ What we never store

  • ·Your master password in plaintext
  • ·Your vault key (derived client-side only)
  • ·Passwords, API keys, SSH keys in plaintext
  • ·TOTP seeds in plaintext
  • ·Recovery codes in plaintext
  • ·Any secret value in any readable form

MVP security caveats

This codebase has NOT undergone an independent security audit. Do not store production credentials until an audit is complete.
Item names and URLs are stored unencrypted. If this is a concern, use generic names.
SRP is not yet implemented — master password traverses the network (protected by TLS).
The vault key is held in JavaScript heap memory. A sophisticated XSS could extract it. CSP headers (roadmap) will mitigate this.
No MFA yet — a stolen credential allows full vault access.
Key rotation (changing master password) is not yet implemented.

Security roadmap

SRP protocol (no password traversal)
Passkeys / WebAuthn support
TOTP and hardware key MFA
Per-org encryption keys
Content Security Policy headers
Independent security audit
Argon2id key derivation upgrade
Key versioning (re-encrypt on password change)
Breach monitoring (HIBP integration)
End-to-end encrypted export/import

The encryption implementation lives in lib/crypto.ts and uses only the browser's built-in window.crypto.subtle API — no third-party cryptography dependencies.