Skip to content

xero/leviathan-crypto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 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

WebAssembly cryptography library built on the paranoia of Serpent-256 and the elegance of XChaCha20-Poly1305.

Serpent-256 won the AES security vote but lost the competition. The deciding factor was performance on embedded hardware, a constraint that doesn't apply to modern software. 32 rounds. S-boxes in pure Boolean logic with no table lookups. It processes every bit in every block. You use it because you trust the cryptanalysis, not because a committee endorsed it.

XChaCha20-Poly1305 has nothing to hide. An ARX construction with 20 rounds. Add, rotate, XOR with no S-boxes or cache-timing leakage. It needs no hardware acceleration to be fast. Poly1305 adds an unconditional forgery bound. The security proof is readable.

Two ciphers from opposite design philosophies, only agreeing on security properties.

WebAssembly is the correctness layer. Every primitive runs in its own isolated binary with its own linear memory. Execution is deterministic with no JIT speculation. Key material in one module can't interact with another, even in principle. See the security policy.

TypeScript is the ergonomics layer. The Seal and SealStream family are cipher-agnostic. Drop in SerpentCipher or XChaCha20Cipher and they handle nonces, key derivation, and authentication for you. Explicit init() gates give you full control over how and when WASM loads. Strict typing catches misuse before it reaches production.

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. See the audit index.


Installation

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

Note

Serpent, ChaCha20, ML-KEM, and constantTimeEqual require WebAssembly SIMD support. This has been a baseline feature of all major browsers and runtimes since 2021. SHA-2 and SHA-3 run on any WASM-capable runtime.

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

See loader.md for the full WASM loading reference.


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)
const pt   = Seal.decrypt(XChaCha20Cipher, key, blob)  // throws AuthenticationError on tamper

Prefer Serpent-256? Swap the cipher object and everything else stays the same.

import { init, Seal, SerpentCipher } from 'leviathan-crypto'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
import { sha2Wasm }     from 'leviathan-crypto/sha2/embedded'

await init({ serpent: serpentWasm, sha2: sha2Wasm })

const key  = SerpentCipher.keygen()
const blob = Seal.encrypt(SerpentCipher, key, plaintext)

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)

Looking for more examples including hashing, key derivation, Fortuna, and raw primitives? See the examples page.


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 it's own interactive competitions 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.


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

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

Zero-dependency WASM cryptography for TypeScript. Paranoid ciphers, post-quantum key encapsulation, and Fortuna CSPRNG behind a strictly typed API. All computation runs outside the JS JIT on vector-verified primitives.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Contributors