Der Diffie-Hellman-Schlüsselaustausch ist das Fundament moderner Kryptographie: Von TLS 1.3 bis zu Signal-Protokollen basiert sichere Kommunikation darauf, dass zwei Parteien über einen unsicheren Kanal einen gemeinsamen geheimen Schlüssel vereinbaren, ohne ihn jemals direkt zu übertragen. Dieses Tutorial zeigt, wie Sie klassisches DH, ECDH mit P-256, X25519 und HKDF-Schlüsselableitung mit dem eingebauten node:crypto-Modul in Node.js v22+ implementieren.

Was ist der Diffie-Hellman-Schlüsselaustausch?

1976 veröffentlichten Whitfield Diffie und Martin Hellman ein Konzept, das die Kryptographie revolutionierte: Zwei Parteien können sich auf ein gemeinsames Geheimnis einigen, ohne es jemals direkt zu übertragen. Das Verfahren basiert auf der mathematischen Schwierigkeit, diskrete Logarithmen in einer großen Primzahlgruppe zu berechnen.

Die moderne Variante, der Elliptic Curve Diffie-Hellman (ECDH), verwendet elliptische Kurven statt klassischer Primzahlgruppen. Das Ergebnis: bei deutlich kleineren Schlüsselgrößen wird dasselbe Sicherheitsniveau erreicht. Ein 256-bit ECDH-Schlüssel entspricht laut NIST SP 800-57 etwa 128 Bit symmetrischer Sicherheit, also dem Niveau von AES-128.

Node.js stellt über das eingebaute node:crypto-Modul drei APIs zur Verfügung: createDiffieHellman() für klassisches DH, createECDH() für ECDH auf benannten Kurven und diffieHellman() für moderne KeyObject-basierte Workflows inklusive X25519 und X448.

Voraussetzungen

Bevor Sie starten, stellen Sie sicher, dass folgende Komponenten in den angegebenen Versionen vorliegen:

VoraussetzungMindestversionEmpfohlenHinweis
Node.jsv18.0.0v22.x LTSESM-Support, hkdfSync stabil
npm9.010.xFür package.json-Verwaltung
OpenSSL3.03.3+Wird von Node mitgeliefert
BetriebssystemLinux/macOS/WindowsUbuntu 22.04+Alle Plattformen unterstützt
TypeScript (optional)5.05.4+Für typisierte Projekte

Grundkenntnisse in JavaScript/Node.js werden vorausgesetzt. Für Kryptographie-Grundlagen empfiehlt sich zuerst die Lektüre des Node.js Crypto Module Tutorials. Kenntnisse zu AES-256-Verschlüsselung in Node.js sind hilfreich, aber nicht zwingend notwendig.

Schritt 1: Projekt anlegen und Abhängigkeiten installieren

Das gesamte Projekt kommt ohne externe npm-Pakete aus. Das node:crypto-Modul ist seit Node.js v0.1.92 Teil der Standardbibliothek und seit Node.js v22 vollständig stabil für alle hier verwendeten APIs.

mkdir ecdh-tutorial
cd ecdh-tutorial
npm init -y
# package.json auf ESM umstellen
node -e "
  const fs = require('fs');
  const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
  pkg.type = 'module';
  fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
echo "Node.js Version: $(node -v)"
echo "OpenSSL Version: $(node -e 'const c = require(\"crypto\"); console.log(c.constants.OPENSSL_VERSION_NUMBER)')"

Erstellen Sie jetzt die Projektstruktur:

ecdh-tutorial/
├── package.json
├── 01-classic-dh.js
├── 02-ecdh-p256.js
├── 03-x25519.js
├── 04-hkdf-derivation.js
├── 05-secure-messaging.js
└── 06-tls-client.js

Schritt 2: Klassischer Diffie-Hellman mit 2048-Bit-Prime

Der klassische Diffie-Hellman-Algorithmus funktioniert auf Basis einer großen Primzahl p und einem Generator g. Beide Parteien wählen zufällige private Exponenten, tauschen die berechneten öffentlichen Werte aus und berechnen unabhängig voneinander dasselbe Shared Secret.

Für 2026 gilt laut BSI-Empfehlung (TR-02102-1) und NIST SP 800-131A: 2048 Bit sind das absolute Minimum für neue Anwendungen. 3072 Bit werden für Systeme empfohlen, die über 2030 hinaus sicher bleiben sollen. Ein 3072-Bit-DH-Schlüssel entspricht einem 128-Bit symmetrischen Sicherheitsniveau.

// 01-classic-dh.js
import crypto from 'node:crypto';
import assert from 'node:assert';

// Alice generiert eine neue 2048-Bit-DH-Gruppe
const alice = crypto.createDiffieHellman(2048);
alice.generateKeys();

// Bob muss dieselbe Primzahl und denselben Generator verwenden
const bob = crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator());
bob.generateKeys();

// Öffentliche Schlüssel werden ausgetauscht (unverschlüsselt möglich)
const alicePublicKey = alice.getPublicKey();
const bobPublicKey = bob.getPublicKey();

// Jede Seite berechnet unabhängig das Shared Secret
const aliceSecret = alice.computeSecret(bobPublicKey);
const bobSecret = bob.computeSecret(alicePublicKey);

// Beide Secrets müssen identisch sein
assert.strictEqual(
  aliceSecret.toString('hex'),
  bobSecret.toString('hex'),
  'Shared Secrets stimmen nicht überein!'
);

console.log('DH-2048 erfolgreich!');
console.log('Shared Secret (hex, erste 32 Zeichen):', aliceSecret.toString('hex').slice(0, 32) + '...');
console.log('Shared Secret Länge:', aliceSecret.length, 'Bytes');
console.log('Prime Länge:', alice.getPrime().length * 8, 'Bit');

// WICHTIG: Das Raw Shared Secret NICHT direkt als Schlüssel verwenden
// Stattdessen: HKDF zur Schlüsselableitung (siehe Schritt 7)

Ausgabe nach node 01-classic-dh.js:

DH-2048 erfolgreich!
Shared Secret (hex, erste 32 Zeichen): a3f12e8b9c4d7e2f1a5b6c8d9e0f3a4b...
Shared Secret Länge: 256 Bytes
Prime Länge: 2048 Bit

Wichtig: Die Erzeugung einer 2048-Bit-DH-Gruppe dauert je nach Hardware zwischen 100 und 800 Millisekunden. Das ist einer der Hauptgründe, warum moderne Protokolle auf ECDH umgestiegen sind.

Schritt 3: ECDH mit P-256 (prime256v1)

