// kit 01
PQC Toolkit
Production-ready post-quantum crypto. Pick stack, pick primitive, copy the code.
$ app/api/kem/route.ts
L3
1 import { NextResponse } from 'next/server'; 2 import { ml_kem768 } from '@noble/post-quantum/ml-kem'; 3 import { x25519 } from '@noble/curves/ed25519'; 4 import { hkdf } from '@noble/hashes/hkdf'; 5 import { sha384 } from '@noble/hashes/sha2'; 6 7 function fromB64(s: string) { return Uint8Array.from(Buffer.from(s, 'base64')); } 8 function toB64(b: Uint8Array) { return Buffer.from(b).toString('base64'); } 9 function concat(...arr: Uint8Array[]) { 10 const total = arr.reduce((n, a) => n + a.length, 0); 11 const out = new Uint8Array(total); 12 let off = 0; for (const a of arr) { out.set(a, off); off += a.length; } 13 return out; 14 } 15 16 function loadKeys() { 17 const kemSeed = Buffer.from(process.env.PQC_KEM_SEED!, 'hex'); 18 const xPriv = Buffer.from(process.env.PQC_X25519_SEED!, 'hex'); 19 const kemKp = ml_kem768.keygen(kemSeed); 20 const xPub = x25519.getPublicKey(xPriv); 21 return { kemKp, xPriv: new Uint8Array(xPriv), xPub }; 22 } 23 24 export async function GET() { 25 const { kemKp, xPub } = loadKeys(); 26 return NextResponse.json({ 27 suite: 'hybrid-x25519-mlkem768', 28 kem_pub: toB64(kemKp.publicKey), 29 x_pub: toB64(xPub), 30 }); 31 } 32 33 export async function POST(req: Request) { 34 const { kem_ct, x_client_pub } = await req.json(); 35 const { kemKp, xPriv } = loadKeys(); 36 37 const ssKem = ml_kem768.decapsulate(fromB64(kem_ct), kemKp.secretKey); 38 const ssX = x25519.getSharedSecret(xPriv, fromB64(x_client_pub)); 39 40 const transcript = sha384(concat(fromB64(kem_ct), fromB64(x_client_pub))); 41 const sessionKey = hkdf(sha384, concat(ssX, ssKem), transcript, 'qbit404-hybrid', 32); 42 43 return NextResponse.json({ ok: true, sid: toB64(sha384(sessionKey).slice(0, 16)) }); 44 }
KEY SIZE
1184 B pub
SIG / CT
1088 B ct
NIST LEVEL
L3
NOTES
ML-KEM-768 is the post-quantum half of a TLS-like handshake. Always wrap it in a hybrid with X25519 so a broken PQ doesn't break the channel.
- Wire
PQC_KEM_SEEDandPQC_X25519_SEEDas 64 / 64 hex chars in a KMS, not env. - Cache the server's public bundle aggressively — it doesn't change between sessions.
- The transcript hash binds client and server contributions so the session key is unique per handshake.