examples - xero/leviathan-crypto GitHub Wiki

Examples

This document covers the library's full API surface with working examples. Follow the section header links for complete API documentation on each class.


High Level API

It provides safe defaults with authentication built in.

Seal: one-shot authenticated encryption

Use Seal for one-shot AEAD with any CipherSuite. Pass the cipher, the key, and the plaintext; no instantiation or dispose() call is needed.

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, new TextEncoder().encode('Authenticated secret message.'))
const pt   = Seal.decrypt(XChaCha20Cipher, key, blob)  // throws on tamper

console.log(new TextDecoder().decode(pt))
// => "Authenticated secret message."

It works identically with SerpentCipher:

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)
const pt   = Seal.decrypt(SerpentCipher, key, blob)

KyberSuite: post-quantum hybrid encryption

KyberSuite wraps an ML-KEM instance and a CipherSuite into a hybrid KEM+AEAD suite. Each call to encrypt creates a fresh shared secret via the KEM, then the inner cipher performs AEAD. The KEM ciphertext is prepended to the blob automatically.

import { init, Seal, KyberSuite, MlKem768, XChaCha20Cipher } from 'leviathan-crypto'
import { kyberWasm }    from 'leviathan-crypto/kyber/embedded'
import { sha3Wasm }     from 'leviathan-crypto/sha3/embedded'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
import { sha2Wasm }     from 'leviathan-crypto/sha2/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 blob = Seal.encrypt(suite, ek, plaintext)

// recipient — decrypts with the private key
const pt = Seal.decrypt(suite, dk, blob)

SealStream / OpenStream: streaming encryption

For data arriving in chunks, such as network streams, file processors, or live feeds, use SealStream and OpenStream. The cipher plugs in identically to Seal.

import { init, SealStream, OpenStream, XChaCha20Cipher, randomBytes } 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      = randomBytes(32)
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 ctFinal = sealer.finalize(lastChunk)

// recipient
const opener  = new OpenStream(XChaCha20Cipher, key, preamble)
const pt0     = opener.pull(ct0)
const pt1     = opener.pull(ct1)
const ptFinal = opener.finalize(ctFinal)

To use Serpent-256, swap the cipher object; everything else stays the same:

import { init, SealStream, OpenStream, SerpentCipher, randomBytes } 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      = randomBytes(32)
const sealer   = new SealStream(SerpentCipher, key, { chunkSize: 65536 })
const preamble = sealer.preamble

const ct0    = sealer.push(chunk0)
const ct1    = sealer.push(chunk1)
const ctLast = sealer.finalize(lastChunk)

const opener = new OpenStream(SerpentCipher, key, preamble)
const pt0    = opener.pull(ct0)
const pt1    = opener.pull(ct1)
const ptLast = opener.finalize(ctLast)

SealStreamPool: parallel batch encryption

SealStreamPool distributes chunks across Web Workers. It produces the same wire format as SealStream and works as a drop-in for large files or batch workloads.

import { init, SealStreamPool, XChaCha20Cipher, randomBytes } 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  = randomBytes(32)
const pool = await SealStreamPool.create(XChaCha20Cipher, key, {
  wasm: chacha20Wasm, chunkSize: 65536,
})
const ciphertext = await pool.seal(plaintext)
const decrypted  = await pool.open(ciphertext)
pool.destroy()

Post-quantum key encapsulation with ML-KEM

ML-KEM provides post-quantum key encapsulation. The sender encapsulates a shared secret to the recipient's public encapsulation key. The recipient recovers the same shared secret using their private decapsulation key.

import { init, MlKem768 } from 'leviathan-crypto'
import { kyberWasm } from 'leviathan-crypto/kyber/embedded'
import { sha3Wasm }  from 'leviathan-crypto/sha3/embedded'

await init({ kyber: kyberWasm, sha3: sha3Wasm })

const kem = new MlKem768()

// keygen — once, store securely
const { encapsulationKey, decapsulationKey } = kem.keygen()

// sender — has only encapsulationKey
const { ciphertext, sharedSecret: senderSecret } = kem.encapsulate(encapsulationKey)

// recipient — has decapsulationKey
const recipientSecret = kem.decapsulate(decapsulationKey, ciphertext)

// senderSecret and recipientSecret are identical 32-byte values
// use as a symmetric key input: derive with HKDF or pass directly to KyberSuite
kem.dispose()

