Ein Angreifer, der heute Ihren verschlüsselten Datenverkehr mitschneidet, braucht den passenden Quantencomputer noch gar nicht. Er speichert die Daten einfach und entschlüsselt sie, sobald die Hardware bereit ist. Dieses Muster heißt Harvest now, decrypt later, und es macht den Wechsel zu quantensicherer Kryptografie schon 2026 dringend. In diesem Tutorial bauen Sie in 12 Schritten ein vollständiges, lauffähiges Node.js-Modul, das mit ML-KEM (Kyber) nach dem NIST-Standard FIPS 203 einen quantensicheren Schlüssel austauscht und damit Nachrichten verschlüsselt.

Sie lernen, warum ein reines ML-KEM-Setup riskant ist, wie ein hybrider Schlüsselaustausch aus X25519 und ML-KEM-768 funktioniert, und wie Sie das Ergebnis mit AES-256-GCM zu einer echten Ende-zu-Ende-Verschlüsselung zusammenfügen. Jeder Codeblock in diesem Artikel wurde unter Node.js 20 mit der Bibliothek @noble/post-quantum 0.6.1 getestet. Planen Sie etwa 45 bis 60 Minuten für den vollständigen Durchlauf ein.

Warum Post-Quanten-Kryptografie 2026 keine Theorie mehr ist

Klassische Public-Key-Verfahren wie RSA und Elliptische-Kurven-Kryptografie (ECC) beruhen auf Rechenproblemen, die für normale Computer praktisch unlösbar sind. Ein hinreichend großer Quantencomputer löst sie mit dem Shor-Algorithmus jedoch in polynomieller Zeit. Damit fallen RSA, Diffie-Hellman und ECDH auf einen Schlag. Symmetrische Verfahren wie AES-256 sind weniger betroffen: Der Grover-Algorithmus halbiert nur die effektive Schlüssellänge, weshalb AES-256 quantensicher bleibt und AES-128 auf das Sicherheitsniveau von AES-64 sinkt.

Das Problem ist nicht, ob ein solcher Quantencomputer 2026 schon existiert. Das Problem ist die Vorlaufzeit. Daten mit langer Schutzdauer, etwa Gesundheitsakten, Behördenkommunikation, Quellcode oder Staatsgeheimnisse, müssen auch in 10 oder 15 Jahren noch vertraulich sein. Wer sie heute mit klassischem ECDH austauscht, liefert sie einem Angreifer aus, der geduldig mitschneidet und später entschlüsselt. Genau diese Logik hat die Standardisierung beschleunigt.

Das US-amerikanische NIST hat im August 2024 die ersten drei Post-Quanten-Standards finalisiert. ML-KEM für den Schlüsselaustausch wurde als FIPS 203 veröffentlicht, ML-DSA als FIPS 204 und SLH-DSA als FIPS 205 für digitale Signaturen. Das deutsche BSI empfiehlt in seinen Technischen Richtlinien ausdrücklich den hybriden Einsatz, also die Kombination eines klassischen Verfahrens mit einem Post-Quanten-Verfahren, bis die neuen Algorithmen langfristig erprobt sind. Für DACH-Unternehmen ist die Migration damit keine Forschungsfrage mehr, sondern eine Planungsaufgabe.

Wichtig für das Verständnis dieses Tutorials: ML-KEM ist kein Verschlüsselungsverfahren im klassischen Sinn und auch keine Signatur. Es ist ein Key-Encapsulation-Mechanism (KEM). Seine einzige Aufgabe besteht darin, zwischen zwei Parteien ein gemeinsames 32-Byte-Geheimnis zu etablieren. Was Sie mit diesem Geheimnis tun, etwa AES-256-GCM oder ChaCha20-Poly1305 ankoppeln, bleibt Ihnen überlassen. Diese Trennung ist die häufigste Verständnishürde und der Grund, warum so viele PQC-Tutorials in der Praxis scheitern.

ML-KEM, Kyber und FIPS 203: Die Grundlagen

ML-KEM steht für Module-Lattice-Based Key-Encapsulation Mechanism. Der Algorithmus basiert auf dem Forschungsentwurf CRYSTALS-Kyber, weshalb viele ältere Bibliotheken und Artikel weiterhin von Kyber sprechen. Seit der Standardisierung als FIPS 203 ist der offizielle Name ML-KEM, die kryptografische Konstruktion ist nahezu identisch. Wer nach Code sucht, sollte beide Begriffe kennen: Suchergebnisse für Kyber und ML-KEM überschneiden sich stark, und npm-Pakete tragen oft noch beide Bezeichnungen.

Die Sicherheit von ML-KEM beruht auf dem Module Learning With Errors-Problem (MLWE). Vereinfacht gesagt versteckt der Algorithmus ein Geheimnis in einem Gitter aus absichtlich verrauschten linearen Gleichungen. Ohne den geheimen Schlüssel ist es selbst für einen Quantencomputer extrem aufwendig, das Rauschen vom Signal zu trennen. Dieses Gitterproblem gilt nach heutigem Kenntnisstand als quantenresistent.

FIPS 203 definiert drei Parameter-Sätze mit unterschiedlichem Sicherheitsniveau. ML-KEM-768 ist der von vielen Behörden empfohlene Standardwert und entspricht grob der Sicherheit von AES-192. Die folgende Tabelle zeigt die exakten Größen, gemessen mit @noble/post-quantum 0.6.1. Beachten Sie, wie groß die Schlüssel im Vergleich zu klassischer ECC sind: Ein X25519-Schlüssel ist nur 32 Byte groß, ein ML-KEM-768-Public-Key bereits 1.184 Byte.

