12 KiB
Post-Quantum Cryptography: JavaScript Package Survey
⚠️ Research document. A library survey; not an implementation spec.
Executive Summary
We do NOT need to write our own implementations. There are mature, well-maintained JavaScript packages for all NIST-standardized post-quantum algorithms. The standout recommendation is @noble/post-quantum — a pure JavaScript implementation of all FIPS 203/204/205 algorithms, from the highly respected noble cryptography family (335 GitHub stars, MIT licensed, self-audited, actively maintained).
For our PQ Nostr web app, @noble/post-quantum covers everything we need: ML-DSA (signatures), SLH-DSA (signatures), ML-KEM (encryption key agreement), and hybrid KEMs (X-Wing). No WASM required, works in browsers natively.
NIST-Standardized PQ Algorithms (FIPS)
| Algorithm | FIPS Standard | Type | Use Case | Status |
|---|---|---|---|---|
| ML-KEM (Kyber) | FIPS 203 | KEM (key encapsulation) | Encryption key agreement | Finalized 2024 |
| ML-DSA (Dilithium) | FIPS 204 | Digital signature | Authentication | Finalized 2024 |
| SLH-DSA (SPHINCS+) | FIPS 205 | Digital signature | Authentication (conservative) | Finalized 2024 |
| FN-DSA (Falcon) | FIPS 206 | Digital signature | Authentication (compact) | Still draft |
Key and Signature Sizes
| Variant | Public key | Secret key | Signature / Ciphertext |
|---|---|---|---|
| ML-KEM-512 | 800 B | 1632 B | 768 B |
| ML-KEM-768 | 1184 B | 2400 B | 1088 B |
| ML-KEM-1024 | 1568 B | 3168 B | 1568 B |
| ML-DSA-44 | 1312 B | 2560 B | 2420 B |
| ML-DSA-65 | 1952 B | 4032 B | 3309 B |
| ML-DSA-87 | 2592 B | 4896 B | 4627 B |
| Falcon-512 | 897 B | 1281 B | ~666 B |
| Falcon-1024 | 1793 B | 2305 B | ~1280 B |
| SLH-DSA-128f | 32 B | 64 B | 17088 B |
| SLH-DSA-128s | 32 B | 64 B | 7856 B |
| SLH-DSA-192f | 48 B | 96 B | 35664 B |
| SLH-DSA-192s | 48 B | 96 B | 16224 B |
| SLH-DSA-256f | 64 B | 128 B | 49856 B |
| SLH-DSA-256s | 64 B | 128 B | 29792 B |
Recommended Package: @noble/post-quantum
npm: https://www.npmjs.com/package/@noble/post-quantum GitHub: https://github.com/paulmillr/noble-post-quantum Version: 0.6.1 | License: MIT | Stars: 335
Why This Is the Best Choice
- Covers all algorithms we need: ML-KEM, ML-DSA, SLH-DSA, Falcon, and hybrid KEMs (X-Wing, KitchenSink) — all in one package
- Pure JavaScript — no WASM, no native bindings, works in any browser and Node.js
- From the noble family — Paul Miller's noble libraries (noble-hashes, noble-curves, noble-ciphers, noble-secp256k1) are the most trusted JS crypto libraries, used widely across the ecosystem
- Tree-shakeable — only the algorithms you import are bundled (16KB gzipped for everything)
- Self-audited at v0.6.1 (April 2026)
- Deterministic key generation from seed —
keygen(seed)accepts an optional seed, which is exactly what we need for deriving PQ keys from a BIP39 seed phrase - Actively maintained — last updated June 2026
- No external dependencies beyond noble-hashes and noble-curves (same author, same trust model)
API Examples
ML-DSA-65 (Dilithium) — for PQ signatures
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
// Deterministic keygen from seed (for our seed-phrase derivation)
const seed = new Uint8Array(32); // derived from BIP39 seed via HKDF
const keys = ml_dsa65.keygen(seed);
// Sign
const msg = new TextEncoder().encode('link statement');
const sig = ml_dsa65.sign(msg, keys.secretKey);
// Verify
const isValid = ml_dsa65.verify(sig, msg, keys.publicKey);
SLH-DSA-128s (SPHINCS+) — conservative hash-based signatures
import { slh_dsa_sha2_128s } from '@noble/post-quantum/slh-dsa.js';
const keys = slh_dsa_sha2_128s.keygen(seed);
const sig = slh_dsa_sha2_128s.sign(msg, keys.secretKey);
const isValid = slh_dsa_sha2_128s.verify(sig, msg, keys.publicKey);
ML-KEM-768 (Kyber) — for PQ encryption key agreement
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
// Alice generates keys
const aliceKeys = ml_kem768.keygen(seed);
// Bob encapsulates a shared secret for Alice
const { cipherText, sharedSecret: bobShared } = ml_kem768.encapsulate(aliceKeys.publicKey);
// Alice decapsulates
const aliceShared = ml_kem768.decapsulate(cipherText, aliceKeys.secretKey);
// aliceShared === bobShared
X-Wing (Hybrid KEM — ML-KEM-768 + X25519)
import { XWing } from '@noble/post-quantum/hybrid.js';
const keys = XWing.keygen();
const { cipherText, sharedSecret } = XWing.encapsulate(keys.publicKey);
const decapSecret = XWing.decapsulate(cipherText, keys.secretKey);
Speed Benchmarks (Apple M4, operations/sec)
| Primitive | Keygen | Signing | Verification | Shared secret |
|---|---|---|---|---|
| ML-KEM-768 | 4661 | — | — | 4089 |
| ML-DSA-65 | 669 | 271 | 565 | — |
| Falcon-512 | 14 | 749 | 2160 | — |
| SLH-DSA-SHA2-192f | 235 | 8 | 159 | — |
| SLH-DSA-SHA2-128s | ~5 | <1 | ~1000 | — |
| x/ed25519 (pre-quantum) | 12648 | 6157 | 1255 | 1981 |
Key takeaways for our use case:
- ML-DSA-65 signing: ~3.7ms per signature — fast enough for a one-time key-link event
- ML-KEM-768 keygen + encapsulate: ~0.4ms — instant
- SLH-DSA is slow (117ms-2900ms per signature depending on variant) but it's a one-time cost for the key-link event
- There's an experimental WASM branch with 80% faster ML-KEM, 30% faster ML-DSA, 15x faster SLH-DSA-SHAKE
Security Notes
- "The library has not been independently audited yet" (self-audited at v0.6.1)
- No constant-time guarantees (side-channel attacks possible) — acceptable for our use case since PQ private keys are used in the browser, not on a server
- Uses
crypto.getRandomValuesfor randomness (browser CSPRNG)
Alternative Packages
@oqs/liboqs-js — WASM bindings to liboqs
- npm: https://www.npmjs.com/package/@oqs/liboqs-js
- GitHub: https://github.com/open-quantum-safe/liboqs-js
- Version: 0.15.1 | License: MIT (liboqs is OQS license)
- Stars: 11 (new repo, created Feb 2026)
Pros:
- Wraps liboqs (the reference implementation from Open Quantum Safe project)
- Supports the widest range of algorithms: ML-KEM, ML-DSA, Falcon, Classic McEliece, FrodoKEM, NTRU, HQC, CROSS, Mayo, UOV, SNOVA
- WASM performance may be better for some algorithms
Cons:
- Very new (created Feb 2026, only 11 stars)
- WASM adds complexity (loading .wasm files, larger bundle)
- Less established than noble
- License is "NOASSERTION" (liboqs has its own license, not standard MIT)
When to use: If we need algorithms not in noble (e.g., Classic McEliece, FrodoKEM), or if WASM performance is critical.
mlkem (crystals-kyber-js)
- npm: https://www.npmjs.com/package/mlkem
- GitHub: https://github.com/dajiaji/crystals-kyber-js
- Version: 2.7.0 | License: MIT | Stars: 62
Pros:
- Focused, well-maintained ML-KEM implementation
- Pure TypeScript, no dependencies
- Used by
@hpke/hybridkem-x-wingfor HPKE integration - "The fastest pure TypeScript ML-KEM implementation"
Cons:
- Only ML-KEM (no signature algorithms)
- Would need a separate package for ML-DSA/SLH-DSA
When to use: If we only need ML-KEM for encryption and want a focused, tested library.
@hpke/hybridkem-x-wing
- npm: https://www.npmjs.com/package/@hpke/hybridkem-x-wing
- Version: 0.7.0 | License: MIT
HPKE (Hybrid Public Key Encryption) module for X-Wing (ML-KEM-768 + X25519 hybrid KEM). Part of the hpke-js family. Useful if we want to use the HPKE framework for PQ encryption, which is the IETF standard for hybrid key exchange.
dilithium-crystals-js
- npm: https://www.npmjs.com/package/dilithium-crystals-js
- Version: 1.1.3
Dilithium implementation for Node.js and browsers. Older, less maintained than noble.
@stenvault/pqc-wasm
- npm: https://www.npmjs.com/package/@stenvault/pqc-wasm
- Version: 0.2.2
ML-KEM-768 + ML-DSA-65 WASM wrapper (RustCrypto compiled to WebAssembly). New, untested.
pqclean
- npm: https://www.npmjs.com/package/pqclean
- Version: 0.8.1
Node.js bindings for PQClean (all PQ implementations). Node-only, not browser-compatible.
What We Need for the PQ Nostr Web App
| Need | Algorithm | Package | Status |
|---|---|---|---|
| PQ signature (primary) | ML-DSA-65 | @noble/post-quantum |
✅ Ready |
| PQ signature (conservative) | SLH-DSA-128s | @noble/post-quantum |
✅ Ready |
| PQ encryption key agreement | ML-KEM-768 | @noble/post-quantum |
✅ Ready |
| Hybrid KEM (PQ + classical) | X-Wing (ML-KEM-768 + X25519) | @noble/post-quantum |
✅ Ready |
| Deterministic keygen from seed | All above | @noble/post-quantum |
✅ keygen(seed) supported |
| Browser compatibility | All above | @noble/post-quantum |
✅ Pure JS, no WASM needed |
| BIP39 seed phrase | Mnemonic → seed | @scure/bip39 or similar |
✅ Separate package needed |
Recommended Package Set for the Web App
{
"dependencies": {
"@noble/post-quantum": "^0.6.1",
"@noble/curves": "^2.2.0",
"@noble/hashes": "^2.2.0",
"@scure/bip39": "^1.5.0",
"@scure/bip32": "^1.6.0",
"nostr-tools": "^2.10.0"
}
}
@noble/post-quantum— PQ algorithms (ML-DSA, SLH-DSA, ML-KEM, X-Wing)@noble/curves— secp256k1 for NIP-01 signing and NIP-06 key derivation@noble/hashes— SHA-256, HKDF, etc.@scure/bip39— BIP39 mnemonic generation and validation@scure/bip32— BIP32 HD wallet derivation (for NIP-06)nostr-tools— Nostr event creation, relay communication, NIP-46
All of these are from the same trust ecosystem (noble / scure family by Paul Miller and Alex Obukhov).
PQ Key Derivation from BIP39 Seed
One critical detail: @noble/post-quantum's keygen(seed) functions accept a seed parameter for deterministic key generation. The seed length varies by algorithm:
| Algorithm | Seed length |
|---|---|
| ML-KEM-768 | 64 bytes |
| ML-DSA-65 | 32 bytes |
| SLH-DSA-128s | 32 bytes (or 64 for some variants) |
| Falcon-512 | 48 bytes |
Our derivation scheme from BIP39 seed:
import { hkdf } from '@noble/hashes/hkdf';
import { sha512 } from '@noble/hashes/sha512';
// BIP39 seed (64 bytes from PBKDF2-HMAC-SHA512)
const bip39Seed = ...; // 64 bytes
// Derive PQ key seeds using HKDF with algorithm-specific labels
const mlDsaSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-ml-dsa-65', 32);
const slhDsaSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-slh-dsa-128s', 32);
const mlKemSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-ml-kem-768', 64);
// Generate PQ keypairs deterministically
const mlDsaKeys = ml_dsa65.keygen(mlDsaSeed);
const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed);
const mlKemKeys = ml_kem768.keygen(mlKemSeed);
This means: same seed phrase → same PQ keys, every time. The user can recover their PQ keys by entering their seed phrase in any implementation of this scheme.
Summary: No Custom Implementations Needed
| Question | Answer |
|---|---|
| Do we need to write PQ crypto implementations? | No — @noble/post-quantum covers all algorithms |
| Do we need WASM? | No — pure JS is fast enough for our one-time key-link event |
| Are the packages maintained? | Yes — noble-post-quantum updated June 2026, 335 stars |
| Are they audited? | Self-audited at v0.6.1. No independent audit yet. |
| Do they support deterministic keygen from seed? | Yes — keygen(seed) for all algorithms |
| Do they work in browsers? | Yes — pure JS, uses crypto.getRandomValues |
| What's the bundle size? | 16KB gzipped for all algorithms (tree-shakeable) |
| What do we need to write ourselves? | The NIP-QR event builder, key derivation scheme (HKDF labels), relay publishing, OTS client, UI |