Node.js 24.22.0, med kodnamnet “Krypton”, blev den aktuella LTS-versionen den 14 oktober 2026. Releasen lade till stöd för SIV- och GCM-SIV-lägen direkt i Cipher– och Decipher-API:erna, vilket gör det här ett bra tillfälle att bygga om din filkryptering från grunden med den inbyggda crypto-modulen. I den här guiden bygger du ett komplett CLI-verktyg som krypterar och dekrypterar filer med AES-256-GCM, härleder nycklar säkert från lösenord och strömmar stora filer utan att fylla minnet.

Du behöver inga externa kryptobibliotek. Allt som visas här körs med Node.js inbyggda node:crypto-modul, som bygger på OpenSSL. Projektet du bygger blir en liten men produktionsduglig grund du kan ta med dig in i riktiga system: backup-skript, säker lagring av API-nycklar, eller kryptering av känsliga kundfiler innan de laddas upp till molnet.

Behovet är sällan hypotetiskt för ett team som hanterar kunddata. Så snart en fil innehåller personnummer, hälsodata, avtal eller källkod för ett kommande produktsläpp blir frågan inte om den ska skyddas, utan hur. Att skriva en egen lösning i stället för att lita blint på en molnleverantörs standardinställningar ger dig full kontroll över var nyckeln lagras, vem som kan dekryptera och hur snabbt du kan rotera bort en komprometterad nyckel. Den kontrollen är själva poängen med att bygga verktyget från grunden i den här guiden, i stället för att bara anropa ett färdigt SDK utan att förstå vad som händer under huven.

Varför AES-256-GCM fortfarande är förstahandsvalet 2026

AES-256-GCM kombinerar kryptering och äkthetskontroll i ett enda steg. Det betyder att verktyget inte bara gömmer innehållet i en fil, det bevisar också att filen inte har manipulerats sedan den krypterades. Äldre lägen som CBC kräver att du bygger en separat MAC (Message Authentication Code) ovanpå krypteringen för att få samma skydd, och glömmer en utvecklare det steget uppstår klassiska paddning-orakel-sårbarheter.

Node.js kryptostack rör sig snabbt just nu. Den 8 september 2026 uppgraderade Node.js 24.21.0 det inbyggda OpenSSL-biblioteket till version 3.5.8 och lade samtidigt till stöd för att läsa privata nycklar via OpenSSL STORE-loaders, en funktion som gör det enklare att koppla Node.js mot HSM:er och PKCS#11-enheter i produktionsmiljöer. Bara fem veckor senare landade SIV- och GCM-SIV-lägena i 24.22.0. Två releaser, två kryptoförbättringar, samma kvartal. Det är ett tydligt tecken på att AES-GCM-familjen fortsätter utvecklas snarare än att ersättas.

Uppdateringarna kommer inte bara som nya funktioner. Den 18 juni 2026 gav Node.js-projektet ut 24.17.0 som en renodlad säkerhetsrelease som åtgärdade CVE-2026-48933, en sårbarhet med hög allvarlighetsgrad i hur WebCrypto skyddade output-längden för chiffer. Håller du inte dina Node.js-versioner uppdaterade riskerar du alltså inte bara att missa nya funktioner, du riskerar att köra kod med redan kända kryptosårbarheter kvar i produktion.

Den här guiden fokuserar på AES-256-GCM eftersom det är det läge som redan stöds överallt, från webbläsarens WebCrypto-API till mobila SDK:er och molntjänster. Du får en fungerande implementation redan i dag, med anteckningar om var de nyare SIV-lägena passar in när de mognar.

Samma algoritm dyker upp i betydligt fler sammanhang än filkryptering. TLS 1.3, protokollet som skyddar i princip all webbtrafik i dag, bygger på cipher-sviter där GCM-läget är standard snarare än undantag. Lär du dig mönstren i den här guiden, nyckelhärledning, unik IV per operation och strikt autentisering innan data litas på, går samma tänkande att återanvända i allt från krypterade loggfiler till meddelanden mellan mikrotjänster.

Förutsättningar: detta behöver du innan du börjar

Kontrollera att din miljö matchar listan nedan innan du går vidare. Hela projektet bygger enbart på Node.js inbyggda moduler, så du slipper hantera externa beroenden för själva kryptologiken.

VerktygVersionSyfte i projektet
Node.js24.22.0 LTS (“Krypton”) eller senare 24.xKör CLI-verktyget och exponerar node:crypto
npmMedföljer Node.js 24.xSkript och enkla testverktyg
OpenSSL (bundlad)3.5.8Underliggande kryptoimplementation i node:crypto
TerminalBash, zsh eller PowerShellKöra CLI-kommandon och skript
TextredigerareValfri (VS Code rekommenderas)Skriva och läsa källkoden

Grundläggande kunskap om JavaScript och asynkrona funktioner räcker. Du behöver inte ha arbetat med kryptografi tidigare, men det hjälper om du vet vad en buffer är i Node.js.

