ECDH, Elliptic Curve Diffie-Hellman, är det nyckelutbytesprotokoll som driver TLS 1.3, Signal-protokollet och merparten av modern krypterad kommunikation. I Node.js finns protokollet inbyggt i crypto-modulen sedan version 0.11.8. Med Node.js v22 LTS kan du implementera ett fullständigt krypterat meddelandeflöde med 30 rader kod, utan externa bibliotek. Den här handledningen lär dig att göra det på 12 steg och 30 minuter.

Elliptisk kurvkryptografi erbjuder 128-bitars säkerhet med 256-bitars nycklar, vilket innebär att en P-256-nyckel ger samma skyddsnivå som en RSA-3072-nyckel men är 12 gånger kortare. Det är därför NIST SP 800-186, publicerad och uppdaterad 2023, rekommenderar P-256 och P-384 för statliga system. Med Sveriges Cybersäkerhetslag (2025:1506), som trädde i kraft den 15 januari 2026 och implementerar NIS2-direktivet, ställs nu krav på att organisationer inom kritisk infrastruktur dokumenterar och säkrar sina kryptografiska val. Denna handledning ger dig den tekniska grunden.

Vad är ECDH och varför det spelar roll 2026

Diffie-Hellman-nyckelutbyte löser ett klassiskt problem: hur kan två parter komma överens om en hemlig nyckel via en osäker kanal utan att nyckelns värde någonsin sänds i klartext? ECDH löser samma problem med elliptiska kurvor i stället för klassisk modulär aritmetik. Resultatet är dramatiskt kortare nycklar och snabbare beräkningar.

I praktiken används ECDH i dessa sammanhang:

  • TLS 1.3: Alla TLS 1.3-handskakningar kräver ephemeral ECDHE (Elliptic Curve Diffie-Hellman Ephemeral). Det gamla RSA-nyckelutbytet är borttaget ur specifikationen.
  • Signal-protokollet: X3DH, Extended Triple Diffie-Hellman, bygger på elliptiska kurvor och används i Signal, WhatsApp och iMessage.
  • SSH: Modern OpenSSH väljer som standard curve25519-sha256 för nyckelutbyte.
  • WireGuard: Protokollet använder Curve25519 uteslutande för nyckelutbyte.
  • Hybridkryptering: ECDH genererar en delad hemlighet som sedan krypterar ett symmetriskt AES-256-nyckel, vilket kombinerar asymmetrisk och symmetrisk kryptografi.

Skillnaden mot RSA-kryptering är fundamental: RSA krypterar data direkt med den asymmetriska nyckeln. ECDH genererar aldrig ett chiffer, det skapar en delad hemlighet som sedan driver symmetrisk kryptering. Det gör ECDH till ett nyckelutbytesprotokoll, inte ett krypteringssystem.

Hur elliptisk kurvkryptografi fungerar matematiskt

En elliptisk kurva är en mängd punkter som uppfyller ekvationen y² = x³ + ax + b (Weierstrass-form) över ett ändligt fält. Kurvan P-256, vars tekniska namn är prime256v1 och secp256r1, opererar över ett primtal med 256 bitar och innehåller ungefär 2^256 punkter.

ECDH-nyckelutbytet fungerar i fyra steg:

  1. Alice och Bob kommer överens om en kurva och en offentlig baspunkt G (detta är del av kurvoparametrarna, inte hemligt).
  2. Alice väljer ett slumpmässigt heltal a (sin privata nyckel) och beräknar A = a · G (skalärprodukt på kurvan). A är Alices publika nyckel.
  3. Bob väljer ett slumpmässigt heltal b och beräknar B = b · G. B är Bobs publika nyckel.
  4. Alice beräknar S = a · B = a · b · G. Bob beräknar S = b · A = b · a · G. Båda får samma punkt S, den delade hemligheten, utan att a eller b lämnat respektive part.

Säkerheten vilar på det diskreta logaritmproblemet på elliptiska kurvor (ECDLP): givet G och A = a · G är det beräkningsmässigt ogörligt att hitta a. Det bästa kända klassiska angreppet tar 2^128 operationer mot P-256, vilket är säkert i all överskådlig framtid mot konventionella datorer.

Förutsättningar och verktyg

Kontrollera att du har dessa versioner installerade innan du börjar:

VerktygMinimiversionRekommenderadSyfte
Node.jsv18.0.0v22.16.0 LTSRuntime med inbyggd crypto-modul
npm8.x10.xPakethantering
OpenSSL1.1.13.xUnderliggande kryptografibibliotek
TypeScript (valfritt)5.05.5Typsäkerhet vid produktion
OperativsystemLinux, macOS, WindowsUbuntu 22.04 LTSAlla plattformar stöds

Verifiera din Node.js-version och OpenSSL-konfiguration:

node --version
# v22.16.0

node -e "const crypto = require('crypto'); console.log(crypto.getCurves().filter(c => c.includes('256') || c.includes('384') || c.includes('521')))"
# [ 'prime256v1', 'secp384r1', 'secp521r1', ... ]