To combine classical and post-quantum security, pair an X25519 shared secret with an ML-KEM shared secret. Both must be broken simultaneously for an attacker to succeed. X25519 is not in this library; use WebCrypto or a dedicated library for it.

import { init, MlKem768, HKDF_SHA256 } from 'leviathan-crypto'
import { kyberWasm } from 'leviathan-crypto/kyber/embedded'
import { sha3Wasm }  from 'leviathan-crypto/sha3/embedded'
import { sha2Wasm }  from 'leviathan-crypto/sha2/embedded'

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

// x25519SharedSecret: 32 bytes from WebCrypto ECDH
const kem = new MlKem768()
const { encapsulationKey, decapsulationKey } = kem.keygen()
const { ciphertext, sharedSecret: kyberSecret } = kem.encapsulate(encapsulationKey)

const hkdf     = new HKDF_SHA256()
const combined = new Uint8Array(64)
combined.set(x25519SharedSecret, 0)
combined.set(kyberSecret, 32)

const hybridKey = hkdf.derive(combined, salt, info, 32)

kem.dispose()
hkdf.dispose()

To validate keys before use, for example after deserializing from storage:

const kem = new MlKem768()
const { encapsulationKey, decapsulationKey } = kem.keygen()

const ekValid = kem.checkEncapsulationKey(encapsulationKey)
const dkValid = kem.checkDecapsulationKey(decapsulationKey)

if (!ekValid || !dkValid) {
  throw new Error('Key validation failed — key may be corrupted')
}

kem.dispose()

Post-quantum forward-secret messaging

These five primitives cover building a Signal-like session on top of ML-KEM. Each example shows the API shape of one export. For the full Alice-and-Bob round trip with message encryption, epoch transitions, and transport layout, see the usage example and bilateral chain exchange sections of the ratchet guide.

ratchetInit(sharedSecret, context?) — derive the initial root key and both chain keys from a shared secret established out of band.

import { init, ratchetInit } from 'leviathan-crypto'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'

await init({ sha2: sha2Wasm })

// sharedSecret: 32 bytes from a prior KEM or key-agreement protocol
const { rootKey, sendChainKey, recvChainKey } = ratchetInit(sharedSecret)

KDFChain — stateful per-message KDF. Each step() advances the chain and returns a single-use 32-byte message key. stepWithCounter() returns the key and the post-step counter atomically.

import { KDFChain } from 'leviathan-crypto'

const chain = new KDFChain(sendChainKey)

const msgKey1 = chain.step()                     // counter 1
const { key: msgKey2, counter } = chain.stepWithCounter()  // counter 2

// Use each msgKey exactly once to encrypt, then discard it.
chain.dispose()

kemRatchetEncap / kemRatchetDecap — advance the root key with a fresh ML-KEM encapsulation. Both sides arrive at the same pair of chain keys; Alice's send chain is Bob's receive chain and vice versa.

import {
  init,
  MlKem768,
  kemRatchetEncap,
  kemRatchetDecap,
  constantTimeEqual,
} from 'leviathan-crypto'

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

const kem = new MlKem768()

// Bob publishes his ek; Alice receives it. Both sides share a 32-byte rootKey.
const { encapsulationKey: bobEk, decapsulationKey: bobDk } = kem.keygen()
const rootKey = /* 32 bytes, from a prior ratchetInit or previous KEM step */

// Alice: encap side
const alice = kemRatchetEncap(kem, rootKey, bobEk)
// alice.kemCt goes on the wire; alice.sendChainKey / recvChainKey stay local

// Bob: decap side (receives kemCt from Alice)
const bob = kemRatchetDecap(kem, rootKey, bobDk, alice.kemCt, bobEk)

console.log(constantTimeEqual(alice.sendChainKey, bob.recvChainKey))  // => true
console.log(constantTimeEqual(alice.recvChainKey, bob.sendChainKey))  // => true

// Both sides now feed these chain keys into KDFChain for per-message keys.
kem.dispose()

SkippedKeyStore — caches message keys for out-of-order delivery. resolve() returns a ResolveHandle; settle with commit() on successful decrypt (wipes the key) or rollback() on auth failure (preserves the key for a legitimate retry at the same counter).

import { SkippedKeyStore } from 'leviathan-crypto'

const store = new SkippedKeyStore({ maxCacheSize: 100, maxSkipPerResolve: 50 })