Att hålla sig till inbyggda moduler är inte bara bekvämt, det är också en säkerhetsfördel. Varje extra npm-paket i beroendeträdet är en extra part som måste lita på i en leveranskedja, och kryptokod är ett av de områden där ett sårbart eller skadligt tredjepartspaket får särskilt allvarliga konsekvenser. node:crypto underhålls som en del av Node.js kärna och får säkerhetsuppdateringar i samma takt som resten av körmiljön, vilket du ser exempel på i nästa avsnitt.

Steg 1–2: Skapa projektet och installera Node.js 24.22 LTS

Börja med att kontrollera din Node-version och skapa projektmappen. Kör du en äldre 20.x- eller 22.x-version fungerar grundkoden ändå, men du missar STORE-loader-stödet och de nya cipher-lägena som nämns senare i artikeln.

node --version
# v24.22.0

mkdir aes-kryptera-cli && cd aes-kryptera-cli
npm init -y
mkdir src test

# Sätt type: module i package.json för att använda import/export
npm pkg set type=module
npm pkg set engines.node=">=24.0.0"

Projektstrukturen hålls medvetet platt. Du behöver fyra filer: en modul för nyckelhärledning, en för själva krypteringen, ett filformat, ett CLI-skript och en testfil. Ingen build-process, ingen TypeScript-kompilering, bara ren ESM-kod som Node.js kör direkt.

Steg 3: Så fungerar AES-256-GCM i praktiken

Innan du skriver kod behöver du förstå fyra byggstenar som tillsammans utgör en krypterad fil.

  • Nyckeln (key): 32 byte (256 bitar) hemlig data. Den härleds från lösenordet och används aldrig direkt i klartext.
  • Initieringsvektorn (IV): 12 byte slumpdata, unik för varje kryptering. Återanvänds samma IV med samma nyckel läcker GCM information om klartexten och kan i värsta fall avslöja hela nyckeln.
  • Saltet: 16 byte slumpdata som skickas in i nyckelhärledningen tillsammans med lösenordet, så att samma lösenord aldrig ger samma nyckel två gånger.
  • Autentiseringstaggen (auth tag): 16 byte som GCM genererar vid kryptering. Dekrypteringen vägrar leverera klartext om taggen inte stämmer, vilket stoppar manipulerade filer.

Alla fyra måste sparas tillsammans med krypteringstexten, utom själva nyckeln som aldrig lagras. Nästa steg visar hur du bygger den delen säkert.

Det är värt att se hur dessa delar hänger ihop konceptuellt innan du skriver en enda rad kod. Saltet gör nyckelhärledningen unik per fil, nyckeln är den hemlighet som faktiskt skyddar innehållet, IV gör varje kryptering unik även om samma nyckel används flera gånger, och auth-taggen fungerar som ett sigill som bevisar att ingenting ändrats sedan sigillet sattes. Tar du bort vilken som helst av de fyra brister antingen sekretessen eller äktheten i det färdiga systemet.

Steg 4: Härled en säker nyckel från lösenord med scrypt

Ett lösenord är inte en kryptografisk nyckel. Det är kort, förutsägbart och ofta återanvänt. scrypt löser det genom att göra nyckelhärledningen medvetet långsam och minneskrävande, vilket gör brute-force-attacker dyra även om angriparen har tillgång till GPU-kluster.

// src/keys.js
import { scrypt, randomBytes } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

export const KEY_LENGTH = 32;
export const SALT_LENGTH = 16;

export async function deriveKey(password, salt) {
  if (!password || password.length < 8) {
    throw new Error('Lösenordet måste vara minst 8 tecken.');
  }
  return scryptAsync(password, salt, KEY_LENGTH, {
    N: 16384,
    r: 8,
    p: 1,
    maxmem: 64 * 1024 * 1024,
  });
}

export function generateSalt() {
  return randomBytes(SALT_LENGTH);
}

Parametrarna N, r och p styr hur mycket minne och CPU-tid härledningen kräver. N=16384 är ett rimligt standardvärde för ett CLI-verktyg som kör lokalt. Kör du detta i en serverlös miljö med strikt minnesbudget bör du sänka N och testa prestandan noggrant innan du rör värdena i produktion.

Steg 5–6: Bygg krypterings- och dekrypteringsfunktionerna

Med nyckelhärledningen klar är själva krypteringen förvånansvärt kort kod. Komplexiteten ligger i att hantera IV, salt och autentiseringstagg korrekt runt omkring.

// src/cipher.js
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const ALGORITHM = 'aes-256-gcm';
export const IV_LENGTH = 12;
export const AUTH_TAG_LENGTH = 16;

export function encryptBuffer(plaintext, key, aad) {
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  if (aad) cipher.setAAD(aad);

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

  return { iv, ciphertext, authTag };
}