Elliptic Curve Diffie-Hellman (ECDH) verwendet die algebraische Struktur elliptischer Kurven, um dasselbe mathematische Problem auf einem anderen Rechenfeld zu lösen. Das Ergebnis: deutlich kleinere Schlüsselgrößen bei gleichem oder höherem Sicherheitsniveau.

Die am häufigsten verwendete Kurve in Node.js ist P-256, die in Node.js unter dem OpenSSL-Namen prime256v1 (auch bekannt als secp256r1) angesprochen wird. P-256 ist von NIST in FIPS 186-5 standardisiert und in TLS 1.3 als empfohlene Kurve gelistet.

// 02-ecdh-p256.js
import crypto from 'node:crypto';
import assert from 'node:assert';

// Alice erstellt ein ECDH-Objekt auf der P-256-Kurve
// ACHTUNG: In Node.js heißt P-256 intern 'prime256v1', NICHT 'P-256'
const alice = crypto.createECDH('prime256v1');
alice.generateKeys();

// Bob macht dasselbe
const bob = crypto.createECDH('prime256v1');
bob.generateKeys();

// Öffentliche Schlüssel werden ausgetauscht
// Format: 'uncompressed' (65 Bytes) oder 'compressed' (33 Bytes)
const alicePublicKey = alice.getPublicKey(null, 'uncompressed');
const bobPublicKey = bob.getPublicKey(null, 'compressed');

console.log('Alice Public Key (uncompressed, 65 Bytes):', alicePublicKey.length, 'Bytes');
console.log('Bob Public Key (compressed, 33 Bytes):', bobPublicKey.length, 'Bytes');

// Shared Secrets berechnen
const aliceSecret = alice.computeSecret(bobPublicKey);
const bobSecret = bob.computeSecret(alicePublicKey);

assert.strictEqual(
  aliceSecret.toString('hex'),
  bobSecret.toString('hex'),
  'ECDH: Shared Secrets stimmen nicht überein!'
);

console.log('ECDH P-256 erfolgreich!');
console.log('Shared Secret Länge:', aliceSecret.length, 'Bytes (32 = 256 Bit)');
console.log('Shared Secret:', aliceSecret.toString('hex'))

ECDH mit P-256 ist etwa 10- bis 20-mal schneller als klassisches DH-2048 und produziert trotzdem einen 32-Byte (256-Bit) Shared Secret. Das liegt daran, dass elliptische Kurven auf einem anderen mathematischen Problem basieren, für das kein sub-exponentieller Algorithmus bekannt ist.

Schritt 4: Verfügbare ECDH-Kurven in Node.js prüfen

Node.js unterstützt alle von der installierten OpenSSL-Version bereitgestellten elliptischen Kurven. Die wichtigsten für 2026 sind:

Kurve (Node.js Name)AliasSchlüsselgrößeSicherheitsniveauEmpfehlung 2026
prime256v1P-256, secp256r1256 Bit128 BitStandard, weit verbreitet
secp384r1P-384384 Bit192 BitFür höhere Sicherheit
secp521r1P-521521 Bit260 BitMaximum bei ECDH
brainpoolP256r1BP-256256 Bit128 BitBSI-empfohlen (DE/AT)
brainpoolP384r1BP-384384 Bit192 BitBSI-empfohlen für hohe Sicherheit

Für österreichische Behörden und Unternehmen, die BSI-Empfehlungen (TR-02102-1) folgen müssen, sind die Brainpool-Kurven oft die bevorzugte Wahl, da diese europäischen Ursprungs sind. Für internationale Interoperabilität (TLS, WebCrypto API, Signal-Protokoll) sind P-256 und X25519 jedoch universeller.

// Alle verfügbaren Kurven auflisten
import crypto from 'node:crypto';

const curves = crypto.getCurves();
// Relevante Kurven filtern
const relevant = curves.filter(c =>
  c.includes('256') || c.includes('384') || c.includes('521') ||
  c.includes('25519') || c.includes('448')
);
console.log('Relevante Kurven:', relevant.sort());

Schritt 5: X25519 – Die moderne Empfehlung

X25519 (Diffie-Hellman auf Curve25519) ist die moderne Empfehlung für neuen Code. Die Kurve wurde von Daniel J. Bernstein mit Fokus auf Geschwindigkeit, Sicherheit und Implementierungssicherheit entworfen. Sie ist resistenter gegen bestimmte Seitenkanalangriffe als klassische NIST-Kurven und ist in RFC 7748 (2016) standardisiert.

In Node.js wird X25519 über die KeyObject-basierte API und crypto.diffieHellman() verwendet, nicht über createECDH(). Diese API ist sauberer und typsicherer:

// 03-x25519.js
import crypto from 'node:crypto';
import assert from 'node:assert';

// X25519-Schlüsselpaare generieren
const alice = crypto.generateKeyPairSync('x25519');
const bob = crypto.generateKeyPairSync('x25519');

// Shared Secrets über die moderne diffieHellman()-API berechnen
const aliceSecret = crypto.diffieHellman({
  privateKey: alice.privateKey,
  publicKey: bob.publicKey,   // Alices Private Key + Bobs Public Key
});

const bobSecret = crypto.diffieHellman({
  privateKey: bob.privateKey,
  publicKey: alice.publicKey,   // Bobs Private Key + Alice's Public Key
});

assert.strictEqual(
  aliceSecret.toString('hex'),
  bobSecret.toString('hex'),
  'X25519: Shared Secrets stimmen nicht überein!'
);

console.log('X25519 erfolgreich!');
console.log('Shared Secret Länge:', aliceSecret.length, 'Bytes (32 = 256 Bit)');

// Public Key exportieren (für Übertragung an Gegenseite)
const alicePublicKeyDer = alice.publicKey.export({ type: 'spki', format: 'der' });
console.log('Public Key (SPKI DER) Größe:', alicePublicKeyDer.length, 'Bytes');

// Public Key aus DER importieren (Empfängerseite)
const importedKey = crypto.createPublicKey({ key: alicePublicKeyDer, type: 'spki', format: 'der' });
console.log('Key Type nach Import:', importedKey.type); // 'public'

X25519 hat gegenüber ECDH P-256 einige Vorteile: Die Implementierung ist weniger fehleranfällig (kein Punkt-Validierungsproblem), schneller auf vielen Architekturen und resistent gegen bestimmte Timing-Angriffe, die bei NIST-Kurven theoretisch möglich sind.

Schritt 6: Das kritische Problem – Raw Shared Secret ist kein Schlüssel

Ein häufiger und gefährlicher Fehler: Entwickler verwenden das Raw Shared Secret direkt als AES-Schlüssel. Das ist aus mehreren Gründen falsch:

1. Ungleichmäßige Verteilung: Das Shared Secret aus ECDH ist ein Punkt auf der elliptischen Kurve. Die x-Koordinate dieses Punkts ist nicht gleichmäßig über den Keyspace verteilt, was statistische Angriffe ermöglicht.

2. Fehlende Kontextbindung: Wenn beide Parteien denselben Schlüssel für verschiedene Zwecke verwenden (Verschlüsselung und MAC), müssen diese Schlüssel voneinander unabhängig sein.

3. Protokoll-Flexibilität: HKDF ermöglicht es, aus einem Shared Secret beliebig viele unabhängige Schlüssel abzuleiten, ohne neuen Schlüsselaustausch zu benötigen.

HKDF – RFC 5869 Schlüsselableitung in Node.js

// 04-hkdf-derivation.js
import crypto from 'node:crypto';

/**
 * Leitet mehrere kryptographische Schlüssel aus einem Shared Secret ab.
 * Basiert auf RFC 5869 HKDF mit SHA-256.
 */
function deriveKeys(sharedSecret, salt, context) {
  // Verschlüsselungsschlüssel (32 Bytes = AES-256)
  const encKey = crypto.hkdfSync(
    'sha256',
    sharedSecret,
    salt,
    Buffer.from(`${context}:enc-key`, 'utf8'),
    32
  );

  // MAC-Schlüssel (32 Bytes für HMAC-SHA256)
  const macKey = crypto.hkdfSync(
    'sha256',
    sharedSecret,
    salt,
    Buffer.from(`${context}:mac-key`, 'utf8'),
    32
  );

  // IV-Material (16 Bytes)
  const ivMaterial = crypto.hkdfSync(
    'sha256',
    sharedSecret,
    salt,
    Buffer.from(`${context}:iv-material`, 'utf8'),
    16
  );

  return { encKey, macKey, ivMaterial };
}

// Beispiel: ECDH P-256 + HKDF
const alice = crypto.createECDH('prime256v1');
alice.generateKeys();
const bob = crypto.createECDH('prime256v1');
bob.generateKeys();

const sharedSecret = alice.computeSecret(bob.getPublicKey());

// Salt sollte pro Session frisch generiert und dem Empfänger mitgeteilt werden
const salt = crypto.randomBytes(32);
const context = 'myapp-v1-session';

const { encKey, macKey, ivMaterial } = deriveKeys(sharedSecret, salt, context);

console.log('Abgeleitete Schlüssel:');
console.log('Enc Key (hex):', Buffer.from(encKey).toString('hex'));
console.log('MAC Key (hex):', Buffer.from(macKey).toString('hex'));
console.log('IV Material (hex):', Buffer.from(ivMaterial).toString('hex'));

// Überprüfen: Beide Seiten leiten dieselben Schlüssel ab
const sharedSecretBob = bob.computeSecret(alice.getPublicKey());
const { encKey: encKeyBob } = deriveKeys(sharedSecretBob, salt, context);

console.log('\nSchlüssel identisch:', Buffer.from(encKey).toString('hex') === Buffer.from(encKeyBob).toString('hex'));

Schritt 7: Authentifizierte Verschlüsselung mit AES-256-GCM

Nachdem der Schlüssel sicher über ECDH+HKDF abgeleitet wurde, kommt AES-256-GCM für die eigentliche Verschlüsselung zum Einsatz. GCM (Galois/Counter Mode) bietet nicht nur Vertraulichkeit, sondern auch Authentizität: Ein Empfänger kann erkennen, ob die Nachricht manipuliert wurde.

Für Hintergrundinformationen zu AES-256 empfiehlt sich die Lektüre des AES-256-Verschlüsselung in Node.js-Tutorials. Grundlagen zu HMAC-basierten Signaturen finden Sie im HMAC-SHA256 Tutorial.

Schritt 8: Vollständiges Secure Messaging Projekt

Jetzt werden alle Komponenten zu einem vollständigen Secure Messaging System zusammengefügt. Das System implementiert:

  • Ephemere ECDH P-256 Schlüsselpaare (frisch pro Session)
  • HKDF-SHA256 Schlüsselableitung mit Session-Salt
  • AES-256-GCM Verschlüsselung mit AAD (Authenticated Additional Data)
  • Vollständige Fehlerbehandlung und Input-Validierung
// 05-secure-messaging.js
import crypto from 'node:crypto';
import assert from 'node:assert';

class SecureSession {
  #privateKey;
  #curve;

  constructor(curve = 'prime256v1') {
    this.#curve = curve;
    const ecdh = crypto.createECDH(curve);
    ecdh.generateKeys();
    this.#privateKey = ecdh;
  }

  getPublicKey(format = 'uncompressed') {
    return this.#privateKey.getPublicKey(null, format);
  }

  /**
   * Berechnet Shared Secret + leitet Session-Schlüssel ab
   * @param {Buffer} peerPublicKey - Öffentlicher Schlüssel der Gegenseite
   * @param {Buffer} salt - Gemeinsamer Session-Salt
   * @param {string} context - Protokoll-Kontext für HKDF
   */
  deriveSessionKey(peerPublicKey, salt, context = 'secure-messaging-v1') {
    const sharedSecret = this.#privateKey.computeSecret(peerPublicKey);

    // HKDF: Shared Secret -> 32-Byte AES-256 Schlüssel
    return crypto.hkdfSync(
      'sha256',
      sharedSecret,
      salt,
      Buffer.from(context, 'utf8'),
      32
    );
  }
}

function encryptMessage(key, plaintext, threadId = '') {
  // Frischer IV pro Nachricht (PFLICHT bei GCM)
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

  // AAD bindet die Nachricht an den Thread-Kontext
  if (threadId) {
    cipher.setAAD(Buffer.from(threadId, 'utf8'));
  }

  const ciphertext = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);
  const authTag = cipher.getAuthTag();

  return { iv, ciphertext, authTag };
}

function decryptMessage(key, { iv, ciphertext, authTag }, threadId = '') {
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);

  if (threadId) {
    decipher.setAAD(Buffer.from(threadId, 'utf8'));
  }
  decipher.setAuthTag(authTag);

  const plaintext = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final(), // Wirft Error wenn Auth Tag ungültig
  ]);

  return plaintext.toString('utf8');
}

// --- Simulation: Alice und Bob tauschen Schlüssel aus ---

const alice = new SecureSession('prime256v1');
const bob = new SecureSession('prime256v1');

// Öffentliche Schlüssel werden über einen ungesicherten Kanal übertragen
const alicePubKey = alice.getPublicKey();
const bobPubKey = bob.getPublicKey();