node -e "console.log(process.versions.openssl)"
# 3.0.15 (eller liknande)

Inga externa npm-paket krävs för de första 10 stegen. Node.js crypto-modul täcker all nödvändig funktionalitet. I steg 11 visar jag hur du lägger till typberedning med TypeScript.

Steg 1-2: Projektstruktur och grundläggande nyckelutbyte

Steg 1: Skapa projektstruktur

mkdir ecdh-demo && cd ecdh-demo
npm init -y
mkdir -p src/{alice,bob,shared}
touch src/basic-exchange.js src/hkdf-derivation.js src/hybrid-encryption.js

Steg 2: Grundläggande ECDH-nyckelutbyte

Det enklaste möjliga ECDH-utbytet i Node.js ser ut så här. Alice och Bob genererar varsin nyckelpar, utbyter publika nycklar och beräknar den delade hemligheten oberoende av varandra:

// src/basic-exchange.js
const crypto = require('crypto');

// Alice genererar sitt nyckelpar
const alice = crypto.createECDH('prime256v1');
const alicePublicKey = alice.generateKeys();

// Bob genererar sitt nyckelpar
const bob = crypto.createECDH('prime256v1');
const bobPublicKey = bob.generateKeys();

// Båda beräknar den delade hemligheten med motpartens publika nyckel
const aliceSecret = alice.computeSecret(bobPublicKey);
const bobSecret = bob.computeSecret(alicePublicKey);

// Verifiera att båda fick samma hemlighet
console.log('Alices hemlighet:  ', aliceSecret.toString('hex'));
console.log('Bobs hemlighet:    ', bobSecret.toString('hex'));
console.log('Matchar?           ', aliceSecret.equals(bobSecret));

// Förväntad utdata:
// Alices hemlighet:   a3f8... (32 byte hex, varierar per körning)
// Bobs hemlighet:     a3f8... (identisk)
// Matchar?            true

Viktigt: computeSecret() returnerar x-koordinaten för den delade punkten S. Den är 32 byte för P-256, 48 byte för P-384 och 66 byte för P-521. Använd aldrig denna råa hemlighet som en symmetrisk nyckel direkt, utan kör den först genom en nyckelderivat-funktion (se steg 5).

Steg 3: Välja rätt EC-kurva

Node.js och OpenSSL stöder dussintals kurvor, men bara ett fåtal är lämpliga för produktionsbruk. NIST SP 800-186 rekommenderar P-256, P-384 och P-521 för statliga tillämpningar. RFC 7748 (publicerad 2016, fortfarande giltig 2026) definierar X25519 och X448 som modernare alternativ med bättre motståndskraft mot implementeringsfel.

Kurva (tekniskt namn)NyckelstorlekSäkerhetsnivåPrestandaNIST-godkändRekommendation 2026
P-256 (prime256v1)256 bit128 bitSnabbJa (SP 800-186)Standard, bred kompatibilitet
P-384 (secp384r1)384 bit192 bitMåttligJa (SP 800-186)För känsliga data och NIS2-krav
P-521 (secp521r1)521 bit256 bitLångsamJa (SP 800-186)Topphemliga system, sällan nödvändigt
X25519 (Curve25519)255 bit128 bitMycket snabbNej (men NIST-godkänt i SP 800-186 som X25519)Rekommenderas för nya system
X448 (Curve448)448 bit224 bitSnabbJa (SP 800-186)Hög säkerhet utan P-384:s overhead
secp256k1256 bit128 bitSnabbNejUndvik utanför blockchain-kontext

Tumregel för 2026: Välj P-256 för kompatibilitet med äldre system och X25519 för moderna applikationer. P-384 är rätt val om din organisation omfattas av NIS2 och bearbetar känsliga personuppgifter eller kritisk infrastrukturdata. Undvik P-521 om inte regelverket specifikt kräver 256-bitars säkerhet.

// Visa alla tillgängliga kurvor i din Node.js-installation
const crypto = require('crypto');
const kurvor = crypto.getCurves();
console.log('Antal tillgängliga kurvor:', kurvor.length);
// Antal tillgängliga kurvor: 100+ (beroende på OpenSSL-version)

// Kontrollera att en specifik kurva finns
const stödjer384 = kurvor.includes('secp384r1');
console.log('Stöder P-384:', stödjer384); // true

Steg 4: Nyckelformat, export och serialisering

I verkliga system skickar Alice och Bob sina publika nycklar via ett API eller ett nätverksprotokoll. Det kräver serialisering till ett bärbart format. Node.js stöder tre format för ECDH-publika nycklar:

  • uncompressed (standard): 65 byte för P-256. Börjar med byte 0x04 följt av x- och y-koordinaterna. Mest kompatibelt.
  • compressed: 33 byte för P-256. Börjar med 0x02 eller 0x03. Halverar nyckelns storlek men kräver att motparten stöder okomprimerat format.
  • hybrid: 65 byte, som uncompressed men med komprimerad paritetsinformation i det första bytet. Sällsynt och undviks i modern kod.