export function decryptBuffer(ciphertext, key, iv, authTag, aad) {
  const decipher = createDecipheriv(ALGORITHM, key, iv);
  if (aad) decipher.setAAD(aad);
  decipher.setAuthTag(authTag);

  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

Lägg märke till att decipher.final() kastar ett fel om autentiseringstaggen inte matchar. Det är precis det beteendet du vill ha. En trasig eller manipulerad fil ska aldrig lämna tillbaka data, den ska ge ett tydligt fel.

Steg 7: Designa ett filformat för krypterade filer

En krypterad fil behöver en header innan ciphertext, annars vet dekrypteringen inte vilket salt, vilken IV eller vilken auth tag som hör ihop med vilken fil. Ett enkelt och robust format är: en magisk byte-sekvens, en versionsbyte, salt, IV, auth tag och sist ciphertext.

// src/format.js
export const MAGIC = Buffer.from('AESK');
export const FORMAT_VERSION = 1;

// Header-layout:
// [4 byte magic][1 byte version][16 byte salt][12 byte iv][16 byte authTag]
export function buildHeader(salt, iv, authTag) {
  return Buffer.concat([
    MAGIC,
    Buffer.from([FORMAT_VERSION]),
    salt,
    iv,
    authTag,
  ]);
}

export function parseHeader(buffer) {
  const magic = buffer.subarray(0, 4);
  if (!magic.equals(MAGIC)) {
    throw new Error('Filen är inte en giltig krypterad fil från det här verktyget.');
  }
  const version = buffer.readUInt8(4);
  const salt = buffer.subarray(5, 21);
  const iv = buffer.subarray(21, 33);
  const authTag = buffer.subarray(33, 49);
  return { version, salt, iv, authTag, headerLength: 49 };
}

Versionsbyten är inte dekoration. Den dagen du vill byta nyckelhärledning, lägga till fler metadatafält eller gå över till ett SIV-läge kan du läsa versionen först och välja rätt tolkningslogik, utan att gamla filer blir oläsbara.

Ett binärt fast-längd-format som det ovan är medvetet valt framför till exempel JSON med base64-kodade fält. Binärt format tar mindre plats på disk, är snabbare att parsa eftersom varje fälts position och längd är känd i förväg, och undviker ett helt problemområde kring teckenkodning som annars kan introducera subtila buggar när filer flyttas mellan olika operativsystem.

Steg 8: Streama stora filer utan att krascha minnet

Funktionerna ovan läser hela filen i minnet, vilket fungerar fint för konfigurationsfiler och mindre dokument. Försöker du kryptera en videofil på 20 GB med samma kod kraschar processen. Lösningen är att strömma data genom cipher-objektet med node:stream/promises.

// src/stream.js
import { createReadStream } from 'node:fs';
import { open } from 'node:fs/promises';
import { pipeline } from 'node:stream/promises';
import { createCipheriv, randomBytes } from 'node:crypto';
import { buildHeader } from './format.js';
import { deriveKey, generateSalt } from './keys.js';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const HEADER_LENGTH = 49;

export async function encryptFileStream(inputPath, outputPath, password) {
  const salt = generateSalt();
  const key = await deriveKey(password, salt);
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);

  const outHandle = await open(outputPath, 'w');
  const output = outHandle.createWriteStream();

  // Auth-taggen är inte känd förrän strömmen är klar, så vi reserverar
  // platsen för headern och fyller i den riktiga när krypteringen är klar.
  await outHandle.write(Buffer.alloc(HEADER_LENGTH), 0, HEADER_LENGTH, 0);

  await pipeline(createReadStream(inputPath), cipher, output);
  const authTag = cipher.getAuthTag();

  const header = buildHeader(salt, iv, authTag);
  await outHandle.write(header, 0, header.length, 0);
  await outHandle.close();
}

Mönstret är enkelt när du väl sett det: reservera plats för headern, strömma igenom cipher-objektet, och gå sedan tillbaka och skriv den riktiga headern när auth-taggen är känd. pipeline() sköter felhantering och stänger strömmarna korrekt åt dig, vilket är säkrare än att koppla ihop flera .pipe()-anrop manuellt.

Minnesanvändningen blir i stort sett konstant oavsett om filen är 10 kilobyte eller 10 gigabyte, eftersom data flödar genom cipher-objektet i bitar (vanligtvis 64 kilobyte åt gången som standard i Node.js strömmar) i stället för att hållas kvar i sin helhet. Det är samma princip som gör att du kan strömma en video utan att ladda ner hela filen först, bara tillämpad på kryptering i stället för uppspelning.

Steg 9: Lägg till Additional Authenticated Data (AAD)

AAD låter dig autentisera data som inte krypteras, till exempel filnamnet eller en kategori-tagg. Om någon byter namn på filen eller flyttar den till fel mapp, och din applikationslogik läser AAD ur separat metadata, upptäcker dekrypteringen manipulationen direkt.

import { encryptBuffer, decryptBuffer } from './cipher.js';

const aad = Buffer.from(JSON.stringify({ filnamn: 'kontrakt.pdf', ägare: 'ekonomi' }));

const { iv, ciphertext, authTag } = encryptBuffer(plaintext, key, aad);

