Skip to content

xero/leviathan-crypto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Release npm package minimized gzipped size GitHub Actions Workflow Status GitHub Actions Workflow Status

simd webassembly side-effect free tree-shakeable zero dependencies MIT Licensed

Leviathan logo

Leviathan Crypto: post-quantum WASM cryptography

JS is the problem, SIMD WASM is the solution. JavaScript engines offer no formal constant-time guarantees. JIT compilers optimize based on runtime patterns, which leak secrets through cache access and instruction timing. By contrast, WebAssembly executes outside the JIT entirely, running compiled bytecode with linear memory you control. No speculative optimization, no value-dependent branches between source and execution.

WebAssembly is the correctness layer. All algorithm logic lives in WASM. Six AssemblyScript modules (serpent, chacha20, sha2, sha3, kyber, and ct) compile independently to WASM with SIMD where it pays off. Each module is its own instance with its own linear memory. Within a module, stateful primitives share the instance, and a runtime exclusivity model keeps them from interfering with each other.

TypeScript is the ergonomics layer. The strongly-typed public API covers Seal, SealStream, SealStreamPool, Fortuna, HKDF, SkippedKeyStore, and others. The design is misuse-resistant by default. Authentication is verify-then-decrypt; key material wipes on dispose; validation runs before any crypto path; one-shot AEADs lock on first call. TypeScript never implements cryptographic algorithms. It orchestrates the WASM layer and enforces best practice through API shape, not convention.

Serpent-256: maximum paranoia. 32 rounds of S-boxes in pure Boolean logic with no table lookups. An ouroboros devouring every bit, in every block, through every round.

XChaCha20-Poly1305: precise elegance. 20 rounds of add-rotate-XOR, choreography without S-boxes or cache-timing leakage. A dance closing with Poly1305's unconditional forgery bound.

Two ciphers, one interface. Both share the CipherSuite shape and slot into Seal, SealStream, and SealStreamPool interchangeably. Post-quantum extends the same model, KyberSuite wraps MlKem512, MlKem768, or MlKem1024 around either cipher, and the SPQR ratchet builds forward-secret sessions on top.

Zero dependencies. No npm graph to audit. No supply chain attack surface.

Tree-shakeable. Import only what you use. Subpath exports let bundlers exclude everything else.

Side-effect free. Nothing runs on import. init() is explicit and asynchronous.

Audited primitives. Every implementation is verified against its specification.


Architecture: TypeScript over WASM

Typescript Over Wasm Layered Diagram

The TypeScript layer never implements cryptographic algorithms. It manages the boundary between JavaScript and WebAssembly by writing inputs into WASM linear memory, calling exported functions, and reading back outputs. All algorithm logic resides within AssemblyScript.

Higher-level classes like Seal, SealStream, and SealStreamPool are pure TypeScript, but they compose WASM-backed primitives (Serpent-CBC, HMAC-SHA256, ChaCha20-Poly1305, and HKDF-SHA256) rather than implementing new cryptographic logic. TypeScript orchestrates, while WASM computes. Pool workers instantiate their own WASM modules and directly call primitives, bypassing the main-thread module cache.


Installation

# use bun
bun i leviathan-crypto
# or npm
npm install leviathan-crypto

Important

Serpent, ChaCha20, ML-KEM, and constantTimeEqual require WebAssembly SIMD support. This has been a baseline feature of all major browsers and runtimes since 2021.

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

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 kyber + sha3
import { kyberInit } from 'leviathan-crypto/kyber'
import { kyberWasm } from 'leviathan-crypto/kyber/embedded'
import { sha3Init } from 'leviathan-crypto/sha3'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'

await kyberInit(kyberWasm)
await sha3Init(sha3Wasm)
Subpath Entry point
leviathan-crypto ./dist/index.js
leviathan-crypto/stream ./dist/stream/index.js
leviathan-crypto/serpent ./dist/serpent/index.js
leviathan-crypto/serpent/embedded ./dist/serpent/embedded.js
leviathan-crypto/chacha20 ./dist/chacha20/index.js
leviathan-crypto/chacha20/embedded ./dist/chacha20/embedded.js
leviathan-crypto/sha2 ./dist/sha2/index.js
leviathan-crypto/sha2/embedded ./dist/sha2/embedded.js
leviathan-crypto/sha3 ./dist/sha3/index.js
leviathan-crypto/sha3/embedded ./dist/sha3/embedded.js
leviathan-crypto/kyber ./dist/kyber/index.js
leviathan-crypto/kyber/embedded ./dist/kyber/embedded.js
leviathan-crypto/ratchet ./dist/ratchet/index.js