// src/key-formats.js
const crypto = require('crypto');

const alice = crypto.createECDH('prime256v1');
alice.generateKeys();

// Hämta publik nyckel i olika format
const okomprimerad = alice.getPublicKey('hex', 'uncompressed');
const komprimerad = alice.getPublicKey('hex', 'compressed');
const base64Nyckel = alice.getPublicKey('base64', 'uncompressed');

console.log('Okomprimerad (hex):  ', okomprimerad.length / 2, 'byte');
// Okomprimerad (hex):   65 byte

console.log('Komprimerad (hex):   ', komprimerad.length / 2, 'byte');
// Komprimerad (hex):    33 byte

console.log('Privat nyckel (hex): ', alice.getPrivateKey('hex').length / 2, 'byte');
// Privat nyckel (hex):  32 byte

// Spara och ladda om privat nyckel
const sparadPrivatNyckel = alice.getPrivateKey();
const aliceKlonad = crypto.createECDH('prime256v1');
aliceKlonad.setPrivateKey(sparadPrivatNyckel);

// Publika nyckeln återskapas automatiskt från den privata
console.log('Nycklar matchar:', alice.getPublicKey('hex') === aliceKlonad.getPublicKey('hex'));
// Nycklar matchar: true

// OBS: Använd ALDRIG ecdh.setPublicKey() direkt - metoden är föråldrad
// och kan lämna nyckelobjektet i ett inkonsekvent tillstånd

Vid serialisering för API-kommunikation, använd Base64url (utan utfyllnadstecken) för att undvika teckenproblem i URL-parametrar och JSON:

// Konvertera publik nyckel till Base64url för API-transport
function nyckelTillBase64url(ecdh) {
  const bytes = ecdh.getPublicKey();
  return bytes.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function base64urlTillBuffer(str) {
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  return Buffer.from(base64, 'base64');
}

const alice = crypto.createECDH('prime256v1');
alice.generateKeys();
const nyckelStr = nyckelTillBase64url(alice);
console.log('API-format:', nyckelStr);
// API-format: BIaKtF... (88 tecken, ingen = utfyllnad)

Steg 5: Säker nyckelderivering med HKDF

Den råa delade hemligheten från computeSecret() är inte en bra symmetrisk nyckel av tre skäl: den är inte garanterat jämnt fördelad, den innehåller ingen kontextinformation (risk för nyckelåteranvändning), och alla bitar har inte lika stor entropi. HKDF, definierat i RFC 5869, löser detta.

Node.js v22 har inbyggt stöd för HKDF via crypto.hkdfSync():

// src/hkdf-derivation.js
const crypto = require('crypto');

function härledException(deladHemlighet, salt, info, längd = 32) {
  // HKDF med SHA-256
  // salt: slumpmässigt värde (minst 32 byte), kan delas öppet
  // info: kontextbeskrivning, t.ex. 'chat-session-2026'
  // längd: önskad nyckelstorlek i byte
  return crypto.hkdfSync(
    'sha256',
    deladHemlighet,
    salt,
    Buffer.from(info, 'utf8'),
    längd
  );
}

// Fullständigt exempel
const alice = crypto.createECDH('prime256v1');
const bob   = crypto.createECDH('prime256v1');
const alicePub = alice.generateKeys();
const bobPub   = bob.generateKeys();

const råHemlighet = alice.computeSecret(bobPub);

// Salt bör vara slumpmässigt och delas via nyckelutbytesmeddelandet
const salt = crypto.randomBytes(32);

const sessionNyckel = härledException(råHemlighet, salt, 'ecdh-demo/session/v1', 32);
const macNyckel     = härledException(råHemlighet, salt, 'ecdh-demo/mac/v1', 32);

console.log('Sessionsnyckel (hex):', Buffer.from(sessionNyckel).toString('hex'));
console.log('MAC-nyckel (hex):    ', Buffer.from(macNyckel).toString('hex'));
console.log('Samma källa, olika nycklar:', !Buffer.from(sessionNyckel).equals(Buffer.from(macNyckel)));
// Samma källa, olika nycklar: true

Info-parametern bör vara specifik för din applikation och session. Den hindrar att nycklar läcker mellan olika sammanhang om koden återanvänds. Bra värden: 'myapp/v1/alice-to-bob/encryption'. Dåliga värden: 'key' eller tom sträng.

Steg 6-7: Hybridkryptering med AES-256-GCM

Steg 6: Kryptering

Hybridkryptering kombinerar ECDH (för nyckelutbyte) med AES-256-GCM (för effektiv symmetrisk kryptering). Det är exakt det mönster som TLS 1.3 och moderna krypterade meddelandeapplikationer använder. Här implementeras ett fullständigt hybridkrypteringssystem:

// src/hybrid-encryption.js
const crypto = require('crypto');

// --- Hjälpfunktioner ---

function härledException(deladHemlighet, salt, info) {
  return Buffer.from(
    crypto.hkdfSync('sha256', deladHemlighet, salt, Buffer.from(info), 32)
  );
}

function kryptera(meddelande, mottagarPublikNyckel) {
  // 1. Skapa ett tillfälligt (ephemeral) nyckelpar för detta meddelande
  const avsändare = crypto.createECDH('prime256v1');
  const avsändarePub = avsändare.generateKeys();

  // 2. Beräkna delad hemlighet
  const deladHemlighet = avsändare.computeSecret(mottagarPublikNyckel);

  // 3. Härled symmetrisk nyckel via HKDF
  const salt = crypto.randomBytes(32);
  const symmetriskNyckel = härledException(
    deladHemlighet,
    salt,
    'hybrid-crypto/aes256gcm/v1'
  );

  // 4. Kryptera med AES-256-GCM
  const iv = crypto.randomBytes(12); // 96 bit IV för GCM
  const cipher = crypto.createCipheriv('aes-256-gcm', symmetriskNyckel, iv);
  const krypterat = Buffer.concat([
    cipher.update(meddelande, 'utf8'),
    cipher.final()
  ]);
  const autentiseringstagg = cipher.getAuthTag(); // 16 byte GCM-tagg

  // 5. Paketera allt tillsammans
  return {
    avsändarePub: avsändarePub.toString('base64'),
    salt:            salt.toString('base64'),
    iv:              iv.toString('base64'),
    krypterat:       krypterat.toString('base64'),
    tagg:            autentiseringstagg.toString('base64')
  };
}

// Exempel
const mottagare = crypto.createECDH('prime256v1');
const mottagarPub = mottagare.generateKeys();

const paket = kryptera('Hemligt meddelande till Bob', mottagarPub);
console.log('Krypterat paket:', JSON.stringify(paket, null, 2));

Steg 7: Dekryptering

function dekryptera(paket, mottagareECDH) {
  // 1. Läs in avsändarens tillfälliga publika nyckel
  const avsändarePub = Buffer.from(paket.avsändarePub, 'base64');
  const salt = Buffer.from(paket.salt, 'base64');

  // 2. Beräkna samma delade hemlighet (mottagaren + avsändarens pub)
  const deladHemlighet = mottagareECDH.computeSecret(avsändarePub);

  // 3. Härled samma symmetriska nyckel
  const symmetriskNyckel = härledException(
    deladHemlighet,
    salt,
    'hybrid-crypto/aes256gcm/v1'
  );

  // 4. Dekryptera med AES-256-GCM (autentiseringen sker automatiskt)
  const iv          = Buffer.from(paket.iv, 'base64');
  const krypterat   = Buffer.from(paket.krypterat, 'base64');
  const tagg        = Buffer.from(paket.tagg, 'base64');

  const decipher = crypto.createDecipheriv('aes-256-gcm', symmetriskNyckel, iv);
  decipher.setAuthTag(tagg);

  try {
    const klartext = Buffer.concat([
      decipher.update(krypterat),
      decipher.final()
    ]);
    return klartext.toString('utf8');
  } catch {
    // Autentiseringstaggen stämde inte - meddelandet är manipulerat
    throw new Error('Autentisering misslyckades: meddelandet är skadat eller manipulerat');
  }
}

// Komplett test
const dekrypterat = dekryptera(paket, mottagare);
console.log('Dekrypterat:', dekrypterat);
// Dekrypterat: Hemligt meddelande till Bob

GCM-läget ger både kryptering och autentisering. Om en angripare ändrar ett enda byte i det krypterade meddelandet kastar decipher.final() ett fel. Det gör att du aldrig riskerar att bearbeta manipulerad data.

Steg 8: X25519 och modern nyckelutbyte

X25519 är det moderna alternativet till P-256 för ECDH. Det använder Curve25519, designad 2005 av kryptografen Daniel J. Bernstein, och har tre fördelar jämfört med NIST-kurvorna: enklare och mer implementeringssäker matematik, inget beroende av slumpmässiga konstanter som NIST inte fullständigt motiverat, och snabbare beräkningar på modern hårdvara.

I Node.js hanteras X25519 via generateKeyPair och diffieHellman (eller WebCrypto API), inte via den äldre createECDH-klassen:

// src/x25519-exchange.js
const crypto = require('crypto');

// Generera X25519-nyckelpar (asynkron version)
async function x25519Utbyte() {
  // Alice
  const { privateKey: alicePriv, publicKey: alicePub } =
    await crypto.generateKeyPair('x25519', {
      publicKeyEncoding:  { type: 'spki',  format: 'der' },
      privateKeyEncoding: { type: 'pkcs8', format: 'der' }
    });

  // Bob
  const { privateKey: bobPriv, publicKey: bobPub } =
    await crypto.generateKeyPair('x25519', {
      publicKeyEncoding:  { type: 'spki',  format: 'der' },
      privateKeyEncoding: { type: 'pkcs8', format: 'der' }
    });

  // Beräkna delade hemligheter (DiffieHellman-funktion)
  const aliceHemlighet = crypto.diffieHellman({
    privateKey: crypto.createPrivateKey({ key: alicePriv, format: 'der', type: 'pkcs8' }),
    publicKey:  crypto.createPublicKey({  key: bobPub,   format: 'der', type: 'spki'  })
  });

  const bobHemlighet = crypto.diffieHellman({
    privateKey: crypto.createPrivateKey({ key: bobPriv, format: 'der', type: 'pkcs8' }),
    publicKey:  crypto.createPublicKey({  key: alicePub, format: 'der', type: 'spki'  })
  });

  console.log('X25519 - Matchar:', aliceHemlighet.equals(bobHemlighet));
  // X25519 - Matchar: true
  console.log('Delad hemlighet:', aliceHemlighet.toString('hex'));
  // Delad hemlighet: 32 byte hex

  return { aliceHemlighet, bobHemlighet };
}

x25519Utbyte().catch(console.error);

X25519-nycklar är alltid 32 byte. Det finns ingen komprimerad kontra okomprimerad variant och inga svaga punkter (cofactor-angrepp) att oroa sig för. Det är dessa egenskaper som gör kurvan till standardvalet i moderna protokoll som TLS 1.3, WireGuard och Signal.

Steg 9: ECDH i en fullständig Express.js-applikation

Att integrera ECDH i ett REST API kräver att man hanterar nyckelutbyte som ett flöde i flera steg. Här är ett minimalt men komplett exempel med Express.js som implementerar ett krypterat meddelandeflöde:

// src/server.js - Krypterad meddelandeserver med ECDH
const express = require('express');
const crypto  = require('crypto');

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

// Servernsyckelpar (skapas vid start, roteras i produktion)
const server = crypto.createECDH('prime256v1');
const serverPub = server.generateKeys('base64', 'uncompressed');

// In-memory sessioner (använd Redis i produktion)
const sessioner = new Map();

// Steg 1: Klienten hämtar serverns publika nyckel och skapar en session
app.get('/handshake', (req, res) => {
  const sessionId = crypto.randomUUID();
  sessioner.set(sessionId, { skapad: Date.now() });
  res.json({ sessionId, serverPublikNyckel: serverPub });
});

// Steg 2: Klienten skickar sin publika nyckel och krypterat meddelande
app.post('/message', (req, res) => {
  const { sessionId, klientPub, krypterat, iv, salt, tagg } = req.body;

  if (!sessioner.has(sessionId)) {
    return res.status(401).json({ fel: 'Ogiltig session' });
  }

  try {
    // Beräkna delad hemlighet
    const klientPubBuf  = Buffer.from(klientPub, 'base64');
    const deladHemlighet = server.computeSecret(klientPubBuf);

    // Härled nyckel
    const symmetriskNyckel = Buffer.from(
      crypto.hkdfSync(
        'sha256',
        deladHemlighet,
        Buffer.from(salt, 'base64'),
        Buffer.from('express-ecdh-demo/v1'),
        32
      )
    );

    // Dekryptera
    const decipher = crypto.createDecipheriv(
      'aes-256-gcm',
      symmetriskNyckel,
      Buffer.from(iv, 'base64')
    );
    decipher.setAuthTag(Buffer.from(tagg, 'base64'));

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

    sessioner.delete(sessionId); // Varje session används bara en gång
    res.json({ status: 'ok', mottaget: klartext });

  } catch (err) {
    res.status(400).json({ fel: 'Dekryptering misslyckades' });
  }
});

app.listen(3000, () => console.log('Server på port 3000'));
module.exports = app;

Servern lagrar sin privata ECDH-nyckel i minnet och exponerar bara den publika nyckeln. Varje klientmeddelande innehåller ett tillfälligt nyckelpar, vilket garanterar forward secrecy: om servernyckeln komprometteras kan gamla meddelanden inte dekrypteras i efterhand.

Steg 10-11: Nyckelrotation och prestandaoptimering

Steg 10: Nyckelrotation

NIST rekommenderar att kryptografiska nycklar roteras regelbundet. För ECDH-servernycklar är en rimlig rotationsperiod 24 timmar i hög-trafiksystem och 7 dagar i lågtrafikssystem. Under en rotation måste du hantera en övergångsperiod där både den gamla och nya nyckeln accepteras:

// src/key-rotation.js
const crypto = require('crypto');

class ECDHNyckelRotering {
  constructor(kurva = 'prime256v1', livstidMs = 24 * 60 * 60 * 1000) {
    this.kurva = kurva;
    this.livstid = livstidMs;
    this.nuvarandeNyckel = null;
    this.förraaNyckel = null;
    this.rotera(); // Skapa initial nyckel
  }

  rotera() {
    this.förraaNyckel = this.nuvarandeNyckel;
    const ecdh = crypto.createECDH(this.kurva);
    ecdh.generateKeys();
    this.nuvarandeNyckel = {
      ecdh,
      id: crypto.randomBytes(8).toString('hex'),
      skapad: Date.now(),
      publik: ecdh.getPublicKey('base64', 'uncompressed')
    };
    // Schemalägg nästa rotation
    setTimeout(() => this.rotera(), this.livstid).unref();
    console.log(`Nyckel roterad. Nytt nyckel-ID: ${this.nuvarandeNyckel.id}`);
  }

  hämtaNyckelInfo() {
    return {
      id:     this.nuvarandeNyckel.id,
      publik: this.nuvarandeNyckel.publik,
      skapad: new Date(this.nuvarandeNyckel.skapad).toISOString()
    };
  }

  beräknaHemlighet(klientPub, nyckelId) {
    // Kontrollera aktuell nyckel
    if (this.nuvarandeNyckel.id === nyckelId) {
      return this.nuvarandeNyckel.ecdh.computeSecret(Buffer.from(klientPub, 'base64'));
    }
    // Kontrollera föregående nyckel (under rotationsperiod)
    if (this.förraaNyckel && this.förraaNyckel.id === nyckelId) {
      return this.förraaNyckel.ecdh.computeSecret(Buffer.from(klientPub, 'base64'));
    }
    throw new Error(`Okänt nyckel-ID: ${nyckelId}`);
  }
}

const rotator = new ECDHNyckelRotering('prime256v1', 30_000); // 30 sek för test
console.log('Aktuell nyckelinfo:', rotator.hämtaNyckelInfo());

Steg 11: Prestandajämförelse

ECDH är avsevärt snabbare än RSA för nyckelutbyte, men det är nyttigt att känna till de faktiska talen. Dessa jämförelser baseras på benchmarks från Node.js crypto-modulens dokumentation och OpenSSL-prestanda på typisk serverhårdvara 2026 (AMD EPYC 9454, 2.75 GHz):

AlgoritmNyckelgenering (op/sek)Nyckelutbyte (op/sek)Nyckelstorlek (byte)Säkerhetsnivå
ECDH P-256~15 000~7 50065 (publik)128 bit
ECDH P-384~5 000~2 50097 (publik)192 bit
ECDH P-521~2 000~1 000133 (publik)256 bit
X25519~25 000~12 50032 (publik)128 bit
RSA-2048~2 000~50 000 (pub) / ~700 (priv)256 (publik)112 bit
RSA-3072~500~25 000 (pub) / ~200 (priv)384 (publik)128 bit

Observera: RSA-dekryptering (privat nyckeloperation) är drastiskt långsammare än ECDH. Det förklarar varför TLS 1.3 övergav RSA-nyckelutbyte. X25519 genererar dessutom nycklar 30 gånger snabbare än RSA-3072 vid ekvivalent säkerhetsnivå.

Steg 12: NIS2-compliance och produktionssäkerhet

Sveriges Cybersäkerhetslag (2025:1506), som implementerar EU:s NIS2-direktiv, trädde i kraft den 15 januari 2026. Föreskrifter om säkerhetsåtgärder och utbildning började gälla i april 2026, och föreskrifter om säkerhetsrevisioner och säkerhetsskanningar trädde i kraft i juni 2026. Det innebär att om din organisation faller under lagen (väsentliga eller viktiga aktörer) behöver du dokumentera dina kryptografiska val.

För ECDH-implementeringar gäller dessa krav enligt NIST SP 800-186 och praktiska NIS2-rekommendationer:

  • Kurvval: Använd P-256, P-384, P-521, X25519 eller X448. Undvik secp256k1 (Bitcoin-kurvan) i icke-blockchain-sammanhang och äldre kurvor som sect163r2.
  • HKDF-derivering: Alla delade hemligheter måste bearbetas via en godkänd KDF (HKDF med SHA-256 eller SHA-384) innan de används som symmetriska nycklar.
  • Ephemeral nycklar: Forward secrecy kräver att nycklarna är tillfälliga (ECDHE), inte statiska. Logga inte de tillfälliga privata nycklarna.
  • Nyckellagring: Privata servernycklar ska lagras i hårdvarusäkerhetsmoduler (HSM) eller betrodda exekveringsmiljöer (TEE) för väsentliga aktörer.
  • Rotation: Dokumentera och automatisera nyckelrotation. 90 dagars livstid är ett vanligt krav i säkerhetsramverk.
  • Loggning: Logga nyckelgenererings- och rotationshändelser, men aldrig privata nycklar eller råa delade hemligheter.
// src/compliance-checklist.js - NIS2-checklista för ECDH
const crypto = require('crypto');

function valideraNIS2Konfiguration(kurva, kdfAlgoritm, ephemeral) {
  const godkändaKurvor = ['prime256v1', 'secp384r1', 'secp521r1', 'x25519', 'x448'];
  const godkändaKDF    = ['sha256', 'sha384', 'sha512'];

  const resultat = {
    kurvOK:    godkändaKurvor.includes(kurva.toLowerCase()),
    kdfOK:     godkändaKDF.includes(kdfAlgoritm.toLowerCase()),
    ephemeralOK: ephemeral === true,
    godkänd: false
  };

  resultat.godkänd = resultat.kurvOK && resultat.kdfOK && resultat.ephemeralOK;

  if (!resultat.kurvOK) {
    console.warn(`Varning: Kurva '${kurva}' är inte NIS2-godkänd. Välj P-256, P-384, P-521, X25519 eller X448.`);
  }
  if (!resultat.ephemeralOK) {
    console.warn('Varning: Statiska ECDH-nycklar bryter mot forward secrecy-kravet.');
  }

  return resultat;
}

const kontroll = valideraNIS2Konfiguration('prime256v1', 'sha256', true);
console.log('NIS2-kontroll:', kontroll);
// NIS2-kontroll: { kurvOK: true, kdfOK: true, ephemeralOK: true, godkänd: true }

Vanliga fallgropar och misstag

Dessa misstag förekommer i produktionskod och kan leda till allvarliga säkerhetssvagheter:

  1. Använda den råa delade hemligheten som symmetrisk nyckel. Hemligheten från computeSecret() är en punktkoordinat, inte en jämnt fördelad nyckel. Den måste alltid gå igenom HKDF. Kod som gör aes256gcm(deladHemlighet, meddelande) direkt är felaktig.
  2. Anropa ecdh.setPublicKey(). Metoden är föråldrad (deprecated) sedan Node.js v6 och kan lämna ECDH-objektet i ett inkonsekvent tillstånd. Använd setPrivateKey() i stället, som automatiskt härledde den publika nyckeln.
  3. Återanvända statiska ECDH-nycklar utan rotation. Statiska nycklar bryter mot forward secrecy. Om en statisk privat nyckel läcker kan alla historiska meddelanden dekrypteras. Använd alltid ephemeral (tillfälliga) nycklar per session eller meddelande.
  4. Använda secp256k1 utanför blockchain-kontext. Bitcoin-kurvan saknar NIST-godkännande och stöds inte i TLS. Den är designad för effektiv signering, inte nyckelutbyte. Välj P-256 eller X25519 i stället.
  5. Validera inte motpartens publika nyckel. I klassisk DH kan en angripare skicka en svag publik nyckel (small subgroup attack). Node.js validerar automatiskt att nyckeln tillhör kurvan, men i äldre kod eller anpassade implementeringar bör du alltid verifiera detta explicit.
  6. Förvara privata nycklar i miljövariabler som strängar. Privata ECDH-nycklar i PEM-format i en .env-fil i klartext är ett vanligt misstag. Använd Node.js KeyObject och nyckelhanteringstjänster som AWS Secrets Manager eller HashiCorp Vault.
  7. Logga delade hemligheter för felsökning. Det förekommer att console.log(deladHemlighet.toString('hex')) lämnas kvar efter felsökning. Kör ett statiskt analysverktyg som eslint-plugin-security för att fånga dessa mönster.

Felsökning

De vanligaste felen och hur du löser dem:

  • Error: Invalid EC key: Den publika nyckeln är korrupt, felkodad eller tillhör en annan kurva. Kontrollera att båda parter använder exakt samma kurva och att Base64-strängen inte har tappat utfyllnadstecken.
  • Error: error:0906D06C:PEM routines:PEM_read_bio:no start line: Du försöker ladda en DER-kodad nyckel med PEM-metoder. Kontrollera att format-parametern matchar det faktiska nyckelformatet (‘der’ vs ‘pem’).
  • Delade hemligheter matchar inte: Vanligaste orsaken är att en part använder den komprimerade och den andra den okomprimerade publika nyckeln utan att konvertera. Standardisera på okomprimerat format och konvertera explicit vid behov.
  • Error: Unsupported state or unable to authenticate data: AES-GCM-autentiseringstaggen stämmer inte. Antingen har ciffertexten manipulerats, eller också matchar IV, nyckel eller tagg inte. Skicka alltid IV, salt och tagg med samma paket som ciffertexten.
  • crypto.hkdfSync is not a function: hkdfSync introducerades i Node.js v15.0.0. Uppgradera till minst v18 LTS. Som tillfällig lösning kan du använda crypto.hkdf (asynkron version) eller det externa paketet hkdf.
  • RangeError: Invalid key length: AES-256-GCM kräver exakt 32 byte nyckel. Om HKDF-utdata är 48 byte (P-384) och du försöker använda hela bufferten som AES-nyckel misslyckas det. Ange alltid längd = 32 i HKDF-anropet.
  • X25519 stöds inte: Kontrollera att OpenSSL-versionen i din Node.js-build är 1.1.1 eller senare. Kör node -e "console.log(process.versions.openssl)". Om versionen är äldre, uppgradera Node.js.
  • Prestanda degenerering vid hög last: ECDH-nyckelgenerering är CPU-intensiv. Vid hög last kan du begränsa antalet samtida nyckelgenereringssessioner med en kösemaforer eller använda ett Worker Thread-pool för att flytta arbetet från event loop.

Avancerade tips för produktionssystem

Dessa tekniker förbättrar säkerhet och prestanda i produktionsmiljöer:

Implementera ECDH med WebCrypto API för Edge-miljöer (Cloudflare Workers, Deno, Bun) där Node.js crypto-modulen inte är tillgänglig. WebCrypto API är standardiserat och stöder P-256, P-384, P-521 och X25519 via SubtleCrypto.deriveBits().

Cachelagra nyckelpar med tidsbegränsning. Om din applikation genererar tusentals ECDH-sessioner per sekund kan du cachelagra nyckelpar i 1-5 sekunder och återanvända dem för parallella anslutningar. Det minskar CPU-lasten med 60-80 procent vid hög last, på bekostnad av reducerad men fortfarande acceptabel forward secrecy-granularitet.

Använd det strukturerade KeyObject-API:et i stället för råa buffertoperationer. crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) returnerar KeyObject-instanser som inte kan exporteras av misstag och har stöd för serialisering via keyObject.export().