const handle = store.resolve(chain, incomingCounter)
try {
  const plaintext = Seal.decrypt(cipher, handle.key, ciphertext)
  handle.commit()        // success — key is wiped
  return plaintext
} catch (e) {
  handle.rollback()      // auth failed — key returns to the store
  throw e
}

RatchetKeypair — single-use ek/dk wrapper for the decap side. The dk is wiped automatically after the one permitted decap call.

import { RatchetKeypair, MlKem768 } from 'leviathan-crypto'

const kem     = new MlKem768()
const keypair = new RatchetKeypair(kem)

// Share keypair.ek with the encap side, then receive kemCt back
const { sendChainKey, recvChainKey } = keypair.decap(kem, rootKey, kemCt)

keypair.dispose()   // idempotent; safe to call after decap()
kem.dispose()

For the full round-trip story covering message counters, epoch transitions, and how these primitives compose into a complete session, see the ratchet guide.


Hashing + HMAC verify

Hash a message, then use HMAC to authenticate it. Always use constantTimeEqual for tag comparison. See sha2.md and sha3.md for the full per-class reference.

import {
  init,
  SHA256,
  HMAC_SHA256,
  constantTimeEqual,
  randomBytes,
  bytesToHex,
  utf8ToBytes,
} from 'leviathan-crypto'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'

await init({
  sha2: sha2Wasm,
})

// Hash
const sha    = new SHA256()
const digest = sha.hash(utf8ToBytes('Hello, world!'))
console.log(bytesToHex(digest))
// => "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
sha.dispose()

// HMAC: generate and verify
const key     = randomBytes(32)
const hmac    = new HMAC_SHA256()
const message = utf8ToBytes('Transfer $100 to Alice')
const tag     = hmac.hash(key, message)

// Always verify constant-time
const recomputed = hmac.hash(key, message)
if (constantTimeEqual(tag, recomputed)) {
  console.log('Authentic.')
} else {
  console.log('Tampered!')
}

hmac.dispose()

Other hash algorithms follow the same shape. Swap the class name, the init module, and expect a different digest size. The hash(msg) and dispose() signatures are identical across every class.

Class Init Digest size
SHA256 sha2 32 bytes
SHA384 sha2 48 bytes
SHA512 sha2 64 bytes
SHA3_224 sha3 28 bytes
SHA3_256 sha3 32 bytes
SHA3_384 sha3 48 bytes
SHA3_512 sha3 64 bytes

Example: to compute a SHA3-512 digest instead of SHA-256, change the imports, the class name, and add sha3 to the init call. The digest comes back 64 bytes long; everything else stays the same.

 import {
   init,
-  SHA256,
+  SHA3_512,
   HMAC_SHA256,
   constantTimeEqual,
   randomBytes,
   bytesToHex,
   utf8ToBytes,
 } from 'leviathan-crypto'
-import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
+import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'

 await init({
   sha2: sha2Wasm,
+  sha3: sha3Wasm,
 })

-const sha    = new SHA256()
+const sha    = new SHA3_512()
 const digest = sha.hash(utf8ToBytes('Hello, world!'))
 console.log(bytesToHex(digest))
-// => "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
+// => "8e47f1185ffd014d238fabd02a1a32defe698cbf38c037a90e3c0a0a32370fb52cbd641250508502295fcabcbf676c09470b27443868c8e5f70e26dc337288af"

HMAC variants follow the same pattern. HMAC_SHA256, HMAC_SHA384, and HMAC_SHA512 all expose hash(key, message) and dispose() with matching tag sizes.


Fortuna CSPRNG

Fortuna provides cryptographically secure random bytes with forward secrecy and 32 entropy pools. Pick a Generator (cipher PRF) and a HashFn (accumulator and reseed key derivation) at create time.

Minimum bundle: ChaCha20 + SHA-256

import { init, Fortuna } from 'leviathan-crypto'
import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
import { SHA256Hash } from 'leviathan-crypto/sha2'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'

await init({ chacha20: chacha20Wasm, sha2: sha2Wasm })

const rng   = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA256Hash })
const key   = rng.get(32)
const nonce = rng.get(24)
rng.stop()

Original Fortuna pair: Serpent + SHA-256

import { init, Fortuna } from 'leviathan-crypto'
import { SerpentGenerator } from 'leviathan-crypto/serpent'
import { SHA256Hash } from 'leviathan-crypto/sha2'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'

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