// Vid dekryptering måste exakt samma AAD skickas in igen
const result = decryptBuffer(ciphertext, key, iv, authTag, aad);

AAD är valfritt men kraftfullt i system där filer och metadata hanteras separat, till exempel när ciphertext ligger i objektlagring och metadata i en databas. Binder du ihop dem kryptografiskt kan ingen i tysthet byta ut vilken databaspost som pekar på vilken fil.

Ett vanligt användningsfall är multi-tenant-system, där flera kunder delar samma lagringsinfrastruktur. Lägg kundens organisations-ID i AAD, så misslyckas dekrypteringen omedelbart om en bugg i applikationslogiken av misstag försöker läsa en fil med fel kund-ID kopplat till anropet, även om själva lösenordet eller nyckeln råkar vara korrekt för en annan kund i samma system.

Steg 10: Bygg ett komplett CLI-verktyg

Nu binder du ihop alla delar till ett körbart kommandoradsverktyg. Node.js inbyggda parseArgs från node:util räcker gott för enkel argumenthantering, så du slipper ett extra npm-paket bara för flaggor.

#!/usr/bin/env node
// src/cli.js
import { parseArgs } from 'node:util';
import { encryptFileStream, decryptFileStream } from './stream.js';

const { values, positionals } = parseArgs({
  options: {
    encrypt: { type: 'boolean', short: 'e' },
    decrypt: { type: 'boolean', short: 'd' },
    password: { type: 'string', short: 'p' },
    output: { type: 'string', short: 'o' },
  },
  allowPositionals: true,
});

const [inputFile] = positionals;

if (!inputFile || !values.password || !values.output) {
  console.error('Användning: kryptera -e|-d -p  -o  ');
  process.exit(1);
}

try {
  if (values.encrypt) {
    await encryptFileStream(inputFile, values.output, values.password);
    console.log(`Krypterade ${inputFile} -> ${values.output}`);
  } else if (values.decrypt) {
    await decryptFileStream(inputFile, values.output, values.password);
    console.log(`Dekrypterade ${inputFile} -> ${values.output}`);
  }
} catch (error) {
  console.error('Åtgärden misslyckades:', error.message);
  process.exit(1);
}

Gör filen körbar och testa den direkt mot en riktig fil:

chmod +x src/cli.js
node src/cli.js -e -p "ett-starkt-losenord-2026" -o hemlig.enc kontrakt.pdf
# Krypterade kontrakt.pdf -> hemlig.enc

node src/cli.js -d -p "ett-starkt-losenord-2026" -o aterskapad.pdf hemlig.enc
# Dekrypterade hemlig.enc -> aterskapad.pdf

diff kontrakt.pdf aterskapad.pdf && echo "Filerna är identiska"
# Filerna är identiska

Testa felaktigt lösenord för att se autentiseringen i praktiken:

node src/cli.js -d -p "fel-losenord" -o test.pdf hemlig.enc
# Åtgärden misslyckades: Unsupported state or unable to authenticate data

Det färdiga projektet: fullständig filstruktur

Vid det här laget har du byggt varje enskild del av verktyget. Så här ser hela projektet ut när alla bitar från stegen ovan är på plats.

aes-kryptera-cli/
├── package.json          # type: module, engines.node >= 24.0.0
├── src/
│   ├── keys.js           # deriveKey(), generateSalt() – Steg 4
│   ├── cipher.js         # encryptBuffer(), decryptBuffer() – Steg 5–6
│   ├── format.js         # buildHeader(), parseHeader() – Steg 7
│   ├── stream.js         # encryptFileStream(), decryptFileStream() – Steg 8–9
│   └── cli.js            # Kommandoradsgränssnittet – Steg 10
└── test/
    └── roundtrip.test.js # Automatiserade tester – Steg 11–12

Sju filer, inga externa beroenden för själva kryptologiken, och varje fil har ett enda tydligt ansvar. Den uppdelningen gör det enkelt att byta ut en enskild del senare, till exempel att ersätta scrypt-baserad nyckelhärledning med en molnbaserad nyckeltjänst, utan att röra resten av koden.

Steg 11–12: Testa kryptot och hantera fel utan att läcka information

Ett litet testskript räcker för att bevisa att hela kedjan fungerar, inklusive att en manipulerad fil faktiskt avvisas. Node.js har ett inbyggt testrunner-API sedan version 18, så du slipper installera Jest eller Mocha för ett projekt i den här storleken.

// test/roundtrip.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { writeFile, readFile, unlink } from 'node:fs/promises';
import { encryptFileStream, decryptFileStream } from '../src/stream.js';

test('kryptering och dekryptering ger tillbaka originalet', async () => {
  await writeFile('plain.tmp', 'hemlig data för testet');
  await encryptFileStream('plain.tmp', 'cipher.tmp', 'testlosenord123');
  await decryptFileStream('cipher.tmp', 'decrypted.tmp', 'testlosenord123');

  const original = await readFile('plain.tmp', 'utf8');
  const result = await readFile('decrypted.tmp', 'utf8');
  assert.equal(result, original);

  await Promise.all(['plain.tmp', 'cipher.tmp', 'decrypted.tmp'].map(unlink));
});

