Every security claim we make is verifiable. Here's how to check each one yourself.
The SDK splits your key into two encrypted shares locally in your browser before anything hits the network. Open DevTools (F12 > Network) before storing a key. You'll see:
// Step 1 — SDK splits the key locally (in your browser): apiKey => share1 + share2 // Split-key encryption, client-side only // Step 2 — Each share is encrypted with a different key: share1 => encrypt(share1, server_key) // Sent to server share2 => encrypt(share2, vp_live_xxxx...) // Stays on your device // What your browser sends to our server: POST /api/v1/keys/store { "share1_encrypted": "0xa8f3b2...", // Encrypted Share 1 only "vaultCommitment": "a3f8..." // Hash commitment } // What is NOT in the request: // Your actual API key — never leaves your browser. // Share 2 — encrypted with your vp_live_ key, stays on your device. // The plaintext of Share 1 — server only receives the encrypted blob.
How to verify: Open DevTools > Network tab > store a key > inspect the POST request body. You'll see only an encrypted Share 1 blob and a hash commitment. The full API key and Share 2 never appear in network traffic.
Each share is encrypted with a key stored in a completely separate place. To breach a key, an attacker needs all three:
// Where each piece lives: 1. Database => share1_encrypted // Authenticated encryption ciphertext // (random salt + IV + auth tag + ciphertext) 2. Environment variable => SERVER_ENCRYPTION_KEY // Decrypts Share 1 // (not in DB, not in code) 3. Developer's device => share2_encrypted // Authenticated encryption ciphertext + vp_live_xxxx... // Decrypts Share 2 // (never sent to server) // To reconstruct a key, an attacker would need: // 1. Breach the database (get encrypted Share 1) // 2. Steal SERVER_ENCRYPTION_KEY (decrypt Share 1) // 3. Steal the developer's vp_live_ key + their Share 2 // All three. From three separate places. Simultaneously.
How to verify: Both shares use authenticated encryption with a random salt and IV per encryption — same plaintext produces different ciphertext every time. Tampered data is detected by the auth tag. We have 14 security tests proving this.
During a proxy call, the key is reconstructed just long enough to make the upstream API request, then immediately zeroed from memory:
// What happens during a proxy call: Step 1 Verify ZK proof (cryptographic authorization) Step 2 Decrypt Share 1 using server-side key Step 3 Receive Share 2 from client (decrypted with vp_live_ key client-side) Step 4 Reconstruct full API key from Share 1 + Share 2 Step 5 Make upstream API call (e.g., OpenAI /v1/chat/completions) Step 6 Response received Step 7 Key buffer zeroed. Overwritten with 0x00. Gone. // Send a fake proof and nothing happens: curl https://staging-api.vaultproof.dev/api/v1/proxy/call \ -H "Content-Type: application/json" \ -d '{"keySlotId":"any-uuid","share2":"fake","zkProof":"fake-proof","nullifier":"fake","appId":"test","targetPath":"/v1/models","method":"GET"}' // Response: { "error": "Invalid ZK proof", "reason": "Proof verification failed" }
How to verify: Run the curl command above — fake proofs are rejected before reconstruction even begins. The server verifies every cryptographic proof. No valid proof = no key reconstruction. After a valid call, the key buffer is explicitly zeroed (verified by our security tests).
Every proxy call requires a unique nullifier. Reusing one is immediately caught:
// First request with nullifier "abc123": 200 OK — Call goes through // Second request with same nullifier "abc123": 403 Forbidden — "Proof already used (replay detected)"
How to verify: Make two proxy calls with the same nullifier. The second one is rejected. Nullifiers are stored in a database with a unique constraint — duplicates are impossible.
All requests must go through our edge proxy with cryptographic signatures. Direct access returns 403:
// Through edge proxy (HMAC signed): curl https://staging-api.vaultproof.dev/health {"status":"ok","service":"vaultproof-edge","secured":true} // Direct to backend (no signature): curl https://[backend-url]/api/v1/keys/list {"error":"Forbidden"}
How to verify: Every request is cryptographically signed with a short-lived timestamp. Expired or forged signatures are rejected. The signing secret never travels over the wire.
Our ZK circuit is compact. Here's what it proves:
// key_auth.nr — The entire ZK circuit fn main( // Private (you know these, server doesn't) slot_secret: Field, // Your device secret share_hash: Field, // Hash of your Share 2 app_auth_path: [Field; 10], // Merkle proof of app nonce: Field, // Fresh random value // Public (server verifies these) vault_commitment: pub Field, // Proves you own the key app_id_hash: pub Field, // Proves app is authorized nullifier: pub Field // Prevents replay ) { // 1. Prove ownership assert(poseidon(slot_secret, share_hash) == vault_commitment); // 2. Prove app is authorized (Merkle membership) assert(verify_merkle_path(app_id_hash, ...)); // 3. Prove freshness (anti-replay) assert(poseidon(slot_secret, nonce) == nullifier); }
How to verify: Read the full circuit source on GitHub. Compile and test it yourself. The math is the proof.
How to verify: Clone the repo. Run npm test. All 40 tests are deterministic and reproducible.
Database + SERVER_ENCRYPTION_KEY + developer's vp_live_ key. That's what it takes. Not policy — architecture.