Kombinera ECDH med Post-Quantum-algoritmer (hybridkryptografi) för framtidssäkring. NIST färdigställde sina Post-Quantum-standarder 2024 (ML-KEM/Kyber för nyckelutbyte). Moderna TLS-implementeringar använder X25519+ML-KEM-768 för att kombinera bevisad klassisk säkerhet med kvantsäkerhet.

Testa med Node.js crypto.timingSafeEqual när du jämför delade hemligheter eller HMAC-taggar. Reguljär ===-jämförelse av buffertinnehåll kan läcka information via tidsskillnader (timing attack).

Vanliga frågor om ECDH i Node.js

Kan jag använda ECDH för att kryptera data direkt?

Nej. ECDH är ett nyckelutbytesprotokoll, inte ett krypteringssystem. Det genererar en delad hemlighet som du sedan använder för att driva symmetrisk kryptering (AES-256-GCM, ChaCha20-Poly1305). Det är hybridkryptografi, och det är exakt hur TLS 1.3 fungerar.

Är P-256 säkert mot kvantdatorer?

Nej. En tillräckligt stor kvantdator som kör Shors algoritm kan bryta P-256 (och alla andra elliptiska kurvor). NIST rekommenderar att organisationer som hanterar data med lång sekretessperiod börjar implementera hybridkryptografi (ECDH + ML-KEM/Kyber) nu. Node.js har ännu inget inbyggt stöd för ML-KEM, men tredjepartsbibliotek finns tillgängliga.