See the WASM loading reference for details.


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? KyberSuite 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 { KyberSuite, MlKem768 } from 'leviathan-crypto/kyber'
import { kyberWasm }    from 'leviathan-crypto/kyber/embedded'
import { sha3Wasm }     from 'leviathan-crypto/sha3/embedded'

await init({ kyber: kyberWasm, sha3: sha3Wasm, chacha20: chacha20Wasm, sha2: sha2Wasm })

const suite = KyberSuite(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)

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 kyber + 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 Argon2id 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 Argon2id parameters are all intentional examples worth studying.

chat [ demo · source · readme ]

End-to-end encrypted chat over X25519 key exchange and XChaCha20-Poly1305 message encryption. The relay server is a dumb WebSocket pipe that never sees plaintext. Messages carry sequence numbers so the protocol detects and rejects replayed messages. The demo deconstructs the protocol step by step with visual feedback for injection and replay attacks.

cli [ npm · source · readme ]

Command-line file encryption tool supporting both Serpent-256 and XChaCha20-Poly1305 via --cipher. A single keyfile works with both 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.

COVCOM [ demo · source · readme ]

A covert communications application for end-to-end encrypted group conversations. Share an invite, talk, exit, and it's gone. Clients available for both the web and cli, along with a containerized dumb server for managing rooms. No secrets or cleartext beyond the handle you chose to join a room with are ever visible to the server. Featuring sparse post-quantum ratcheting, ML-KEM-768, KDFChains, Seal+KyberSuite, and a XChaCha20-Poly1305 core.


Highlights

I want to...
Encrypt data Seal with SerpentCipher or XChaCha20Cipher
Encrypt a stream or large file SealStream to encrypt, OpenStream to decrypt
Encrypt in parallel SealStreamPool distributes chunks across Web Workers
Add post-quantum security KyberSuite wraps MlKem512, MlKem768, or MlKem1024 with any cipher suite
Build a forward-secret session ratchetInit, KDFChain, kemRatchetEncap / kemRatchetDecap, SkippedKeyStore
Hash data SHA256, SHA384, SHA512, SHA3_256, SHA3_512, SHAKE256 ...
Authenticate a message HMAC_SHA256, HMAC_SHA384, or HMAC_SHA512
Derive keys HKDF_SHA256 or HKDF_SHA512
Generate random bytes Fortuna for forward-secret generation, randomBytes for one-off use
Compare secrets safely constantTimeEqual uses a WASM SIMD path to prevent timing attacks
Work with bytes hexToBytes, bytesToHex, wipe, xor, concat ...

For raw primitives, low-level cipher access, and ASM internals see the full API reference.


Going deeper

Document
Architecture Repository structure, module relationships, build pipeline, and buffer layouts
Test Suite How the test suite works, vector corpus, and gate discipline
Security Policy Security posture and vulnerability disclosure details
Lexicon Glossary of cryptographic terms
WASM Primer WebAssembly primer in the context of this library
CDN Use leviathan-crypto directly from a CDN with no bundler
argon2id Passphrase-based encryption using Argon2id alongside leviathan primitives

License

leviathan-crypto is released under the MIT license.

                ▄▄▄▄▄▄▄▄▄▄
         ▄████████████████████▄▄
      ▄██████████████████████ ▀████▄
    ▄█████████▀▀▀     ▀███████▄▄███████▌
   ▐████████▀   ▄▄▄▄     ▀████████▀██▀█▌
   ████████      ███▀▀     ████▀  █▀ █▀
   ███████▌    ▀██▀         ██
    ███████   ▀███           ▀██ ▀█▄
     ▀██████   ▄▄██            ▀▀  ██▄
       ▀█████▄   ▄██▄             ▄▀▄▀
          ▀████▄   ▄██▄
            ▐████   ▐███
     ▄▄██████████    ▐███         ▄▄
  ▄██▀▀▀▀▀▀▀▀▀▀     ▄████      ▄██▀
▄▀  ▄▄█████████▄▄  ▀▀▀▀▀     ▄███
 ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀
████▀    ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀  ▄▄▄▄
█████▄▄█████▀▀▀▀▀▀▄ ▀███▄      ▄███▀
▀██████▀             ▀████▄▄▄████▀
                        ▀█████▀

About

Post-quantum SIMD WASM cryptography with paranoid ciphers (Serpent-256, XChaCha20-Poly1305), ML-KEM, and forward-secret ratcheting. Zero dependencies. Constant-time verified and strictly typed.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors