Technology
This page is technical. It describes the cryptographic architecture behind ZeroKVault for those who want to understand exactly how your data stays private.
Zero-Knowledge Architecture
ZeroKVault is built on a simple principle: we should never be able to read your data. All encryption and decryption happens in your browser using the Web Crypto API. Our servers store only ciphertext and encrypted keys — we have no way to decrypt them, even if compelled.
Your master key is a 12-word BIP-39 seed phrase that never leaves your device. From it, we deterministically derive an ECDH P-256 key pair. Only the public key is ever sent to our servers. The private key stays in your browser’s memory and is discarded when you close the tab.
How Encryption Works
Seed Phrase Generation
A 12-word BIP-39 mnemonic is generated from 128 bits of cryptographic entropy. This is your master key — write it down and store it safely.
Key Derivation
The mnemonic is converted to a 64-byte seed via PBKDF2 (BIP-39 standard), then passed through HKDF-SHA256 with domain separation to produce a deterministic P-256 ECDH key pair. Only the public key is sent to the server; the private key never leaves your browser.
Per-Item Encryption
Each vault item gets a random AES-256-GCM Data Encryption Key (DEK). Content is encrypted with this DEK. A 12-byte random IV is prepended to the ciphertext.
DEK Wrapping
The DEK is wrapped using the recipient’s public key: a fresh ephemeral P-256 key pair is generated, ECDH key agreement is performed between the ephemeral private key and the recipient’s public key, then the shared secret is passed through HKDF to derive an AES-KW wrapping key. Each item gets a unique ephemeral key, providing forward secrecy.
Server Storage
The server receives only the encrypted content and the wrapped DEK. It cannot unwrap the DEK because it does not have the recipient’s private key.
Decryption
The recipient enters their seed phrase, derives their key pair, validates the fingerprint, unwraps each item’s DEK, and decrypts the content — all in the browser.
Under the Hood
The snippets below are written in TypeScript, which is compiled to JavaScript at build time. The resulting JavaScript is what actually runs in your browser — and you can verify it yourself. Open your browser’s Developer Tools (F12), go to the Sources tab, and inspect the code that’s executing. What you see there should match the logic shown here. That’s the point of zero-knowledge: you don’t have to trust us, you can check.
Key Derivation (BIP-39 Seed → P-256 Key Pair)
Deterministic derivation using only the Web Crypto API. The same seed always produces the same key pair.
async function deriveKeyPair(seed: Uint8Array): Promise<{
publicKeyJwk: JsonWebKey;
privateKeyJwk: JsonWebKey;
}> {
// Step 1: Import seed as HKDF base key material
const baseKey = await crypto.subtle.importKey(
"raw", seed.buffer as ArrayBuffer,
"HKDF", false, ["deriveBits"],
);
// Step 2: Derive 32 bytes via HKDF-SHA256
const derivedBits = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(32),
info: new TextEncoder().encode("zerokvault-ecdh-p256-v1"),
},
baseKey, 256,
);
// Step 3: Import as P-256 private key
const dBytes = new Uint8Array(derivedBits);
const privateKey = await crypto.subtle.importKey(
"jwk",
{ kty: "EC", crv: "P-256", d: uint8ArrayToBase64url(dBytes) },
{ name: "ECDH", namedCurve: "P-256" },
true, ["deriveBits"],
);
// Step 4: Export both JWK forms
const privateKeyJwk = await crypto.subtle.exportKey("jwk", privateKey);
const publicKeyJwk = exportPublicKey(privateKeyJwk);
return { publicKeyJwk, privateKeyJwk };
}Content Encryption (AES-256-GCM)
Each item is encrypted with a random DEK. The IV is prepended to the ciphertext for self-contained payloads.
async function encryptContent(
plaintext: string,
dek: CryptoKey,
): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
dek, encoded,
);
// Concatenate IV (12 bytes) + ciphertext
const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.byteLength);
return uint8ArrayToBase64(combined);
}DEK Wrapping (Ephemeral ECDH + AES-KW)
Each item’s DEK is wrapped with a fresh ephemeral key, ensuring forward secrecy. Only the recipient’s private key can unwrap it.
async function wrapDEK(
dek: CryptoKey,
recipientPublicKeyJwk: JsonWebKey,
): Promise<string> {
// Generate ephemeral ECDH key pair (forward secrecy)
const ephemeral = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true, ["deriveBits"],
);
// Import recipient public key
const recipientPublicKey = await crypto.subtle.importKey(
"jwk", recipientPublicKeyJwk,
{ name: "ECDH", namedCurve: "P-256" },
false, [],
);
// ECDH key agreement -> shared secret
const sharedBits = await crypto.subtle.deriveBits(
{ name: "ECDH", public: recipientPublicKey },
ephemeral.privateKey, 256,
);
// HKDF -> AES-KW wrapping key, then wrap
const wrappingKey = await deriveWrappingKey(sharedBits);
const wrappedKeyBuffer = await crypto.subtle.wrapKey(
"raw", dek, wrappingKey, "AES-KW",
);
return JSON.stringify({
wrappedKey: uint8ArrayToBase64(new Uint8Array(wrappedKeyBuffer)),
ephemeralPublicKey: await crypto.subtle.exportKey("jwk", ephemeral.publicKey),
});
}Air-Gapped Key Generation
The weakest link in any zero-knowledge system is the machine that generates the key pair. If that device is compromised — by malware, a browser extension, or spyware — an attacker could silently capture your seed phrase or private key before they ever leave the device.
For users with elevated security requirements, ZeroKVault supports importing a public key generated entirely offline. You generate the key pair on a trusted, air-gapped machine using a short Python script, then import only the public key into the app. The private key and seed phrase never touch the live system.
The script is a single file that runs with Python 3 and one package (pip install cryptography). It requires no internet access and replicates the exact same derivation pipeline as the app: BIP-39 mnemonic → PBKDF2-HMAC-SHA512 seed → HKDF-SHA256 → P-256 key pair. At roughly 120 lines, it is short enough to read and audit yourself before running it.
Review it before running. The script never connects to the internet and outputs only the public key — the seed phrase stays on your machine.
Architecture Overview
Your Browser
- Seed phrase entry
- Key derivation (HKDF)
- AES-256-GCM encryption
- ECDH key agreement
- DEK wrapping / unwrapping
ZeroKVault Server
- User authentication
- Heartbeat orchestration
- Public key storage
- Rejects private key material
Encrypted Storage
- Encrypted vault items
- Wrapped DEKs
- Encrypted attachments (S3)
Security Properties
| Property | Description |
|---|---|
| Zero-Knowledge | The server never sees plaintext, DEKs, or private keys. |
| Forward Secrecy | Each item uses a unique ephemeral key pair for DEK wrapping. |
| Deterministic Keys | The same seed phrase always produces the same key pair, enabling recovery. |
| Native Crypto | All operations use the browser’s Web Crypto API — no external JS crypto libraries. |
| Envelope Encryption | Content is encrypted with a random DEK; the DEK is encrypted with the user’s public key. |
| Fingerprint Verification | A SHA-256 hash of the public key confirms the correct seed was entered before decryption. |
Client (Browser)
All cryptographic operations: key generation, encryption, decryption, DEK wrapping/unwrapping. Uses the native Web Crypto API.
Server (API)
Stores encrypted data, manages user accounts and Heartbeat triggers. Never handles plaintext or private keys. Rejects any request containing private key material.
Storage (Database + S3)
PostgreSQL stores encrypted vault items and wrapped DEKs. S3-compatible storage holds encrypted file attachments. All data is ciphertext.