const rng = await Fortuna.create({ generator: SerpentGenerator, hash: SHA256Hash })
const key = rng.get(32)
rng.stop()

Modern combination: ChaCha20 + SHA3-256

import { init, Fortuna } from 'leviathan-crypto'
import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
import { SHA3_256Hash } from 'leviathan-crypto/sha3'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'

await init({ chacha20: chacha20Wasm, sha3: sha3Wasm })

const rng   = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA3_256Hash })
const token = rng.get(16)
rng.stop()

Utilities

None of these utilities require init().

import { hexToBytes, bytesToHex, utf8ToBytes, randomBytes, constantTimeEqual, wipe } from 'leviathan-crypto'

// Encoding round-trips
const bytes = hexToBytes('ac1dc0de')
console.log(bytesToHex(bytes))  // => "ac1dc0de"

// Random keys
const key   = randomBytes(32)
const nonce = randomBytes(24)

// Constant-time comparison
const a = randomBytes(16)
let b   = randomBytes(16)
while (constantTimeEqual(a, b)) b = randomBytes(16)
console.log(constantTimeEqual(a, b))  // false

// Wipe sensitive material
wipe(key)  // key is now all zeroes

[!CAUTION]

Danger Zone: Raw primitives with no built-in authentication

The classes below give you direct access to unauthenticated cipher modes and low-level MAC and KDF primitives. They exist for protocol implementors and advanced use cases. If you are building general-purpose encryption, stop here and use Seal with SerpentCipher or XChaCha20Cipher instead.

SerpentCbc: raw CBC mode

Provides confidentiality only. An attacker can modify ciphertext without detection. Pair with HMAC_SHA256 using Encrypt-then-MAC.

import { init, SerpentCbc, randomBytes } from 'leviathan-crypto'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'

await init({ serpent: serpentWasm })

const key = randomBytes(32)
const iv  = randomBytes(16)  // unique per message: never reuse with the same key

const cbc = new SerpentCbc({ dangerUnauthenticated: true })

const ciphertext = cbc.encrypt(key, iv, new TextEncoder().encode('raw and naked'))
const decrypted  = cbc.decrypt(key, iv, ciphertext)

cbc.dispose()

SerpentCtr: raw CTR mode

Unauthenticated stream mode. A single flipped ciphertext bit flips the corresponding plaintext bit with no error. Pair with HMAC_SHA256 using Encrypt-then-MAC.

import { init, SerpentCtr, randomBytes } from 'leviathan-crypto'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'

await init({ serpent: serpentWasm })

const key   = randomBytes(32)
const nonce = randomBytes(16)  // never reuse with the same key

const ctr = new SerpentCtr({ dangerUnauthenticated: true })

ctr.beginEncrypt(key, nonce)
const ct1 = ctr.encryptChunk(new Uint8Array([1, 2, 3, 4]))
const ct2 = ctr.encryptChunk(new Uint8Array([5, 6, 7, 8]))

ctr.beginDecrypt(key, nonce)
const pt1 = ctr.decryptChunk(ct1)  // => [1, 2, 3, 4]
const pt2 = ctr.decryptChunk(ct2)  // => [5, 6, 7, 8]

ctr.dispose()

Serpent: raw ECB block cipher

Single 16-byte block operations with no mode, no IV, no authentication. Identical plaintext blocks produce identical ciphertext. Use Seal with SerpentCipher unless you are implementing a custom mode.

import { init, Serpent, randomBytes } from 'leviathan-crypto'
import { serpentWasm } from 'leviathan-crypto/serpent/embedded'

await init({ serpent: serpentWasm })

const cipher = new Serpent()
cipher.loadKey(randomBytes(32))

const block = new Uint8Array([
  0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
  0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
])

const encrypted = cipher.encryptBlock(block)
const decrypted = cipher.decryptBlock(encrypted)  // identical to block

cipher.dispose()

ChaCha20: raw stream cipher

Keystream XOR with no authentication tag. Bit flips in ciphertext produce bit flips in plaintext with no error. Pair with Poly1305 using Encrypt-then-MAC.

import { init, ChaCha20, randomBytes } from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'

await init({ chacha20: chacha20Wasm })

const key    = randomBytes(32)
const nonce  = randomBytes(12)  // 12-byte nonce for raw ChaCha20
const cipher = new ChaCha20()