test('fel lösenord ger ett fel, inte skräpdata', async () => {
  await writeFile('plain2.tmp', 'annan hemlig data');
  await encryptFileStream('plain2.tmp', 'cipher2.tmp', 'ratt-losenord');

  await assert.rejects(
    () => decryptFileStream('cipher2.tmp', 'out2.tmp', 'fel-losenord'),
  );

  await Promise.all(['plain2.tmp', 'cipher2.tmp'].map(unlink));
});
node --test test/
# ▶ kryptering och dekryptering ger tillbaka originalet
# ✔ kryptering och dekryptering ger tillbaka originalet (42.1ms)
# ▶ fel lösenord ger ett fel, inte skräpdata
# ✔ fel lösenord ger ett fel, inte skräpdata (38.6ms)
# ℹ tests 2
# ℹ pass 2
# ℹ fail 0

Felhanteringen i CLI-koden ovan skriver bara ut error.message, aldrig hela felstacken eller delar av den dekrypterade datan. Det är medvetet. Ett vanligt misstag är att logga hela felobjektet, inklusive interna buffertar, till en loggfil som senare blir läsbar för fler personer än tänkt.

Steg 13–14: Nyckelrotation, versionering och produktionshärdning

Filformatets versionsbyte gör nyckelrotation hanterbar. Behöver du byta nyckelhärledning, till exempel höja scrypt-parametrarna efter ett par år när hårdvaran blivit snabbare, skriver du nya filer med version 2 medan version 1 fortfarande kan läsas av samma dekrypteringskod.

// Exempel på hur version styr vilken scrypt-konfiguration som används
const SCRYPT_PARAMS_BY_VERSION = {
  1: { N: 16384, r: 8, p: 1 },
  2: { N: 32768, r: 8, p: 1 }, // starkare parametrar för nya filer
};

function getScryptParams(version) {
  const params = SCRYPT_PARAMS_BY_VERSION[version];
  if (!params) throw new Error(`Okänd filformatversion: ${version}`);
  return params;
}

I produktion bör lösenordet eller den härledda nyckeln aldrig ligga hårdkodad i källkoden eller i en vanlig miljövariabel som loggas av ett övervakningsverktyg. Node.js 24.21.0 gjorde det enklare att göra rätt här genom stödet för att läsa privata nycklar via OpenSSL STORE-loaders, vilket öppnar för att koppla nyckelhantering direkt mot en HSM eller en molnbaserad nyckeltjänst i stället för att lösenordet någonsin rör vid applikationskoden. Kör du i en molnmiljö är AWS KMS, Google Cloud KMS eller Azure Key Vault ett rimligt steg upp från lösenordsbaserad härledning, särskilt för system som hanterar många användares data samtidigt.

Både 24.21.0 och den parallella 26.x-linjen fick samma OpenSSL 3.5.8-uppgradering inom loppet av ett dygn från varandra, vilket visar att Node.js-teamet numera behandlar kryptorelaterade beroenden som en gemensam release-linje snarare än något som uppdateras separat för varje större version.

I juni 2026 lade Node.js 24.18.0 även till de nyare hashalgoritmerna TurboSHAKE och KangarooTwelve i WebCrypto-gränssnittet. Det är inget du behöver för AES-256-GCM specifikt, men det visar riktningen: kryptomodulen i Node.js uppdateras oftare än en gång per kvartal, så bygg in en rutin för att läsa release-anteckningarna varje gång du planerar en ny LTS-uppgradering.

AES-256-GCM jämfört med CBC och de nya SIV-lägena

Node.js stödjer flera AES-lägen, men de passar olika syften. Tabellen nedan ger en snabb överblick över var GCM står i förhållande till det äldre CBC-läget och de SIV-baserade lägena som landade i Node.js 24.22.0.

LägeInbyggd äkthetskontrollKräver unik IV/nonceStatus i Node.js 24.xRekommenderad användning
AES-256-CBCNej, kräver separat HMACJa, annars läcker mönster i datanStöds, men kräver manuell MAC-implementationUndvik för nya projekt om inte kompatibilitet kräver det
AES-256-GCMJa, inbyggd auth tagJa, katastrofalt om återanvändFullt stöd, väl beprövat sedan längeStandardvalet för filkryptering och API-nyttolaster
AES-256-SIV / GCM-SIVJa, inbyggdMer förlåtande vid IV-återanvändningNytt i Node.js 24.22.0 (oktober 2026)Lovande för miljöer där unik IV är svår att garantera, men fortfarande ny i Node.js

SIV-lägena är intressanta eftersom de tål ett misstag som annars är förödande i GCM: om samma nyckel och IV av misstag återanvänds en gång blir följderna betydligt mindre allvarliga än i klassisk GCM. Det gör dem värda att hålla ögonen på för system med många parallella processer som annars riskerar att återanvända slumpkällor felaktigt. Eftersom stödet är nytt i Node.js 24.22.0 rekommenderas ändå GCM som förstahandsval i produktion tills SIV-lägena har hunnit testas brett av fler team och biblioteksstöd mognat ytterligare.

