scanner docs insights pricing sign in

Don't Trust Us. Verify.

Every security claim we make is verifiable. Here's how to check each one yourself.

1

Your full API key is never stored at rest

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.

2

Both shares are encrypted with different keys

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.

3

Key exists in memory briefly, then it's gone

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).

4

Replayed requests are detected instantly

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.

5

Backend is locked — edge proxy only

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.

6

The ZK circuit is open source

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.

7

40 automated tests prove every claim

Split-Key Encryption (10 tests)
  • 2-of-2 split and reconstruct
  • 2-of-n team threshold
  • Single share reveals nothing
  • 1000 brute-force attempts fail
  • Wrong shares produce wrong output
ZK Circuit (4 tests)
  • Valid authorization passes
  • Wrong secret rejected
  • Unauthorized app rejected
  • Replayed nullifier rejected
Integration (7 tests)
  • Store + retrieve end-to-end
  • Share 1 encrypted in DB
  • Revocation zeroes Share 1
  • Replay prevention works
  • Unauthorized apps blocked
Security (14 tests)
  • Encryption tamper detection
  • Random IV per encryption
  • Buffer zeroing verified
  • No Math.random (crypto only)
  • Missing encryption key throws

How to verify: Clone the repo. Run npm test. All 40 tests are deterministic and reproducible.

Three breaches, three places, simultaneously.

Database + SERVER_ENCRYPTION_KEY + developer's vp_live_ key. That's what it takes. Not policy — architecture.