BlindKeep is a zero-knowledge encrypted vault. All encryption and decryption happen exclusively in your browser (via WebAssembly) or mobile app. The server never has access to your plaintext data, decryption keys, or any material to derive them.
Threat model: We assume the server is an honest-but-curious adversary — it faithfully executes the protocol but may inspect all stored data. Even with full database and object storage access, an attacker learns nothing about your plaintext content.
What the server can see (inherent to any server-mediated system):
Your password never leaves your device. Instead, it's stretched into a high-entropy key, which is then split into two cryptographically independent branches:
Password
|
v
Argon2id(password, client_salt) -> password_key (256-bit)
[64 MiB memory, 3 iterations]
|
+-- HKDF("vault-auth") -> auth_key
| '-- Sent to server, hashed again with Argon2id
| Server stores only: auth_key_hash
|
+-- HKDF("vault-enc") -> key_wrapping_key
| '-- Wraps/unwraps master_key (never sent to server)
|
'-- (password_key is discarded)
master_key (random 256-bit, created at registration)
|
+-- HKDF("vault-enc") -> enc_key
| +-- Encrypts per-item keys
| '-- Encrypts your X25519 private key
|
'-- HKDF("vault-wrap") -> wrap_key (reserved)
The auth path ("vault-auth") is completely separated from the encryption path ("vault-enc") via HKDF domain separation — knowing one reveals nothing about the other.
Every item in your vault is encrypted with its own unique random key — per-item key isolation.
item_key is generatedXChaCha20-Poly1305(item_key, plaintext, aad="item:{userId}")item_key itself is wrapped: XChaCha20-Poly1305(enc_key, item_key, aad="wrap:{userId}")Associated Authenticated Data (AAD) binds each ciphertext to its owner and context — even with database write access, an attacker cannot swap encrypted data between users.
Two methods, both zero-knowledge:
X25519 Key Exchange (registered users): A fresh ephemeral keypair computes a shared secret with the recipient's public key. Key-bound HKDF prevents misdirection attacks. The ephemeral key is discarded immediately, providing sender forward secrecy.
Link-Secret (anyone): A random 32-byte secret is embedded in the URL fragment (#), which is never sent to the server per the HTTP specification.
Policy controls include view limits, expiry dates, IP restrictions, and instant revocation.
Designate an heir to receive selected vault items. A random will_key wraps the item keys, and is itself encrypted for the heir using X25519 or a BIP39 mnemonic. A dead man's switch checks in with you on a schedule — miss enough check-ins and the will activates.
| Algorithm | Parameters | Use |
|---|---|---|
| XChaCha20-Poly1305 | 256-bit key, 192-bit nonce | All symmetric encryption |
| X25519 | Curve25519, 32-byte keys | Key agreement for grants and wills |
| Argon2id | 64 MiB, t=3, p=1 | Password stretching, auth key hashing |
| HKDF-SHA256 | Domain-separated info | Subkey derivation |
| Ed25519 | Strict verification | Document notarization signatures |
| PBKDF2-SHA512 | 600,000 iterations | BIP39 mnemonic key derivation |
#[zeroize(drop)] overwrites key material on drop