Padding-orakel-attacker mot CBC-läge är en välkänd attackfamilj inom tillämpad kryptografi, där en angripare utnyttjar hur ett system svarar på felaktig utfyllnad för att gradvis dekryptera data utan att känna till nyckeln. Just den klassen av sårbarhet är en viktig anledning till att GCM blev standardrekommendationen i de flesta moderna protokoll och ramverk: autentiseringen sker som en oskiljaktig del av krypteringen i stället för att läggas till som ett separat, lätt att glömma steg i applikationskoden.

En annan vanlig fråga gäller nyckelhärledning. scrypt och PBKDF2 är båda inbyggda i node:crypto och kräver ingen extra installation, medan Argon2 (vinnaren av Password Hashing Competition) kräver ett separat npm-paket eftersom det inte ingår i Node.js kärnbibliotek. Tabellen nedan ställer de tre metoderna mot varandra rent praktiskt, utan att hävda någon exakt prestandasiffra som varierar kraftigt mellan hårdvara och konfiguration.

MetodInbyggd i node:cryptoResursprofilTypisk användning
PBKDF2JaCPU-bunden, lågt minnesbehovÄldre system och miljöer med strikta minnesgränser
scryptJaCPU- och minnesbunden (konfigurerbar via N/r/p)Standardval för nya fristående verktyg och filkryptering
Argon2Nej, kräver externt npm-paketMinnesbunden, flera varianter (Argon2id rekommenderas oftast)Lösenordshashning i webbapplikationer med inloggning

För det CLI-verktyg du byggt i den här guiden är scrypt ett pragmatiskt val just för att det redan finns i node:crypto och är väl dokumenterat i Node.js officiella dokumentation. Bygger du i stället en webbapplikation med vanlig användarinloggning är Argon2id oftast förstahandsvalet i branschen, men då behöver du lägga till ett paket som argon2 eller @node-rs/argon2 separat.

Vanliga fallgropar när du bygger AES-256-kryptering

De flesta säkerhetsbrister i hemmabyggd kryptering kommer inte från AES-algoritmen själv, utan från hur den används runt omkring. Här är de misstag som dyker upp oftast, både i kodgranskningar och i verkliga incidentrapporter om svag eller felimplementerad kryptering.

  • Återanvänd IV med samma nyckel. Det här är det allvarligaste misstaget med GCM. Generera alltid en ny slumpmässig IV per kryptering, aldrig en räknare som kan starta om eller ett fast värde. Återanvänds IV och nyckel tillsammans kan en angripare i värsta fall räkna ut nyckelströmmen och läsa klartext från flera filer.
  • Lösenordet används direkt som AES-nyckel. Ett textlösenord har inte samma entropi som en riktig 256-bitars nyckel. Kör alltid lösenordet genom scrypt, PBKDF2 eller Argon2 innan det blir en AES-nyckel, aldrig bara en enkel hash av lösenordet.
  • Auth-taggen sparas inte, eller sparas på fel plats. Utan auth-taggen kan dekrypteringen inte verifiera att filen är oförändrad, och hela poängen med GCM går förlorad. Det är lätt att missa om du kopierar kod skriven för CBC, där ingen auth-tagg existerar.
  • Samma salt återanvänds för flera filer. Då ger samma lösenord samma nyckel för alla filer, vilket gör att en angripare som knäcker en fil automatiskt knäcker alla filer krypterade med samma lösenord.
  • Hela filen läses in i minnet. Fungerar för småfiler, men kraschar processen eller tvingar fram dyr vertikal skalning när filerna blir stora. Använd strömmar från start, även om det känns som en genväg att hoppa över det i en prototyp.
  • Felmeddelanden läcker information. Att skriva ut hela felobjekt eller delar av klartexten i loggar kan avslöja mer än du tror om vad som gick fel och för vem. Håll loggningen till en generisk felkod utåt och den detaljerade informationen i ett internt, åtkomstbegränsat loggsystem.
  • Algoritmsträngen hårdkodas inte konsekvent. Om encryptBuffer och decryptBuffer hämtar algoritmnamnet från olika ställen i koden riskerar en framtida ändring att bara uppdatera ena sidan, vilket ger kryptiska fel vid dekryptering långt senare.
  • Nyckeln hålls kvar i minnet längre än nödvändigt. Buffertar med känsligt nyckelmaterial bör nollställas så snart de inte längre behövs, i stället för att lita på att skräpsamlaren städar upp dem vid ett obestämt framtida tillfälle.

Felsökning: vanliga fel och lösningar