// Session-Salt wird einmal ausgehandelt (z.B. vom Initiator generiert und übertragen)
const sessionSalt = crypto.randomBytes(32);
const threadId = 'conversation-' + crypto.randomBytes(8).toString('hex');

// Beide Seiten leiten denselben Session-Schlüssel ab
const aliceKey = alice.deriveSessionKey(bobPubKey, sessionSalt);
const bobKey = bob.deriveSessionKey(alicePubKey, sessionSalt);

// Schlüssel müssen identisch sein
assert.strictEqual(
  Buffer.from(aliceKey).toString('hex'),
  Buffer.from(bobKey).toString('hex'),
  'Session-Schlüssel stimmen nicht überein!'
);

// Alice verschlüsselt eine Nachricht
const message = 'Treffen um 18:00 Uhr am Stephansplatz';
const encrypted = encryptMessage(aliceKey, message, threadId);

console.log('Verschlüsselte Nachricht:');
console.log('  IV:', encrypted.iv.toString('hex'));
console.log('  Ciphertext:', encrypted.ciphertext.toString('hex'));
console.log('  Auth Tag:', encrypted.authTag.toString('hex'));

// Bob entschlüsselt
const decrypted = decryptMessage(bobKey, encrypted, threadId);
assert.strictEqual(message, decrypted, 'Entschlüsselte Nachricht stimmt nicht überein!');
console.log('\nEntschlüsselte Nachricht:', decrypted);
console.log('Integritätsprüfung: OK');

Führen Sie das Script aus:

node 05-secure-messaging.js

# Erwartete Ausgabe:
Verschlüsselte Nachricht:
  IV: a1b2c3d4e5f60708090a0b0c
  Ciphertext: 8f3e2a1b7c9d4e5f6a...
  Auth Tag: 1234567890abcdef1234567890abcdef

Entschlüsselte Nachricht: Treffen um 18:00 Uhr am Stephansplatz
Integritätsprüfung: OK

Schritt 9: Forward Secrecy – Warum ephemere Schlüssel entscheidend sind

Forward Secrecy (auch Perfect Forward Secrecy, PFS) bedeutet: Wenn ein langfristiger privater Schlüssel kompromittiert wird, bleibt vergangene verschlüsselte Kommunikation trotzdem geschützt. Das ist nur dann möglich, wenn ephemere Schlüsselpaare verwendet werden, die nach jeder Session gelöscht werden.

TLS 1.3 macht Forward Secrecy durch die Verwendung von ECDHE (Ephemeral ECDH) zur Pflicht. Statisches DH oder ECDH, bei dem derselbe private Schlüssel für viele Sessions verwendet wird, bietet keine Forward Secrecy.

MethodeForward SecrecyPerformanceTLS 1.3 KompatibelEmpfehlung
Statisches ECDHNeinGut (kein neues KeyGen)Nicht verwendetNur für Archivverschlüsselung
Ephemeres ECDHE P-256JaGutJaStandard für neue Anwendungen
Ephemeres X25519JaSehr gutJa (bevorzugt)Erste Wahl 2026
Klassisches DHE-2048JaLangsamNein (aus TLS 1.3 entfernt)Nur Legacy-Systeme
RSA Key ExchangeNeinSehr langsamNein (aus TLS 1.3 entfernt)Veraltet, nicht verwenden

TLS 1.3 hat RSA-Key-Exchange und nicht-ephemere DH-Varianten vollständig entfernt. Jede TLS-1.3-Verbindung verwendet obligatorisch ECDHE oder DHE für Forward Secrecy. Das erklärt, warum moderne Browser nur noch TLS 1.3 unterstützen.

Schritt 10: TLS-Server mit ECDHE in Node.js

In der Praxis kommt der direkte ECDH-API-Aufruf selten vor. In den meisten Anwendungsfällen kümmert sich das TLS-Modul automatisch um den Schlüsselaustausch. Trotzdem lohnt sich ein Blick auf die Konfiguration, um sicherzustellen, dass veraltete Cipher-Suites ausgeschlossen werden:

// 06-tls-server.js
import tls from 'node:tls';
import fs from 'node:fs';

// Produktions-Server-Konfiguration
const serverOptions = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),

  // Nur TLS 1.3 zulassen (ECDHE ist Pflicht)
  minVersion: 'TLSv1.3',

  // Explizite Cipher-Suite-Konfiguration (TLS 1.3 Standard ist bereits gut)
  // In TLS 1.3 werden nur ECDHE-basierte Suites verwendet:
  // TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256
  ciphers: [
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256',
    'TLS_AES_128_GCM_SHA256',
  ].join(':'),

  // ECDH-Kurven für den Key Exchange festlegen
  ecdhCurve: 'X25519:prime256v1:secp384r1',
};

const server = tls.createServer(serverOptions, (socket) => {
  console.log('Verbunden:', socket.remoteAddress);
  console.log('Protokoll:', socket.getProtocol());  // 'TLSv1.3'
  console.log('Cipher:', socket.getCipher().name);

  socket.write('Willkommen auf dem sicheren Server!\n');
  socket.end();
});

server.listen(8443, () => {
  console.log('TLS-Server läuft auf Port 8443');
  console.log('Mindestprotokoll: TLS 1.3 (ECDHE Pflicht)');
});

// Client-seitig: Protokoll und Cipher prüfen
import tls_client from 'node:tls';

const client = tls_client.connect({
  host: 'localhost',
  port: 8443,
  minVersion: 'TLSv1.3',
  rejectUnauthorized: false, // Nur für Tests; in Produktion auf true setzen
}, () => {
  console.log('\nClient verbunden');
  console.log('Protokoll:', client.getProtocol());
  console.log('Cipher:', client.getCipher().name);
  console.log('Key Exchange:', client.getCipher().kx || 'ECDHE (TLS 1.3 Standard)');
  client.end();
});

Schritt 11: Schlüssel persistieren und importieren

In realen Anwendungen müssen ECDH-Schlüsselpaare oft serialisiert, gespeichert und wieder geladen werden. Node.js unterstützt PEM- und DER-Formate über die KeyObject-API:

// Schlüsselpaar generieren und exportieren
import crypto from 'node:crypto';
import fs from 'node:fs';

// X25519 Schlüsselpaar generieren
const { privateKey, publicKey } = crypto.generateKeyPairSync('x25519', {
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem',
  },
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem',
  },
});

// Auf Festplatte speichern
fs.writeFileSync('alice-private.pem', privateKey);
fs.writeFileSync('alice-public.pem', publicKey);

console.log('Private Key (PKCS8 PEM):');
console.log(privateKey);
console.log('Public Key (SPKI PEM):');
console.log(publicKey);