EigenschaftML-KEM-512ML-KEM-768ML-KEM-1024
NIST-SicherheitsstufeStufe 1 (AES-128)Stufe 3 (AES-192)Stufe 5 (AES-256)
Öffentlicher Schlüssel800 Byte1.184 Byte1.568 Byte
Geheimer Schlüssel1.632 Byte2.400 Byte3.168 Byte
Ciphertext (Kapsel)768 Byte1.088 Byte1.568 Byte
Shared Secret32 Byte32 Byte32 Byte
Empfohlen fürressourcenarmStandardHochsicherheit
Parameter-Sätze von ML-KEM nach FIPS 203, Schlüsselgrößen aus eigener Messung.

Der KEM-Ablauf besteht aus drei Operationen. Bei KeyGen erzeugt der Empfänger ein Schlüsselpaar und veröffentlicht den Public Key. Bei Encapsulate nimmt der Absender diesen Public Key, würfelt ein zufälliges Geheimnis und gibt zwei Dinge zurück: das gemeinsame Geheimnis (für sich selbst) und einen Ciphertext (für den Empfänger). Bei Decapsulate rekonstruiert der Empfänger aus Ciphertext und geheimem Schlüssel exakt dasselbe Geheimnis. Anders als bei Diffie-Hellman ist der Ablauf also asymmetrisch: Nur eine Seite braucht ein dauerhaftes Schlüsselpaar.

Voraussetzungen mit Versionen

Bevor Sie loslegen, brauchen Sie eine funktionierende Node.js-Umgebung und ein grundlegendes Verständnis von asynchronem JavaScript. Sie müssen kein Kryptograf sein, aber Sie sollten wissen, was ein öffentlicher und ein privater Schlüssel ist. Die folgende Tabelle listet alle benötigten Komponenten mit getesteten Versionen.

KomponenteGetestete VersionZweck
Node.js20 LTS (oder 22 LTS)Laufzeitumgebung, ESM-Support
npm10.xPaketverwaltung
@noble/post-quantum0.6.1ML-KEM nach FIPS 203
@noble/curves2.2.0X25519 für den Hybrid-Teil
@noble/hashes2.2.0HKDF und SHA-256
node:cryptointegriertAES-256-GCM, Zufall, Konstantzeit
Voraussetzungen für das ML-KEM-Tutorial, alle Versionen getestet.

Warum @noble/post-quantum und nicht das Node-Core-Modul? Node.js bietet ML-KEM in Version 20 und 22 noch nicht nativ im crypto-Modul an. Die offizielle Node.js-Sicherheitsleitlinie empfiehlt für nicht nativ unterstützte Algorithmen bewährte Bibliotheken oder WebAssembly. @noble/post-quantum von Paul Miller ist eine auditierte, abhängigkeitsfreie JavaScript-Implementierung der FIPS-203/204/205-Familie und damit eine solide Wahl. Sie laufen damit identisch in Node.js, Deno, Bun und im Browser.

Prüfen Sie zuerst Ihre Node-Version. Alles ab Node 18 funktioniert, aber 20 LTS oder 22 LTS sind die empfohlenen Stände für 2026.

$ node --version
v20.20.2

$ npm --version
10.8.2

Schritt 1 und 2: Projekt anlegen und Bibliotheken installieren

Legen Sie ein neues Projektverzeichnis an und initialisieren Sie es. Wichtig: @noble/post-quantum ist ein reines ESM-Paket. Damit Sie import statt require nutzen können, setzen Sie in der package.json den Typ auf module. Das ist die häufigste Fehlerquelle bei Einsteigern, dazu später mehr im Troubleshooting.

$ mkdir pqc-nodejs && cd pqc-nodejs
$ npm init -y
$ npm pkg set type=module
$ npm install @noble/[email protected] @noble/[email protected] @noble/[email protected]

Der Aufruf npm pkg set type=module trägt "type": "module" direkt in die package.json ein, ohne dass Sie die Datei manuell öffnen müssen. Prüfen Sie nach der Installation kurz, ob alle drei Pakete in den Abhängigkeiten stehen. Die @noble-Familie ist bewusst klein gehalten und zieht keine transitiven Abhängigkeiten nach, was die Angriffsfläche reduziert.

$ npm ls
[email protected]
├── @noble/[email protected]
├── @noble/[email protected]
└── @noble/[email protected]

Schritt 3: Erstes ML-KEM-768-Schlüsselpaar erzeugen

Erstellen Sie eine Datei 01-keygen.mjs. Der wichtigste Punkt ist der Importpfad: Die Bibliothek exportiert ML-KEM unter dem Subpfad @noble/post-quantum/ml-kem.js, inklusive Dateiendung. Vergessen Sie das .js, bricht der Import mit ERR_PACKAGE_PATH_NOT_EXPORTED ab.

// 01-keygen.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';

const keys = ml_kem768.keygen();

console.log('Public Key  :', keys.publicKey.length, 'Byte');
console.log('Secret Key  :', keys.secretKey.length, 'Byte');
console.log('Public (hex):', Buffer.from(keys.publicKey).toString('hex').slice(0, 48), '...');

Führen Sie das Skript aus. Die keygen()-Funktion liefert ein Objekt mit zwei Uint8Array-Feldern: publicKey und secretKey. Diese Typen sind absichtlich keine Node-Buffer, damit der Code auch im Browser läuft. Für die Ausgabe wandeln wir sie mit Buffer.from() in Hex um.

