Post-quantum cryptography
Tyravel ships post-quantum primitives in @tyravel/crypto, with optional integration into session storage and the OAuth2 authorization server.
Install
Scaffold configuration, then add the package:
tyravel crypto:install
npm install @tyravel/cryptoGenerate key material with the CLI (no config file required):
tyravel crypto:generate-keys --algorithm=hybrid-x25519-ml-kem-768
tyravel crypto:generate-keys --algorithm=ml-dsa-65Configuration
Add config/crypto.ts to your application:
import type { CryptoConfig } from '@tyravel/crypto';
import { env } from '@tyravel/config';
export default {
kem: 'hybrid-x25519-ml-kem-768',
signature: 'ml-dsa-65',
session: {
encrypt: env('SESSION_ENCRYPT', 'false') === 'true',
key: env('SESSION_ENCRYPTION_KEY', ''),
fallbackKey: env('APP_KEY', ''),
},
oauth: {
signTokens: env('OAUTH_SIGN_TOKENS', 'false') === 'true',
algorithm: 'ml-dsa-65',
publicKey: env('OAUTH_TOKEN_PUBLIC_KEY', ''),
secretKey: env('OAUTH_TOKEN_SECRET_KEY', ''),
},
} satisfies CryptoConfig;AuthServiceProvider reads crypto.session to encrypt database and Redis session payloads. OAuthServerServiceProvider reads crypto.oauth and enables ML-DSA signed access tokens when signTokens is true.
Algorithms
| Family | Variants | Use |
|---|---|---|
| ML-KEM | 512, 768, 1024 | Key encapsulation (encrypt shared secrets) |
| ML-DSA | 44, 65, 87 | Digital signatures |
| SLH-DSA | sha2-128f/s … sha2-256f/s | Hash-based signatures (conservative, slower) |
| Hybrid | hybrid-x25519-ml-kem-768 | X25519 + ML-KEM-768 transition mode (recommended default) |
NIST category 3 (~AES-192 equivalent): ML-KEM-768, ML-DSA-65. Category 5 (~AES-256): ML-KEM-1024, ML-DSA-87.
Runtime backend
Tyravel requires Node.js 26+ and uses native OpenSSL PQC for all algorithms, including the hybrid X25519 + ML-KEM-768 construction. The @noble/post-quantum fallback was removed in v0.12.1.
Detect support at runtime:
import { supportsNativePqc } from '@tyravel/crypto';
if (supportsNativePqc()) {
// OpenSSL PQC is available
}High-level API
CryptoManager is the main facade:
import { CryptoManager } from '@tyravel/crypto';
const crypto = new CryptoManager({
kem: 'hybrid-x25519-ml-kem-768',
signature: 'ml-dsa-65',
});
// Key generation
const kemKeys = crypto.generateKeys('hybrid-x25519-ml-kem-768');
const signKeys = crypto.generateKeys('ml-dsa-65');
// Encrypt / decrypt (KEM + AES-256-GCM envelope)
const envelope = crypto.encrypt('secret payload', kemKeys.publicKey);
const plaintext = crypto.decrypt(envelope, kemKeys.secretKey);
// Sign / verify
const signature = crypto.sign('message', signKeys.secretKey);
const valid = crypto.verify(signature, 'message', signKeys.publicKey);Serialize keys for storage or .env:
import { serializeKeyMaterial, deserializeKeyMaterial } from '@tyravel/crypto';
const json = serializeKeyMaterial(signKeys);
// { algorithm, publicKey: '<base64>', secretKey: '<base64>' }Lower-level classes
Use these when you need algorithm-specific control:
import { MlKem, MlDsa, SlhDsa, HybridEncryptor } from '@tyravel/crypto';
const hybrid = new HybridEncryptor();
const keys = hybrid.generateKeyPair();
const envelope = hybrid.encrypt(plaintext, keys.publicKey);
const decrypted = hybrid.decrypt(envelope, keys.secretKey);MlKem— ML-KEM-512/768/1024 encapsulation and envelope encryptionMlDsa— ML-DSA-44/65/87 signaturesSlhDsa— SLH-DSA hash-based signaturesHybridEncryptor— X25519 + ML-KEM-768 combined KEM
Session encryption at rest
When crypto.session.encrypt is true, AuthServiceProvider wraps the database and Redis session drivers with a PayloadCipher. Session JSON is encrypted with AES-256-GCM before persistence and prefixed with pqc1: so plaintext sessions remain readable during rollout.
SESSION_ENCRYPT=true
SESSION_ENCRYPTION_KEY=<base64-32-bytes> # optional if APP_KEY is setThe encryption key is either:
SESSION_ENCRYPTION_KEY— base64, at least 16 bytes decoded, or- Derived from
APP_KEYvia scrypt when no dedicated key is set.
The in-memory array session driver is never encrypted (intended for tests).
OAuth access token signing
When crypto.oauth.signTokens is true, @tyravel/auth-oauth issues signed bearer tokens instead of opaque random secrets:
oat_<base64-payload>.<base64-signature>The payload includes token id, client id, user id, scopes, and expiry. ML-DSA verifies integrity; revocation and expiry still consult the database.
tyravel crypto:generate-keys --algorithm=ml-dsa-65
OAUTH_SIGN_TOKENS=true
OAUTH_TOKEN_PUBLIC_KEY=<base64-public-key>
OAUTH_TOKEN_SECRET_KEY=<base64-secret-key>Legacy opaque tokens continue to work when signing is disabled.
You can also configure signing directly on oauthServer.tokenSigning in config/oauthServer.ts (created by tyravel oauth:install) without using config/crypto.ts.
CLI reference
tyravel crypto:generate-keys [--algorithm=<name>] [--format=json|env]Use --format=env to print .env-ready lines for signing keys (OAUTH_TOKEN_*) or session keys (SESSION_ENCRYPTION_KEY).
Supported --algorithm values:
ml-kem-512,ml-kem-768,ml-kem-1024hybrid-x25519-ml-kem-768(default)ml-dsa-44,ml-dsa-65,ml-dsa-87slh-dsa-sha2-128f,slh-dsa-sha2-128s,slh-dsa-sha2-192f,slh-dsa-sha2-192s,slh-dsa-sha2-256f,slh-dsa-sha2-256s
Output is JSON with algorithm, publicKey, and secretKey as base64 strings.
Security notes
- Store secret keys in environment variables or a secrets manager, never in source control.
- ML-KEM decapsulation does not authenticate the sender — combine with signatures or channel binding when both confidentiality and authenticity matter.
- SLH-DSA variants with
s(small) produce shorter signatures but are much slower to sign. - Native OpenSSL PQC on Node.js 26+ is used for production throughput.
- Hybrid secret keys are packed PKCS#8 material (134 bytes), not the 32-byte Noble root seeds from v0.12.0 — regenerate hybrid keys after upgrading.
- Session encryption protects data at rest in the session store; it does not replace HTTPS for data in transit.
See Authentication for guards, OAuth providers, the OAuth2 server, CSRF, and API token hardening.