// --- Auf der anderen Seite: Schlüssel importieren ---
const loadedPrivate = crypto.createPrivateKey(fs.readFileSync('alice-private.pem'));
const loadedPublic = crypto.createPublicKey(fs.readFileSync('alice-public.pem'));

console.log('Geladener Private Key Type:', loadedPrivate.type); // 'private'
console.log('Geladener Public Key Type:', loadedPublic.type);  // 'public'
console.log('Kurve:', loadedPrivate.asymmetricKeyDetails);     // { namedCurve: 'x25519' }

Schritt 12: Performance-Vergleich und Entscheidungshilfe

Die Wahl des richtigen Schlüsselaustauschverfahrens hängt von Sicherheitsanforderungen, Interoperabilität und Performance ab. Folgende Benchmarks wurden auf einem typischen Linux-Server mit Node.js v22 gemessen:

// performance-bench.js
import crypto from 'node:crypto';

function bench(name, fn, iterations = 1000) {
  const start = process.hrtime.bigint();
  for (let i = 0; i < iterations; i++) fn();
  const end = process.hrtime.bigint();
  const ms = Number(end - start) / 1_000_000;
  console.log(`${name}: ${(ms / iterations).toFixed(3)}ms/op (${iterations} ops in ${ms.toFixed(0)}ms)`);
}

// Classic DH 2048
bench('DH-2048 KeyGen', () => {
  const dh = crypto.createDiffieHellman(2048);
  dh.generateKeys();
}, 50); // Weniger Iterationen da langsam

// ECDH P-256
bench('ECDH P-256 KeyGen+ComputeSecret', () => {
  const alice = crypto.createECDH('prime256v1');
  alice.generateKeys();
  const bob = crypto.createECDH('prime256v1');
  bob.generateKeys();
  alice.computeSecret(bob.getPublicKey());
});

// X25519
bench('X25519 KeyGen+DH', () => {
  const alice = crypto.generateKeyPairSync('x25519');
  const bob = crypto.generateKeyPairSync('x25519');
  crypto.diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey });
});

// HKDF SHA-256
bench('HKDF-SHA256 (32 Bytes)', () => {
  const secret = crypto.randomBytes(32);
  const salt = crypto.randomBytes(16);
  crypto.hkdfSync('sha256', secret, salt, Buffer.from('info'), 32);
}, 10000);

Typische Ergebnisse auf einem modernen Linux-Server (AMD EPYC, Node.js v22):

DH-2048 KeyGen: 312.450ms/op (50 ops in 15622ms)
ECDH P-256 KeyGen+ComputeSecret: 0.187ms/op (1000 ops in 187ms)
X25519 KeyGen+DH: 0.091ms/op (1000 ops in 91ms)
HKDF-SHA256 (32 Bytes): 0.002ms/op (10000 ops in 18ms)

Das Ergebnis: X25519 ist etwa 2× schneller als ECDH P-256 und über 3000× schneller als klassisches DH-2048. HKDF ist so schnell, dass es in der Praxis keine messbare Latenz verursacht.

Häufige Fehler und Sicherheitsfallen

Die folgenden Fehler sind in der Praxis häufig anzutreffen und können die gesamte Sicherheit des Systems untergraben:

Fehler 1: Raw Shared Secret direkt als AES-Schlüssel verwenden

Problem: Das Raw Shared Secret aus ECDH ist kein kryptographisch sicherer Schlüssel. Es ist nicht gleichmäßig verteilt und kann statistische Schwächen aufweisen.

// FALSCH: Raw Secret direkt verwenden
const sharedSecret = alice.computeSecret(bobPublicKey);
const cipher = crypto.createCipheriv('aes-256-gcm', sharedSecret, iv); // GEFÄHRLICH!

// RICHTIG: HKDF verwenden
const sharedSecret = alice.computeSecret(bobPublicKey);
const aesKey = crypto.hkdfSync('sha256', sharedSecret, salt, Buffer.from('enc-v1'), 32);
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv); // Sicher

Fehler 2: IV-Wiederverwendung bei AES-GCM

Problem: AES-GCM ist katastrophal unsicher bei IV-Wiederverwendung. Zwei Nachrichten mit demselben IV+Key ermöglichen es einem Angreifer, beide Plaintexts zu rekonstruieren und den Auth-Tag zu fälschen.

// FALSCH: Fester oder inkrementeller IV
const iv = Buffer.alloc(12, 0); // Immer derselbe IV!
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

// RICHTIG: Kryptographisch zufälliger IV pro Nachricht
const iv = crypto.randomBytes(12); // Frisch für jede Nachricht

Fehler 3: Falscher Kurvenname in Node.js

Problem: In Node.js heißt P-256 intern prime256v1, nicht P-256. Die Verwendung des falschen Namens führt zu einem Error: invalid curve name.

// FALSCH:
crypto.createECDH('P-256'); // Error: invalid curve name

// RICHTIG:
crypto.createECDH('prime256v1'); // Funktioniert

// Korrekte Kurven-Namen-Tabelle:
// P-256  -> prime256v1 oder secp256r1
// P-384  -> secp384r1
// P-521  -> secp521r1

Fehler 4: Fehlende Public-Key-Authentifizierung. ECDH und DH bieten ausschließlich Vertraulichkeit durch Schlüsselvereinbarung, aber keine Identitätsauthentifizierung. Ohne zusätzliche Authentifizierung (digitale Signatur, zertifikatsbasierte TLS-Authentifizierung, etc.) ist ein Man-in-the-Middle-Angriff trivial möglich. Mehr dazu im Digitale Signatur in Node.js Tutorial.

Fehler 5: Statische Schlüssel ohne Forward Secrecy. Ein ECDH-Schlüsselpaar, das für viele Sessions wiederverwendet wird, bietet keine Forward Secrecy. Jede Session sollte ein frisches ephemeres Schlüsselpaar verwenden.

Fehler 6: Logjam-Angriff durch schwache DH-Gruppen. Der 2015 entdeckte Logjam-Angriff zeigte, dass 512-bit und 768-bit DH-Gruppen von Angreifern vorberechnet werden können. Minimum für 2026: 2048 Bit. Verwenden Sie niemals createDiffieHellman(512) oder ähnlich kleine Gruppengrößen.

Fehler 7: Small-Subgroup-Angriff. Bei selbst implementierten DH-Protokollen müssen eingehende Public Keys validiert werden. Ein Angreifer könnte einen Wert senden, der nicht zur gewählten Gruppe gehört, um Informationen über den privaten Schlüssel zu extrahieren. In Node.js ist diese Validierung für die Standard-APIs eingebaut, aber bei manuellen Implementierungen kritisch.