$ node 01-keygen.mjs
Public Key  : 1184 Byte
Secret Key  : 2400 Byte
Public (hex): a3f1c0b27e94d6...  ...

Die Größen stimmen mit der Tabelle oben überein und bestätigen, dass Sie tatsächlich ML-KEM-768 nutzen. Behandeln Sie den secretKey ab jetzt wie jeden privaten Schlüssel: niemals loggen, niemals ins Repository committen, niemals über unverschlüsselte Kanäle übertragen.

Schritt 4: Verkapselung und Entkapselung

Jetzt kommt der Kern von ML-KEM. Stellen Sie sich zwei Parteien vor: Bob besitzt das Schlüsselpaar aus Schritt 3 und veröffentlicht seinen Public Key. Alice will Bob ein gemeinsames Geheimnis schicken. Sie ruft encapsulate() mit Bobs Public Key auf und erhält zwei Werte zurück.

// 02-kem.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
import { timingSafeEqual } from 'node:crypto';

// Bob erzeugt sein Schlüsselpaar
const bob = ml_kem768.keygen();

// Alice verkapselt gegen Bobs Public Key
const { cipherText, sharedSecret: aliceSecret } = ml_kem768.encapsulate(bob.publicKey);

// Bob entkapselt mit seinem Secret Key
const bobSecret = ml_kem768.decapsulate(cipherText, bob.secretKey);

console.log('Ciphertext  :', cipherText.length, 'Byte');
console.log('Alice Secret:', Buffer.from(aliceSecret).toString('hex'));
console.log('Bob Secret  :', Buffer.from(bobSecret).toString('hex'));
console.log('Identisch   :', timingSafeEqual(Buffer.from(aliceSecret), Buffer.from(bobSecret)));

Der entscheidende Punkt: Alice und Bob haben jetzt dasselbe 32-Byte-Geheimnis, ohne dass dieses Geheimnis jemals über die Leitung ging. Übertragen wurde nur der 1.088 Byte große Ciphertext, der ohne Bobs geheimen Schlüssel wertlos ist. Genau das ist der Sinn eines KEM.

$ node 02-kem.mjs
Ciphertext  : 1088 Byte
Alice Secret: 7d2e9a4b... (64 Hex-Zeichen)
Bob Secret  : 7d2e9a4b... (64 Hex-Zeichen)
Identisch   : true

Beachten Sie, dass wir die beiden Geheimnisse mit timingSafeEqual vergleichen, nicht mit ===. Ein naiver Vergleich bricht beim ersten abweichenden Byte ab und verrät über die Laufzeit, an welcher Stelle sich zwei Werte unterscheiden. Bei geheimen Schlüsseln ist das ein echtes Seitenkanal-Risiko. Mehr dazu in Schritt 9.

Schritt 5: Schlüssel serialisieren und speichern

In der Praxis müssen Sie Schlüssel speichern und über das Netzwerk übertragen. Uint8Array lässt sich nicht direkt in JSON schreiben, deshalb kodieren Sie Schlüssel als Base64. Beim Einlesen wandeln Sie zurück. Die folgende Datei zeigt einen sauberen Roundtrip und schreibt Bobs Public Key in eine Datei.

// 03-serialize.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
import { writeFileSync, readFileSync } from 'node:fs';

const bob = ml_kem768.keygen();

// Kodieren
const pubB64 = Buffer.from(bob.publicKey).toString('base64');
writeFileSync('bob.pub', pubB64, 'utf8');

// Dekodieren
const loaded = new Uint8Array(Buffer.from(readFileSync('bob.pub', 'utf8'), 'base64'));

console.log('Roundtrip korrekt:',
  Buffer.from(bob.publicKey).equals(Buffer.from(loaded)));
console.log('Public Key gespeichert in bob.pub (' + pubB64.length + ' Zeichen)');

Den geheimen Schlüssel sollten Sie nie im Klartext auf die Platte schreiben. Verschlüsseln Sie ihn mit einem aus einer Passphrase abgeleiteten Schlüssel, etwa über scrypt aus dem Node-Core-Modul, oder bewahren Sie ihn in einem Hardware-Sicherheitsmodul beziehungsweise einem Secret-Manager auf. Für dieses Tutorial halten wir den geheimen Schlüssel ausschließlich im Speicher.

Schritt 6: Hybrider Schlüsselaustausch mit X25519 und ML-KEM

Jetzt kommt der wichtigste konzeptionelle Schritt. ML-KEM ist neu, und niemand garantiert, dass nicht doch eine bislang unbekannte mathematische Schwäche gefunden wird. Deshalb empfehlen NIST, BSI und der IETF-Entwurf für hybride TLS-Schlüsselaustausche, ML-KEM nicht allein einzusetzen, sondern mit einem bewährten klassischen Verfahren zu kombinieren. Der resultierende Schlüssel ist genau dann sicher, wenn mindestens eines der beiden Verfahren sicher ist.

Wir kombinieren X25519 (klassischer ECDH) mit ML-KEM-768. Beide liefern ein Geheimnis, das wir aneinanderhängen und durch eine Schlüsselableitungsfunktion (HKDF mit SHA-256) schicken. Das Verketten allein genügt nicht: HKDF sorgt dafür, dass das Ergebnis gleichmäßig zufällig verteilt ist und ein eindeutiger Kontext (das info-Feld) in den Schlüssel einfließt.

