AES-256-GCM, ECDH, ECDSA, PBKDF2, and BIP-39 mnemonic recovery — all built on the native Web Crypto API. No native addons. No heavy dependencies. Works in any modern browser or Node 18+.
Open-source · Auditable · No server required · ESM only
Keys never leave the browser. Plaintext never touches your server. The architecture enforces privacy — it is not a policy you can accidentally break.
Every cipher is a thin wrapper around SubtleCrypto. Hardware-accelerated, zero-dependency, same API in browser and Node 18+.
Generate, wrap, store, and recover keys. PBKDF2 passphrase wrapping, IndexedDB storage, and BIP-39 mnemonic recovery form a complete lifecycle.
Full typings included. Every function signature is precise — CryptoKey, Uint8Array, SealedEnvelope. No implicit any.
One runtime dependency: @scure/bip39 for mnemonic wordlists. Everything else is the platform. No OpenSSL, no polyfills, no wasm blobs.
Each module is independent and tree-shakeable. Use only what you need. Bring your own storage, your own transport, your own UI.
Initialise once per device, then seal and unseal data anywhere in your app.
consealAll functions are async, return typed values, and throw on authentication failure.
| Function | Description |
|---|---|
| seal(data, key) | Encrypt a Uint8Array with an AES-GCM key. Returns IV + ciphertext. |
| unseal(ct, key) | Decrypt. Throws DOMException if the ciphertext has been tampered with. |
| generateAesKey() | Generate a fresh extractable AES-256-GCM CryptoKey. |
| importAesKey(raw) | Import 32 raw bytes as an AES-GCM key. |
| Function | Description |
|---|---|
| wrapKey(passphrase, key, secretKey?) | Derive a wrapping key from a passphrase (PBKDF2 + 600,000 iterations) and AES-KW wrap the AES key. Optional 128-bit Secret Key hardens the KDF input. |
| unwrapKey(passphrase, wrapped, salt, secretKey?) | Reverse of wrapKey. Returns extractable: false. Throws on wrong passphrase or Secret Key. |
| rekey(oldPass, newPass, wrapped, salt, secretKey?) | Change the passphrase without re-encrypting content. Secret Key stays the same. |
| rekeySecretKey(pass, oldSK, newSK, wrapped, salt) | Rotate the Secret Key while keeping the passphrase. Use only on compromise — requires re-enrolling all devices. |
| generateSecretKey() | Generate a random 128-bit Secret Key. Call once at setup; store device-side and keep an offline copy. |
| combinePassphraseAndSecretKey(pass, sk) | SHA-256 hash of passphrase + Secret Key into a single opaque PBKDF2 input. Used internally by wrap/unwrap/rekey. |
| Function | Description |
|---|---|
| generateECDHKeyPair() | Generate an ECDH P-256 key pair for message encryption. |
| sealMessage(msg, recipientPub) | Encrypt a message for a recipient's public key. Ephemeral ECDH + AES-GCM. |
| unsealMessage(sealed, privateKey) | Decrypt using the recipient's private key. |
| Function | Description |
|---|---|
| generateECDSAKeyPair() | Generate an ECDSA P-256 key pair for signing. |
| sign(data, privateKey) | Sign arbitrary bytes. Returns a DER-encoded signature. |
| verify(data, sig, publicKey) | Verify a signature. Returns boolean. |
| Function | Description |
|---|---|
| sealEnvelope(data, passphrase) | Encrypt data under a passphrase. Derives key via PBKDF2, encrypts with AES-GCM. Returns a SealedEnvelope. |
| unsealEnvelope(env, passphrase) | Decrypt a SealedEnvelope. Throws on wrong passphrase. |
| encodeEnvelope(env) | Serialise a SealedEnvelope to a URL-safe Base64 string for transport. |
| decodeEnvelope(str) | Deserialise from a URL-safe Base64 string back to a SealedEnvelope. |
| Function | Description |
|---|---|
| initCircle(passphrase, secretKey) | Founding device generates the shared AEK and wraps it. Returns wrappedAEK, aekCommitment, and deviceId. |
| createJoinRequest(deviceMeta?) | New device generates an ephemeral ECDH key pair. Returns the join request payload, the ephemeral private key (hold in memory only), and a verificationCode to display prominently. |
| authorizeJoin(joinRequest, wrappedAEK, passphrase, secretKey) | Trusted device unwraps its AEK and seals it for the new device via ECDH. Rejects requests older than 5 minutes. |
| finalizeJoin(sealedAEK, ephemeralPrivateKey, passphrase, secretKey, aekCommitment) | New device unseals the AEK, verifies it against aekCommitment, and re-wraps it under its own credentials. Throws on commitment mismatch. |
| deriveVerificationCode(ephemeralPublicKey) | Derives a XX-XX-XX hex verification code from a public key. Both devices must display matching codes before approval to prevent MITM. |
| Function | Module | Description |
|---|---|---|
| init(wrapped, salt, pass, secretKey?) | conseal | Unwrap the AEK with passphrase (+ optional Secret Key) and persist it to IndexedDB under AEK_KEY_ID. |
| exportPublicKeyAsJwk(key) | conseal | Serialise a public CryptoKey to a JWK object for distribution. |
| importPublicKeyFromJwk(jwk) | conseal | Restore a CryptoKey from a JWK object. |
| generateMnemonic(aek) | conseal | Derive a BIP-39 mnemonic from an AES key for human-readable backup. |
| recoverWithMnemonic(phrase) | conseal | Reconstruct an AES key from a BIP-39 mnemonic and persist it. |
| digest(data) | conseal | SHA-256 hash. Returns Uint8Array. |
| toBase64 / fromBase64 | conseal | Standard Base64 encode / decode. |
| toBase64Url / fromBase64Url | conseal | URL-safe Base64 (no +/=) encode / decode. |
| saveCryptoKey / loadCryptoKey / deleteCryptoKey | conseal | Read/write/delete a CryptoKey from IndexedDB by string id. Used internally by init(). |
The root symmetric key for a device. Generated by init(), stored in IndexedDB as a non-exportable CryptoKey. Can be re-exported via mnemonic for cross-device recovery.
A self-contained encrypted parcel with embedded salt and IV. Designed for passcode-based sharing where no pre-shared key exists — the passphrase is the only credential.
sealMessage encrypts to a known recipient's public key — ideal for registered users. sealEnvelope encrypts to a passphrase — ideal for anonymous or one-time recipients.
The interactive demo covers all seven modules. No server, no sign-up — just open your DevTools and watch the encryption happen.
Cryptography is dense with abbreviations. Here is what every acronym in conseal means, how it is used, and why it was chosen.
Advanced Encryption Standard · 256-bit key · Galois/Counter Mode
The symmetric cipher used for all data encryption in conseal. AES operates on 128-bit blocks; a 256-bit key makes exhaustive-search attacks computationally infeasible. GCM is an authenticated mode that simultaneously encrypts and produces an integrity tag — any tampering causes decryption to fail with an explicit error rather than silently returning corrupt data.
Used by: seal, unseal, sealMessage, sealEnvelope
Password-Based Key Derivation Function 2 (RFC 8018)
Stretches a human passphrase into a fixed-length cryptographic key by running HMAC-SHA-256 many times in sequence. conseal uses 600,000 iterations — deliberately slow so that an attacker who captures a wrapped key must spend enormous computation to guess the passphrase. A random salt ensures identical passphrases produce different keys.
Used by: wrapKey, unwrapKey, sealEnvelope
Elliptic Curve Diffie-Hellman · NIST P-256 curve
A key-agreement protocol that lets two parties independently derive the same shared secret using only their public keys — no prior shared secret required. conseal generates an ephemeral key pair per message, derives a one-time AES key from it, then discards the ephemeral private key, providing forward secrecy: compromising a long-term key does not expose past messages.
Used by: sealMessage, unsealMessage
Elliptic Curve Digital Signature Algorithm · NIST P-256 curve
A digital signature scheme that proves data was produced by the holder of a specific private key — without revealing that key. Anyone with the corresponding public key can verify the signature. P-256 gives strong security in a compact 64-byte signature and is natively supported by the Web Crypto API in all modern browsers.
Used by: sign, verify
Bitcoin Improvement Proposal 39
A standard that encodes binary entropy as a sequence of common English words drawn from a fixed 2,048-word list. The resulting phrase is far easier to write down and transcribe accurately than a raw hex key. conseal uses 24-word phrases (256 bits of entropy + 8-bit checksum) to let users back up and restore their AEK across devices.
Used by: generateMnemonic, recoverWithMnemonic
JSON Web Key (RFC 7517)
A standardised JSON representation for cryptographic keys. It encodes the key type, algorithm parameters, and key material in a portable, human-readable object. conseal uses JWK to distribute ECDH and ECDSA public keys so that any Web Crypto-capable environment can import them without custom serialisation logic.
Used by: exportPublicKeyAsJwk, importPublicKeyFromJwk
Account Encryption Key
The root AES-256-GCM key for a device, generated by init() and stored in IndexedDB as a non-extractable CryptoKey. All application data is encrypted with the AEK. It never leaves the device in plaintext — it is always transported wrapped under a PBKDF2-derived key, so the server sees only ciphertext it cannot read.
Used by: init, wrapKey, generateMnemonic
Initialisation Vector · Number Used Once
A random byte sequence prepended to every ciphertext. Encrypting the same plaintext twice with the same key but different IVs produces completely different ciphertexts, preventing pattern analysis. In GCM, the IV must never be reused under the same key; conseal generates a fresh cryptographically secure random IV per operation using crypto.getRandomValues.
Format: 12-byte random prefix embedded in every seal output; stripped automatically on decrypt.
ECMAScript Modules
The native JavaScript module system, using import / export syntax. conseal is ESM-only, which enables tree-shaking: bundlers can statically determine which functions are actually used and drop everything else, keeping your production bundle as small as possible.
Requires: Node 18+, a modern bundler (Vite, esbuild, webpack 5), or a browser supporting native ESM with type="module".