@zoza/vault
v0.1.1
Published
Client-side field encryption for Zoza Vault — X25519 ECDH + HKDF-SHA256 + AES-256-GCM. Tokens never hit your servers in plaintext.
Downloads
198
Maintainers
Readme
@zoza/vault
Client-side field encryption. Your servers never touch plaintext. X25519 · HKDF-SHA256 · AES-256-GCM.
The Zoza Vault JavaScript SDK encrypts sensitive fields (card numbers, SSNs, health records, tokens, API keys) in the browser before they ever leave the user's machine. Ciphertext is stored in your database like any other string. Decryption happens only inside the Zoza backend, which holds the application private key in an HSM.
Design mirrors what Basis Theory, Evervault, VGS, and Skyflow ship — only difference is this one is fully auditable: all crypto code you're about to run is in this repo.
Install
npm install @zoza/vaultOr via CDN:
<script type="module">
import { VaultClient } from 'https://esm.sh/@zoza/vault';
</script>Quick start
import { VaultClient } from '@zoza/vault';
const vault = new VaultClient({ appId: 'app_a99b96dd23fb8414d17ec48df7984802' });
// Encrypts client-side. `token` is safe to store/log/transmit.
const token = await vault.encrypt('card_number', '4242 4242 4242 4242');
// → "zv1:<ephPub>.<iv>.<ct+tag>"
// Encrypt a whole object:
const body = await vault.encryptObject({
card_number: '4242424242424242',
cvv: '123',
cardholder: 'Ada Lovelace',
});
// send `body` to your own server as normal — every field is a vault tokenYour server then POSTs the token to vault-api.zoza.world to receive the
plaintext, only when it's needed, and only from servers you've authorised.
Why another SDK?
| | Zoza Vault | Basis Theory | Evervault | |---|:---:|:---:|:---:| | Open-source SDK (MIT) | ✅ | ❌ | ❌ | | Self-host option | ✅ | ❌ | ❌ | | Public crypto spec | ✅ | partial | partial | | India DPDPA aware | ✅ | ❌ | ❌ |
The crypto scheme is specified byte-for-byte in src/crypto.ts
and the whole client is under 300 lines of TypeScript. Auditable by one
engineer in an afternoon.
Cryptographic scheme
For every encrypt() call:
- A fresh X25519 ephemeral key-pair is generated in the browser.
- ECDH against the application's public key produces a 32-byte shared secret. The ephemeral private key is discarded immediately.
- HKDF-SHA256 stretches the secret using
ephPubas salt and"zoza-vault/v1/" + fieldas info, yielding a 32-byte AES key bound to the field name. (A token forssncannot be replayed ascard_number.) - AES-256-GCM encrypts the UTF-8 plaintext with a random 12-byte IV, the field name bound as Additional Authenticated Data.
- The output token packs
ephPub · iv · ciphertext+tagas URL-safe base64, prefixedzv1:.
Decryption (server-side only) reverses the process using the application's private key.
Token wire format
zv1:<ephPub_b64url>.<iv_b64url>.<ct_and_tag_b64url>| Field | Size | Purpose |
|---|---|---|
| ephPub | 32 B | X25519 ephemeral public key |
| iv | 12 B | random, per-token |
| ct+tag | N + 16 B | AES-GCM ciphertext + auth tag |
Storable in any TEXT column. URL-safe, no padding.
API
new VaultClient(opts)
| Option | Type | Notes |
|---|---|---|
| appId | string (required) | From zoza.world/developers/vault |
| apiUrl | string | Default https://vault-api.zoza.world |
| appPublicKey | Uint8Array \| string | Pre-fetched 32-byte key (hex or base64url) — skips the network round-trip |
| fetch | typeof fetch | Override for runtimes without a global fetch |
vault.encrypt(field, plaintext) → Promise<string>
Returns a zv1: token.
vault.encryptObject(values) → Promise<{...}>
Encrypts every string value, preserves null / undefined.
Low-level helpers
import { encryptField, parseToken, decryptFieldWithPrivateKey } from '@zoza/vault';decryptFieldWithPrivateKey() is only for self-hosted deployments where
you legitimately hold the private key. Never embed a private key in
client code.
Security properties
- Forward secrecy per token — compromising one token's plaintext reveals nothing about any other token.
- Field binding — both the HKDF
infoand the GCM AAD bind the key and ciphertext to the declared field, preventing cross-field replay. - No plaintext transit — even TLS-stripping MITMs see only ciphertext.
- No SDK secret — the SDK is publicly visible, so leaking it is a non-event; security lives in the server's private key custody.
Known threat model boundaries are documented in SECURITY.md.
Running the tests
npm install
npm testTests run on Node ≥18 (uses the native WebCrypto) and cover round-trip encrypt/decrypt, field-binding, key mismatch, UTF-8, and token parsing.
License
MIT © Zoza