// 04-hybrid.mjs
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
import { x25519 } from '@noble/curves/ed25519.js';
import { hkdf } from '@noble/hashes/hkdf.js';
import { sha256 } from '@noble/hashes/sha2.js';

// Hybrides Schlüsselpaar des Empfängers
export function generateHybridKeypair() {
  const xPriv = x25519.utils.randomSecretKey();
  const xPub  = x25519.getPublicKey(xPriv);
  const kem   = ml_kem768.keygen();
  return {
    publicKey:  { x25519: xPub,  mlkem: kem.publicKey },
    privateKey: { x25519: xPriv, mlkem: kem.secretKey },
  };
}

// Absender leitet gemeinsamen Schlüssel ab
export function senderEncapsulate(recipientPub) {
  const ephPriv = x25519.utils.randomSecretKey();
  const ephPub  = x25519.getPublicKey(ephPriv);
  const classical = x25519.getSharedSecret(ephPriv, recipientPub.x25519);
  const { cipherText, sharedSecret: pq } = ml_kem768.encapsulate(recipientPub.mlkem);

  const ikm  = new Uint8Array([...classical, ...pq]);
  const info = new TextEncoder().encode('mlkem-hybrid-v1');
  const key  = hkdf(sha256, ikm, undefined, info, 32);
  return { key, header: { ephPub, cipherText } };
}

// Empfänger leitet denselben Schlüssel ab
export function recipientDecapsulate(privateKey, header) {
  const classical = x25519.getSharedSecret(privateKey.x25519, header.ephPub);
  const pq        = ml_kem768.decapsulate(header.cipherText, privateKey.mlkem);

  const ikm  = new Uint8Array([...classical, ...pq]);
  const info = new TextEncoder().encode('mlkem-hybrid-v1');
  return hkdf(sha256, ikm, undefined, info, 32);
}

Der Absender erzeugt für X25519 ein flüchtiges (ephemeres) Schlüsselpaar pro Nachricht, was Forward Secrecy auf der klassischen Seite liefert. Den ML-KEM-Teil verkapselt er gegen den dauerhaften ML-KEM-Public-Key des Empfängers. Das info-Feld mlkem-hybrid-v1 bindet den Schlüssel an Ihren Anwendungskontext und verhindert, dass derselbe Schlüssel versehentlich in einem anderen Protokoll wiederverwendet wird.

Schritt 7: Nutzlast mit AES-256-GCM verschlüsseln

Der abgeleitete 32-Byte-Schlüssel ist jetzt bereit für die eigentliche Verschlüsselung. Wir nutzen AES-256-GCM aus dem Node-Core-Modul crypto. GCM ist ein AEAD-Verfahren, das gleichzeitig verschlüsselt und authentifiziert: Jede Manipulation am Ciphertext fällt beim Entschlüsseln durch eine fehlerhafte Auth-Tag-Prüfung auf. Der Initialisierungsvektor (IV) muss pro Schlüssel einzigartig sein, deshalb würfeln wir ihn mit randomBytes.

// 05-aead.mjs
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';

export function encrypt(key, plaintext) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', Buffer.from(key), iv);
  const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  return { iv, ct, tag: cipher.getAuthTag() };
}

export function decrypt(key, box) {
  const decipher = createDecipheriv('aes-256-gcm', Buffer.from(key), box.iv);
  decipher.setAuthTag(box.tag);
  return Buffer.concat([decipher.update(box.ct), decipher.final()]).toString('utf8');
}

Falls decipher.final() mit der Meldung Unsupported state or unable to authenticate data abbricht, stimmt entweder der Schlüssel nicht, oder der Ciphertext wurde verändert. Das ist kein Bug, sondern die Schutzfunktion von GCM. Speichern Sie IV und Auth-Tag immer zusammen mit dem Ciphertext, sonst können Sie nicht entschlüsseln.

Schritt 8: Das vollständige Projekt zusammenfügen

Jetzt verbinden wir alle Teile zu einem lauffähigen Programm. Diese Datei importiert die Funktionen aus Schritt 6 und 7 und simuliert einen kompletten Nachrichtenaustausch zwischen Alice und Bob. Sie ist das Herzstück des Tutorials und läuft ohne weitere Vorbereitung.

// main.mjs
import { generateHybridKeypair, senderEncapsulate, recipientDecapsulate } from './04-hybrid.mjs';
import { encrypt, decrypt } from './05-aead.mjs';
import { timingSafeEqual } from 'node:crypto';

// 1. Bob erzeugt sein dauerhaftes hybrides Schlüsselpaar
const bob = generateHybridKeypair();

// 2. Alice leitet einen Schlüssel ab und verschlüsselt eine Nachricht
const { key: aliceKey, header } = senderEncapsulate(bob.publicKey);
const box = encrypt(aliceKey, 'Vertraulich: Migrationsplan PQC Q3 2026');

// 3. Bob leitet denselben Schlüssel ab und entschlüsselt
const bobKey = recipientDecapsulate(bob.privateKey, header);
const klartext = decrypt(bobKey, box);

console.log('Schlüssel identisch:', timingSafeEqual(Buffer.from(aliceKey), Buffer.from(bobKey)));
console.log('Ciphertext-Größe   :', box.ct.length, 'Byte');
console.log('Header (Kapsel)    :', header.cipherText.length, 'Byte');
console.log('Entschlüsselt      :', klartext);