cipher.beginEncrypt(key, nonce)
const ct1 = cipher.encryptChunk(new Uint8Array([1, 2, 3, 4]))
const ct2 = cipher.encryptChunk(new Uint8Array([5, 6, 7, 8]))

cipher.beginDecrypt(key, nonce)
const pt1 = cipher.decryptChunk(ct1)  // => [1, 2, 3, 4]
const pt2 = cipher.decryptChunk(ct2)  // => [5, 6, 7, 8]

cipher.dispose()

Poly1305: standalone one-time MAC

The key must never be reused across messages. Reuse allows an attacker to recover r and forge arbitrary tags. The AEAD classes handle key derivation automatically. Use them unless you have a specific reason not to.

import { init, Poly1305, randomBytes } from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'

await init({ chacha20: chacha20Wasm })

// Key must be fresh for every message: derive with ChaCha20 counter=0
// or use a KDF. Never use the same key twice.
const key     = randomBytes(32)
const message = new TextEncoder().encode('authenticate this')

const mac = new Poly1305()
const tag = mac.mac(key, message)  // 16-byte tag

mac.dispose()

XChaCha20Poly1305: stateless AEAD primitive

RFC-faithful stateless primitive. The caller manages nonces. Reusing a nonce with the same key is catastrophic.

import { init, XChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'

await init({ chacha20: chacha20Wasm })

const key   = randomBytes(32)
const nonce = randomBytes(24)  // caller must guarantee uniqueness per key
const aead  = new XChaCha20Poly1305()

const sealed    = aead.encrypt(key, nonce, utf8ToBytes('Hello, world!'))  // ct || tag(16)
const decrypted = aead.decrypt(key, nonce, sealed)                        // throws on tamper

console.log(bytesToUtf8(decrypted))  // => "Hello, world!"

aead.dispose()

ChaCha20Poly1305: stateless AEAD primitive (RFC 8439)

12-byte nonce variant. Same caller-managed-nonce hazard as XChaCha20Poly1305. The 12-byte nonce space makes random generation risky at scale.

import { init, ChaCha20Poly1305, randomBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'

await init({ chacha20: chacha20Wasm })

const key   = randomBytes(32)
const nonce = randomBytes(12)  // 12-byte nonce: collision risk at ~2^32 messages
const aead  = new ChaCha20Poly1305()

const sealed    = aead.encrypt(key, nonce, utf8ToBytes('Hello, world!'))  // ct || tag(16)
const decrypted = aead.decrypt(key, nonce, sealed)

aead.dispose()

HKDF_SHA256: manual key derivation

Extract-then-expand. The info parameter is your domain separator: use different values for different derived keys from the same root material.

import { init, HKDF_SHA256, randomBytes, utf8ToBytes, bytesToHex } from 'leviathan-crypto'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'

await init({ sha2: sha2Wasm })

const hkdf = new HKDF_SHA256()

const ikm  = randomBytes(32)                           // input keying material
const salt = randomBytes(32)                           // random, per-session
const info = utf8ToBytes('my-app-v1-encryption-key')   // domain separation

const derived = hkdf.derive(ikm, salt, info, 64)       // 64 bytes out
const encKey  = derived.subarray(0, 32)                // first 32: enc key
const macKey  = derived.subarray(32, 64)               // last 32: MAC key

console.log(bytesToHex(encKey))

hkdf.dispose()

SHAKE128 / SHAKE256: XOF squeeze

Extendable output functions: squeeze as many bytes as you need from a single absorption. The capacity determines security, not the output length. SHAKE128 provides 128-bit security. SHAKE256 provides 256-bit security.

import { init, SHAKE256, randomBytes, bytesToHex } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'

await init({ sha3: sha3Wasm })

const shake   = new SHAKE256()
const entropy = randomBytes(32)

// Squeeze 96 bytes in one shot
const output = shake.hash(entropy, 96)
console.log(bytesToHex(output))  // 192 hex chars

// Or squeeze incrementally
shake.absorb(entropy)
const block1 = shake.squeeze(32)  // first 32 bytes
const block2 = shake.squeeze(32)  // next 32 bytes
const block3 = shake.squeeze(32)  // next 32 bytes
// block1 + block2 + block3 === output

shake.dispose()

Cross-References

Document Description
index Project Documentation index
architecture architecture overview, module relationships, buffer layouts, and build pipeline
lexicon Glossary of cryptographic terms
cdn CDN usage: no bundler required