Fehler 8: Fehlender HKDF-Kontext (info-Parameter). Der info-Parameter von HKDF bindet den abgeleiteten Schlüssel an einen bestimmten Verwendungszweck. Ohne ihn können Schlüssel, die für verschiedene Zwecke abgeleitet wurden, identisch sein.

Troubleshooting – 8 häufige Fehlermeldungen

Folgende Fehler treten bei der Implementierung von Diffie-Hellman und ECDH in Node.js häufig auf:

1. Error: Invalid EC key
Ursache: Der eingehende Public Key ist korrumpiert, hat das falsche Format oder gehört zu einer anderen Kurve.
Lösung: Sicherstellen, dass beide Parteien dieselbe Kurve verwenden. Public Key im richtigen Format übertragen (uncompressed oder compressed konsistent nutzen).

2. Error: invalid curve name
Ursache: Der verwendete Kurvenname ist in der Node.js/OpenSSL-Umgebung nicht bekannt.
Lösung: crypto.getCurves() aufrufen und den korrekten Namen aus der Liste verwenden. P-256 heißt in Node.js prime256v1.

3. Error: Unsupported state or unable to authenticate data (bei AES-GCM decrypt)
Ursache: Der Auth-Tag ist ungültig. Entweder wurden Schlüssel, IV, AAD oder Ciphertext manipuliert.
Lösung: Überprüfen, dass Schlüssel, IV und AAD auf beiden Seiten identisch sind. Auch ein falscher Salt bei HKDF führt zu diesem Fehler.

4. assert.strictEqual schlägt fehl bei Shared Secret Vergleich
Ursache: Unterschiedliche Prime/Generator auf beiden Seiten bei klassischem DH; oder Public Key einer anderen Kurve bei ECDH.
Lösung: Sicherstellen, dass Bob exakt dieselbe Prime und denselben Generator verwendet wie Alice: crypto.createDiffieHellman(alice.getPrime(), alice.getGenerator()).

5. RangeError: Invalid typed array length bei hkdfSync
Ursache: Die angeforderte Schlüssellänge überschreitet das HKDF-Maximum.
Lösung: HKDF-SHA256 kann maximal 255 × 32 = 8160 Bytes ableiten. Für typische Anwendungsfälle (32 Bytes AES-256) ist das kein Problem.

6. Sehr langsame DH-Schlüsselgenerierung (mehrere Sekunden)
Ursache: Klassisches DH-2048 mit frischer Primzahlgenerierung ist rechenintensiv.
Lösung: Auf ECDH P-256 oder X25519 umsteigen (1000× schneller). Falls klassisches DH benötigt wird, vorgenerierte Gruppen (MODP-Gruppen aus RFC 3526) verwenden.

7. Error: error:10080069:elliptic curve routines:EC_POINT_set_affine_coordinates:point is not on curve
Ursache: Der übermittelte Public Key liegt nicht auf der erwarteten elliptischen Kurve.
Lösung: Mögliche Ursachen: Übertragungsfehler, falsche Kurve auf der Gegenseite, oder aktiver Small-Subgroup-Angriff. Public Keys immer über authentifizierte Kanäle übertragen.

8. Kompilierungsfehler bei älteren Node.js-Versionen für crypto.diffieHellman()
Ursache: Die crypto.diffieHellman() API (für X25519) wurde in Node.js v13.9.0 als experimentell eingeführt und ist ab v15.0.0 stabil.
Lösung: Node.js auf v18 LTS oder höher aktualisieren. Für X25519 auf ältere Versionen: externes noble-curves-Paket verwenden.

Sicherheitsempfehlungen für 2026

Für österreichische Unternehmen und Entwickler, die nach BSI-Richtlinien (TR-02102-1) oder eIDAS-Anforderungen arbeiten, gelten folgende Empfehlungen für 2026:

Kurvenauswahl: Für neue Systeme ist X25519 die erste Wahl. Für Systeme, die BSI-zertifiziert sein müssen oder europäische Standards bevorzugen, sind Brainpool P-256 (brainpoolP256r1) oder Brainpool P-384 (brainpoolP384r1) die empfohlene Alternative. NIST P-256 (prime256v1) ist für internationale Interoperabilität der Standard.

Schlüsselgrößen: Für klassisches DH: mindestens 2048 Bit, empfohlen 3072 Bit für Systeme mit langem Betriebszeitraum. Für ECDH: P-256 / X25519 für 128-Bit-Sicherheit; P-384 für 192-Bit-Sicherheit bei erhöhten Anforderungen.

Post-Quantum-Überlegungen: Quantencomputer könnten klassisches DH und ECDH durch den Shor-Algorithmus brechen. NIST hat 2024 die ersten Post-Quantum-Standards veröffentlicht (ML-KEM/Kyber für Key Encapsulation). Für hochsensible Systeme sollte der Übergang zu hybriden Verfahren (klassisch + post-quantum) evaluiert werden. Mehr dazu im Artikel über Post-Quantum-Kryptographie.

Die vollständige NIST-Referenz für Kurven findet sich in NIST FIPS 186-5. RFC 7748 (RFC 7748) standardisiert X25519/X448. Die TLS 1.3 Spezifikation ist in RFC 8446 definiert. Für sichere Kryptographie-Implementierungen empfiehlt sich der OWASP Cryptographic Storage Cheat Sheet.

Advanced Tips für Produktionssysteme

Tip 1: Asynchrones KeyGen für Server-Anwendungen. generateKeyPairSync() blockiert den Event Loop. In Express- oder Fastify-Servern ist die asynchrone Variante Pflicht:

// Asynchron (non-blocking für Server)
const { privateKey, publicKey } = await new Promise((resolve, reject) => {
  crypto.generateKeyPair('x25519', (err, privateKey, publicKey) => {
    if (err) reject(err);
    else resolve({ privateKey, publicKey });
  });
});

Tip 2: ECDH-Public-Keys komprimiert übertragen. Der uncompressed-Format-Public-Key für P-256 ist 65 Bytes; compressed ist nur 33 Bytes. Bei vielen Nachrichten (IoT, Messaging) spart das fast 50% Bandbreite, ohne Sicherheit zu opfern:

// Compressed Public Key (33 Bytes statt 65)
const pubKeyCompressed = ecdh.getPublicKey(null, 'compressed');
// Auf der Empfängerseite: compressed Keys werden direkt von computeSecret() akzeptiert
const secret = otherEcdh.computeSecret(pubKeyCompressed);

Tip 3: Session-Key-Rotation. Für langlebige Verbindungen (WebSocket, gRPC-Streams) empfiehlt sich regelmäßige Schlüsselrotation, ähnlich wie TLS Session Tickets. Dabei wird ein neuer ECDH-Austausch durchgeführt, ohne die Verbindung zu unterbrechen.