Die Ausgabe bestätigt, dass beide Seiten denselben Schlüssel abgeleitet haben und der Klartext korrekt rekonstruiert wurde. Über die Leitung gingen nur die Header-Daten (ephemerer X25519-Public-Key plus ML-KEM-Kapsel) und der GCM-Ciphertext mit IV und Tag. Das dauerhafte Geheimnis hat nie das System verlassen.

$ node main.mjs
Schlüssel identisch: true
Ciphertext-Größe   : 39 Byte
Header (Kapsel)    : 1088 Byte
Entschlüsselt      : Vertraulich: Migrationsplan PQC Q3 2026

Schritt 9: Konstantzeit-Vergleich und Seitenkanäle

Kryptografie scheitert in der Praxis selten an gebrochenen Algorithmen, sondern an Implementierungsfehlern. Der häufigste ist der zeitabhängige Vergleich. Wenn Sie zwei Auth-Tags oder zwei Geheimnisse mit === oder einem normalen Schleifenvergleich prüfen, bricht der Vergleich beim ersten unterschiedlichen Byte ab. Ein Angreifer, der die Antwortzeit misst, kann darüber Byte für Byte erraten.

Die Node.js-Sicherheitsleitlinie empfiehlt deshalb ausdrücklich crypto.timingSafeEqual für den Vergleich sensibler Werte. Die Funktion läuft immer in konstanter Zeit, unabhängig davon, wo die erste Abweichung liegt. Wichtig: Beide Buffer müssen gleich lang sein, sonst wirft die Funktion eine Ausnahme. Vergleichen Sie deshalb vorher die Länge oder hashen Sie beide Werte auf eine feste Länge.

// 06-constant-time.mjs
import { timingSafeEqual } from 'node:crypto';

export function safeEqual(a, b) {
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);
  if (bufA.length !== bufB.length) return false;   // Länge ist nicht geheim
  return timingSafeEqual(bufA, bufB);
}

console.log(safeEqual('7d2e9a4b', '7d2e9a4b')); // true
console.log(safeEqual('7d2e9a4b', '7d2e9a4c')); // false, konstante Zeit

Vermeiden Sie außerdem, geheime Schlüssel zu loggen, in Fehlermeldungen einzubetten oder als Funktionsargumente in Stack-Traces auftauchen zu lassen. In langlebigen Prozessen können Sie sensible Buffer nach Gebrauch mit buf.fill(0) überschreiben. Das ist in JavaScript wegen der Garbage Collection keine Garantie, reduziert aber das Zeitfenster, in dem Geheimnisse im Speicher liegen.

Schritt 10: Eine Datei quantensicher verschlüsseln

Um das Gelernte praktisch zu machen, verschlüsseln wir eine echte Datei. Das folgende CLI-Skript nimmt einen Dateipfad und Bobs Public-Key-Datei, leitet einen Schlüssel über ML-KEM ab und schreibt eine .pqc-Datei, die Kapsel, IV, Tag und Ciphertext enthält.

// encrypt-file.mjs
import { readFileSync, writeFileSync } from 'node:fs';
import { randomBytes, createCipheriv } from 'node:crypto';
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';

const [,, inputPath, pubPath] = process.argv;
if (!inputPath || !pubPath) {
  console.error('Aufruf: node encrypt-file.mjs <datei> <public-key>');
  process.exit(1);
}

const recipientPub = new Uint8Array(Buffer.from(readFileSync(pubPath, 'utf8'), 'base64'));
const { cipherText, sharedSecret } = ml_kem768.encapsulate(recipientPub);

const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);
const data = readFileSync(inputPath);
const ct = Buffer.concat([cipher.update(data), cipher.final()]);
const tag = cipher.getAuthTag();

const out = Buffer.concat([
  Buffer.from(cipherText), iv, tag, ct,   // Kapsel + IV + Tag + Ciphertext
]);
writeFileSync(inputPath + '.pqc', out);
console.log('Verschlüsselt:', inputPath + '.pqc', '(' + out.length + ' Byte)');

Hier nutzen wir bewusst die einfache KEM-Variante ohne Hybrid, um das Dateiformat überschaubar zu halten. Beachten Sie die feste Reihenfolge im Container: Die ersten 1.088 Byte sind die ML-KEM-Kapsel, dann 12 Byte IV, dann 16 Byte Auth-Tag, dann der Ciphertext. Beim Entschlüsseln lesen Sie diese Blöcke in derselben Reihenfolge wieder aus und rufen decapsulate auf, um den Schlüssel zurückzugewinnen.

$ echo "Testinhalt fuer PQC" > geheim.txt
$ node 03-serialize.mjs        # erzeugt bob.pub
$ node encrypt-file.mjs geheim.txt bob.pub
Verschlüsselt: geheim.txt.pqc (1136 Byte)

Das Gegenstück liest den Container in derselben festen Reihenfolge wieder ein. Es schneidet Kapsel, IV und Tag heraus, gewinnt über decapsulate den Schlüssel zurück und entschlüsselt den Rest. Bob braucht dafür seinen geheimen Schlüssel, den wir hier der Einfachheit halber frisch im Skript erzeugen. In der Praxis würden Sie genau das Schlüsselpaar laden, dessen Public Key Sie zuvor an den Absender gegeben haben.

// decrypt-file.mjs
import { readFileSync, writeFileSync } from 'node:fs';
import { createDecipheriv } from 'node:crypto';
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';

const [,, encPath] = process.argv;
const blob = readFileSync(encPath);

// Feste Offsets: 1088 Byte Kapsel, 12 Byte IV, 16 Byte Tag, Rest = Ciphertext
const cipherText = new Uint8Array(blob.subarray(0, 1088));
const iv  = blob.subarray(1088, 1100);
const tag = blob.subarray(1100, 1116);
const ct  = blob.subarray(1116);

