// kit 01

PQC Toolkit

Production-ready post-quantum crypto. Pick stack, pick primitive, copy the code.

$ app/api/kem/route.ts
L3
TS
1import { NextResponse } from 'next/server';
2import { ml_kem768 } from '@noble/post-quantum/ml-kem';
3import { x25519 } from '@noble/curves/ed25519';
4import { hkdf } from '@noble/hashes/hkdf';
5import { sha384 } from '@noble/hashes/sha2';
6 
7function fromB64(s: string) { return Uint8Array.from(Buffer.from(s, 'base64')); }
8function toB64(b: Uint8Array) { return Buffer.from(b).toString('base64'); }
9function 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 
16function 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 
24export 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 
33export 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_SEED and PQC_X25519_SEED as 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.