Tip 4: Key Derivation mit Protokoll-Version im Info-Parameter. Wenn Ihr Protokoll aktualisiert wird, ändern Sie den info-Parameter von HKDF. Damit sind Schlüssel aus verschiedenen Protokollversionen garantiert verschieden, auch wenn das Shared Secret identisch ist.

Tip 5: Secrets aus dem Speicher tilgen. Node.js Buffers sind garbage-collected, aber sensitives Material kann länger als nötig im Speicher verbleiben. Für hochsensible Anwendungen kann buffer.fill(0) verwendet werden, um Shared Secrets nach der Schlüsselableitung zu überschreiben.

Häufig gestellte Fragen (FAQ)

Was ist der Unterschied zwischen DH und ECDH?
Klassisches Diffie-Hellman (DH) basiert auf der Schwierigkeit diskreter Logarithmen in einer Primzahlgruppe. ECDH löst dasselbe Problem auf elliptischen Kurven. Der Vorteil: ECDH erreicht bei viel kleineren Schlüsseln dieselbe Sicherheit. Ein 256-Bit ECDH-Schlüssel entspricht einem 3072-Bit DH-Schlüssel hinsichtlich des Sicherheitsniveaus.

Warum sollte ich HKDF verwenden und nicht SHA-256 direkt auf das Shared Secret?
HKDF (RFC 5869) ist speziell für diesen Anwendungsfall konzipiert. Direktes SHA-256 auf das Shared Secret hat mehrere Schwächen: keine Kontextbindung, kein formales Sicherheitsmodell, keine Unterstützung für mehrere unabhängige Schlüssel. HKDF ist der korrekte Standard für Key Derivation aus Shared Secrets.

Schützt ECDH vor Man-in-the-Middle-Angriffen?
Nein. ECDH allein schützt nicht vor Man-in-the-Middle. Ein Angreifer zwischen Alice und Bob könnte seinen eigenen Public Key an beide senden und zwei separate Shared Secrets aufbauen. Der Schutz kommt durch Authentifizierung: digitale Signaturen oder Zertifikate. In TLS übernimmt das die Zertifikatskette. Für eigene Protokolle: Public Keys müssen über einen authentifizierten Kanal ausgetauscht oder mit digitalen Signaturen verknüpft werden.

Ist X25519 sicherer als ECDH P-256?
Beide bieten 128-Bit-Sicherheit und gelten 2026 als sicher. X25519 hat Vorteile: die Implementierung ist schwieriger falsch zu machen (keine Punkt-Validierung nötig), resistenter gegen bestimmte Timing-Angriffe, und schneller auf vielen Architekturen. Für neue Protokolle ist X25519 die erste Wahl. P-256 bleibt der universelle Standard für Interoperabilität mit älteren Systemen.

Kann ich ECDH für Post-Quantum-Sicherheit verwenden?
Nein. ECDH und alle klassischen asymmetrischen Verfahren werden durch Shor’s Algorithmus auf einem ausreichend großen Quantencomputer gebrochen. Für Post-Quantum-Sicherheit sind hybride Verfahren nötig: klassisches ECDH kombiniert mit ML-KEM (Kyber). Node.js hat noch keine native Post-Quantum-API; externe Bibliotheken wie @noble/post-quantum können verwendet werden.

Wie werden DH-Parameter sicher bei einem API ausgetauscht?
In der Praxis werden ECDH Public Keys als Teil des Handshake-Protokolls übertragen. Bei REST-APIs: Base64-encoded Public Keys im JSON-Body. Der Salt für HKDF wird typischerweise vom Session-Initiator generiert und mitgesendet. Wichtig: Der Transport der Public Keys muss über TLS erfolgen, wenn keine Out-of-Band-Authentifizierung stattfindet.

Unterstützt Node.js X448 (Goldilocks)?
Ja, Node.js v22 unterstützt X448 über dieselbe crypto.diffieHellman() API. X448 bietet 224-Bit-Sicherheit und ist langsamer als X25519. Es wird für Anwendungen empfohlen, die über 128-Bit-Sicherheit hinausgehen müssen, ohne auf ECDH P-521 zurückgreifen zu wollen. Die API-Nutzung ist identisch zu X25519, nur der Key-Type ändert sich auf 'x448'.

Vollständiges Projektbeispiel: Secure Key Exchange API

Das folgende Beispiel zeigt eine vollständige Express-basierte REST-API, die ECDH-Schlüsselaustausch als Service implementiert. Dieses Muster findet Anwendung bei sicheren Messaging-Apps, End-to-End-verschlüsselten Datei-Uploads und IoT-Device-Onboarding.

// secure-key-exchange-api.js
// Hinweis: npm install express für dieses Beispiel erforderlich
import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.json());

// In-Memory Session-Store (für Produktion: Redis verwenden)
const sessions = new Map();

/**
 * POST /api/session/init
 * Client sendet seinen ECDH Public Key.
 * Server antwortet mit seinem Public Key und einem Session-Salt.
 */
app.post('/api/session/init', (req, res) => {
  const { clientPublicKey, curve = 'prime256v1' } = req.body;

  if (!clientPublicKey) {
    return res.status(400).json({ error: 'clientPublicKey fehlt' });
  }

  try {
    // Server generiert ephemeres ECDH-Schlüsselpaar
    const serverEcdh = crypto.createECDH(curve);
    serverEcdh.generateKeys();

    // Shared Secret berechnen
    const clientPubKeyBuffer = Buffer.from(clientPublicKey, 'base64');
    const sharedSecret = serverEcdh.computeSecret(clientPubKeyBuffer);

    // Session-Salt generieren
    const salt = crypto.randomBytes(32);

    // Session-Schlüssel ableiten
    const sessionKey = crypto.hkdfSync(
      'sha256',
      sharedSecret,
      salt,
      Buffer.from('secure-api-v1:session-key', 'utf8'),
      32
    );

    // Session speichern
    const sessionId = crypto.randomUUID();
    sessions.set(sessionId, {
      sessionKey,
      createdAt: Date.now(),
      expiresAt: Date.now() + 3600_000, // 1 Stunde
    });

    res.json({
      sessionId,
      serverPublicKey: serverEcdh.getPublicKey('base64'),
      salt: salt.toString('base64'),
      expiresIn: 3600,
    });
  } catch (err) {
    res.status(400).json({ error: 'Ungültiger Public Key: ' + err.message });
  }
});

/**
 * POST /api/message/send
 * Sendet eine verschlüsselte Nachricht im Kontext einer Session.
 */