const secretKey = bobSecretKeyFromSecureStore();           // Ihr Secret Key
const sharedSecret = ml_kem768.decapsulate(cipherText, secretKey);

const decipher = createDecipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);
decipher.setAuthTag(tag);
const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
writeFileSync(encPath.replace('.pqc', '.dec'), plain);
console.log('Entschlüsselt nach', encPath.replace('.pqc', '.dec'));

Die festen Offsets 1088, 1100 und 1116 ergeben sich direkt aus den Größen von ML-KEM-768-Kapsel (1.088 Byte), GCM-IV (12 Byte) und Auth-Tag (16 Byte). Würden Sie auf ML-KEM-1024 wechseln, müssten Sie den ersten Offset auf 1.568 anpassen. Genau deshalb empfiehlt sich eine Versionskennung im Container, wie in Schritt 12 beschrieben: Sie liest die richtigen Offsets selbst aus, statt sie fest zu verdrahten.

ML-KEM im Vergleich zu RSA und ECDH

Um ML-KEM richtig einzuordnen, hilft ein direkter Vergleich mit den klassischen Verfahren, die es ablösen soll. RSA und ECDH (über X25519) sind seit Jahrzehnten erprobt, fallen aber gegen einen Quantencomputer. ML-KEM ist quantensicher, dafür jung und mit deutlich größeren Schlüsseln. Die folgende Tabelle stellt die wichtigsten Eigenschaften gegenüber.

EigenschaftRSA-3072X25519 (ECDH)ML-KEM-768
TypVerschlüsselung/KEMSchlüsselaustauschKEM
Quantensicherneinneinja
Öffentlicher Schlüssel~384 Byte32 Byte1.184 Byte
Übertragene Daten~384 Byte32 Byte1.088 Byte
Mathematische BasisFaktorisierungDiskreter LogarithmusGitterproblem (MLWE)
Reifesehr hochhochstandardisiert seit 2024
ML-KEM-768 im Vergleich zu klassischen Verfahren, Werte gerundet.

Die Tabelle macht das Kernargument für den hybriden Ansatz sichtbar. RSA und X25519 sind reif und klein, aber nicht quantensicher. ML-KEM ist quantensicher, aber jung und groß. Kombinieren Sie X25519 mit ML-KEM-768, erhalten Sie das Beste aus beiden Welten: die jahrzehntelange Erprobung des klassischen Verfahrens gegen heutige Angreifer und den Quantenschutz von ML-KEM gegen zukünftige. Der Preis sind rund 1,1 Kilobyte zusätzliche Übertragung pro Schlüsselaustausch, ein für die meisten Anwendungen kleiner Aufpreis.

Schritt 11: Performance und Benchmarks

Ein verbreitetes Missverständnis ist, Post-Quanten-Kryptografie sei zu langsam für den Produktiveinsatz. Bei ML-KEM stimmt das nicht. Die Operationen liegen im Bereich von ein bis zwei Millisekunden und sind damit für die meisten Anwendungen vernachlässigbar. Die folgende Tabelle zeigt eigene Messungen mit @noble/post-quantum 0.6.1 unter Node.js 20 auf einem durchschnittlichen Testsystem. Die absoluten Werte hängen von Ihrer Hardware ab, die Verhältnisse zwischen den Parameter-Sätzen sind aber aussagekräftig.

OperationML-KEM-512ML-KEM-768ML-KEM-1024
Schlüsselerzeugung~0,9 ms~1,0 ms~1,7 ms
Verkapselung~1,0 ms~1,6 ms~2,2 ms
Entkapselung~1,2 ms~1,6 ms~2,4 ms
Public Key800 Byte1.184 Byte1.568 Byte
Ciphertext768 Byte1.088 Byte1.568 Byte
Eigene Messungen, @noble/post-quantum 0.6.1, Node.js 20, Werte auf einem Testsystem gerundet.

Der eigentliche Kostentreiber ist nicht die Rechenzeit, sondern die Größe. Eine ML-KEM-768-Kapsel ist mit 1.088 Byte rund 34-mal größer als ein X25519-Schlüsselaustausch mit 32 Byte. Im hybriden TLS-Handshake erhöht das die Größe des ClientHello und kann in seltenen Fällen Probleme mit Middleboxes oder MTU-Grenzen verursachen. Für die meisten Anwendungen ist der zusätzliche Kilobyte pro Verbindung jedoch unkritisch. Für eine reine JavaScript-Implementierung sind ein bis zwei Millisekunden ein sehr guter Wert. Native C-Bibliotheken sind nochmals deutlich schneller.

Schritt 12: Migration und Best Practices für die Produktion

Der Sprung vom Tutorial in den Produktivbetrieb braucht ein paar zusätzliche Entscheidungen. Die wichtigste haben Sie bereits getroffen: immer hybrid. Setzen Sie ML-KEM niemals allein ein, solange das BSI und der IETF-Hybrid-Entwurf den kombinierten Betrieb empfehlen. Der hybride Ansatz kostet kaum Performance, schützt Sie aber vor einem überraschenden Bruch des jungen Gitterverfahrens.

Krypto-Agilität einplanen

Verdrahten Sie den Algorithmus nicht fest. Speichern Sie in jedem verschlüsselten Container eine Versionskennung, etwa mlkem768-x25519-v1. So können Sie später auf ML-KEM-1024 oder einen Nachfolge-Algorithmus wechseln, ohne alte Daten unlesbar zu machen. Diese Krypto-Agilität ist die zentrale Lehre aus jeder bisherigen Krypto-Migration: Wer sich auf ein einziges Verfahren festlegt, zahlt beim nächsten Bruch teuer.

Checkliste für den Produktiveinsatz

  • Geheime Schlüssel ausschließlich verschlüsselt oder im HSM speichern, nie im Klartext.
  • Für jede Verschlüsselung einen frischen, zufälligen IV verwenden, niemals wiederverwenden.
  • Auth-Tag und IV immer gemeinsam mit dem Ciphertext speichern.
  • Sensible Vergleiche stets über timingSafeEqual laufen lassen.
  • Abhängigkeiten über npm audit und Lockfiles fixieren, um Supply-Chain-Risiken zu senken.
  • Eine Bestandsaufnahme aller Stellen führen, an denen heute RSA oder ECDH genutzt wird.

Für TLS-Verbindungen müssen Sie ML-KEM übrigens nicht selbst implementieren. Moderne Browser und Server unterstützen den hybriden Schlüsselaustausch X25519MLKEM768 bereits nativ, und ein wachsender Anteil des Web-Traffics ist dadurch schon quantensicher abgesichert. Ihr selbst gebautes Modul ist vor allem für Anwendungsschicht-Verschlüsselung sinnvoll, etwa für Datei-Container, Nachrichten oder gespeicherte Geheimnisse.

Häufige Fehler und wie Sie sie vermeiden

Diese fünf Fehler tauchen bei fast jeder ersten PQC-Implementierung auf. Wenn Sie sie kennen, sparen Sie sich Stunden Fehlersuche.

  1. ML-KEM allein einsetzen. Ohne klassischen Hybrid-Partner verlassen Sie sich vollständig auf ein junges Verfahren. Kombinieren Sie immer mit X25519, wie in Schritt 6 gezeigt.
  2. Das Shared Secret direkt als AES-Schlüssel nehmen. Schicken Sie es zuerst durch HKDF mit einem Kontext-Label. Nur so binden Sie den Schlüssel an Ihre Anwendung und stellen eine gleichmäßige Verteilung sicher.
  3. Den IV wiederverwenden. Ein zweimal genutzter IV bei AES-GCM bricht die gesamte Vertraulichkeit. Würfeln Sie ihn pro Nachricht neu mit randomBytes(12).
  4. Geheimnisse mit === vergleichen. Das öffnet einen Timing-Seitenkanal. Nutzen Sie ausschließlich timingSafeEqual.
  5. Den Importpfad ohne .js schreiben. @noble/post-quantum exportiert Subpfade mit Dateiendung. Der Import '@noble/post-quantum/ml-kem' schlägt fehl, '.../ml-kem.js' funktioniert.

Troubleshooting: Die häufigsten Fehlermeldungen

Die folgende Tabelle sammelt die acht häufigsten Fehler beim Bau dieses Moduls samt Ursache und Lösung. Die meisten haben mit ESM, Importpfaden oder Datentypen zu tun, nicht mit der Kryptografie selbst.

FehlermeldungUrsacheLösung
ERR_PACKAGE_PATH_NOT_EXPORTEDImportpfad ohne .jsSubpfad @noble/post-quantum/ml-kem.js mit Endung importieren
ERR_REQUIRE_ESMrequire() auf ESM-Paketimport nutzen und type=module setzen
Cannot use import statement outside a moduletype: module fehltnpm pkg set type=module ausführen
Unsupported state or unable to authenticate dataFalscher Schlüssel oder manipulierter CiphertextSchlüsselableitung prüfen, IV und Tag korrekt laden
Input buffers must have the same byte lengthtimingSafeEqual mit ungleich langen BuffernLängen vorab vergleichen, dann timingSafeEqual
x25519.utils.randomSecretKey is not a functionVeraltete @noble/curves-VersionAuf 2.x aktualisieren, API hat sich geändert
Invalid key lengthShared Secret nicht 32 Byte für AES-256Immer 32-Byte-Schlüssel aus HKDF verwenden
decapsulate liefert anderes GeheimnisFalscher Secret Key oder vertauschte KapselPublic/Secret-Zuordnung und Kapsel-Reihenfolge prüfen
Acht häufige Fehler bei der ML-KEM-Implementierung in Node.js.

Ein Sonderfall verdient Erwähnung: Wenn decapsulate bei einem manipulierten Ciphertext nicht abbricht, sondern ein zufällig wirkendes Geheimnis zurückgibt, ist das beabsichtigt. ML-KEM nutzt sogenannte implizite Ablehnung (implicit rejection). Statt einen Fehler zu werfen, liefert es ein deterministisch falsches Geheimnis, das nicht zum Absender passt. Die eigentliche Fehlererkennung übernimmt dann die AEAD-Schicht über das Auth-Tag.

Fortgeschrittene Tipps

Wenn das Grundgerüst steht, lohnen sich ein paar Erweiterungen für robustere Systeme. Authentifizierung der Kapsel: Das hier gezeigte Verfahren schützt vor passivem Mitlesen, nicht aber vor einem aktiven Man-in-the-Middle, der Bobs Public Key fälscht. Binden Sie den Public Key an eine Identität, etwa über eine Signatur mit ML-DSA (FIPS 204) oder über ein etabliertes PKI-Zertifikat.

