Releases: xero/leviathan-crypto
Release list
v3.0.1
- updates devDependencies and adds overrides pinning for brace-expansion and postcss clearing two moderate transitive advisories
- typescript6 no longer auto-injects @types/node ambient globals:
- fortuna: reads Node entropy sources (process.*) via globalThis with a minimal local type (keeping the lib free of any dependencies)
- tsconfig.test.json: adds "types": ["node"]
- ci: re-pin actions/checkout to v6.0.3 across all workflows
- verify-vectors: fixes rust warnings and sign_sth adds ed25519 and ml-dsa-44 secret-key parity checks
2 commits since v3.0.0 · v3.0.0...v3.0.1
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v3.0.1 | ↗ 320637c |
| 𛲜 | chore(deps): bump dev toolchain to latest stable, patch transitive advisories | ↗ 9073207 |
legend
- ⚠ Breaking Changes.
breaking change - ✰ Features.
feat - 🕱 Bug Fixes.
fix - ♽ Refactors.
refactor - ▧ Build & CI.
build - 𛲜 Chores.
chore - 🖹 Documentation.
docs - 🗹 Tests.
test - ◈ Other.
other
v3.0.0
Warning
This release contains breaking changes. Review carefully before upgrading.
Added
- AES-256-GCM-SIV cipher suite.
AESGCMSIVCipher(RFC 8452, AES-GCM-SIV) joinsSerpentCipherandXChaCha20Cipheras a first-class peer atformatEnum 0x04, with a 32-byte key, a 16-byte tag, and nonce-misuse resistance. It carries the same HKDF-SHA-256 explicit-commitment construction as the XChaCha20 suite and works withSeal,SealStream,OpenStream,SealStreamPool, andMlKemSuite. The standaloneAESGCMSIVprimitive supports both AES-128 and AES-256. See the AES guide and the AESGCMSIVCipher reference. MlKemSuiteAES variants.MlKemSuite(MlKem512/768/1024, AESGCMSIVCipher)producesformatEnum0x14, 0x24, and 0x34 with preambles of 820, 1140, and 1620 bytes, matching the XChaCha20 KEM variants. See the MlKemSuite reference.AESGenerator. AES-256 counter-mode PRF for theFortunapluggableGeneratorslot, restoring the Practical Cryptography §9.4 spec primitive. Pair it withSHA256HashorSHA3_256Hash. See Fortuna.- AES pool worker.
AESGCMSIVCipher.createPoolWorker()spawns classic workers from a bundled IIFE, matching theXChaCha20CipherandSerpentCipherpipeline. Sign,SignStream,VerifyStream. Single-shot envelope signing and its streaming counterparts, structurally parallel toSeal.SignStream.finalizeis byte-identical toSign.signfor the same(suite, sk, msg, ctx), so producers and consumers choose independently between in-memory and incremental. See the signing guide.SignatureSuiteabstraction.SignatureSuite,StreamableSignatureSuite, andPrehashAlgorithmare the template for every signature scheme, parallel toCipherSuite. See the SignatureSuite reference.SigningError. Discriminator-first error class mirroringAuthenticationError, with twelve stable discriminators spanning the suite, envelope, and stream layers.- ML-DSA (FIPS 204).
MlDsa44,MlDsa65, andMlDsa87cover pure ML-DSA (keygen,keygenDerand, hedgedsign,signDeterministic,signDerand,verify) and HashML-DSA, whosesignHashfamily exposes all twelve approved pre-hash functions from FIPS 204 §5.4.1. A prehashed-input surface (signHashPrehashed,signHashPrehashedDeterministic,signHashPrehashedDerand,verifyHashPrehashed) accepts a precomputed digest instead of rehashing; every shipped prehash suite throwsSigningError('sig-malformed-input')on a wrong-length digest. Subpathsleviathan-crypto/mldsaandleviathan-crypto/mldsa/embedded. See the ML-DSA guide. - Six ML-DSA suite consts.
MlDsa44Suite,MlDsa65Suite,MlDsa87Suite(catalog bytes 0x03-0x05) cover pure ML-DSA;MlDsa44PreHashSuite,MlDsa65PreHashSuite,MlDsa87PreHashSuite(0x13-0x15) cover HashML-DSA, defaulting to SHA3-256 for 44 and 65 and SHA3-512 for 87. See the SignatureSuite catalog. - SLH-DSA (FIPS 205).
SlhDsa128f,SlhDsa192f, andSlhDsa256f(SHAKE-family fast parameter sets, NIST categories 1, 3, and 5) cover pure SLH-DSA and HashSLH-DSA per FIPS 205 §10.2.2, including thesignHashandsignHashPrehashedfamilies. The category-1 SHA2-256 / SHAKE128 restriction is enforced at the public surface. Subpathsleviathan-crypto/slhdsaandleviathan-crypto/slhdsa/embedded. See the SLH-DSA guide. - Six SLH-DSA suite consts.
SlhDsa128fSuite,SlhDsa192fSuite,SlhDsa256fSuite(catalog bytes 0x06-0x08) cover pure SLH-DSA;SlhDsa128fPreHashSuite,SlhDsa192fPreHashSuite,SlhDsa256fPreHashSuite(0x16-0x18) cover HashSLH-DSA, with SHAKE128 for 128f and SHAKE256 for 192f and 256f. See the SignatureSuite catalog. - Three PQ-only hybrid suite consts.
MlDsa44SlhDsa128fSuite,MlDsa65SlhDsa192fSuite, andMlDsa87SlhDsa256fSuite(catalog bytes 0x30-0x32) bind ML-DSA and SLH-DSA in one composite (pk = pk_mldsa || pk_slhdsa,sig = sig_mldsa || sig_slhdsa, ML-DSA first). Both sub-verifies always run. These are prehash-only; streaming throughSignStream/VerifyStreamis mandatory. See the PQ-only hybrid encoding. - Ed25519 (RFC 8032). The first classical signature primitive.
Ed25519covers pure Ed25519 (§5.1.6) and Ed25519ph (§5.1.7) with the dom2 prehash binding.signPrehashedtakes an explicitctxargument; passnew Uint8Array(0)for no context binding. Subpathleviathan-crypto/ed25519. See the Ed25519 guide. - X25519 (RFC 7748). The first classical key-agreement primitive.
X25519exposeskeygen,keygenDerand, anddh, with §5 clamping handled internally so a round-tripped secretKey preserves byte equality. Subpathleviathan-crypto/x25519. See the X25519 guide. KeyAgreementError. New error class.X25519.dhthrows it on an all-zero shared secret (a small-order peer pk), distinguishing a degenerate exchange from a caller-contractTypeErrororRangeError.Ed25519SuiteandEd25519PreHashSuite. Format bytes 0x01 and 0x11.Ed25519SuiteisSignatureSuiteonly and rejects non-emptyuser_ctxwithSigningError('sig-ctx-unsupported');Ed25519PreHashSuiteisStreamableSignatureSuite, pins SHA-512, and binds dom2 context inside the curve WASM. See the SignatureSuite catalog.- ECDSA over NIST P-256 (FIPS 186-5). The second classical signature primitive.
EcdsaP256consumes a 32-byte SHA-256 digest and supports both RFC 6979 §3.2 deterministic anddraft-irtf-cfrg-det-sigs-with-noise-05§4 hedged K derivation. The signer normalisessto low-S per RFC 6979 §3.5 and the verifier rejects high-S. Subpathsleviathan-crypto/ecdsaandleviathan-crypto/ecdsa/embedded. See the ECDSA-P256 guide. EcdsaP256Suite.StreamableSignatureSuiteat format byte 0x02, pinned to SHA-256 prehash. It rejects non-emptyuser_ctx(FIPS 186-5 has no ctx parameter; context-bound ECDSA lives in the classical+PQ hybrids) and is hedged by default. Consumers needing byte-deterministic signatures drop toEcdsaP256directly with an all-zerornd. See the SignatureSuite catalog.ecdsaSignatureToDer/ecdsaSignatureFromDer. Strict-DER codec for X.509, JWS, and TLS interop per RFC 3279 §2.2.3. The decoder rejects non-minimal lengths, excess leading zeroes, negative INTEGERs, wrong tags, and trailing bytes viaSigningError('sig-malformed-input'). See the ECDSA-P256 DER utility.pointDecompress,EcdsaP256.keygenUncompressed,encodeEcPrivateKey/decodeEcPrivateKey. Three ECDSA exports for the composite-sigs §4 wire encodings: the SEC 1 §2.3.3 and §2.3.4 point codec and the RFC 5915 §3 DERECPrivateKeycodec for P-256. See the ECDSA-P256 guide.- Four classical+PQ composite hybrid suites.
MlDsa44Ed25519Suite,MlDsa65Ed25519Suite,MlDsa44EcdsaP256Suite, andMlDsa65EcdsaP256Suite(format bytes 0x20-0x23) perdraft-ietf-lamps-pq-composite-sigs. Each pre-hashes the message, builds the composite-sigs §3.2M'construction with theCompositeAlgorithmSignatures2025prefix and a per-suite label, and signs with both components in parallel (PQ-first,sig = sig_mldsa || sig_trad). Composite private keys carry the 32-byte ML-DSA seed only. See the classical+PQ hybrid encoding. - BLAKE3 hash family.
blake3.wasm, the tenth WASM binary, joins SHA-2 and SHA-3 with all three modes: default hash (§2.5), keyed_hash (§2.6), and derive_key (§2.7). Seven classes ship:BLAKE3,BLAKE3KeyedHash, andBLAKE3DeriveKey(one-shot);BLAKE3Stream,BLAKE3KeyedHashStream, andBLAKE3DeriveKeyStream(streaming); andBLAKE3OutputReader, the XOF reader fromfinalizeXof().BLAKE3Hashis a stateless 32-byteHashFnfor theFortunaaccumulator slot, peer toSHA256HashandSHA3_256Hash. Subpathsleviathan-crypto/blake3andleviathan-crypto/blake3/embedded. See the BLAKE3 guide. - SP 800-185 cSHAKE and KMAC. Six classes on the existing
sha3module:CSHAKE128,CSHAKE256,KMAC128,KMAC256,KMACXOF128, andKMACXOF256. The fixed-output KMAC variants ship a staticverify(tag, key, msg, customization)that throwsAuthenticationErroron mismatch. cSHAKE rejects empty customization and KMAC rejects empty keys. KMACXOF has no staticverifyby design; squeeze the expected bytes and useconstantTimeEqual. See the KMAC guide. - Streaming hash classes.
SHA3_256Stream,SHA3_512Stream,SHAKE128Stream, andSHAKE256Streamadd an incrementalupdate/finalizelifecycle to the sha3 module, required bySignStreamfor prehash suites. See the SHA-3 guide. merklemodule. A...
v2.1.0 - spqr ratchet primitives
v2.1.0 - post-quantum ratchet primitives
Warning
10 breaking API changes with this release!
See Breaking Changes below and review CHANGELOG for full migration notes.
Adds the four KDF constructions from Signal's Double Ratchet spec §5 and §7.2 (Sparse Post-Quantum Ratchet), plus two tier-1 session utilities that make those primitives misuse-resistant. Pure TypeScript over existing WASM; no new binaries.
Ratchet primitives
ratchetInit(sk, context?) — KDF_SCKA_INIT. Expands a 32-byte shared secret (e.g., a PQXDH output SK) into an initial root key, send chain key, and receive chain key. Both parties call this with the same sk to synchronize their starting state.
KDFChain — KDF_SCKA_CK. Stateful symmetric chain. Each step() call derives a message key and advances the internal chain key via HKDF-SHA-256 with a big-endian uint64 counter in the info bytes. stepWithCounter() returns { key, counter } atomically, removing the two-step step() + .n read pattern. The old chain key is wiped before replacement. Calling step() after dispose() throws.
kemRatchetEncap(kem, rk, peerEk, context?) — KDF_SCKA_RK, encapsulator side. Atomically calls kem.encapsulate(peerEk), mixes the shared secret into the root key via HKDF, wipes the shared secret, and returns { nextRootKey, sendChainKey, recvChainKey, kemCt }. The kemCt field must travel in-band; the peer cannot advance without it. Both sendChainKey and recvChainKey are used: they simultaneously seed the two directions of a bilateral exchange from a single KEM encapsulation. The HKDF info string now binds peerEk, kemCt, and context with u32be length prefixes — see F-010 below.
kemRatchetDecap(kem, rk, dk, kemCt, ownEk, context?) — KDF_SCKA_RK, decapsulator side. Mirrors encap: calls kem.decapsulate, mixes, wipes, returns keys. Chain key slots are swapped relative to the encap result so alice.sendChainKey === bob.recvChainKey. The ownEk parameter is the local party's ML-KEM encapsulation key — the same public key the encap side targeted as peerEk — so both sides bind the identical (peerEk, kemCt, context) tuple into the HKDF info string.
SkippedKeyStore — tier-1 MKSKIPPED management (DR spec §3.2/§3.5). Encapsulates the three delivery paths (in-order, skip-ahead, past lookup), ceiling-enforced eviction with wipe, and the advanceToBoundary(chain, pn) method used at epoch transitions. resolve(chain, counter) now returns a ResolveHandle with commit() / rollback() for transactional auth — see the breaking change note below. Scoped to one chain and one epoch — the application maintains one store per sender per epoch.
RatchetKeypair — tier-1 ek/dk lifecycle. Calls kem.keygen() on construction, enforces single-use via a consumed flag, and wipes the dk in a try/finally block inside decap() so the key is always zeroed regardless of whether decap succeeds or throws. decap() threads the stored ek through kemRatchetDecap as ownEk automatically, so callers using this helper are unaffected by the kemRatchetDecap signature change. After decap, generate a new RatchetKeypair and broadcast the new ek to all senders for the next ratchet step.
RatchetMessageHeader (type) — canonical shape for what goes in a message header when using these primitives: { epoch, counter, pn?, kemCt? }. pn and kemCt are present only on the first message of a new epoch.
Session state, header encryption, and transport are application concerns and are intentionally out of scope.
Breaking changes
Consumers of 2.0.1 will need to address these before upgrading to 2.1.0:
kemRatchetDecap adds a required ownEk parameter (F-010). The new signature is kemRatchetDecap(kem, rk, dk, kemCt, ownEk, context?). ownEk is the local party's encapsulation key so both sides bind the identical transcript into the HKDF info string. RatchetKeypair.decap threads its stored ek through automatically — callers using the high-level helper are unaffected. Direct kemRatchetDecap users must pass ownEk explicitly. Ratchet test vectors regenerated: existing test/vectors/ratchet_kat.ts outputs are obsolete because the HKDF info string changed. The KEM inputs (ACVP tcId 77 dk / kemCt) are unchanged — only the HKDF output hex fields.
OpenStream.seek(index) rejects backward seeks (F-012). index < this.counter now throws RangeError with 'forward-only' in the message. Backward seeks would re-use an already-consumed per-chunk counter nonce against a new ciphertext, permitting plaintext replay against a stale opener. Seek to the current counter is still a no-op. Construct a fresh OpenStream from the same preamble to restart.
ML-KEM auto-validates keys on use (FIPS 203 §7.2 / §7.3). MlKemBase.encapsulate(ek), encapsulateDerand(ek, m), and decapsulate(dk, c) now invoke the FIPS 203 §7.2 / §7.3 validity checks before any cryptographic work. Invalid keys throw RangeError with messages leviathan-crypto: encapsulation key failed FIPS 203 §7.2 validity check or leviathan-crypto: decapsulation key failed FIPS 203 §7.3 validity check. The §7.2 check is a direct coefficient-range scan (polyvec_modulus_check) — not the naïve round-trip check, which is bit-exact identity on any length-valid ek and therefore inert. Public checkEncapsulationKey / checkDecapsulationKey probes remain available for callers that want to inspect a key without triggering an exception.
Strict single-use on ChaCha20Poly1305.encrypt / XChaCha20Poly1305.encrypt. Any throw from encrypt() — including RangeError on key or nonce length — now terminates the instance. A retry always raises the single-use guard, never a fresh length error. Always allocate a new AEAD per message.
SealStream / OpenStream add a 'failed' terminal state. Any throw from push(), pull(), or finalize() wipes the derived keys and transitions the stream to 'failed'. Subsequent operations (including OpenStream.seek) throw with 'failed' in the message. dispose() on a failed instance is a no-op.
SkippedKeyStore.resolve returns ResolveHandle, not a raw key. Migration: replace const key = store.resolve(chain, n); try { ... } finally { wipe(key); } with const handle = store.resolve(chain, n); try { useKey(handle.key); handle.commit(); } catch (err) { handle.rollback(); throw err; }. See the design-decisions section above for the rationale.
SkippedKeyStore splits ceiling into maxCacheSize + maxSkipPerResolve. New defaults: maxCacheSize = 100 (memory bound), maxSkipPerResolve = 50 (per-message HKDF work bound). Legacy { ceiling: N } still accepted and sets both budgets to N. advanceToBoundary also enforces maxSkipPerResolve. Closes the F-007 skip-ahead CPU amplification (~500× asymmetric HKDF work previously possible via high-counter headers) and the accompanying O(n²) eviction (eviction is now O(1) via Map insertion order).
hexToBytes throws on non-hex characters. Previously parseInt('0g', 16) returned 0 (not NaN) because it stops at the first invalid character, silently producing wrong bytes. The leading regex now rejects any input containing characters outside [0-9a-fA-F]. Odd-length input already threw — that's preserved.
base64ToBytes throws on invalid input. Previously returned undefined on illegal charsets or rem=1 length. New behaviour matches the throw-don't-return-null convention elsewhere in the library. Return type is now Uint8Array, not Uint8Array | undefined. Callers doing if (bytes) { ... } should drop the check and let throws propagate.
Security hardening
Exclusivity guards closed (F-001 / R-011 / R-012 / R-025). TASK-A landed a runtime exclusivity registry preventing construction of a second stateful instance while the first is live. TASK-A2 closed three remaining sub-cases: post-dispose method calls on SHAKE128/256, ChaCha20, SerpentCtr, SerpentCbc now throw instead of silently clobbering whatever owns the WASM module; kyber operations now hold the sha3 and kyber exclusivity guards (previously a live SHAKE128 plus any kyber op would silently re-initialise the sha3 sponge mid-stream); MlKemBase.dispose() no longer wipes sha3 buffers (it never owned them).
FIPS 203 §7.2 validator is now functional (TASK-A2 + TASK-A3). The initial TASK-A2 wiring used a ByteDecode₁₂ → ByteEncode₁₂ round-trip check that is bit-exact identity on any 12-bit input, making the modulus check effectively a no-op. TASK-A3 replaced the round-trip with a direct coefficient-range scan in WASM (polyvec_modulus_check in src/asm/kyber/polyvec.ts). §7.3 inherits the fix automatically via the recursive checkEncapsulationKey call on the embedded ek — a dk whose embedded ek has been modulus-tampered is now rejected even when the embedded H has been correctly recomputed.
Fortuna wipe-before-reassign across all state transitions (F-002). pseudoRandomData, reseed, addRandomEvent, and the pool-reset branch inside get() zero the prior Uint8Array before dropping its reference. Fortuna.stop() is now a complete teardown: wipes genKey, genCnt, all 32 pool-hash chain values, and disposes the inner Serpent and SHA256 instances so their exclusivity tokens are released.
Stream / cipher-suite wipe-on-throw (F-003). SerpentCipher.sealChunk / openChunk wrap their scratch allocations in try/finally — intermediate IV, tag input, and expected-tag buffers are zeroed regardless of whether the function returns or throws. The auth-failure explicit-wipe path remains in place as defense in depth. Subsequently refactored in TASK-H to go through shared-ops pure functions, removing the per-chunk new HMAC_SHA256() / new SerpentCbc() construction.
aeadDecrypt auth-failure wipes keystream residuals (R-013, R-027). The 64-byte CHACHA_BLOCK_OFFSET (keystream block) is zeroed alongside CHUNK_CT on auth failure. TASK-B2 extends the wipe to POLY_KEY_OFFSET (32-byte Poly1305 one-time subkey cop...
⚠ v2.0.1 — Breaking Changes
v2.0.1
2 commits since v2.0.0 · v2.0.0...v2.0.1
Warning
This release contains breaking changes to the stream pools wire-format and backend to fix a critical bug in serpent stream pools
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v2.0.1 | ↗ 41a8011 |
| ⚠ | fix(serpent)!: expand CBC chunk buffer to fit PKCS7-padded 65536-byte blocks | ↗ 228ea23 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
v2.0.0 ⚠
Warning
This release contains breaking changes. Review carefully before upgrading.
KYBER ML-KEM (Post-Quantum KEM: FIPS 203)
Adds MlKem512, MlKem768, and MlKem1024 classes implementing the full Fujisaki-Okamoto KEM transform as a new kyber.wasm WASM module. Key details:
- SIMD NTT/invNTT, Montgomery/Barrett reduction, CBD sampling, division-free compression
- Constant-time decapsulation via dedicated ct_verify/ct_cmov in WASM (never touches JS)
- Requires sha3 for Keccak sponge ops (SHAKE128/256, SHA3-256/512)
- Validated against 240 NIST ACVP vectors (75 keygen, 75 encap, 30 decap w/ implicit rejection, 60 key validity)
- New leviathan-crypto/kyber and leviathan-crypto/kyber/embedded subpath exports
- New KyberSuite factory for hybrid KEM+AEAD workflows
- 'keccak' module name added as an alias for 'sha3' (same binary, same instance slot)
Stream API refactor (⚠ breaking)
- SerpentSeal and XChaCha20Seal removed; replaced by the unified Seal static class
- New API:
Seal.encrypt(CipherSuite, key, plaintext)/Seal.decrypt(...) - SealStream preamble field renamed from header → preamble
- SealStream/OpenStream/SealStreamPool barrel re-exported under stream/index.ts
- Read more here
New WASM modules
- kyber.wasm: ML-KEM polynomial arithmetic (6th WASM binary, joining serpent/chacha20/sha2/sha3/ct)
- ct.wasm: internal SIMD constant-time compare utility
CICD / test suite reorganization
- Workflows reorganized and split by cipher family: unit-serpent, unit-chacha20, unit-stream, unit-kyber, unit-core (replacing unit-fast, unit-pools, unit-utils)
- New kyber_audit.md covering NTT correctness, IND-CCA2 decapsulation, NIST KAT coverage
5 commits since v1.4.0 · v1.4.0...v2.0.0
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v2.0.0 | ↗ c461bd3 |
| 🖹 | docs: v2 api | ↗ 3673786 |
| ⚠ | feat(v2): cipher-agnostic AEAD, WASM SIMD constantTimeEqual, polymorphic init, kyber | ↗ 256ff27 |
| 🕱 | fix: strip test seams from public API, harden pool open() parsing, wipe worker buffers | ↗ d59659c |
| ✰ | feat: XChaCha20StreamPool | ↗ 07d3625 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
v1.4.0 ⚠ XChaCha20Seal
Warning
⚠ Breaking Changes
SerpentStreamSealer wire format: explicit isLast byte.
Each sealed chunk now prepends a cleartext isLast(1) byte. Wire format: isLast(1) || IV(16) || ciphertext || HMAC(32). Previously non-final chunks required two HKDF derivations and two HMAC verifications to distinguish from final chunks — a per-chunk timing differential that leaked stream position to observers with oracle access. Existing SerpentStream ciphertexts are unreadable by the new opener.
SerpentSeal / SerpentStreamSealer: AAD support changes wire format.
Optional aad?: Uint8Array parameter added to encrypt/decrypt/seal/final/open/feed. AAD is folded into the HMAC input with a 4-byte length prefix for domain separation. Existing ciphertexts are not compatible with v1.4.0, even with empty AAD, due to the length prefix changing the HMAC input construction. Groups cleanly with the isLast break — one migration, one version bump.
hexToBytes: odd-length input now throws.
Previously right-padded silently: 'abc' → 'abc0' → [0xab, 0xc0]. Now throws RangeError('hexToBytes: odd-length string'). Silent corruption in a crypto context is unacceptable.
base64url: encoding drops padding per RFC 4648 §5.
bytesToBase64(bytes, true) previously emitted %3d for = padding. Now omits padding entirely per spec. Decode path still accepts %3d indefinitely for backward compatibility with existing encoded data.
Fortuna.get() always returns Uint8Array.
Return type changed from Uint8Array | undefined to Uint8Array. create() now guarantees the instance is seeded before resolving. The undefined path was unreachable in any runtime that supports WASM SIMD.
Tip
New Stuff
XChaCha20Seal
the new recommended ChaCha20 AEAD. Binds key at construction, generates a fresh random 24-byte nonce per encrypt() call. No nonce management needed. Wire format: nonce(24) || ciphertext || tag(16). Supports optional AAD. This is what most users should reach for.
XChaCha20StreamSealer / XChaCha20StreamOpener
chunked streaming authenticated encryption for large payloads. Per-chunk random nonces, position-bound AAD preventing reorder/splice/truncation, framed mode for network transports. API mirrors SerpentStreamSealer/Opener. 32-byte key.
AAD on SerpentSeal and SerpentStreamSealer.
Both Serpent authenticated encryption paths now support associated data, bringing them to parity with the ChaCha20 side. Bind metadata (filenames, user IDs, version headers) cryptographically without manual HMAC composition.
concat() is now variadic.
concat(a, b) → concat(...arrays). Single allocation for N arrays instead of N-1 intermediate allocations. Backward compatible.
Gzip-compressed embedded WASM.
198KB → ~33KB total embedded footprint. Serpent alone: 167KB → 20KB. Fully internal to the loader — init('embedded') works identically.
Note
Fixes & Cleanup
decodeWasm() DecompressionStream guard.
Throws a descriptive error when DecompressionStream is unavailable instead of a bare ReferenceError. Directs callers to streaming or manual init mode.
embed-wasm.ts deterministic gzip output.
Zeroes the gzip mtime header for reproducible builds across identical WASM input.
XChaCha20Poly1305 docs no longer claims to implement the AEAD interface.
The class is the RFC-faithful stateless primitive, its method signature never matched the AEAD interface. Docs and types corrected. XChaCha20Seal is now the recommended AEAD entry point.
Consolidated internal base64ToBytes copies.
Three divergent private copies in the loader and pool modules replaced with a single import from utils.
503 unit tests · 183 e2e tests · 686 total
4 commits since v1.3.1 · v1.3.1...v1.4.0
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v1.4.0 | ↗ 1839f7a |
| 🖹 | docs: release prep | ↗ 663c5b9 |
| ✰ | feat: xchacha20seal & xchacha20stream | ↗ 5413e98 |
| ◈ | docs | ↗ dc48a1d |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
v1.3.1
v1.3.1
Note
normalizes the chacha.wasm name to chacha20.wasm to match other primitives with no consumer api changes.
3 commits since v1.3.0 · v1.3.0...v1.3.1
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v1.3.1 | ↗ 1b94a33 |
| ◈ | docs | ↗ 4dbdb92 |
| 🕱 | fix(nomenclature): normalize to chacha20 wasm | ↗ cbf0af6 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
⚠ v1.3.0 - SSSA
v1.3.0 - Streamlining the Serpent Streaming API (Breaking Changes)
Warning
This release contains breaking changes. Review carefully before upgrading.
Breaking Changes
- Removed:
SerpentStreamEncoderandSerpentStreamDecoder: deleted with no grace period
Migration
| Old | New |
|---|---|
new SerpentStreamEncoder(key, cs) |
new SerpentStreamSealer(key, cs, { framed: true }) |
encoder.encode(pt) |
sealer.seal(pt) |
encoder.encodeFinal(pt) |
sealer.final(pt) |
new SerpentStreamDecoder(key, hdr) |
new SerpentStreamOpener(key, hdr, { framed: true }) |
decoder.feed(bytes) |
opener.feed(bytes) (same signature) |
Added
SerpentStreamSealer:opts?: { framed?: boolean }constructor parameter. Whenframed: true,
seal()andfinal()prependu32be(sealedLen)to each output chunk.SerpentStreamOpener:opts?: { framed?: boolean }constructor parameter andfeed()method.
feed(bytes: Uint8Array): Uint8Array[]accumulates incoming bytes, parsesu32belength
prefixes, and dispatches complete frames toopen()internally.
Wire format compatibility
Ciphertext produced by SerpentStreamEncoder is byte-identical to
SerpentStreamSealer({ framed: true }) output. No re-encryption required
when upgrading.
No cryptographic changes
No algorithm, key format, or protocol changes in this release. All existing
ciphertext produced by any prior version remains valid and decryptable.
Tests
456 unit · 183 e2e (61 tests × Chromium, Firefox, WebKit): all passing
4 commits since v1.2.0 · v1.2.0...v1.3.0
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v1.3.0 | ↗ 7342788 |
| ◈ | docs | ↗ de70e4b |
| ⚠ | BREAKING: serpert stream cipher api opts (#5) | ↗ 24997bf |
| 🖹 | docs: simd | ↗ c2cacd7 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
v1.2.0 WASM SIMD
WebAssembly SIMD acceleration for Serpent-256 and ChaCha20.
Inter-block 4-wide parallelism delivering 2–3× single-thread throughput gains across V8, SpiderMonkey, and JSC.
11 commits since v1.1.0 · v1.1.0...v1.2.0
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v1.2.0 | ↗ 01a11c7 |
| 𛲜 | chore: release prep | ↗ 59545fe |
| 🖹 | docs: simd | ↗ 4470260 |
| 🖹 | docs: simd | ↗ fe537de |
| ✰ | feat(chacha): simd parallelism (#4) | ↗ cdd52cf |
| ✰ | feat(serpent): WASM SMID (#3) | ↗ ade3182 |
| 🖹 | docs(linx): local and wiki | ↗ 5092567 |
| 🖹 | docs(security): expand scope, posture, and disclosure policy | ↗ 74d3db9 |
| 🖹 | docs(security): add audit citations | ↗ 7327b67 |
| 🖹 | docs(security): add audit citations | ↗ 998d581 |
| 🖹 | docs(security): add audit citations | ↗ b83a848 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other
v1.1.0
v1.1.0
9 commits since v1.0.0 · v1.0.0...v1.1.0
| Type | Commit | Meta |
|---|---|---|
| 𛲜 | chore(release): v1.1.0 | ↗ e3519fa |
| 𛲜 | chore(devDeps): bump versions | ↗ 6e95fdb |
| 🖹 | docs(cicd): wiki gen | ↗ 92a0bdb |
| 🕱 | fix(hmac|hkdf): zero all intermediate key material | ↗ ad094c3 |
| 𛲜 | chore(deps): bump the npm_and_yarn group across 1 directory with 1 update | ↗ aba83ad |
| 🖹 | docs: cross refs | ↗ 2a73b69 |
| 🕱 | fix(cicd): script tweaks | ↗ 5fe2dd3 |
| 🖹 | docs: algo correctness audits | ↗ 0b22fc1 |
| 🕱 | fix(cicd): publish workflow | ↗ a7650c2 |
legend
- ⚠ Breaking Changes —
breaking change - ✰ Features —
feat - 🕱 Bug Fixes —
fix - ♽ Refactors —
refactor - ▧ Build & CI —
build - 𛲜 Chores —
chore - 🖹 Documentation —
docs - 🗹 Tests —
test - ◈ Other —
other