Installation
# use bun
bun i leviathan-crypto
# or npm
npm install leviathan-crypto
v3 is the current stable line; semver applies. Runs in modern browsers, Node.js 22+, Bun, Deno, and Cloudflare Workers.
SIMD throughput on Apple Silicon peaks at ~1.3 GB/s for ChaCha20 and ~40 MB/s for Serpent, single-threaded; 1.2-3.2× over scalar. Full matrix across V8, SpiderMonkey, and JSC in benchmarks.
Note
Found a security issue? Don't open a public issue. See SECURITY.md for the disclosure policy.
Loading
Three loading strategies are available. Choose based on your runtime and bundler setup.
import { init } from 'leviathan-crypto'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
// Embedded: gzip+base64 blobs bundled in the package
await init({ serpent: serpentWasm, sha2: sha2Wasm })
// URL: streaming compilation from a served .wasm file
await init({ serpent: new URL('/assets/wasm/serpent.wasm', import.meta.url) })
// Pre-compiled: pass a WebAssembly.Module directly (edge runtimes, KV cache)
const compiledModule = await WebAssembly.compileStreaming(fetch('/assets/wasm/serpent.wasm'))
await init({ serpent: compiledModule })
All three patterns also work straight from a CDN with no install or bundler:
<script type="module">
import { init, Seal, SerpentCipher } from 'https://unpkg.com/leviathan-crypto/dist/index.js'
import { serpentWasm } from 'https://unpkg.com/leviathan-crypto/dist/serpent/embedded.js'
import { sha2Wasm } from 'https://unpkg.com/leviathan-crypto/dist/sha2/embedded.js'
await init({ serpent: serpentWasm, sha2: sha2Wasm })
// ... use as normal
</script>
See the CDN reference for unpkg/esm.sh, version pinning, SRI, and import maps.
Tree-shaking with subpath imports
Each module ships as its own subpath export. Bundlers with tree-shaking support and "sideEffects": false drop every module you don't import.
// Only serpent.wasm + sha2.wasm end up in your bundle
import { serpentInit } from 'leviathan-crypto/serpent'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
import { sha2Init } from 'leviathan-crypto/sha2'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
await serpentInit(serpentWasm)
await sha2Init(sha2Wasm)
// ML-KEM requires mlkem + sha3
import { mlkemInit } from 'leviathan-crypto/mlkem'
import { mlkemWasm } from 'leviathan-crypto/mlkem/embedded'
import { sha3Init } from 'leviathan-crypto/sha3'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await mlkemInit(mlkemWasm)
await sha3Init(sha3Wasm)
Real bundle sizes (esbuild minified + gzip):
| Use case | gzip bundle |
Seal + XChaCha20Cipher | ~17 KB |
Seal + SerpentCipher | ~29 KB |
| Merkle log + ML-DSA-44 cosig | ~29 KB |
| Full root barrel (every export) | ~53 KB |
| Subpath | Module |
leviathan-crypto | root barrel (all exports) |
leviathan-crypto/stream | cipher-agnostic seal layer |
leviathan-crypto/serpent | Serpent-256 |
leviathan-crypto/serpent/embedded | Serpent-256 WASM blob |
leviathan-crypto/chacha20 | XChaCha20-Poly1305 |
leviathan-crypto/chacha20/embedded | XChaCha20-Poly1305 WASM blob |
leviathan-crypto/sha2 | SHA-2 family (224 / 256 / 384 / 512, HMAC, HKDF) |
leviathan-crypto/sha2/embedded | SHA-2 WASM blob |
leviathan-crypto/sha3 | SHA-3 / SHAKE family |
leviathan-crypto/sha3/embedded | SHA-3 WASM blob |
leviathan-crypto/keccak | Keccak alias for SHA-3 |
leviathan-crypto/keccak/embedded | Keccak WASM blob (same bytes as sha3/embedded) |
leviathan-crypto/mlkem | ML-KEM |
leviathan-crypto/mlkem/embedded | ML-KEM WASM blob |
leviathan-crypto/aes | AES-256-GCM-SIV |
leviathan-crypto/aes/embedded | AES WASM blob |
leviathan-crypto/blake3 | BLAKE3 |
leviathan-crypto/blake3/embedded | BLAKE3 WASM blob |
leviathan-crypto/ecdsa | ECDSA-P256 |
leviathan-crypto/ecdsa/embedded | NIST P-256 WASM blob |
leviathan-crypto/ed25519 | Ed25519 (pure and Ed25519ph) |
leviathan-crypto/ed25519/embedded | Curve25519 WASM blob |
leviathan-crypto/mldsa | ML-DSA |
leviathan-crypto/mldsa/embedded | ML-DSA WASM blob |
leviathan-crypto/slhdsa | SLH-DSA |
leviathan-crypto/slhdsa/embedded | SLH-DSA WASM blob |
leviathan-crypto/x25519 | X25519 (Curve25519 Diffie-Hellman) |
leviathan-crypto/x25519/embedded | Curve25519 WASM blob (same bytes as ed25519/embedded) |
leviathan-crypto/ratchet | forward-secret ratchet (SPQR) |
leviathan-crypto/sign | scheme-agnostic signature layer |
leviathan-crypto/merkle | Merkle log substrate |
Subpaths resolve to ./dist/<mod>/index.js and ./dist/<mod>/embedded.js.
Quick Start
One-shot authenticated encryption. Seal handles nonces, key derivation, and authentication. Zero config beyond init().
import {
init,
Seal,
XChaCha20Cipher,
} from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
const key = XChaCha20Cipher.keygen()
const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
// throws AuthenticationError on tamper
const pt = Seal.decrypt(XChaCha20Cipher, key, blob)
Prefer Serpent-256? Swap the cipher and its module.
import {
init,
Seal,
XChaCha20Cipher,
SerpentCipher,
} from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })
await init({ serpent: serpentWasm, sha2: sha2Wasm })
const key = XChaCha20Cipher.keygen()
const key = SerpentCipher.keygen()
const blob = Seal.encrypt(XChaCha20Cipher, key, plaintext)
const blob = Seal.encrypt(SerpentCipher, key, plaintext)
// throws AuthenticationError on tamper
const pt = Seal.decrypt(XChaCha20Cipher, key, blob)
const pt = Seal.decrypt(SerpentCipher, key, blob)
Data too large to buffer in memory? SealStream and OpenStream encrypt and decrypt in chunks without loading the full message.
import { SealStream, OpenStream } from 'leviathan-crypto/stream'
const sealer = new SealStream(XChaCha20Cipher, key, { chunkSize: 65536 })
const preamble = sealer.preamble // send first
const ct0 = sealer.push(chunk0)
const ct1 = sealer.push(chunk1)
const ctLast = sealer.finalize(lastChunk)
const opener = new OpenStream(XChaCha20Cipher, key, preamble)
const pt0 = opener.pull(ct0)
const pt1 = opener.pull(ct1)
const ptLast = opener.finalize(ctLast)
Need parallel throughput? SealStreamPool distributes chunks across Web Workers with the same wire format.
import { SealStreamPool } from 'leviathan-crypto/stream'
const pool = await SealStreamPool.create(XChaCha20Cipher, key, { wasm: chacha20Wasm })
const encrypted = await pool.seal(plaintext)
const decrypted = await pool.open(encrypted)
pool.destroy()
Want post-quantum security? MlKemSuite wraps ML-KEM and a cipher suite into a hybrid construction. It plugs directly into SealStream. The sender encrypts with the public encapsulation key and only the recipient's private decapsulation key can open it.
import { MlKemSuite, MlKem768 } from 'leviathan-crypto/mlkem'
import { mlkemWasm } from 'leviathan-crypto/mlkem/embedded'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ mlkem: mlkemWasm, sha3: sha3Wasm, chacha20: chacha20Wasm, sha2: sha2Wasm })
const suite = MlKemSuite(new MlKem768(), XChaCha20Cipher)
const { encapsulationKey: ek, decapsulationKey: dk } = suite.keygen()
// sender: encrypts with the public key
const sealer = new SealStream(suite, ek)
const preamble = sealer.preamble // 1108 bytes: 20B header + 1088B KEM ciphertext
const ct0 = sealer.push(chunk0)
const ctLast = sealer.finalize(lastChunk)
// recipient: decrypts with the private key
const opener = new OpenStream(suite, dk, preamble)
const pt0 = opener.pull(ct0)
const ptLast = opener.finalize(ctLast)
Need post-quantum signatures? The sign module wraps ML-DSA (FIPS 204) behind a SignatureSuite abstraction. Sign covers single-shot attached / detached signatures; SignStream and VerifyStream handle chunked input via HashML-DSA.
import { init, Sign, SignStream, MlDsa65Suite, MlDsa65PreHashSuite } from 'leviathan-crypto'
import { mldsaWasm } from 'leviathan-crypto/mldsa/embedded'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ mldsa: mldsaWasm, sha3: sha3Wasm })
const { pk, sk } = MlDsa65Suite.keygen()
const msg = new TextEncoder().encode('hello world')
const ctx = new TextEncoder().encode('myapp/v1')
// single-shot
const blob = Sign.sign(MlDsa65Suite, sk, msg, ctx)
const payload = Sign.verify(MlDsa65Suite, pk, blob, ctx)
// streamed (over chunked input)
const signer = new SignStream(MlDsa65PreHashSuite, sk, ctx)
signer.update(chunk1)
signer.update(chunk2)
const sig = signer.finalize()
// wire output is signer.preamble + chunk1 + chunk2 + sig
Six ML-DSA suites ship: MlDsa44Suite / MlDsa65Suite / MlDsa87Suite for pure ML-DSA, and MlDsa44PreHashSuite / MlDsa65PreHashSuite / MlDsa87PreHashSuite for HashML-DSA. See the signing reference for the wire format and error reference, and the signaturesuite reference for the full 22-entry catalog.
Want belt-and-suspenders post-quantum signatures? Three PQ-only hybrid suites pair ML-DSA (lattice) with SLH-DSA (hash-based) at each NIST security category. The combined signature is secure as long as either family holds; a future break in one PQ assumption does not transfer to the other. The wire is one combined byte string the receiver verifies through the same Sign entry points.
import { init, Sign, MlDsa65SlhDsa192fSuite } from 'leviathan-crypto'
import { mldsaWasm } from 'leviathan-crypto/mldsa/embedded'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
import { slhdsaWasm } from 'leviathan-crypto/slhdsa/embedded'
await init({ mldsa: mldsaWasm, sha3: sha3Wasm, slhdsa: slhdsaWasm })
const { pk, sk } = MlDsa65SlhDsa192fSuite.keygen()
const msg = new TextEncoder().encode('release manifest v1.2.3')
const ctx = new TextEncoder().encode('release-signing/v1')
const blob = Sign.sign (MlDsa65SlhDsa192fSuite, sk, msg, ctx)
const payload = Sign.verify(MlDsa65SlhDsa192fSuite, pk, blob, ctx)
// throws SigningError if either half fails to verify
Three hybrid suites ship at the matching NIST categories: MlDsa44SlhDsa128fSuite (category 1), MlDsa65SlhDsa192fSuite (category 3), MlDsa87SlhDsa256fSuite (category 5). The PQ-only hybrids complement the planned classical+PQ hybrids; the two families defend against different threat models and the signaturesuite reference covers when to pick which.
Need classical ECDSA for X.509, JWS, or TLS interop? EcdsaP256Suite wraps ECDSA over NIST P-256 (FIPS 186-5 §6) with SHA-256 prehash baked in. Hedged-by-default per draft-irtf-cfrg-det-sigs-with-noise-05, low-S enforced on signer and verifier per RFC 6979 §3.5. Wire bytes are 64-byte raw r || s; the ecdsaSignatureToDer / ecdsaSignatureFromDer helpers convert between raw and the RFC 3279 §2.2.3 DER form for ecosystem interop.
import { init, Sign, EcdsaP256Suite, ecdsaSignatureToDer } from 'leviathan-crypto'
import { p256Wasm } from 'leviathan-crypto/ecdsa/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
await init({ p256: p256Wasm, sha2: sha2Wasm })
const { pk, sk } = EcdsaP256Suite.keygen()
const msg = new TextEncoder().encode('hello world')
const sig = Sign.signDetached(EcdsaP256Suite, sk, msg, new Uint8Array(0))
const ok = Sign.verifyDetached(EcdsaP256Suite, pk, msg, sig, new Uint8Array(0))
const der = ecdsaSignatureToDer(sig) // X.509 / JWS / TLS interop
ECDSA-P256 is classical (not post-quantum); pair it with an ML-DSA or SLH-DSA suite when the threat model assumes a future CRQC. ECDSA has no native context parameter, so EcdsaP256Suite rejects non-empty user_ctx; the reserved classical+PQ hybrid suites at 0x22 / 0x23 will provide context-bound classical+PQ signing.
Building a secure messenger? The ratchet module provides Sparse Post-Quantum Ratchet primitives for consumers who need forward secrecy and post-compromise security at the session layer. ratchetInit bootstraps the symmetric chains, KDFChain derives per-message keys, kemRatchetEncap / kemRatchetDecap perform the ML-KEM ratchet step, and SkippedKeyStore handles out-of-order delivery.
import { ratchetInit, KDFChain } from 'leviathan-crypto/ratchet'
await init({ sha2: sha2Wasm }) // KDF layer only; add mlkem + sha3 for KEM steps
const { nextRootKey, sendChainKey, recvChainKey } = ratchetInit(sharedSecret)
const chain = new KDFChain(sendChainKey)
const { key: messageKey, counter } = chain.stepWithCounter()
// encrypt a message with messageKey; include counter in the wire header
These are the primitives, not a full session. You compose them into your transport, header format, and epoch orchestration. See the ratchet guide for full construction details.
Looking for examples of hashing, key derivation, Fortuna, and raw primitives? See examples.
Demos
web [ demo · source · readme ]
A self-contained browser encryption tool in a single HTML file. Encrypt text or files with Serpent-256-CBC and scrypt key derivation, then share the armored output. No server, no install, no network connection after initial load. The code is written to be read. The Encrypt-then-MAC construction, HMAC input, and scrypt parameters are all intentional examples worth studying.
tamper [ demo · source · readme ]
A crypto attack-resilience demo. It runs a real two-party encrypted channel, then lets you attack it: forge a replay and the sequence check rejects it, tamper with a frame and the Poly1305 tag fails. Key exchange uses X25519 with HKDF-SHA256, message encryption uses XChaCha20-Poly1305, and the relay server is a dumb WebSocket pipe that never sees plaintext. The demo deconstructs the protocol step by step with visual feedback for injection and replay attacks. For a real, production-ready secure messenger built on the same library, see COVCOM.
cli [ npm · source · readme ]
Command-line file encryption tool supporting Serpent-256-CBC+HMAC-SHA256, XChaCha20-Poly1305, and AES-256-GCM-SIV via --cipher. A single keyfile works with all three ciphers. The header byte determines decryption automatically. Chunks distribute across a worker pool sized to hardwareConcurrency. Each worker owns an isolated WASM instance with no shared memory. The tool can export its own interactive completions for a variety of shells.
bun add -g lvthn
lvthn keygen --armor -o my.key
cat secret.txt | lvthn encrypt -k my.key --armor > secret.enc
kyber [ demo · source · readme ]
Post-quantum cryptography demo simulating a complete ML-KEM key encapsulation ceremony between two browser-side clients. A live wire at the top of the page logs every value that crosses the channel; importantly, the shared secret never appears in the wire. After the ceremony completes, both sides independently derive a symmetric key using HKDF-SHA256 and exchange messages encrypted with XChaCha20-Poly1305. Each wire frame is expandable, revealing the raw nonce, ciphertext, Poly1305 tag, and AAD.
jwt [ demo · source · readme ]
Classical and post-quantum JSON Web Token signing demo in a single self-contained HTML file. It signs the same claims across eleven algorithms: EdDSA and ES256, the post-quantum ML-DSA and SLH-DSA families, and the leviathan hybrid composites. Every algorithm runs through one uniform path on the Sign suite API, with no per-algorithm branching. The token renders with its three segments color-coded and a live byte readout, so the cost of quantum resistance is visible: the same token grows from about 220 bytes under Ed25519 to past 66 kilobytes under SLH-DSA-SHAKE-256f. Tamper with the payload and verification rejects it, because the signature covers the original bytes.
COVCOM [ demo · source · wiki ]
Covert communications app suite for private group conversations. Invite, talk, close the client, and the chat vanishes. Every message is encrypted with XChaCha20 and signed with Ed25519. A BLAKE3 fingerprint on each key allows peers to verify one another. SPQR's manual and epoch ratchets add forward secrecy, while post-quantum ML-KEM-768 encapsulation keeps recorded communications unreadable and secure against future cryptanalysis.