Stöter du på fel när du bygger vidare på koden i den här guiden är det oftast någon av följande orsaker. Gå igenom listan i ordning innan du börjar lägga till egen felhantering ovanpå, eftersom de flesta problem löser sig genom att rätta en enskild rad snarare än att skriva om logiken.

  • "Unsupported state or unable to authenticate data": antingen är lösenordet fel, filen har manipulerats, eller så har du blandat ihop vilken IV och auth-tag som hör till vilken ciphertext.
  • "Invalid key length": din härledda nyckel är inte exakt 32 byte. Kontrollera att KEY_LENGTH i keys.js faktiskt är satt till 32 och att scrypt-anropet inte trunkerar resultatet.
  • "Invalid IV length": AES-256-GCM i Node.js förväntar sig en 12-byte IV som standard. Kontrollera att du inte råkat generera 16 byte, vilket är vanligt om du kopierat kod skriven för CBC-läge.
  • Processen kraschar med "JavaScript heap out of memory": du läser troligen in hela filen med readFile i stället för att strömma den. Byt till createReadStream och pipeline() enligt steg 8.
  • Dekrypterad fil är tom eller 0 byte: headern skrevs troligen inte korrekt efter att strömmen avslutats. Dubbelkolla att filhandtaget öppnas med läge som tillåter omskrivning (till exempel 'r+' eller motsvarande) innan du skriver den slutgiltiga headern.
  • Scriptet hänger sig vid stora filer: kontrollera att du faktiskt väntar in pipeline() med await, annars kan processen avslutas innan strömmen är klarskriven till disk.
  • "ERR_INVALID_ARG_TYPE" vid setAAD eller setAuthTag: dessa metoder förväntar sig Buffer-objekt, inte vanliga strängar. Konvertera alltid med Buffer.from() först.
  • Olika resultat på olika maskiner trots samma lösenord: kontrollera att saltet som genererades vid kryptering verkligen sparas och läses tillbaka vid dekryptering. Ett nytt slumpmässigt salt vid dekryptering ger en helt annan härledd nyckel.
  • "Cannot find module 'node:crypto'": du kör troligen en mycket äldre Node.js-version som inte stödjer node:-prefixet. Uppgradera till en aktuell 24.x LTS-version.
  • Testerna i test/roundtrip.test.js hittas inte av node --test: kontrollera att filen ligger i en mapp som heter test och att filnamnet innehåller .test.js, vilket är den konvention Node.js inbyggda testrunner letar efter som standard.

Ett genomgående tema i listan ovan är att nästan alla fel går att återskapa lokalt med ett litet testfall innan de hanteras i produktionskod. Bygg därför alltid ett misslyckande testfall först, till exempel en fil krypterad med fel version av formatet, innan du skriver den kod som ska hantera felet på ett kontrollerat sätt.

Avancerade tips för produktion

När grundverktyget fungerar finns några förbättringar värda att överväga innan du släpper det i en riktig produktionsmiljö. Ingen av dem är obligatorisk för att verktyget ska fungera, men varje punkt löser ett problem som brukar dyka upp när ett litet CLI-verktyg plötsligt ska hantera riktiga volymer och fler användare.

  • Lägg till en kontrollsumma för hela filen, separat från GCM:s auth-tagg, om du behöver verifiera filintegritet även innan dekryptering påbörjas, exempelvis för stora arkiv som laddas upp i flera delar.
  • Rotera nycklar genom att läsa gamla versioner med deras ursprungliga scrypt-parametrar, samtidigt som nya filer alltid skrivs med den senaste versionen enligt mönstret i steg 13–14.
  • Koppla nyckelhärledningen mot en molnbaserad nyckeltjänst i stället för ett lokalt lösenord så fort fler än en person eller tjänst behöver dekryptera samma data.
  • Använd STORE-loaders, tillagda i Node.js 24.21.0, för att hämta nycklar direkt från en HSM i stället för att någonsin låta nyckelmaterialet passera genom vanlig JavaScript-minneshantering.
  • Testa prestandan för scrypt-parametrarna på den faktiska hårdvara verktyget ska köras på. Vad som tar 200 millisekunder på en utvecklardator kan ta flera sekunder på en mindre container, vilket påverkar användarupplevelsen vid många samtidiga krypteringar.
  • Komprimera data innan kryptering, aldrig efter. Krypterad data ser slumpmässig ut och går i praktiken inte att komprimera, så kompressionssteget måste ligga före cipher-objektet i din pipeline för att ge någon effekt alls.
  • Kryptera flera filer parallellt med Node.js worker_threads om verktyget ska hantera batch-jobb med tusentals filer, eftersom scrypt är CPU-bundet och drar nytta av att spridas över flera kärnor.
  • Håll ett öga på SIV- och GCM-SIV-lägena som introducerades i Node.js 24.22.0. De är tänkta som ett extra skyddsnät mot IV-återanvändning och kan bli relevanta för system med många parallella processer när stödet mognat.

Integrera verktyget med molnlagring

I många produktionsmiljöer ska den krypterade filen inte bli liggande lokalt, den ska laddas upp till ett objektlager direkt efter kryptering. Eftersom encryptFileStream redan arbetar med strömmar är det enkelt att koppla på en uppladdning utan att skriva ciphertext till disk mellan stegen.