Vad är skillnaden mellan ECDH och ECDHE?

E:et i ECDHE betyder Ephemeral (tillfällig). ECDHE genererar ett nytt nyckelpar för varje session, vilket ger forward secrecy. Statisk ECDH återanvänder samma nyckelpar. TLS 1.3 kräver ECDHE och tillåter inte statisk ECDH. I dina Node.js-applikationer bör du alltid generera ett nytt nyckelpar per session.

Hur länge ska jag behålla en ECDH-sessionshemlighet?

Sessionshemligheten ska leva precis så länge som sessionen varar, sedan kastas den. Spara den aldrig i en databas eller loggfil. Om du behöver long-term kryptering, härledd sessionsnyckel och kryptera med den, men kassera ändå sessionsnyckeln när kommunikationen är klar.

Kan jag köra ECDH i en webbläsare?

Ja, via WebCrypto API (window.crypto.subtle). ECDH med P-256 stöds i alla moderna webbläsare sedan 2016. Klientkoden kan använda WebCrypto och servern Node.js crypto-modulen, de är interoperabla om du är konsekvent med nyckelformat (SPKI för publika nycklar, PKCS8 för privata).

Vilken kurva ska jag välja för ett nytt projekt 2026?

Välj X25519 om du kontrollerar båda ändarna av kommunikationen. Välj P-256 om du måste vara kompatibel med äldre system, FIPS 140-3-certifierade miljöer eller regulatoriska krav som specifikt kräver NIST-kurvor. Välj P-384 om din organisation klassificeras som väsentlig aktör under NIS2 och bearbetar särskilt känsliga data.

Behöver jag verifiera motpartens identitet vid ECDH?

ECDH i sig ger ingen autentisering. En man-in-the-middle-angripare kan byta ut de publika nycklarna under utbytet. Lös det med ett av dessa alternativ: signera de publika ECDH-nycklarna med en långsiktig identitetsnyckel (ECDSA eller Ed25519), använd certifikat (PKI/TLS), eller välj ett protokoll som inkluderar autentisering inbyggt, som Signal-protokollets X3DH.

Vilka Node.js-versioner stöder X25519?

X25519 via crypto.generateKeyPair('x25519') och crypto.diffieHellman() finns tillgängligt från Node.js v13.9.0. För produktionsmiljöer, använd v18 LTS (stöd till april 2025) eller v22 LTS (stöd till april 2027). Node.js v16 är inte längre stödd sedan september 2023.

Relaterad täckning

Fördjupa dig i dessa relaterade ämnen på shattered.io:

Externa resurser: