{"id":153,"date":"2026-06-15T16:22:54","date_gmt":"2026-06-15T16:22:54","guid":{"rendered":"https:\/\/shattered.io\/de\/2026\/06\/15\/ml-kem-kyber-nodejs\/"},"modified":"2026-06-15T20:06:04","modified_gmt":"2026-06-15T20:06:04","slug":"ml-kem-kyber-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/de\/2026\/06\/15\/ml-kem-kyber-nodejs\/","title":{"rendered":"ML-KEM (Kyber) in Node.js: 12 Schritte [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Ein Angreifer, der heute Ihren verschl\u00fcsselten Datenverkehr mitschneidet, braucht den passenden Quantencomputer noch gar nicht. Er speichert die Daten einfach und entschl\u00fcsselt sie, sobald die Hardware bereit ist. Dieses Muster hei\u00dft <strong>Harvest now, decrypt later<\/strong>, und es macht den Wechsel zu quantensicherer Kryptografie schon 2026 dringend. In diesem Tutorial bauen Sie in 12 Schritten ein vollst\u00e4ndiges, lauff\u00e4higes Node.js-Modul, das mit <strong>ML-KEM (Kyber)<\/strong> nach dem NIST-Standard FIPS 203 einen quantensicheren Schl\u00fcssel austauscht und damit Nachrichten verschl\u00fcsselt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sie lernen, warum ein reines ML-KEM-Setup riskant ist, wie ein <strong>hybrider Schl\u00fcsselaustausch<\/strong> aus X25519 und ML-KEM-768 funktioniert, und wie Sie das Ergebnis mit AES-256-GCM zu einer echten Ende-zu-Ende-Verschl\u00fcsselung zusammenf\u00fcgen. Jeder Codeblock in diesem Artikel wurde unter Node.js 20 mit der Bibliothek <code>@noble\/post-quantum<\/code> 0.6.1 getestet. Planen Sie etwa 45 bis 60 Minuten f\u00fcr den vollst\u00e4ndigen Durchlauf ein.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"warum-pqc-2026\">Warum Post-Quanten-Kryptografie 2026 keine Theorie mehr ist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Klassische Public-Key-Verfahren wie RSA und Elliptische-Kurven-Kryptografie (ECC) beruhen auf Rechenproblemen, die f\u00fcr normale Computer praktisch unl\u00f6sbar sind. Ein hinreichend gro\u00dfer Quantencomputer l\u00f6st 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\u00fcssell\u00e4nge, weshalb AES-256 quantensicher bleibt und AES-128 auf das Sicherheitsniveau von AES-64 sinkt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das Problem ist nicht, ob ein solcher Quantencomputer 2026 schon existiert. Das Problem ist die Vorlaufzeit. Daten mit langer Schutzdauer, etwa Gesundheitsakten, Beh\u00f6rdenkommunikation, Quellcode oder Staatsgeheimnisse, m\u00fcssen 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\u00e4ter entschl\u00fcsselt. Genau diese Logik hat die Standardisierung beschleunigt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das US-amerikanische NIST hat im August 2024 die ersten drei Post-Quanten-Standards finalisiert. ML-KEM f\u00fcr den Schl\u00fcsselaustausch wurde als <strong>FIPS 203<\/strong> ver\u00f6ffentlicht, ML-DSA als FIPS 204 und SLH-DSA als FIPS 205 f\u00fcr digitale Signaturen. Das deutsche BSI empfiehlt in seinen Technischen Richtlinien ausdr\u00fccklich den hybriden Einsatz, also die Kombination eines klassischen Verfahrens mit einem Post-Quanten-Verfahren, bis die neuen Algorithmen langfristig erprobt sind. F\u00fcr DACH-Unternehmen ist die Migration damit keine Forschungsfrage mehr, sondern eine Planungsaufgabe.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wichtig f\u00fcr das Verst\u00e4ndnis dieses Tutorials: ML-KEM ist <strong>kein<\/strong> Verschl\u00fcsselungsverfahren 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 \u00fcberlassen. Diese Trennung ist die h\u00e4ufigste Verst\u00e4ndnish\u00fcrde und der Grund, warum so viele PQC-Tutorials in der Praxis scheitern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ml-kem-kyber-fips-203\">ML-KEM, Kyber und FIPS 203: Die Grundlagen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ML-KEM steht f\u00fcr <strong>Module-Lattice-Based Key-Encapsulation Mechanism<\/strong>. Der Algorithmus basiert auf dem Forschungsentwurf <strong>CRYSTALS-Kyber<\/strong>, weshalb viele \u00e4ltere 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\u00fcr Kyber und ML-KEM \u00fcberschneiden sich stark, und npm-Pakete tragen oft noch beide Bezeichnungen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Sicherheit von ML-KEM beruht auf dem <strong>Module Learning With Errors<\/strong>-Problem (MLWE). Vereinfacht gesagt versteckt der Algorithmus ein Geheimnis in einem Gitter aus absichtlich verrauschten linearen Gleichungen. Ohne den geheimen Schl\u00fcssel ist es selbst f\u00fcr einen Quantencomputer extrem aufwendig, das Rauschen vom Signal zu trennen. Dieses Gitterproblem gilt nach heutigem Kenntnisstand als quantenresistent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">FIPS 203 definiert drei Parameter-S\u00e4tze mit unterschiedlichem Sicherheitsniveau. ML-KEM-768 ist der von vielen Beh\u00f6rden empfohlene Standardwert und entspricht grob der Sicherheit von AES-192. Die folgende Tabelle zeigt die exakten Gr\u00f6\u00dfen, gemessen mit <code>@noble\/post-quantum<\/code> 0.6.1. Beachten Sie, wie gro\u00df die Schl\u00fcssel im Vergleich zu klassischer ECC sind: Ein X25519-Schl\u00fcssel ist nur 32 Byte gro\u00df, ein ML-KEM-768-Public-Key bereits 1.184 Byte.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Eigenschaft<\/th><th>ML-KEM-512<\/th><th>ML-KEM-768<\/th><th>ML-KEM-1024<\/th><\/tr><\/thead><tbody><tr><td>NIST-Sicherheitsstufe<\/td><td>Stufe 1 (AES-128)<\/td><td>Stufe 3 (AES-192)<\/td><td>Stufe 5 (AES-256)<\/td><\/tr><tr><td>\u00d6ffentlicher Schl\u00fcssel<\/td><td>800 Byte<\/td><td>1.184 Byte<\/td><td>1.568 Byte<\/td><\/tr><tr><td>Geheimer Schl\u00fcssel<\/td><td>1.632 Byte<\/td><td>2.400 Byte<\/td><td>3.168 Byte<\/td><\/tr><tr><td>Ciphertext (Kapsel)<\/td><td>768 Byte<\/td><td>1.088 Byte<\/td><td>1.568 Byte<\/td><\/tr><tr><td>Shared Secret<\/td><td>32 Byte<\/td><td>32 Byte<\/td><td>32 Byte<\/td><\/tr><tr><td>Empfohlen f\u00fcr<\/td><td>ressourcenarm<\/td><td>Standard<\/td><td>Hochsicherheit<\/td><\/tr><\/tbody><\/table><figcaption>Parameter-S\u00e4tze von ML-KEM nach FIPS 203, Schl\u00fcsselgr\u00f6\u00dfen aus eigener Messung.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Der KEM-Ablauf besteht aus drei Operationen. Bei <strong>KeyGen<\/strong> erzeugt der Empf\u00e4nger ein Schl\u00fcsselpaar und ver\u00f6ffentlicht den Public Key. Bei <strong>Encapsulate<\/strong> nimmt der Absender diesen Public Key, w\u00fcrfelt ein zuf\u00e4lliges Geheimnis und gibt zwei Dinge zur\u00fcck: das gemeinsame Geheimnis (f\u00fcr sich selbst) und einen Ciphertext (f\u00fcr den Empf\u00e4nger). Bei <strong>Decapsulate<\/strong> rekonstruiert der Empf\u00e4nger aus Ciphertext und geheimem Schl\u00fcssel exakt dasselbe Geheimnis. Anders als bei Diffie-Hellman ist der Ablauf also asymmetrisch: Nur eine Seite braucht ein dauerhaftes Schl\u00fcsselpaar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"voraussetzungen\">Voraussetzungen mit Versionen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Bevor Sie loslegen, brauchen Sie eine funktionierende Node.js-Umgebung und ein grundlegendes Verst\u00e4ndnis von asynchronem JavaScript. Sie m\u00fcssen kein Kryptograf sein, aber Sie sollten wissen, was ein \u00f6ffentlicher und ein privater Schl\u00fcssel ist. Die folgende Tabelle listet alle ben\u00f6tigten Komponenten mit getesteten Versionen.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Komponente<\/th><th>Getestete Version<\/th><th>Zweck<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>20 LTS (oder 22 LTS)<\/td><td>Laufzeitumgebung, ESM-Support<\/td><\/tr><tr><td>npm<\/td><td>10.x<\/td><td>Paketverwaltung<\/td><\/tr><tr><td>@noble\/post-quantum<\/td><td>0.6.1<\/td><td>ML-KEM nach FIPS 203<\/td><\/tr><tr><td>@noble\/curves<\/td><td>2.2.0<\/td><td>X25519 f\u00fcr den Hybrid-Teil<\/td><\/tr><tr><td>@noble\/hashes<\/td><td>2.2.0<\/td><td>HKDF und SHA-256<\/td><\/tr><tr><td>node:crypto<\/td><td>integriert<\/td><td>AES-256-GCM, Zufall, Konstantzeit<\/td><\/tr><\/tbody><\/table><figcaption>Voraussetzungen f\u00fcr das ML-KEM-Tutorial, alle Versionen getestet.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Warum <code>@noble\/post-quantum<\/code> und nicht das Node-Core-Modul? Node.js bietet ML-KEM in Version 20 und 22 noch nicht nativ im <code>crypto<\/code>-Modul an. Die offizielle Node.js-Sicherheitsleitlinie empfiehlt f\u00fcr nicht nativ unterst\u00fctzte Algorithmen bew\u00e4hrte Bibliotheken oder WebAssembly. <code>@noble\/post-quantum<\/code> von Paul Miller ist eine auditierte, abh\u00e4ngigkeitsfreie 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pr\u00fcfen Sie zuerst Ihre Node-Version. Alles ab Node 18 funktioniert, aber 20 LTS oder 22 LTS sind die empfohlenen St\u00e4nde f\u00fcr 2026.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node --version\nv20.20.2\n\n$ npm --version\n10.8.2<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-1-2-projekt\">Schritt 1 und 2: Projekt anlegen und Bibliotheken installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Legen Sie ein neues Projektverzeichnis an und initialisieren Sie es. Wichtig: <code>@noble\/post-quantum<\/code> ist ein reines ESM-Paket. Damit Sie <code>import<\/code> statt <code>require<\/code> nutzen k\u00f6nnen, setzen Sie in der <code>package.json<\/code> den Typ auf <code>module<\/code>. Das ist die h\u00e4ufigste Fehlerquelle bei Einsteigern, dazu sp\u00e4ter mehr im Troubleshooting.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ mkdir pqc-nodejs && cd pqc-nodejs\n$ npm init -y\n$ npm pkg set type=module\n$ npm install @noble\/post-quantum@0.6.1 @noble\/curves@2.2.0 @noble\/hashes@2.2.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Aufruf <code>npm pkg set type=module<\/code> tr\u00e4gt <code>\"type\": \"module\"<\/code> direkt in die <code>package.json<\/code> ein, ohne dass Sie die Datei manuell \u00f6ffnen m\u00fcssen. Pr\u00fcfen Sie nach der Installation kurz, ob alle drei Pakete in den Abh\u00e4ngigkeiten stehen. Die <code>@noble<\/code>-Familie ist bewusst klein gehalten und zieht keine transitiven Abh\u00e4ngigkeiten nach, was die Angriffsfl\u00e4che reduziert.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ npm ls\npqc-nodejs@1.0.0\n\u251c\u2500\u2500 @noble\/curves@2.2.0\n\u251c\u2500\u2500 @noble\/hashes@2.2.0\n\u2514\u2500\u2500 @noble\/post-quantum@0.6.1<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-3-schluesselpaar\">Schritt 3: Erstes ML-KEM-768-Schl\u00fcsselpaar erzeugen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Erstellen Sie eine Datei <code>01-keygen.mjs<\/code>. Der wichtigste Punkt ist der Importpfad: Die Bibliothek exportiert ML-KEM unter dem Subpfad <code>@noble\/post-quantum\/ml-kem.js<\/code>, inklusive Dateiendung. Vergessen Sie das <code>.js<\/code>, bricht der Import mit <code>ERR_PACKAGE_PATH_NOT_EXPORTED<\/code> ab.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 01-keygen.mjs\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\n\nconst keys = ml_kem768.keygen();\n\nconsole.log('Public Key  :', keys.publicKey.length, 'Byte');\nconsole.log('Secret Key  :', keys.secretKey.length, 'Byte');\nconsole.log('Public (hex):', Buffer.from(keys.publicKey).toString('hex').slice(0, 48), '...');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fchren Sie das Skript aus. Die <code>keygen()<\/code>-Funktion liefert ein Objekt mit zwei <code>Uint8Array<\/code>-Feldern: <code>publicKey<\/code> und <code>secretKey<\/code>. Diese Typen sind absichtlich keine Node-<code>Buffer<\/code>, damit der Code auch im Browser l\u00e4uft. F\u00fcr die Ausgabe wandeln wir sie mit <code>Buffer.from()<\/code> in Hex um.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node 01-keygen.mjs\nPublic Key  : 1184 Byte\nSecret Key  : 2400 Byte\nPublic (hex): a3f1c0b27e94d6...  ...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Gr\u00f6\u00dfen stimmen mit der Tabelle oben \u00fcberein und best\u00e4tigen, dass Sie tats\u00e4chlich ML-KEM-768 nutzen. Behandeln Sie den <code>secretKey<\/code> ab jetzt wie jeden privaten Schl\u00fcssel: niemals loggen, niemals ins Repository committen, niemals \u00fcber unverschl\u00fcsselte Kan\u00e4le \u00fcbertragen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-4-encapsulate\">Schritt 4: Verkapselung und Entkapselung<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt kommt der Kern von ML-KEM. Stellen Sie sich zwei Parteien vor: Bob besitzt das Schl\u00fcsselpaar aus Schritt 3 und ver\u00f6ffentlicht seinen Public Key. Alice will Bob ein gemeinsames Geheimnis schicken. Sie ruft <code>encapsulate()<\/code> mit Bobs Public Key auf und erh\u00e4lt zwei Werte zur\u00fcck.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 02-kem.mjs\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\nimport { timingSafeEqual } from 'node:crypto';\n\n\/\/ Bob erzeugt sein Schl\u00fcsselpaar\nconst bob = ml_kem768.keygen();\n\n\/\/ Alice verkapselt gegen Bobs Public Key\nconst { cipherText, sharedSecret: aliceSecret } = ml_kem768.encapsulate(bob.publicKey);\n\n\/\/ Bob entkapselt mit seinem Secret Key\nconst bobSecret = ml_kem768.decapsulate(cipherText, bob.secretKey);\n\nconsole.log('Ciphertext  :', cipherText.length, 'Byte');\nconsole.log('Alice Secret:', Buffer.from(aliceSecret).toString('hex'));\nconsole.log('Bob Secret  :', Buffer.from(bobSecret).toString('hex'));\nconsole.log('Identisch   :', timingSafeEqual(Buffer.from(aliceSecret), Buffer.from(bobSecret)));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der entscheidende Punkt: Alice und Bob haben jetzt dasselbe 32-Byte-Geheimnis, ohne dass dieses Geheimnis jemals \u00fcber die Leitung ging. \u00dcbertragen wurde nur der 1.088 Byte gro\u00dfe Ciphertext, der ohne Bobs geheimen Schl\u00fcssel wertlos ist. Genau das ist der Sinn eines KEM.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node 02-kem.mjs\nCiphertext  : 1088 Byte\nAlice Secret: 7d2e9a4b... (64 Hex-Zeichen)\nBob Secret  : 7d2e9a4b... (64 Hex-Zeichen)\nIdentisch   : true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Beachten Sie, dass wir die beiden Geheimnisse mit <code>timingSafeEqual<\/code> vergleichen, nicht mit <code>===<\/code>. Ein naiver Vergleich bricht beim ersten abweichenden Byte ab und verr\u00e4t \u00fcber die Laufzeit, an welcher Stelle sich zwei Werte unterscheiden. Bei geheimen Schl\u00fcsseln ist das ein echtes Seitenkanal-Risiko. Mehr dazu in Schritt 9.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-5-serialisieren\">Schritt 5: Schl\u00fcssel serialisieren und speichern<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In der Praxis m\u00fcssen Sie Schl\u00fcssel speichern und \u00fcber das Netzwerk \u00fcbertragen. <code>Uint8Array<\/code> l\u00e4sst sich nicht direkt in JSON schreiben, deshalb kodieren Sie Schl\u00fcssel als Base64. Beim Einlesen wandeln Sie zur\u00fcck. Die folgende Datei zeigt einen sauberen Roundtrip und schreibt Bobs Public Key in eine Datei.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 03-serialize.mjs\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\nimport { writeFileSync, readFileSync } from 'node:fs';\n\nconst bob = ml_kem768.keygen();\n\n\/\/ Kodieren\nconst pubB64 = Buffer.from(bob.publicKey).toString('base64');\nwriteFileSync('bob.pub', pubB64, 'utf8');\n\n\/\/ Dekodieren\nconst loaded = new Uint8Array(Buffer.from(readFileSync('bob.pub', 'utf8'), 'base64'));\n\nconsole.log('Roundtrip korrekt:',\n  Buffer.from(bob.publicKey).equals(Buffer.from(loaded)));\nconsole.log('Public Key gespeichert in bob.pub (' + pubB64.length + ' Zeichen)');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Den geheimen Schl\u00fcssel sollten Sie nie im Klartext auf die Platte schreiben. Verschl\u00fcsseln Sie ihn mit einem aus einer Passphrase abgeleiteten Schl\u00fcssel, etwa \u00fcber <code>scrypt<\/code> aus dem Node-Core-Modul, oder bewahren Sie ihn in einem Hardware-Sicherheitsmodul beziehungsweise einem Secret-Manager auf. F\u00fcr dieses Tutorial halten wir den geheimen Schl\u00fcssel ausschlie\u00dflich im Speicher.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-6-hybrid\">Schritt 6: Hybrider Schl\u00fcsselaustausch mit X25519 und ML-KEM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt kommt der wichtigste konzeptionelle Schritt. ML-KEM ist neu, und niemand garantiert, dass nicht doch eine bislang unbekannte mathematische Schw\u00e4che gefunden wird. Deshalb empfehlen NIST, BSI und der IETF-Entwurf f\u00fcr hybride TLS-Schl\u00fcsselaustausche, ML-KEM <strong>nicht allein<\/strong> einzusetzen, sondern mit einem bew\u00e4hrten klassischen Verfahren zu kombinieren. Der resultierende Schl\u00fcssel ist genau dann sicher, wenn mindestens eines der beiden Verfahren sicher ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wir kombinieren X25519 (klassischer ECDH) mit ML-KEM-768. Beide liefern ein Geheimnis, das wir aneinanderh\u00e4ngen und durch eine Schl\u00fcsselableitungsfunktion (HKDF mit SHA-256) schicken. Das Verketten allein gen\u00fcgt nicht: HKDF sorgt daf\u00fcr, dass das Ergebnis gleichm\u00e4\u00dfig zuf\u00e4llig verteilt ist und ein eindeutiger Kontext (das <code>info<\/code>-Feld) in den Schl\u00fcssel einflie\u00dft.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 04-hybrid.mjs\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\nimport { x25519 } from '@noble\/curves\/ed25519.js';\nimport { hkdf } from '@noble\/hashes\/hkdf.js';\nimport { sha256 } from '@noble\/hashes\/sha2.js';\n\n\/\/ Hybrides Schl\u00fcsselpaar des Empf\u00e4ngers\nexport function generateHybridKeypair() {\n  const xPriv = x25519.utils.randomSecretKey();\n  const xPub  = x25519.getPublicKey(xPriv);\n  const kem   = ml_kem768.keygen();\n  return {\n    publicKey:  { x25519: xPub,  mlkem: kem.publicKey },\n    privateKey: { x25519: xPriv, mlkem: kem.secretKey },\n  };\n}\n\n\/\/ Absender leitet gemeinsamen Schl\u00fcssel ab\nexport function senderEncapsulate(recipientPub) {\n  const ephPriv = x25519.utils.randomSecretKey();\n  const ephPub  = x25519.getPublicKey(ephPriv);\n  const classical = x25519.getSharedSecret(ephPriv, recipientPub.x25519);\n  const { cipherText, sharedSecret: pq } = ml_kem768.encapsulate(recipientPub.mlkem);\n\n  const ikm  = new Uint8Array([...classical, ...pq]);\n  const info = new TextEncoder().encode('mlkem-hybrid-v1');\n  const key  = hkdf(sha256, ikm, undefined, info, 32);\n  return { key, header: { ephPub, cipherText } };\n}\n\n\/\/ Empf\u00e4nger leitet denselben Schl\u00fcssel ab\nexport function recipientDecapsulate(privateKey, header) {\n  const classical = x25519.getSharedSecret(privateKey.x25519, header.ephPub);\n  const pq        = ml_kem768.decapsulate(header.cipherText, privateKey.mlkem);\n\n  const ikm  = new Uint8Array([...classical, ...pq]);\n  const info = new TextEncoder().encode('mlkem-hybrid-v1');\n  return hkdf(sha256, ikm, undefined, info, 32);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Absender erzeugt f\u00fcr X25519 ein fl\u00fcchtiges (ephemeres) Schl\u00fcsselpaar 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\u00e4ngers. Das <code>info<\/code>-Feld <code>mlkem-hybrid-v1<\/code> bindet den Schl\u00fcssel an Ihren Anwendungskontext und verhindert, dass derselbe Schl\u00fcssel versehentlich in einem anderen Protokoll wiederverwendet wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-7-aead\">Schritt 7: Nutzlast mit AES-256-GCM verschl\u00fcsseln<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der abgeleitete 32-Byte-Schl\u00fcssel ist jetzt bereit f\u00fcr die eigentliche Verschl\u00fcsselung. Wir nutzen AES-256-GCM aus dem Node-Core-Modul <code>crypto<\/code>. GCM ist ein AEAD-Verfahren, das gleichzeitig verschl\u00fcsselt und authentifiziert: Jede Manipulation am Ciphertext f\u00e4llt beim Entschl\u00fcsseln durch eine fehlerhafte Auth-Tag-Pr\u00fcfung auf. Der Initialisierungsvektor (IV) muss pro Schl\u00fcssel einzigartig sein, deshalb w\u00fcrfeln wir ihn mit <code>randomBytes<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 05-aead.mjs\nimport { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';\n\nexport function encrypt(key, plaintext) {\n  const iv = randomBytes(12);\n  const cipher = createCipheriv('aes-256-gcm', Buffer.from(key), iv);\n  const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n  return { iv, ct, tag: cipher.getAuthTag() };\n}\n\nexport function decrypt(key, box) {\n  const decipher = createDecipheriv('aes-256-gcm', Buffer.from(key), box.iv);\n  decipher.setAuthTag(box.tag);\n  return Buffer.concat([decipher.update(box.ct), decipher.final()]).toString('utf8');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Falls <code>decipher.final()<\/code> mit der Meldung <code>Unsupported state or unable to authenticate data<\/code> abbricht, stimmt entweder der Schl\u00fcssel nicht, oder der Ciphertext wurde ver\u00e4ndert. Das ist kein Bug, sondern die Schutzfunktion von GCM. Speichern Sie IV und Auth-Tag immer zusammen mit dem Ciphertext, sonst k\u00f6nnen Sie nicht entschl\u00fcsseln.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-8-projekt\">Schritt 8: Das vollst\u00e4ndige Projekt zusammenf\u00fcgen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Jetzt verbinden wir alle Teile zu einem lauff\u00e4higen Programm. Diese Datei importiert die Funktionen aus Schritt 6 und 7 und simuliert einen kompletten Nachrichtenaustausch zwischen Alice und Bob. Sie ist das Herzst\u00fcck des Tutorials und l\u00e4uft ohne weitere Vorbereitung.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ main.mjs\nimport { generateHybridKeypair, senderEncapsulate, recipientDecapsulate } from '.\/04-hybrid.mjs';\nimport { encrypt, decrypt } from '.\/05-aead.mjs';\nimport { timingSafeEqual } from 'node:crypto';\n\n\/\/ 1. Bob erzeugt sein dauerhaftes hybrides Schl\u00fcsselpaar\nconst bob = generateHybridKeypair();\n\n\/\/ 2. Alice leitet einen Schl\u00fcssel ab und verschl\u00fcsselt eine Nachricht\nconst { key: aliceKey, header } = senderEncapsulate(bob.publicKey);\nconst box = encrypt(aliceKey, 'Vertraulich: Migrationsplan PQC Q3 2026');\n\n\/\/ 3. Bob leitet denselben Schl\u00fcssel ab und entschl\u00fcsselt\nconst bobKey = recipientDecapsulate(bob.privateKey, header);\nconst klartext = decrypt(bobKey, box);\n\nconsole.log('Schl\u00fcssel identisch:', timingSafeEqual(Buffer.from(aliceKey), Buffer.from(bobKey)));\nconsole.log('Ciphertext-Gr\u00f6\u00dfe   :', box.ct.length, 'Byte');\nconsole.log('Header (Kapsel)    :', header.cipherText.length, 'Byte');\nconsole.log('Entschl\u00fcsselt      :', klartext);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Ausgabe best\u00e4tigt, dass beide Seiten denselben Schl\u00fcssel abgeleitet haben und der Klartext korrekt rekonstruiert wurde. \u00dcber 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.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ node main.mjs\nSchl\u00fcssel identisch: true\nCiphertext-Gr\u00f6\u00dfe   : 39 Byte\nHeader (Kapsel)    : 1088 Byte\nEntschl\u00fcsselt      : Vertraulich: Migrationsplan PQC Q3 2026<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-9-seitenkanaele\">Schritt 9: Konstantzeit-Vergleich und Seitenkan\u00e4le<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kryptografie scheitert in der Praxis selten an gebrochenen Algorithmen, sondern an Implementierungsfehlern. Der h\u00e4ufigste ist der zeitabh\u00e4ngige Vergleich. Wenn Sie zwei Auth-Tags oder zwei Geheimnisse mit <code>===<\/code> oder einem normalen Schleifenvergleich pr\u00fcfen, bricht der Vergleich beim ersten unterschiedlichen Byte ab. Ein Angreifer, der die Antwortzeit misst, kann dar\u00fcber Byte f\u00fcr Byte erraten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Node.js-Sicherheitsleitlinie empfiehlt deshalb ausdr\u00fccklich <code>crypto.timingSafeEqual<\/code> f\u00fcr den Vergleich sensibler Werte. Die Funktion l\u00e4uft immer in konstanter Zeit, unabh\u00e4ngig davon, wo die erste Abweichung liegt. Wichtig: Beide Buffer m\u00fcssen gleich lang sein, sonst wirft die Funktion eine Ausnahme. Vergleichen Sie deshalb vorher die L\u00e4nge oder hashen Sie beide Werte auf eine feste L\u00e4nge.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 06-constant-time.mjs\nimport { timingSafeEqual } from 'node:crypto';\n\nexport function safeEqual(a, b) {\n  const bufA = Buffer.from(a);\n  const bufB = Buffer.from(b);\n  if (bufA.length !== bufB.length) return false;   \/\/ L\u00e4nge ist nicht geheim\n  return timingSafeEqual(bufA, bufB);\n}\n\nconsole.log(safeEqual('7d2e9a4b', '7d2e9a4b')); \/\/ true\nconsole.log(safeEqual('7d2e9a4b', '7d2e9a4c')); \/\/ false, konstante Zeit<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Vermeiden Sie au\u00dferdem, geheime Schl\u00fcssel zu loggen, in Fehlermeldungen einzubetten oder als Funktionsargumente in Stack-Traces auftauchen zu lassen. In langlebigen Prozessen k\u00f6nnen Sie sensible Buffer nach Gebrauch mit <code>buf.fill(0)<\/code> \u00fcberschreiben. Das ist in JavaScript wegen der Garbage Collection keine Garantie, reduziert aber das Zeitfenster, in dem Geheimnisse im Speicher liegen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-10-datei\">Schritt 10: Eine Datei quantensicher verschl\u00fcsseln<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um das Gelernte praktisch zu machen, verschl\u00fcsseln wir eine echte Datei. Das folgende CLI-Skript nimmt einen Dateipfad und Bobs Public-Key-Datei, leitet einen Schl\u00fcssel \u00fcber ML-KEM ab und schreibt eine <code>.pqc<\/code>-Datei, die Kapsel, IV, Tag und Ciphertext enth\u00e4lt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ encrypt-file.mjs\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { randomBytes, createCipheriv } from 'node:crypto';\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\n\nconst [,, inputPath, pubPath] = process.argv;\nif (!inputPath || !pubPath) {\n  console.error('Aufruf: node encrypt-file.mjs &lt;datei&gt; &lt;public-key&gt;');\n  process.exit(1);\n}\n\nconst recipientPub = new Uint8Array(Buffer.from(readFileSync(pubPath, 'utf8'), 'base64'));\nconst { cipherText, sharedSecret } = ml_kem768.encapsulate(recipientPub);\n\nconst iv = randomBytes(12);\nconst cipher = createCipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);\nconst data = readFileSync(inputPath);\nconst ct = Buffer.concat([cipher.update(data), cipher.final()]);\nconst tag = cipher.getAuthTag();\n\nconst out = Buffer.concat([\n  Buffer.from(cipherText), iv, tag, ct,   \/\/ Kapsel + IV + Tag + Ciphertext\n]);\nwriteFileSync(inputPath + '.pqc', out);\nconsole.log('Verschl\u00fcsselt:', inputPath + '.pqc', '(' + out.length + ' Byte)');<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hier nutzen wir bewusst die einfache KEM-Variante ohne Hybrid, um das Dateiformat \u00fcberschaubar 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\u00fcsseln lesen Sie diese Bl\u00f6cke in derselben Reihenfolge wieder aus und rufen <code>decapsulate<\/code> auf, um den Schl\u00fcssel zur\u00fcckzugewinnen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ echo \"Testinhalt fuer PQC\" &gt; geheim.txt\n$ node 03-serialize.mjs        # erzeugt bob.pub\n$ node encrypt-file.mjs geheim.txt bob.pub\nVerschl\u00fcsselt: geheim.txt.pqc (1136 Byte)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das Gegenst\u00fcck liest den Container in derselben festen Reihenfolge wieder ein. Es schneidet Kapsel, IV und Tag heraus, gewinnt \u00fcber <code>decapsulate<\/code> den Schl\u00fcssel zur\u00fcck und entschl\u00fcsselt den Rest. Bob braucht daf\u00fcr seinen geheimen Schl\u00fcssel, den wir hier der Einfachheit halber frisch im Skript erzeugen. In der Praxis w\u00fcrden Sie genau das Schl\u00fcsselpaar laden, dessen Public Key Sie zuvor an den Absender gegeben haben.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ decrypt-file.mjs\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { createDecipheriv } from 'node:crypto';\nimport { ml_kem768 } from '@noble\/post-quantum\/ml-kem.js';\n\nconst [,, encPath] = process.argv;\nconst blob = readFileSync(encPath);\n\n\/\/ Feste Offsets: 1088 Byte Kapsel, 12 Byte IV, 16 Byte Tag, Rest = Ciphertext\nconst cipherText = new Uint8Array(blob.subarray(0, 1088));\nconst iv  = blob.subarray(1088, 1100);\nconst tag = blob.subarray(1100, 1116);\nconst ct  = blob.subarray(1116);\n\nconst secretKey = bobSecretKeyFromSecureStore();           \/\/ Ihr Secret Key\nconst sharedSecret = ml_kem768.decapsulate(cipherText, secretKey);\n\nconst decipher = createDecipheriv('aes-256-gcm', Buffer.from(sharedSecret), iv);\ndecipher.setAuthTag(tag);\nconst plain = Buffer.concat([decipher.update(ct), decipher.final()]);\nwriteFileSync(encPath.replace('.pqc', '.dec'), plain);\nconsole.log('Entschl\u00fcsselt nach', encPath.replace('.pqc', '.dec'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die festen Offsets 1088, 1100 und 1116 ergeben sich direkt aus den Gr\u00f6\u00dfen von ML-KEM-768-Kapsel (1.088 Byte), GCM-IV (12 Byte) und Auth-Tag (16 Byte). W\u00fcrden Sie auf ML-KEM-1024 wechseln, m\u00fcssten 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vergleich-rsa-ecdh\">ML-KEM im Vergleich zu RSA und ECDH<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um ML-KEM richtig einzuordnen, hilft ein direkter Vergleich mit den klassischen Verfahren, die es abl\u00f6sen soll. RSA und ECDH (\u00fcber X25519) sind seit Jahrzehnten erprobt, fallen aber gegen einen Quantencomputer. ML-KEM ist quantensicher, daf\u00fcr jung und mit deutlich gr\u00f6\u00dferen Schl\u00fcsseln. Die folgende Tabelle stellt die wichtigsten Eigenschaften gegen\u00fcber.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Eigenschaft<\/th><th>RSA-3072<\/th><th>X25519 (ECDH)<\/th><th>ML-KEM-768<\/th><\/tr><\/thead><tbody><tr><td>Typ<\/td><td>Verschl\u00fcsselung\/KEM<\/td><td>Schl\u00fcsselaustausch<\/td><td>KEM<\/td><\/tr><tr><td>Quantensicher<\/td><td>nein<\/td><td>nein<\/td><td>ja<\/td><\/tr><tr><td>\u00d6ffentlicher Schl\u00fcssel<\/td><td>~384 Byte<\/td><td>32 Byte<\/td><td>1.184 Byte<\/td><\/tr><tr><td>\u00dcbertragene Daten<\/td><td>~384 Byte<\/td><td>32 Byte<\/td><td>1.088 Byte<\/td><\/tr><tr><td>Mathematische Basis<\/td><td>Faktorisierung<\/td><td>Diskreter Logarithmus<\/td><td>Gitterproblem (MLWE)<\/td><\/tr><tr><td>Reife<\/td><td>sehr hoch<\/td><td>hoch<\/td><td>standardisiert seit 2024<\/td><\/tr><\/tbody><\/table><figcaption>ML-KEM-768 im Vergleich zu klassischen Verfahren, Werte gerundet.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Die Tabelle macht das Kernargument f\u00fcr den hybriden Ansatz sichtbar. RSA und X25519 sind reif und klein, aber nicht quantensicher. ML-KEM ist quantensicher, aber jung und gro\u00df. 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\u00fcnftige. Der Preis sind rund 1,1 Kilobyte zus\u00e4tzliche \u00dcbertragung pro Schl\u00fcsselaustausch, ein f\u00fcr die meisten Anwendungen kleiner Aufpreis.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-11-benchmarks\">Schritt 11: Performance und Benchmarks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ein verbreitetes Missverst\u00e4ndnis ist, Post-Quanten-Kryptografie sei zu langsam f\u00fcr den Produktiveinsatz. Bei ML-KEM stimmt das nicht. Die Operationen liegen im Bereich von ein bis zwei Millisekunden und sind damit f\u00fcr die meisten Anwendungen vernachl\u00e4ssigbar. Die folgende Tabelle zeigt eigene Messungen mit <code>@noble\/post-quantum<\/code> 0.6.1 unter Node.js 20 auf einem durchschnittlichen Testsystem. Die absoluten Werte h\u00e4ngen von Ihrer Hardware ab, die Verh\u00e4ltnisse zwischen den Parameter-S\u00e4tzen sind aber aussagekr\u00e4ftig.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Operation<\/th><th>ML-KEM-512<\/th><th>ML-KEM-768<\/th><th>ML-KEM-1024<\/th><\/tr><\/thead><tbody><tr><td>Schl\u00fcsselerzeugung<\/td><td>~0,9 ms<\/td><td>~1,0 ms<\/td><td>~1,7 ms<\/td><\/tr><tr><td>Verkapselung<\/td><td>~1,0 ms<\/td><td>~1,6 ms<\/td><td>~2,2 ms<\/td><\/tr><tr><td>Entkapselung<\/td><td>~1,2 ms<\/td><td>~1,6 ms<\/td><td>~2,4 ms<\/td><\/tr><tr><td>Public Key<\/td><td>800 Byte<\/td><td>1.184 Byte<\/td><td>1.568 Byte<\/td><\/tr><tr><td>Ciphertext<\/td><td>768 Byte<\/td><td>1.088 Byte<\/td><td>1.568 Byte<\/td><\/tr><\/tbody><\/table><figcaption>Eigene Messungen, @noble\/post-quantum 0.6.1, Node.js 20, Werte auf einem Testsystem gerundet.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Der eigentliche Kostentreiber ist nicht die Rechenzeit, sondern die Gr\u00f6\u00dfe. Eine ML-KEM-768-Kapsel ist mit 1.088 Byte rund 34-mal gr\u00f6\u00dfer als ein X25519-Schl\u00fcsselaustausch mit 32 Byte. Im hybriden TLS-Handshake erh\u00f6ht das die Gr\u00f6\u00dfe des ClientHello und kann in seltenen F\u00e4llen Probleme mit Middleboxes oder MTU-Grenzen verursachen. F\u00fcr die meisten Anwendungen ist der zus\u00e4tzliche Kilobyte pro Verbindung jedoch unkritisch. F\u00fcr eine reine JavaScript-Implementierung sind ein bis zwei Millisekunden ein sehr guter Wert. Native C-Bibliotheken sind nochmals deutlich schneller.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-12-produktion\">Schritt 12: Migration und Best Practices f\u00fcr die Produktion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der Sprung vom Tutorial in den Produktivbetrieb braucht ein paar zus\u00e4tzliche Entscheidungen. Die wichtigste haben Sie bereits getroffen: <strong>immer hybrid<\/strong>. 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\u00fctzt Sie aber vor einem \u00fcberraschenden Bruch des jungen Gitterverfahrens.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"krypto-agilitaet\">Krypto-Agilit\u00e4t einplanen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verdrahten Sie den Algorithmus nicht fest. Speichern Sie in jedem verschl\u00fcsselten Container eine Versionskennung, etwa <code>mlkem768-x25519-v1<\/code>. So k\u00f6nnen Sie sp\u00e4ter auf ML-KEM-1024 oder einen Nachfolge-Algorithmus wechseln, ohne alte Daten unlesbar zu machen. Diese <strong>Krypto-Agilit\u00e4t<\/strong> ist die zentrale Lehre aus jeder bisherigen Krypto-Migration: Wer sich auf ein einziges Verfahren festlegt, zahlt beim n\u00e4chsten Bruch teuer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"produktiv-checkliste\">Checkliste f\u00fcr den Produktiveinsatz<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li>Geheime Schl\u00fcssel ausschlie\u00dflich verschl\u00fcsselt oder im HSM speichern, nie im Klartext.<\/li><li>F\u00fcr jede Verschl\u00fcsselung einen frischen, zuf\u00e4lligen IV verwenden, niemals wiederverwenden.<\/li><li>Auth-Tag und IV immer gemeinsam mit dem Ciphertext speichern.<\/li><li>Sensible Vergleiche stets \u00fcber <code>timingSafeEqual<\/code> laufen lassen.<\/li><li>Abh\u00e4ngigkeiten \u00fcber <code>npm audit<\/code> und Lockfiles fixieren, um Supply-Chain-Risiken zu senken.<\/li><li>Eine Bestandsaufnahme aller Stellen f\u00fchren, an denen heute RSA oder ECDH genutzt wird.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr TLS-Verbindungen m\u00fcssen Sie ML-KEM \u00fcbrigens nicht selbst implementieren. Moderne Browser und Server unterst\u00fctzen den hybriden Schl\u00fcsselaustausch <code>X25519MLKEM768<\/code> bereits nativ, und ein wachsender Anteil des Web-Traffics ist dadurch schon quantensicher abgesichert. Ihr selbst gebautes Modul ist vor allem f\u00fcr Anwendungsschicht-Verschl\u00fcsselung sinnvoll, etwa f\u00fcr Datei-Container, Nachrichten oder gespeicherte Geheimnisse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"haeufige-fehler\">H\u00e4ufige Fehler und wie Sie sie vermeiden<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Diese f\u00fcnf Fehler tauchen bei fast jeder ersten PQC-Implementierung auf. Wenn Sie sie kennen, sparen Sie sich Stunden Fehlersuche.<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li><strong>ML-KEM allein einsetzen.<\/strong> Ohne klassischen Hybrid-Partner verlassen Sie sich vollst\u00e4ndig auf ein junges Verfahren. Kombinieren Sie immer mit X25519, wie in Schritt 6 gezeigt.<\/li><li><strong>Das Shared Secret direkt als AES-Schl\u00fcssel nehmen.<\/strong> Schicken Sie es zuerst durch HKDF mit einem Kontext-Label. Nur so binden Sie den Schl\u00fcssel an Ihre Anwendung und stellen eine gleichm\u00e4\u00dfige Verteilung sicher.<\/li><li><strong>Den IV wiederverwenden.<\/strong> Ein zweimal genutzter IV bei AES-GCM bricht die gesamte Vertraulichkeit. W\u00fcrfeln Sie ihn pro Nachricht neu mit <code>randomBytes(12)<\/code>.<\/li><li><strong>Geheimnisse mit <code>===<\/code> vergleichen.<\/strong> Das \u00f6ffnet einen Timing-Seitenkanal. Nutzen Sie ausschlie\u00dflich <code>timingSafeEqual<\/code>.<\/li><li><strong>Den Importpfad ohne <code>.js<\/code> schreiben.<\/strong> <code>@noble\/post-quantum<\/code> exportiert Subpfade mit Dateiendung. Der Import <code>'@noble\/post-quantum\/ml-kem'<\/code> schl\u00e4gt fehl, <code>'...\/ml-kem.js'<\/code> funktioniert.<\/li><\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting\">Troubleshooting: Die h\u00e4ufigsten Fehlermeldungen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die folgende Tabelle sammelt die acht h\u00e4ufigsten Fehler beim Bau dieses Moduls samt Ursache und L\u00f6sung. Die meisten haben mit ESM, Importpfaden oder Datentypen zu tun, nicht mit der Kryptografie selbst.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fehlermeldung<\/th><th>Ursache<\/th><th>L\u00f6sung<\/th><\/tr><\/thead><tbody><tr><td>ERR_PACKAGE_PATH_NOT_EXPORTED<\/td><td>Importpfad ohne <code>.js<\/code><\/td><td>Subpfad <code>@noble\/post-quantum\/ml-kem.js<\/code> mit Endung importieren<\/td><\/tr><tr><td>ERR_REQUIRE_ESM<\/td><td><code>require()<\/code> auf ESM-Paket<\/td><td><code>import<\/code> nutzen und <code>type=module<\/code> setzen<\/td><\/tr><tr><td>Cannot use import statement outside a module<\/td><td><code>type: module<\/code> fehlt<\/td><td><code>npm pkg set type=module<\/code> ausf\u00fchren<\/td><\/tr><tr><td>Unsupported state or unable to authenticate data<\/td><td>Falscher Schl\u00fcssel oder manipulierter Ciphertext<\/td><td>Schl\u00fcsselableitung pr\u00fcfen, IV und Tag korrekt laden<\/td><\/tr><tr><td>Input buffers must have the same byte length<\/td><td><code>timingSafeEqual<\/code> mit ungleich langen Buffern<\/td><td>L\u00e4ngen vorab vergleichen, dann <code>timingSafeEqual<\/code><\/td><\/tr><tr><td>x25519.utils.randomSecretKey is not a function<\/td><td>Veraltete <code>@noble\/curves<\/code>-Version<\/td><td>Auf 2.x aktualisieren, API hat sich ge\u00e4ndert<\/td><\/tr><tr><td>Invalid key length<\/td><td>Shared Secret nicht 32 Byte f\u00fcr AES-256<\/td><td>Immer 32-Byte-Schl\u00fcssel aus HKDF verwenden<\/td><\/tr><tr><td>decapsulate liefert anderes Geheimnis<\/td><td>Falscher Secret Key oder vertauschte Kapsel<\/td><td>Public\/Secret-Zuordnung und Kapsel-Reihenfolge pr\u00fcfen<\/td><\/tr><\/tbody><\/table><figcaption>Acht h\u00e4ufige Fehler bei der ML-KEM-Implementierung in Node.js.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Sonderfall verdient Erw\u00e4hnung: Wenn <code>decapsulate<\/code> bei einem manipulierten Ciphertext nicht abbricht, sondern ein zuf\u00e4llig wirkendes Geheimnis zur\u00fcckgibt, 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 \u00fcbernimmt dann die AEAD-Schicht \u00fcber das Auth-Tag.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fortgeschrittene-tipps\">Fortgeschrittene Tipps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn das Grundger\u00fcst steht, lohnen sich ein paar Erweiterungen f\u00fcr robustere Systeme. <strong>Authentifizierung der Kapsel:<\/strong> Das hier gezeigte Verfahren sch\u00fctzt vor passivem Mitlesen, nicht aber vor einem aktiven Man-in-the-Middle, der Bobs Public Key f\u00e4lscht. Binden Sie den Public Key an eine Identit\u00e4t, etwa \u00fcber eine Signatur mit ML-DSA (FIPS 204) oder \u00fcber ein etabliertes PKI-Zertifikat.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>ChaCha20-Poly1305 statt AES-GCM:<\/strong> Auf Servern ohne AES-Hardwarebeschleunigung ist ChaCha20-Poly1305 oft schneller und gegen IV-Wiederverwendung etwas gutm\u00fctiger. Node.js unterst\u00fctzt es \u00fcber <code>createCipheriv('chacha20-poly1305', ...)<\/code>. Der abgeleitete 32-Byte-Schl\u00fcssel passt unver\u00e4ndert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dom\u00e4nentrennung \u00fcber das info-Feld:<\/strong> Verwenden Sie f\u00fcr jeden Einsatzzweck ein eigenes HKDF-Label, etwa <code>mlkem-hybrid-file-v1<\/code> und <code>mlkem-hybrid-message-v1<\/code>. So kann ein f\u00fcr Dateien abgeleiteter Schl\u00fcssel nie versehentlich f\u00fcr Nachrichten genutzt werden, selbst wenn das Ausgangsgeheimnis identisch ist. Diese Trennung kostet nichts und verhindert eine ganze Klasse subtiler Fehler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Parameter-Wahl dokumentieren:<\/strong> ML-KEM-768 ist f\u00fcr die meisten Anwendungen die richtige Wahl und auch der Wert hinter dem TLS-Hybrid <code>X25519MLKEM768<\/code>. Greifen Sie nur dann zu ML-KEM-1024, wenn Sie ein konkretes Schutzbedarfsniveau erf\u00fcllen m\u00fcssen, das NIST-Stufe 5 verlangt. Die gr\u00f6\u00dfere Variante kostet mehr Bandbreite, ohne f\u00fcr die meisten Bedrohungsmodelle einen praktischen Mehrwert zu liefern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">H\u00e4ufig gestellte Fragen<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ist-ml-kem-dasselbe-wie-kyber\">Ist ML-KEM dasselbe wie Kyber?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e4ndert, die kryptografische Konstruktion blieb nahezu identisch. Suchen Sie nach Code oder Dokumentation, kennen Sie am besten beide Begriffe.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kann-ich-ml-kem-ohne-den-klassischen-hybrid-teil-nutzen\">Kann ich ML-KEM ohne den klassischen Hybrid-Teil nutzen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Technisch ja, empfohlen nein. BSI und der IETF-Entwurf f\u00fcr hybride Schl\u00fcsselaustausche raten dazu, ML-KEM mit einem bew\u00e4hrten Verfahren wie X25519 zu kombinieren. So bleibt Ihr Schl\u00fcssel sicher, selbst wenn in einem der beiden Verfahren eine Schw\u00e4che gefunden wird.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"warum-unterstuetzt-node-js-ml-kem-nicht-direkt-im-crypto-modul\">Warum unterst\u00fctzt Node.js ML-KEM nicht direkt im crypto-Modul?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00fcfte Bibliotheken oder WebAssembly, weshalb wir <code>@noble\/post-quantum<\/code> einsetzen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welchen-parameter-satz-soll-ich-waehlen\">Welchen Parameter-Satz soll ich w\u00e4hlen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e4hlen Sie nur, wenn ein konkreter Schutzbedarf die h\u00f6chste Stufe verlangt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ist-ml-kem-schon-sicher-genug-fuer-den-produktiveinsatz\">Ist ML-KEM schon sicher genug f\u00fcr den Produktiveinsatz?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Als Teil eines hybriden Verfahrens ja. Genau daf\u00fcr 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\u00e4hrte Praxis.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"schuetzt-mich-ml-kem-vor-heutigen-angreifern\">Sch\u00fctzt mich ML-KEM vor heutigen Angreifern?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e4ter mit einem Quantencomputer entschl\u00fcsseln will. F\u00fcr klassische Angreifer ohne Quantencomputer sch\u00fctzt bereits der X25519-Anteil.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"brauche-ich-ml-kem-auch-fuer-tls-auf-meinem-server\">Brauche ich ML-KEM auch f\u00fcr TLS auf meinem Server?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Daf\u00fcr m\u00fcssen Sie selten selbst Code schreiben. Aktuelle Browser und Webserver unterst\u00fctzen den hybriden Austausch X25519MLKEM768 bereits. Ihr eigenes Modul ist vor allem f\u00fcr Verschl\u00fcsselung auf Anwendungsebene sinnvoll, etwa Dateien, Nachrichten oder gespeicherte Geheimnisse.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"related-coverage\">Related Coverage<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/de\/ecdsa-nodejs-signaturen\/\">ECDSA in Node.js: Signaturen in 11 Schritten<\/a><\/li><li><a href=\"\/de\/digitale-signaturen\/\">Digitale Signaturen erkl\u00e4rt<\/a><\/li><li><a href=\"\/de\/ssh-key-erstellen-ed25519-2026\/\">SSH-Key erstellen: Ed25519 in 8 Schritten<\/a><\/li><li><a href=\"\/de\/oauth-pkce-nodejs\/\">OAuth 2.1 mit PKCE in Node.js<\/a><\/li><li><a href=\"\/de\/https-und-tls\/\">HTTPS und TLS: Wie das Schloss im Browser Sie sch\u00fctzt<\/a><\/li><li><a href=\"\/de\/cryptography-hub\/\">Hashing und Kryptographie erkl\u00e4rt<\/a><\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fazit\">Fazit: Quantensicher in 12 Schritten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sie haben ein vollst\u00e4ndiges, lauff\u00e4higes Post-Quanten-Modul gebaut: vom ersten ML-KEM-768-Schl\u00fcsselpaar \u00fcber den hybriden Schl\u00fcsselaustausch mit X25519 bis zur authentifizierten Verschl\u00fcsselung mit AES-256-GCM. Die wichtigste Erkenntnis ist nicht der Code, sondern das Prinzip: ML-KEM etabliert nur ein Geheimnis, der hybride Aufbau sch\u00fctzt vor jungen Risiken, und HKDF plus AEAD machen daraus echte Vertraulichkeit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr DACH-Unternehmen ist der n\u00e4chste Schritt eine Bestandsaufnahme: Wo wird heute RSA oder ECDH genutzt, welche Daten haben eine lange Schutzdauer, und welche Systeme lassen sich mit Krypto-Agilit\u00e4t nachr\u00fcsten? Die Algorithmen stehen seit August 2024 als NIST-Standards fest. Die Migration ist 2026 keine Forschungsfrage mehr, sondern eine Frage der Planung und Disziplin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Weiterf\u00fchrende Quellen: <a href=\"https:\/\/csrc.nist.gov\/pubs\/fips\/203\/final\" target=\"_blank\" rel=\"noopener\">NIST FIPS 203 (ML-KEM-Standard)<\/a>, <a href=\"https:\/\/github.com\/paulmillr\/noble-post-quantum\" target=\"_blank\" rel=\"noopener\">noble-post-quantum auf GitHub<\/a>, <a href=\"https:\/\/nodejs.org\/api\/crypto.html\" target=\"_blank\" rel=\"noopener\">Node.js crypto-Dokumentation<\/a>, <a href=\"https:\/\/www.bsi.bund.de\/EN\/Themen\/Unternehmen-und-Organisationen\/Informationen-und-Empfehlungen\/Quantentechnologien-und-Post-Quanten-Kryptografie\/quantentechnologien-und-post-quanten-kryptografie_node.html\" target=\"_blank\" rel=\"noopener\">BSI zu Post-Quanten-Kryptografie<\/a> und der <a href=\"https:\/\/datatracker.ietf.org\/doc\/draft-ietf-tls-hybrid-design\/\" target=\"_blank\" rel=\"noopener\">IETF-Entwurf f\u00fcr hybriden TLS-Schl\u00fcsselaustausch<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ein Angreifer, der heute Ihren verschl\u00fcsselten Datenverkehr mitschneidet, braucht den passenden Quantencomputer noch gar nicht. Er speichert die Daten einfach und entschl\u00fcsselt sie, sobald die Hardware bereit ist. Dieses Muster\u2026<\/p>\n","protected":false},"author":3,"featured_media":154,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-153","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cryptography"],"_links":{"self":[{"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/posts\/153","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/comments?post=153"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/posts\/153\/revisions"}],"predecessor-version":[{"id":155,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/posts\/153\/revisions\/155"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/media\/154"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/media?parent=153"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/categories?post=153"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/de\/wp-json\/wp\/v2\/tags?post=153"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}