npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

subtlepq

v0.4.0

Published

Post-quantum polyfill for the Web Cryptography API: ML-KEM (FIPS 203) and ML-DSA (FIPS 204) per the WICG Modern Algorithms draft, with native delegation

Readme

subtlepq

Post-quantum polyfill for the Web Cryptography API: ML-KEM (FIPS 203) and ML-DSA (FIPS 204) per the WICG Modern Algorithms draft, with native delegation where the platform already supports it.

Status: P4 — engine, core JS layer, all key formats, key wrapping, native-parity suites, DHKEM extension.

Key formats: raw-public, raw-seed, spki, pkcs8 (seed-only encoding per the LAMPS drafts; the both CHOICE is accepted on import and cross-checked), and jwk (kty "AKP", pub/priv with priv = seed). wrapKey/unwrapKey compose export/import with the native wrapping algorithm, so AES-GCM, AES-KW, and RSA-OAEP wrapping of ML-* keys just work.

Use

Ponyfill (no globals touched):

import { subtle } from "subtlepq";

const { publicKey, privateKey } = await subtle.generateKey("ML-KEM-768", false,
    ["encapsulateKey", "decapsulateKey"]);
const { sharedKey, ciphertext } = await subtle.encapsulateKey("ML-KEM-768", publicKey,
    { name: "AES-GCM", length: 256 }, false, ["encrypt"]);
// sharedKey is a genuine native CryptoKey; all downstream crypto is native

True polyfill (patches crypto.subtle; native support is probed per algorithm, so anything the platform already does natively — and every non-ML call — is delegated to it, only the gaps are polyfilled, and with no gaps nothing is patched at all):

import { install } from "subtlepq";
install();              // install(true) also enables the DHKEM extension below
const kp = await crypto.subtle.generateKey("ML-DSA-65", false, ["sign", "verify"]);
const sig = await crypto.subtle.sign({ name: "ML-DSA-65", context: ctx }, kp.privateKey, data);

Known limits (inherent to a script polyfill, made loud rather than silent): ML-* keys are not structured-cloneable — postMessage/IndexedDB throws DataCloneError; export the 32–64-byte seed and re-import instead. Key material lives in wasm linear memory: extractable: false is API-level enforcement, not platform isolation.

DHKEM: classical ECDH in the same KEM shape

subtlepq/dhkem wraps ephemeral-static ECDH as a KEM per RFC 9180 §4.1DHKEM-X25519-HKDF-SHA256 and DHKEM-P256-HKDF-SHA256 — so classical and post-quantum key agreement share one calling convention, and migrating to ML-KEM is a name change:

With the true polyfill it's one flag — install(true) — and the DHKEM-* names work on crypto.subtle. Ponyfill users register the extension explicitly:

import * as dhkem from "subtlepq/dhkem";
dhkem.register();
import { subtle } from "subtlepq";

const kp = await subtle.generateKey("DHKEM-X25519-HKDF-SHA256", false,
    ["encapsulateKey", "decapsulateKey"]);
const { sharedKey, ciphertext } = await subtle.encapsulateKey(
    "DHKEM-X25519-HKDF-SHA256", kp.publicKey,          // -> "ML-KEM-768" later
    { name: "AES-GCM", length: 256 }, false, ["encrypt"]);

These names are not in the WICG draft — this is a subtlepq extension (if the WG adopts DHKEM identifiers, those will be adopted and these aliased). Underneath it is 100% native WebCrypto (ECDH/X25519 + HKDF): no wasm is involved, and the keys are genuine platform CryptoKeys — structured-cloneable, native usage enforcement (surfaced as deriveBits). The ciphertext is the serialized ephemeral public key (32 bytes X25519 / 65 bytes P-256), interoperable with any RFC 9180 DHKEM implementation, and validated against the RFC 9180 Appendix A test vectors.

One asymmetry vs a true KEM: RFC 9180 decapsulation mixes the recipient public key into the KDF. subtlepq remembers the public half for keys created through subtlepq/dhkem; for private keys from elsewhere (or structured-cloned into another realm) pass it explicitly — decapsulateBits({ name: "DHKEM-X25519-HKDF-SHA256", publicKey }, key, ct) — or use an extractable key.

Engine

wasm/ builds a minimal liboqs (pinned submodule, ML-KEM + ML-DSA only) plus a thin shim (subtlepq_engine.c) to WebAssembly:

git submodule update --init
wasm/build.sh          # needs emsdk; override location with EMSDK=...
npm test

Outputs wasm/dist/subtlepq-engine.mjs (+ .wasm sidecar, CSP-clean) and subtlepq-engine.single.mjs (self-contained, requires 'wasm-unsafe-eval' under strict CSP). 75 KB wasm covers all six parameter sets.

Seeded operations liboqs does not expose publicly (ML-DSA keygen from seed, KAT-deterministic encapsulation/signing) use a bracketed RNG-playback override — the same technique liboqs' own ACVP tests use.

Tests run NIST ACVP known-answer vectors (subset, see test/vectors/acvp-subset.json) for all six parameter sets, plus roundtrip, implicit-rejection, context, and polyfill-routing checks. test/dhkem-test.mjs validates the DHKEM extension against the RFC 9180 Appendix A vectors (test/vectors/rfc9180-subset.json).

npm run test:parity (Node >= 24.7) runs the polyfill side-by-side against the platform-native ML-KEM/ML-DSA WebCrypto implementation as an oracle: identical spki/pkcs8/jwk bytes, cross-stack encapsulate/decapsulate and sign/verify, identical AES-KW/AES-GCM wrapKey output, and matching DOMException names on malformed input. npm run test:libves cross-checks ML-KEM against libVES (an independent liboqs build and ASN.1 layer): same-seed keypair equality, SPKI byte parity, and mutual key import and encapsulation.

Built by the libVES team

subtlepq comes from VESvault, the team behind libVES — end-to-end encryption between users with post-quantum vaults (ML-KEM via liboqs) and VESrecovery™ key recovery. subtlepq gives you the primitives; if you need what goes on top — user-to-user key exchange, shared vaults, recovery from a lost key — that's libVES.

License

Apache-2.0, (c) 2026 VESvault Corp.