ChaCha20-Poly1305 statt AES-GCM: Auf Servern ohne AES-Hardwarebeschleunigung ist ChaCha20-Poly1305 oft schneller und gegen IV-Wiederverwendung etwas gutmütiger. Node.js unterstützt es über createCipheriv('chacha20-poly1305', ...). Der abgeleitete 32-Byte-Schlüssel passt unverändert.

Domänentrennung über das info-Feld: Verwenden Sie für jeden Einsatzzweck ein eigenes HKDF-Label, etwa mlkem-hybrid-file-v1 und mlkem-hybrid-message-v1. So kann ein für Dateien abgeleiteter Schlüssel nie versehentlich für Nachrichten genutzt werden, selbst wenn das Ausgangsgeheimnis identisch ist. Diese Trennung kostet nichts und verhindert eine ganze Klasse subtiler Fehler.

Parameter-Wahl dokumentieren: ML-KEM-768 ist für die meisten Anwendungen die richtige Wahl und auch der Wert hinter dem TLS-Hybrid X25519MLKEM768. Greifen Sie nur dann zu ML-KEM-1024, wenn Sie ein konkretes Schutzbedarfsniveau erfüllen müssen, das NIST-Stufe 5 verlangt. Die größere Variante kostet mehr Bandbreite, ohne für die meisten Bedrohungsmodelle einen praktischen Mehrwert zu liefern.

Häufig gestellte Fragen

Ist ML-KEM dasselbe wie Kyber?

Praktisch ja. ML-KEM ist die standardisierte Form von CRYSTALS-Kyber, festgelegt im NIST-Standard FIPS 203. Der Name wurde im Zuge der Standardisierung geändert, die kryptografische Konstruktion blieb nahezu identisch. Suchen Sie nach Code oder Dokumentation, kennen Sie am besten beide Begriffe.

Kann ich ML-KEM ohne den klassischen Hybrid-Teil nutzen?

Technisch ja, empfohlen nein. BSI und der IETF-Entwurf für hybride Schlüsselaustausche raten dazu, ML-KEM mit einem bewährten Verfahren wie X25519 zu kombinieren. So bleibt Ihr Schlüssel sicher, selbst wenn in einem der beiden Verfahren eine Schwäche gefunden wird.

Warum unterstützt Node.js ML-KEM nicht direkt im crypto-Modul?

Node.js 20 und 22 bieten ML-KEM noch nicht nativ. Die Integration der zugrunde liegenden OpenSSL-Funktionen in die JavaScript-API braucht Zeit. Bis dahin empfiehlt die Node.js-Sicherheitsleitlinie geprüfte Bibliotheken oder WebAssembly, weshalb wir @noble/post-quantum einsetzen.

Welchen Parameter-Satz soll ich wählen?

ML-KEM-768 ist der empfohlene Standard und entspricht NIST-Sicherheitsstufe 3. Es ist auch der Wert hinter dem verbreiteten TLS-Hybrid X25519MLKEM768. ML-KEM-1024 wählen Sie nur, wenn ein konkreter Schutzbedarf die höchste Stufe verlangt.

Ist ML-KEM schon sicher genug für den Produktiveinsatz?

Als Teil eines hybriden Verfahrens ja. Genau dafür hat NIST den Standard finalisiert. Das Restrisiko eines unentdeckten Angriffs auf das Gitterverfahren fangen Sie durch die Kombination mit X25519 ab. Der hybride Betrieb gilt 2026 als bewährte Praxis.

Schützt mich ML-KEM vor heutigen Angreifern?

Ja, vor allem gegen das Harvest-now-decrypt-later-Muster. Daten, die Sie heute mit hybridem ML-KEM austauschen, bleiben auch dann vertraulich, wenn ein Angreifer sie speichert und später mit einem Quantencomputer entschlüsseln will. Für klassische Angreifer ohne Quantencomputer schützt bereits der X25519-Anteil.

Brauche ich ML-KEM auch für TLS auf meinem Server?

Dafür müssen Sie selten selbst Code schreiben. Aktuelle Browser und Webserver unterstützen den hybriden Austausch X25519MLKEM768 bereits. Ihr eigenes Modul ist vor allem für Verschlüsselung auf Anwendungsebene sinnvoll, etwa Dateien, Nachrichten oder gespeicherte Geheimnisse.

Fazit: Quantensicher in 12 Schritten

Sie haben ein vollständiges, lauffähiges Post-Quanten-Modul gebaut: vom ersten ML-KEM-768-Schlüsselpaar über den hybriden Schlüsselaustausch mit X25519 bis zur authentifizierten Verschlüsselung mit AES-256-GCM. Die wichtigste Erkenntnis ist nicht der Code, sondern das Prinzip: ML-KEM etabliert nur ein Geheimnis, der hybride Aufbau schützt vor jungen Risiken, und HKDF plus AEAD machen daraus echte Vertraulichkeit.

Für DACH-Unternehmen ist der nächste Schritt eine Bestandsaufnahme: Wo wird heute RSA oder ECDH genutzt, welche Daten haben eine lange Schutzdauer, und welche Systeme lassen sich mit Krypto-Agilität nachrüsten? Die Algorithmen stehen seit August 2024 als NIST-Standards fest. Die Migration ist 2026 keine Forschungsfrage mehr, sondern eine Frage der Planung und Disziplin.

Weiterführende Quellen: NIST FIPS 203 (ML-KEM-Standard), noble-post-quantum auf GitHub, Node.js crypto-Dokumentation, BSI zu Post-Quanten-Kryptografie und der IETF-Entwurf für hybriden TLS-Schlüsselaustausch.