
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
@openforge-sh/liboqs-node
Advanced tools
Post-quantum cryptography for Node.js and browsers via WebAssembly bindings to liboqs
A JavaScript/TypeScript wrapper for liboqs, providing access to post-quantum cryptographic algorithms for key encapsulation mechanisms (KEM) and digital signatures.
This library provides WebAssembly bindings to liboqs, part of the Open Quantum Safe project. It includes:
This library is meant for research, prototyping, and experimentation. While the underlying liboqs library is well-maintained by the Open Quantum Safe project, both projects carry important caveats:
If you must use post-quantum cryptography in production environments, use hybrid approaches that combine post-quantum algorithms with traditional algorithms (e.g., ML-KEM with X25519, ML-DSA with Ed25519). This provides defense-in-depth during the transition period.
For production deployments, follow guidance from NIST's Post-Quantum Cryptography Standardization project.
The algorithms implementing NIST FIPS standards are:
SPHINCS+-* (name migration pending in liboqs)These algorithm names are stable and will be maintained. If NIST updates implementation details, this library will track those changes.
Note: liboqs currently uses legacy SPHINCS+ names for SLH-DSA, and we follow their naming. When liboqs adds SLH-DSA aliases to match FIPS 205 nomenclature, this library will expose them.
The library provides JavaScript wrappers for 97 algorithms including experimental and alternative post-quantum schemes:
Kyber512, Kyber768, Kyber1024Classic-McEliece-348864 through Classic-McEliece-8192128f)HQC-128, HQC-192, HQC-256sntrup761Note: BIKE family is not supported due to WASM incompatibility (requires platform-specific optimizations).
Falcon-512, Falcon-1024, Falcon-padded-512, Falcon-padded-1024MAYO-1, MAYO-2, MAYO-3, MAYO-5See algorithms.json for the complete algorithm registry. All 97 algorithms have WASM modules, JavaScript wrappers, TypeScript definitions, and test coverage.
This package works with all major JavaScript package managers:
# bun (recommended - fastest)
bun add @openforge-sh/liboqs-node
# npm
npm install @openforge-sh/liboqs-node
# pnpm
pnpm add @openforge-sh/liboqs-node
# yarn
yarn add @openforge-sh/liboqs-node
# deno (via npm: specifier - no install needed)
# See "Deno Usage" section below
This project uses bun by default for development, but all package managers are fully supported.
Deno works differently - it doesn't use package.json or require installation:
// Import directly using npm: specifier
import { createMLKEM768 } from "npm:@openforge-sh/liboqs-node";
const kem = await createMLKEM768();
const { publicKey, secretKey } = await kem.generateKeyPair();
kem.destroy();
Optional: Create a deno.json for cleaner imports:
{
"imports": {
"liboqs": "npm:@openforge-sh/liboqs-node"
}
}
Then import like:
import { createMLKEM768 } from "liboqs";
Run with:
deno run --allow-read --allow-env your-script.ts
Deno automatically caches npm packages on first run - no separate install step needed.
For detailed examples and usage patterns, see the API documentation.
import { createMLKEM768 } from '@openforge-sh/liboqs-node';
// Alice generates keypair
const alice = await createMLKEM768();
const { publicKey, secretKey } = await alice.generateKeyPair();
// Bob encapsulates shared secret
const bob = await createMLKEM768();
const { ciphertext, sharedSecret } = await bob.encapsulate(publicKey);
// Alice decapsulates
const aliceSecret = await alice.decapsulate(ciphertext, secretKey);
// Verify shared secrets match
console.log('Secrets match:', Buffer.compare(sharedSecret, aliceSecret) === 0);
// Cleanup
alice.destroy();
bob.destroy();
import { createMLDSA65 } from '@openforge-sh/liboqs-node';
const signer = await createMLDSA65();
const { publicKey, secretKey } = await signer.generateKeyPair();
const message = new TextEncoder().encode('Hello, quantum world!');
const signature = await signer.sign(message, secretKey);
const isValid = await signer.verify(message, signature, publicKey);
console.log('Valid:', isValid); // true
signer.destroy();
createMLKEM512()createMLKEM768()createMLKEM1024()createMLDSA44()createMLDSA65()createMLDSA87()| Algorithm | Security Level | Public Key | Secret Key | Ciphertext/Signature |
|---|---|---|---|---|
| ML-KEM-512 | Level 1 (128-bit) | 800 B | 1,632 B | 768 B |
| ML-KEM-768 | Level 3 (192-bit) | 1,184 B | 2,400 B | 1,088 B |
| ML-KEM-1024 | Level 5 (256-bit) | 1,568 B | 3,168 B | 1,568 B |
| ML-DSA-44 | Level 2 (128-bit) | 1,312 B | 2,560 B | ~2,420 B |
| ML-DSA-65 | Level 3 (192-bit) | 1,952 B | 4,032 B | ~3,309 B |
| ML-DSA-87 | Level 5 (256-bit) | 2,592 B | 4,896 B | ~4,627 B |
Each algorithm is compiled separately into individual WASM modules, so you only bundle what you use:
// Single algorithm (~80-160KB depending on algorithm complexity)
import { createMLKEM768 } from '@openforge-sh/liboqs-node';
const kem = await createMLKEM768();
// Multiple algorithms - each adds its own WASM module
import { createMLKEM768, createMLDSA65 } from '@openforge-sh/liboqs-node';
const kem = await createMLKEM768();
const sig = await createMLDSA65();
Tree-shaking ensures unused algorithms are never included in your bundle. WASM modules are lazy-loaded when you call the factory function.
// Main entry - all 97 algorithm factory functions, classes, and metadata
import { createMLKEM768, MLKEM768, ML_KEM_768_INFO } from '@openforge-sh/liboqs-node';
// KEM-only exports (32 algorithms)
import {
createMLKEM512,
createClassicMcEliece348864,
createFrodoKEM640AES
} from '@openforge-sh/liboqs-node/kem';
// Signature-only exports (65 algorithms)
import {
createMLDSA44,
createFalcon512,
createSphincsSha2128fSimple
} from '@openforge-sh/liboqs-node/sig';
// Error classes only
import { LibOQSError, LibOQSInitError } from '@openforge-sh/liboqs-node/errors';
@openforge-sh/liboqs-node/
├── src/
│ ├── algorithms/
│ │ ├── kem/
│ │ │ ├── ml-kem/ # ML-KEM (3 variants)
│ │ │ ├── kyber/ # Legacy Kyber (3 variants)
│ │ │ ├── classic-mceliece/ # Classic McEliece (10 variants)
│ │ │ ├── frodokem/ # FrodoKEM (6 variants)
│ │ │ ├── hqc/ # HQC (3 variants)
│ │ │ └── ntru/ # NTRU + sntrup761 (7 variants)
│ │ └── sig/
│ │ ├── ml-dsa/ # ML-DSA (3 variants)
│ │ ├── falcon/ # Falcon (4 variants)
│ │ ├── sphincs/ # SPHINCS+ (12 variants)
│ │ ├── cross/ # CROSS (18 variants)
│ │ ├── mayo/ # MAYO (4 variants)
│ │ ├── snova/ # SNOVA (12 variants)
│ │ └── uov/ # UOV (12 variants)
│ ├── core/
│ │ └── errors.js # Error classes
│ ├── types/ # TypeScript definitions
│ ├── index.js # Main entry (all 97 algorithms)
│ ├── kem.js # KEM exports (32 algorithms)
│ └── sig.js # Signature exports (65 algorithms)
└── dist/ # WASM modules (97 total, ~100-200KB each)
├── ml-kem-512.min.js
├── classic-mceliece-348864.min.js
├── ml-dsa-44.min.js
├── falcon-512.min.js
├── sphincs-sha2-128f-simple.min.js
└── ... (and others)
The library is organized in layers:
_OQS_KEM_*, _OQS_SIG_*)MLKEM768, MLDSA65)IMPORTANT: Always call destroy() when finished with an algorithm instance. WASM memory is not garbage-collected by JavaScript.
WebAssembly modules allocate native memory outside the JavaScript heap. When you create an algorithm instance, liboqs allocates C structures that JavaScript's garbage collector cannot reclaim. Without calling destroy(), this memory leaks permanently.
Long-running applications (servers, single-page apps, daemons) that don't call destroy() will experience:
Short-lived scripts are less affected since the OS reclaims all memory when the process exits.
// Pattern 1: Simple cleanup
const kem = await createMLKEM768();
const { publicKey, secretKey } = await kem.generateKeyPair();
kem.destroy();
// Pattern 2: Error-safe cleanup (recommended)
const kem = await createMLKEM768();
try {
const { publicKey, secretKey } = await kem.generateKeyPair();
const { ciphertext, sharedSecret } = await kem.encapsulate(publicKey);
// ... use results ...
} finally {
kem.destroy(); // Always runs, even if errors occur
}
// Pattern 3: Multiple operations
const sig = await createMLDSA65();
try {
const { publicKey, secretKey } = await sig.generateKeyPair();
const message = new TextEncoder().encode('Hello!');
const signature = await sig.sign(message, secretKey);
const isValid = await sig.verify(message, signature, publicKey);
return isValid;
} finally {
sig.destroy();
}
destroy(), the instance cannot be reusedcrypto.randomBytes(), browser crypto.getRandomValues())See SECURITY.md for our vulnerability disclosure policy. Issues specific to the liboqs C library should be reported to the liboqs project.
# Clone repository
git clone https://github.com/openforge-sh/liboqs-node.git
cd liboqs-node
# Build all algorithms
./build.sh
# Build specific algorithm
./build.sh ml-kem-768
# Setup only (clone liboqs without building)
./build.sh --setup-only
# Clean build artifacts
./build.sh --clean
The build system is data-driven using algorithms.json:
{
"kem": {
"ml-kem": {
"ML-KEM-768": {
"slug": "ml-kem-768",
"cmake_var": "ML_KEM_768",
"security": 3,
"standardized": true
}
}
}
}
The build.sh script:
algorithms.json with jq.min.js files with embedded WASMNo build script changes needed to add new algorithms - just update the JSON registry.
algorithms.json./build.sh <algorithm-slug>src/index.js if wrapper was createdThe library includes comprehensive test coverage using Vitest:
# Run all tests (1295+ tests across 97 algorithms)
npm test
# Or use your preferred package manager
bun test
pnpm test
yarn test
Test coverage includes:
Contributions are welcome! Please:
bun run test (or npm run test) before submittingFor larger changes or new algorithms, open an issue first to discuss the approach.
bun install (or npm install, pnpm install, etc.)bun run test (or npm run test)# Using bun (recommended/default for contributors)
bun install
bun run test
bun run build
# Using npm
npm install
npm run test
npm run build
# Using pnpm
pnpm install
pnpm runtest
pnpm run build
# Using yarn
yarn install
yarn run test
yarn run build
Contributions that add new algorithm wrappers, improve documentation, add tests, or enhance the build system are especially appreciated.
MIT License - see LICENSE.md for details.
This library's version tracks the bundled liboqs version:
@openforge-sh/liboqs-node 0.14.0 includes liboqs 0.14.0This library provides access to cryptographic algorithms believed to be quantum-resistant based on current research. The field of post-quantum cryptography is evolving. Algorithm support may change as research advances. Always consult with cryptographic experts for production deployments and follow NIST recommendations.
The liboqs project states: "WE DO NOT CURRENTLY RECOMMEND RELYING ON THIS LIBRARY IN A PRODUCTION ENVIRONMENT OR TO PROTECT ANY SENSITIVE DATA." This guidance applies to this JavaScript/WebAssembly wrapper as well.
FAQs
Post-quantum cryptography for Node.js and browsers via WebAssembly bindings to liboqs
We found that @openforge-sh/liboqs-node demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.