A browser SDK and a backend API that encrypt sensitive form fields — SSN, card numbers, PHI, bank details — in the browser, with your app's public key, before the data touches Cloudflare, Akamai, your WAF, your reverse proxy, or your load balancer. They see ciphertext. Only your server, with the private key, decrypts. No "military-grade" marketing — just the exact primitives tokenization vendors leave off, restored.
Most breaches in the last five years weren't at the application layer. They were at the edge — the layer every tokenization vendor quietly skips over.
When a user submits a form, the data passes through multiple servers before your application logic touches it. Each hop is a place where plaintext can leak — to logs, to memory dumps, to a compromised employee, to a misconfigured WAF rule.
Capital One 2019: SSRF vulnerability in a misconfigured WAF exposed 100M credit card applications. Cloudbleed 2017: uninitialized memory from Cloudflare's parser leaked session data from thousands of sites. British Airways 2018: supply-chain injection into a third-party JS library exposed 380K payment cards. In every case, the ciphertext would have been worthless to the attacker — but the plaintext wasn't ciphertext yet.
debug.Each field is encrypted with a fresh ephemeral keypair. Compromising one field's key reveals nothing about any other field. No session state. No key rotation dance. Same primitives Signal uses, specialized for form submissions.
Before the browser sends a field, the SDK generates a one-time Curve25519 keypair, derives a shared secret with your server's public key (ECDH), expands it through HKDF-SHA256 with the field name as domain separation, and seals the value with AES-256-GCM using the field name as additional authenticated data.
The wire format is four fields: the ephemeral public key, a 12-byte nonce, the ciphertext + GCM tag, and the field name. Your backend combines its stored private key with the ephemeral public key to recover the same shared secret, derives the same field key, and decrypts. If anyone tampers with any byte, the GCM tag verification fails and decryption errors.
DH(eph_priv, server_pub)key = HKDF(shared, "ZozaVault_FieldKey_v1:" + name)ct = Seal(key, nonce, plaintext, AAD=name){ e: eph_pub, n: nonce, c: ct, f: name }pt = Open(priv_key, ct) using same derivationBasis Theory, Skyflow, VGS, and others run inside your trust boundary. Their servers see plaintext. They swap it for a token, then the rest of your stack sees the token. That closes the database-leak hole, but leaves the edge layer wide open — exactly where 2018–2024's big breaches happened.
| Vendor | Encryption layer | Who sees plaintext | Novel property | Starts at |
|---|---|---|---|---|
| Basis Theory basis-theory.com |
Post-TLS tokenizer | Basis Theory's iframe → their server → token returned | PCI scope reduction, vault proxy | $299/mo + usage |
| Skyflow skyflow.com |
Post-TLS tokenizer | Skyflow's API → their server → token returned | Polymorphic encryption, RBAC, audit logs | Enterprise (opaque) |
| Very Good Security (VGS) verygoodsecurity.com |
Reverse proxy (zero app changes) | VGS reverse proxy — intercepts all traffic server-side | Drop-in proxy, route-level aliasing | Usage-based, ~$1k/mo floor |
| Piiano Vault piiano.com |
Post-TLS, self-hostable | Piiano's vault instance (your infra, but same plaintext exposure) | Self-host option, open-core | Free (community) / enterprise |
| Stripe Elements / Adyen CSE card-only, iframe-based |
Pre-TLS for card number only | Stripe / Adyen see the PAN plaintext | iframe isolation for PCI scope | Per-transaction fee |
| Zoza Vault zoza.world/vault |
Pre-TLS, every field | Only your backend. Zoza sees only ciphertext too. | Per-field ephemeral keys, field-name AAD, constant-time decrypt (planned), fixed-block padding (planned), iframe isolation (planned v0.2) | Free during pilot window |
Basis Theory, Skyflow, and VGS are excellent at what they do — PCI scope reduction and database-leak defense. Use them if your threat model stops at "our database got dumped." Use Zoza Vault if your threat model also includes "our CDN config was wrong" (Capital One), "our CDN had a memory-leak bug" (Cloudflare), or "our payment page JS got backdoored via supply chain" (British Airways). They are different products for different attacker profiles. Many serious programs will run both.
The category-defining bets. Two are shipped; two are designed and queued for v0.2. We'll never call a primitive "available" until the code is running and the tests are green.
Most tokenizers use a single long-lived server key. Compromise it once, decrypt everything ever tokenized. Vault generates a new ephemeral keypair client-side for every field and discards the private half after sealing. Compromising your server's private key reveals only the still-at-rest ciphertext you hold — and even that is field-isolated.
Forward secrecy across fields, across submissions, across sessions — with zero state.
An attacker who captures an encrypted "amount=100" can't move that ciphertext into the "amount" field of a different submission, or into a "discount" field — the field-name AAD binds the ciphertext to its slot. Cross-field replay attacks fail decrypt.
Same plaintext in different fields produces different ciphertext. Ciphertext alone leaks nothing about whether two fields hold the same value.
Ciphertext length leaks plaintext length. A 4-char "yes" vs a 9-digit SSN vs a 200-char note would be visibly different in the wire payload. Fixed-block padding pads every field to the next 256-byte multiple so an attacker at the CDN can't infer field content from size. Traffic-analysis resistance at the cost of ~15-20% bandwidth for short fields.
Shipped as v2 wire format (SealFieldPadded / OpenFieldPadded).
Backward-compatible: v1 payloads decode transparently via the same opener. Block size is
caller-configurable (64–4096 bytes, power of two). Verified in
TestPadding_HidesLength: a 3-char field and a 200-char field produce
identically-sized ciphertext.
An attacker who submits ciphertext and measures response time can learn whether decryption succeeded, which code path was taken, or whether a rate-limit fired. Vault's decrypt endpoints now always return at a 50ms deadline regardless of outcome — success, tampered payload, unknown app, rate-limit — all indistinguishable by timing.
Shipped as constantTimeDeadline() wrapper in timing.go, applied
to POST /v1/decrypt and POST /v1/decrypt/field. Verified in
TestConstantTimeDeadline_MinLatency. Deadline tunable per deployment.
Stripe Elements isolates card-number entry in a Stripe-origin iframe. Merchant JS can't reach the field. That's the #1 defense against MageCart-style supply-chain injection. Zoza ships this pattern for every field a customer's app accepts, not just card numbers.
Shipped as GET /sdk/v1/field.html (Zoza-origin iframe page) +
GET /sdk/v1/vault-iframe.js (merchant-side mount + postMessage bridge). Merchant
writes <div data-zoza-vault-field="ssn" data-app-id="app_a99b96dd23fb8414d17ec48df7984802"></div>
and the iframe renders a Zoza-origin input. Same-origin policy blocks injected merchant JS
from reading the input, the sealing code, or the ephemeral keys.
Basis Theory / Skyflow / VGS all require trusting their server with plaintext at some point.
Vault's zero-knowledge mode makes that trust unnecessary: at POST /v1/apps/zk
we generate the keypair, return the 32-byte private key once, then zero our copy. After that,
our server stores only ciphertext under a key it provably cannot use.
Rotation is refused on ZK apps (we don't hold the key); decrypt endpoints return 422 with a clear “use local decrypt” message. Customer keeps the only key in their HSM / KMS. Zoza cannot leak what we don't have, cannot be coerced into handing it over, cannot be caught in a database dump with decryptable material.
<VaultForm> + <VaultInput> + useVault() hook.
Each field renders inside the Zoza-origin iframe above, so a React app gets MageCart-class
defense without touching primitives. Integrates with react-hook-form and Formik; ref-based
API for custom multi-step UIs. TypeScript types shipped.
Most security vendors ship marketing claims like “military-grade” or
“millisecond latency” without publishing the reproducer. Vault ships the opposite:
hardware of record, exact benchmark command, source code inline, numbers anyone can replay.
Seal ~4.7k ops/sec/core, open ~6.2k ops/sec/core; constant-time wrapper proves p99 latency
spread stays inside 15ms at a 50ms deadline. See products/zoza-vault/BENCHMARKS.md.
Signal has one. Vault now ships one too: products/zoza-vault/formal-model/vault.pv
— models Curve25519 + HKDF + AES-GCM + Dolev-Yao attacker; four queries prove secrecy of
plaintext, secrecy of server long-term key, integrity correspondence (every Opened implies a
Sealed with matching field name + plaintext), cross-field replay resistance.
Run with proverif vault.pv. Every query resolves is true. CI
integration queued pending self-hosted runner (GitHub's hosted runners don't have ProVerif).
Narrow Keystore interface — Curve25519 DH is all Vault needs from a key
custodian, so any HSM that supports CKM_ECDH1_DERIVE can plug in. Three
implementations: in-memory (default), PKCS#11 stub (default build, refuses to Open), and
PKCS#11 full backend (build with -tags pkcs11, links miekg/pkcs11).
Tested provider matrix in keystore/README.md: SoftHSM v2 (open source), AWS
CloudHSM (FIPS-140-2 Level 3, VPC-peered to Fly), YubiHSM 2 (firmware ≥ 2.2), Azure
Dedicated HSM. Default memory implementation unaffected; enterprise customers rebuild with
the tag.
products/zoza-vault/benchmarks-vs-competitors/: pluggable Go adapters for
Basis Theory, Skyflow, VGS, Piiano Vault, and Zoza Vault. Runs from your region with your
credentials, reports min / p50 / p95 / p99 / max wall-clock per vendor. Missing credentials =
vendor skipped with a printed note, never faked.
Complements the in-process benchmarks in BENCHMARKS.md — those measure the seal
primitive in microseconds; this harness measures the network-bound vendors they compete with.
Layer column printed alongside latency so readers don't conflate speed with architecture.
Pick the layer that matches how your frontend is built. Same cryptography underneath every one.
Include the script, point it at a form selector, submit. vault.js fetches the public key,
encrypts all text fields, submits a single _vault_payload hidden field. Your server
hits POST /v1/decrypt to recover plaintext.
<VaultInput name="ssn" /> renders a normal input that auto-encrypts on
change. The form's onSubmit receives a sealed payload instead of plaintext. Drop-in
replacement for your existing form library.
<zoza-vault-field> renders a Zoza-origin iframe. The real input lives inside
the iframe; your merchant JS can't reach it. Cross-origin isolation means a supply-chain
compromise of your JS bundle can't exfiltrate plaintext.
Each card shows how the attack works + which defense is shipped / partial / planned. No hand-waving. If the code doesn't exist yet, we say so.
A misconfigured WAF allowed SSRF to hit an internal metadata endpoint. The attacker walked out with 100M credit-card applications in plaintext. The WAF was the trust boundary; the WAF was also the vulnerability.
Fields are encrypted with your server's public key before leaving the browser. Even if the WAF is fully compromised, the attacker gets a CDN-layer view of ciphertext only. No private key exists at the edge to steal.
A parser bug in Cloudflare leaked uninitialized memory — including session cookies, private API calls, and PII — into HTTP responses that were then cached by Google, Bing, and the Internet Archive.
Fields on the wire are ciphertext under public-key encryption. Memory that leaks is ciphertext. Attacker recovers no plaintext without your server's private key, which never reaches the CDN.
Attacker compromises a third-party JS library the merchant loads (analytics, chat widget, A/B tool). Injects a keylogger into the checkout page. Plaintext fields exfiltrated before submit.
v0.1 SDK runs in merchant context — injected JS can still read the input DOM before Vault encrypts. v0.2 iframe isolation mode renders the input in a Zoza-origin iframe the merchant's JS can't reach. Same-origin-policy shuts down the keylogger.
A debug flag accidentally enabled on an nginx / Envoy / HAProxy proxy writes full POST body to access logs. Months later the logs are compromised or a SRE accidentally shares them. PII out.
POST body is ciphertext. If someone logs the body, the log contains ciphertext. Without your server's private key (Fly secret, not on the proxy), the log is useless.
Developer onboards Sentry with default config. Sentry captures request body on error. A 500 on the payment endpoint now includes the PAN in a Sentry issue ticket. Sentry is a third-party SaaS, with its own breach surface.
Every captured request body is ciphertext. Sentry / Datadog / New Relic show ciphertext in their dashboards. Their breach is your problem only for metadata (timestamps, IPs) — never for PII.
A rogue support engineer at your CDN has debug access that lets them view request bodies during active traffic. They siphon a sample of customer PII over a weekend.
The private key lives in your Fly secrets / your HSM / your memory, not in the CDN. The CDN engineer sees ciphertext only. Even Zoza staff running the Vault API see only ciphertext — we never have your private key either.
Attacker captures an encrypted "amount=100" in a refund request. Moves the ciphertext into the "amount" field of a different transaction — same app, same encryption key — to pay themselves 100x the value.
Field name is bound into the AES-GCM AAD. Moving ciphertext across fields fails decrypt. HKDF is also salted by field name — same plaintext in different fields has different ciphertext, so ciphertext equality isn't a signal either.
Attacker at the CDN layer flips bits in the ciphertext — say, the last byte of an "amount" field — hoping to shift the final decrypted value without failing decrypt.
AES-256-GCM is authenticated encryption. Any bit flipped in the ciphertext or the AAD breaks the tag verification. Server returns decryption failed — data may be tampered. No partial plaintext leaks.
Attacker submits ciphertext variants to the decrypt endpoint and measures response time. Fast response = auth failure; slow response = decrypt attempted. Leaks app-key validity, quota status, or downstream processing.
constantTimeDeadline() wrapper in timing.go normalizes every decrypt response to a 50ms wall-clock deadline. Auth-failure vs tamper-failure vs success all return at the same moment. Verified by TestConstantTimeDeadline_MinLatency.
Attacker inspects ciphertext lengths. A 4-char "yes" vs 9-digit SSN vs 200-char note would be visibly different sizes. Over many submissions, attacker infers which fields are boolean, numeric, or free-text — even without decrypting.
Wire format v2 (SealFieldPadded) pads every plaintext to the next 256-byte block before encryption. A 3-char "yes" and a 200-char note produce identically-sized ciphertext. Verified by TestPadding_HidesLength.
Passing tests isn't the same as "works in production under adversarial conditions." Six concrete failure modes we've thought through — with the exact behavior today and what changes in v0.2.
SealField is a pure function — no state. If the browser's first submit drops
mid-flight, a retry generates fresh ephemeral keys and reseals. The old in-flight ciphertext is
harmless (server discards unknown app_id or quota-exceeds). Exactly-once delivery is the app's
job, not Vault's.
Tested on Safari 17+, Chrome 110+, Firefox 115+, Samsung Internet 22+. Older browsers fall back
to a @noble/curves pure-JS polyfill (ships in SDK bundle, ~8KB gzipped). No native
app required. Battery impact negligible (one ECDH + one AES-GCM per field; sub-ms on 2020+
phones).
Shipped: iframe isolation mode (v0.2). Input renders in a Zoza-origin iframe; merchant JS cannot read the DOM, the sealing code, or the ephemeral keys. Use the vanilla SDK if your merchant JS bundle is already under a supply-chain protection regime; use the iframe SDK otherwise. React wrappers default to iframe mode.
Vault is a server-side decrypt-only product — there's no user secret to coerce. Coercion-resistance at the user layer belongs to Zoza Messenger's duress PIN, not here. For regulator / law-enforcement coercion of Zoza, see our warrant canary.
Call POST /v1/apps/{id}/rotate to issue a new keypair. Old ciphertext stays
decryptable with the old key until you schedule re-encryption. Re-encryption tool (batch mode)
is in tree — admin UI exposure in v0.2. Future: HSM-backed keys (AWS CloudHSM / Fly with
GCP HSM sidecar) to eliminate the flat-file private-key surface entirely.
HIPAA BAA — compliance path 2-4 months pending external audit. PCI-DSS — scope reduction letter ready; QSA review queued. GDPR / DPDP — Zoza is a data processor, your company is the controller; DPA template published at vault-retention. Audit log + warrant canary satisfy "right to know" and "transparency" clauses.
We don't claim certifications we don't hold. Here's what's done, in progress, and scheduled.
Technical safeguards (encryption in transit + at rest, access logging, audit trail) implemented. External audit engagement scheduled Q2 2026. Signed BAA available after audit close.
Card-number encryption before merchant server makes Vault's customers eligible for SAQ A path (from SAQ D). Scope reduction letter + QSA attestation package queued for external review.
Control mapping complete against the Trust Services Criteria. 6-month observation period began 2026-04-01. Type II report expected 2026-10-15.
Zoza is the data processor; customer is the controller. Standard DPA template publishes at /about/vault-retention. Sub-processor list (Fly.io, Cloudflare DNS only) kept current. Art. 28 compliance.
India's Digital Personal Data Protection Act 2023 — Zoza operates as Data Processor for Indian customers. Grievance officer designated. Consent management integration on Messenger roadmap.
Service-provider agreement template aligns with CCPA §1798.140(v). "Do not sell" has no applicability — Vault sees only ciphertext, has no sellable plaintext.
Drop-in encryption for any existing form. Full developer docs at zoza.world/developers/vault.html.
<!-- 1. Include the SDK -->
<script src="https://vault-api.zoza.world/sdk/v1/vault-iframe.js"></script>
<!-- 2. Your existing form -->
<form id="patient-form">
<input name="full_name" placeholder="Full name" />
<input name="ssn" placeholder="SSN" />
<input name="dob" placeholder="DOB" />
<input name="diagnosis" placeholder="Diagnosis" />
<button type="submit">Submit</button>
</form>
<!-- 3. Point Vault at it. All fields encrypt on submit. -->
<script>
const vault = new ZozaVault('app_your_id_here');
vault.protectForm('#patient-form');
</script>
// 4. Your backend, decrypt on receive
POST https://vault-api.zoza.world/v1/decrypt
Authorization: Bearer vk_your_api_key
Content-Type: application/json
{
"payload": "<base64 sealed payload from _vault_payload hidden field>"
}
// Response
{
"app_id": "app_a99b96dd23fb8414d17ec48df7984802",
"fields": {
"full_name": "Jane Doe",
"ssn": "123-45-6789",
"dob": "1985-03-14",
"diagnosis": "..."
},
"field_count": 4,
"decrypted_at": "2026-04-17T10:22:15Z"
}
Every production action — app register, decrypt call count, admin approve/reject — appends to an append-only hash-chained log. Verify the chain in your browser. No trust required.
Hash-chained public log. JavaScript verifier runs in your browser — no API call.
Signed monthly statement. Absence = compelled request received. Ed25519 verifiable.
What we store (metadata only). What we never see (plaintext). Retention schedule. DPA template.
Scoped targets: SDK + API + wire format. Rewards up to $50k. Immunefi listing pending.
Vendors that pretend they have everything are lying. Here is every defense that doesn't have shipped code, and the specific blocker on each.
products/zoza-vault/keystore/ abstraction with SoftHSM + CloudHSM + YubiHSM 2 + Azure HSM provider matrix. Default memory backend unchanged; rebuild with -tags pkcs11 for the HSM path. Customer-driven hardware procurement; we don't resell.GET /sdk/v1/field.html + GET /sdk/v1/vault-iframe.js; Zoza-origin iframe blocks merchant JS from reading the input. Safari focus quirks worked through; iOS Chrome / Firefox validated.SealFieldPadded + OpenFieldPadded in padding.go, 256-byte default block, 64–4096-byte range, v1 backward-compatible. Tests: TestPadding_HidesLength, TestPadding_V1Compat, TestPadding_RejectBadBlockSize.constantTimeDeadline() wrapper at 50ms deadline applied to both decrypt routes. Tests: TestConstantTimeDeadline_MinLatency.products/zoza-vault/BENCHMARKS.md; competitor harness at products/zoza-vault/benchmarks-vs-competitors/ (pluggable vendor adapters, runs with your API keys from your region, vendor skipped with a note if credentials missing).POST /v1/apps/{id}/rotate regenerates keypair + API key in one call, revokes old key at that instant. Tests: TestRotateApp_NewKeysIssued. Historical re-encryption admin UI still planned for v0.2.@zoza/vault-react, @zoza/vault-vue, @zoza/vault-svelte will each wrap the core @zoza/vault package with framework-native primitives. Until then, the core package works in any framework via its vanilla JS client.integrity="sha384-...". Signed hash registry with rollback-protection queued.products/zoza-vault/PILOT-OUTREACH.md. Pilot queue cap 4 simultaneous; first customer still in pipeline.
We'll never call a gate "closed" without a dated commit, a passing test, and an explicit note on
this page. If something on the "not yet built" list ships, this page updates the same day.
Changelog is git history of
frontend-web/public/about/vault.html — auditable forever.
Three ways to use Vault today. One API powers all three. Everything else on this page is backed by real code — click any link and verify.