// Exempel: kryptera direkt till en skrivbar molnström i stället för en lokal fil
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createCipheriv, randomBytes } from 'node:crypto';
import { deriveKey, generateSalt } from './keys.js';

export async function encryptToCloudStream(inputPath, uploadStream, password) {
  const salt = generateSalt();
  const key = await deriveKey(password, salt);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  await pipeline(createReadStream(inputPath), cipher, uploadStream);
  const authTag = cipher.getAuthTag();

  // Spara salt, iv och authTag som objektmetadata hos molnleverantören
  // i stället för i en lokal header, så att headern inte behöver
  // skrivas om i efterhand på en ström som redan skickats iväg.
  return { salt, iv, authTag };
}

Skillnaden mot filversionen är att salt, IV och auth-tagg sparas som separat metadata hos molnleverantören i stället för i en header längst fram i filen. Anledningen är att de flesta uppladdningsströmmar till objektlagring inte går att gå tillbaka och skriva om i efterhand, till skillnad från en lokal fil öppnad med skrivbehörighet. Lagrar du metadata separat, till exempel i de anpassade metadatafält som de flesta objektlagringstjänster erbjuder, slipper du det problemet helt.

Du har nu byggt samma grundfunktionalitet som ligger bakom betydligt större system: en nyckel som aldrig lagras i klartext, en IV som aldrig återanvänds, ett filformat som tål att utvecklas över tid, och ett CLI-verktyg som vägrar lämna ut data om något inte stämmer. Resten handlar om att anpassa detaljerna, som hur nycklar roteras eller var metadata lagras, till den miljö verktyget faktiskt ska köras i.

Vanliga frågor

Vad är skillnaden mellan AES-256-GCM och AES-256-CBC?
GCM ger autentiserad kryptering direkt ur lådan, med en inbyggd auth-tagg som bevisar att datan inte manipulerats. CBC kräver att du själv lägger till en separat MAC-funktion ovanpå krypteringen för att få samma garanti, och ett ofullständigt sådant bygge är en vanlig källa till sårbarheter.

Kan jag använda AES-128 i stället för AES-256?
Tekniskt sett ja, node:crypto stödjer flera nyckellängder. AES-256 används i den här guiden eftersom det är standardvalet för nya system och ger en bredare säkerhetsmarginal mot framtida kryptoanalys, utan någon märkbar prestandaskillnad för de flesta filkrypteringsscenarier.

Är scrypt bättre än PBKDF2 för nyckelhärledning?
scrypt är medvetet minneskrävande, vilket gör det dyrare att attackera med specialiserad hårdvara som GPU:er och ASIC:er jämfört med PBKDF2, som främst är CPU-bundet. Båda är inbyggda i node:crypto och fullt användbara, men scrypt ger generellt ett starkare skydd mot brute-force-attacker i moderna hotmodeller.

Vad händer om jag tappar bort lösenordet till en krypterad fil?
Datan är förlorad. Det finns ingen bakdörr inbyggd i AES-256-GCM, och det är hela poängen med stark kryptering. Om det här är en risk i din användning, bygg in en säker återställningsmekanism, till exempel en separat återställningsnyckel som lagras åtskilt från lösenordet, innan verktyget tas i bruk.

Kan kvantdatorer knäcka AES-256?
AES-256 anses tåla kända kvantalgoritmer betydligt bättre än asymmetriska algoritmer som RSA, eftersom de teoretiska kvantattackerna mot symmetrisk kryptering i praktiken bara halverar den effektiva nyckellängden. Det lämnar fortfarande en mycket stor säkerhetsmarginal för AES-256 jämfört med vad som krävs för att knäcka det. Det är en viktig anledning till att nyckelutbytet i TLS, inte själva AES-krypteringen, är den del av dagens internetsäkerhet som branschen just nu bygger om med post-kvantalgoritmer.

Måste jag uppgradera till Node.js 24.22 för att följa den här guiden?
Nej, grundfunktionerna för AES-256-GCM, scrypt och streaming har funnits i Node.js under lång tid. Node.js 24.22.0 krävs bara om du specifikt vill experimentera med de nya SIV- och GCM-SIV-lägena som nämns i jämförelsetabellen.

Varför strömmar jag filen i stället för att bara läsa in den med fs.readFile?
readFile laddar hela filen i minnet samtidigt, vilket fungerar för småfiler men kraschar processen eller kräver mycket mer minne när filerna blir stora. Streaming med pipeline() håller minnesanvändningen konstant oavsett filstorlek.

Hur vet jag att min implementation faktiskt är säker?
Kör alltid test/roundtrip.test.js-mönstret från steg 11–12, inklusive testet som bevisar att ett felaktigt lösenord ger ett fel i stället för skräpdata. Komplettera gärna med en extern kodgranskning innan verktyget hanterar produktionsdata, eftersom subtila fel i hur IV och salt hanteras lätt missas vid egen granskning.