app.post('/api/message/send', (req, res) => {
  const { sessionId, iv, ciphertext, authTag, threadId } = req.body;

  const session = sessions.get(sessionId);
  if (!session) {
    return res.status(401).json({ error: 'Session nicht gefunden oder abgelaufen' });
  }
  if (Date.now() > session.expiresAt) {
    sessions.delete(sessionId);
    return res.status(401).json({ error: 'Session abgelaufen' });
  }

  try {
    const key = session.sessionKey;
    const decipher = crypto.createDecipheriv(
      'aes-256-gcm',
      key,
      Buffer.from(iv, 'base64')
    );
    if (threadId) decipher.setAAD(Buffer.from(threadId, 'utf8'));
    decipher.setAuthTag(Buffer.from(authTag, 'base64'));

    const decrypted = Buffer.concat([
      decipher.update(Buffer.from(ciphertext, 'base64')),
      decipher.final(),
    ]).toString('utf8');

    res.json({ status: 'ok', message: decrypted });
  } catch (err) {
    res.status(400).json({ error: 'Entschlüsselung fehlgeschlagen: ' + err.message });
  }
});

app.listen(3000, () => console.log('Secure Key Exchange API auf Port 3000'));

Der zugehörige Client-Code demonstriert, wie ein Frontend oder ein anderer Microservice diese API nutzt:

// client-example.js
import crypto from 'node:crypto';

async function initiateSecureSession(apiUrl) {
  // Client generiert ephemeres ECDH-Schlüsselpaar
  const clientEcdh = crypto.createECDH('prime256v1');
  clientEcdh.generateKeys();

  const clientPublicKey = clientEcdh.getPublicKey('base64');

  // Session initialisieren
  const initResponse = await fetch(`${apiUrl}/api/session/init`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ clientPublicKey, curve: 'prime256v1' }),
  });

  const { sessionId, serverPublicKey, salt } = await initResponse.json();

  // Client-seitig Shared Secret berechnen
  const serverPubKeyBuffer = Buffer.from(serverPublicKey, 'base64');
  const sharedSecret = clientEcdh.computeSecret(serverPubKeyBuffer);

  // Denselben Session-Schlüssel ableiten
  const saltBuffer = Buffer.from(salt, 'base64');
  const sessionKey = crypto.hkdfSync(
    'sha256',
    sharedSecret,
    saltBuffer,
    Buffer.from('secure-api-v1:session-key', 'utf8'),
    32
  );

  console.log('Session etabliert:', sessionId);
  return { sessionId, sessionKey };
}

async function sendEncryptedMessage(apiUrl, sessionId, sessionKey, message, threadId) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', sessionKey, iv);

  if (threadId) cipher.setAAD(Buffer.from(threadId, 'utf8'));

  const ciphertext = Buffer.concat([cipher.update(message, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();

  const response = await fetch(`${apiUrl}/api/message/send`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      sessionId,
      iv: iv.toString('base64'),
      ciphertext: ciphertext.toString('base64'),
      authTag: authTag.toString('base64'),
      threadId,
    }),
  });

  return response.json();
}

// Verwendung
const { sessionId, sessionKey } = await initiateSecureSession('http://localhost:3000');
const result = await sendEncryptedMessage(
  'http://localhost:3000',
  sessionId,
  sessionKey,
  'Geheime Nachricht an den Server',
  'thread-001'
);
console.log('Server Antwort:', result);

Mathematischer Hintergrund: Warum funktioniert Diffie-Hellman?

Ein kurzer Blick auf die Mathematik hinter DH hilft, die Sicherheitsgarantien und Grenzen besser zu verstehen.

Klassisches DH – Die diskreter Logarithmus-Annahme: Gegeben eine große Primzahl p und ein Generator g. Alice wählt eine geheime Zahl a und berechnet A = g^a mod p. Bob wählt eine geheime Zahl b und berechnet B = g^b mod p. Alice sendet A, Bob sendet B. Alice berechnet B^a mod p = g^(ba) mod p, Bob berechnet A^b mod p = g^(ab) mod p. Da ab = ba, ist das Ergebnis identisch. Ein Angreifer, der A kennt, müsste a aus g^a mod p berechnen, das ist der diskrete Logarithmus, für den kein effizienter klassischer Algorithmus bekannt ist.

ECDH – Die elliptic curve discrete logarithm-Annahme (ECDLP): Statt Primzahlgruppen werden Punkte auf elliptischen Kurven über endlichen Körpern verwendet. Die Gruppenoperation ist die Punktaddition auf der Kurve. Gegeben ist ein öffentlicher Basispunkt G auf der Kurve. Alice wählt ein zufälliges a und berechnet A = a·G (Skalarmultiplikation). Bob berechnet B = b·G. Alice berechnet a·B = a·(b·G) = (ab)·G, Bob berechnet b·A = b·(a·G) = (ab)·G. Identisch. Der Angreifer müsste a aus a·G bestimmen, das ist ECDLP, für den kein sub-exponentieller klassischer Algorithmus bekannt ist.

Warum sind kleine DH-Gruppen gefährlich? Für klassisches DH existiert der Number Field Sieve (NFS) Algorithmus, der sub-exponentielle Komplexität hat. Er ist effizienter als reine Brute-Force, ermöglicht aber bei kleinen Gruppengrößen (512 Bit, 768 Bit, 1024 Bit) praktische Angriffe. Das war die Grundlage des Logjam-Angriffs. Für ECDLP existiert kein vergleichbarer sub-exponentieller Algorithmus auf klassischen Computern, weshalb ECDH bei deutlich kleineren Schlüsseln dasselbe Sicherheitsniveau erreicht.

Vergleich mit RSA und anderen Public-Key-Verfahren

Diffie-Hellman und ECDH lösen ein anderes Problem als RSA. RSA wird für Verschlüsselung und digitale Signaturen eingesetzt. DH/ECDH sind ausschließlich für den Schlüsselaustausch gedacht. Die Verfahren ergänzen sich:

In TLS 1.3 übernimmt ECDHE den Schlüsselaustausch (für Forward Secrecy), während RSA oder ECDSA für die Server-Authentifizierung über Zertifikate zuständig ist. Diese Trennung der Verantwortlichkeiten macht das Protokoll robuster. Mehr zur RSA-Implementierung in Node.js finden Sie im RSA-Verschlüsselung in Node.js Tutorial.

Für digitale Signaturen in Node.js, die häufig in Kombination mit ECDH eingesetzt werden, empfiehlt sich der Artikel über SHA-256 vs SHA-3 im Vergleich sowie das Tutorial zu digitalen Signaturen in Node.js.

Weiterlesen

Die hier behandelten Konzepte ergänzen folgende Artikel auf shattered.io: