Node.js sai lokakuussa 2026 jotain, mitä kehittäjät olivat pyytäneet vuosia: Argon2-algoritmiperheen suoraan ytimeen. Versiosta v24.8.0 alkaen node:crypto-moduulin Web Crypto -rajapinta tukee Argon2-algoritmeja ilman kolmannen osapuolen paketteja, ja Node.js v26 -dokumentaatio kuvaa toteutuksen asynkronisena, mikä on tärkeä yksityiskohta palvelimille, joissa salasanan hajautus ei saa jumittaa tapahtumasilmukkaa. Tämä opas näyttää, miten rakennat tuotantokelpoisen salasanojen hajautusjärjestelmän Argon2id:llä Node.js-sovelluksessa, 13 vaiheessa, noin 90 minuutissa.

Käymme läpi sekä vakiintuneen argon2-npm-paketin käytön että sen, miksi Argon2id on syrjäyttänyt bcryptin ja scryptin uusissa projekteissa. Opit valitsemaan oikeat muisti- ja aikaparametrit, rakentamaan rekisteröinti- ja kirjautumislogiikan Expressillä, suojaamaan kirjautumisen raa’alta voimalta ja migratoimaan vanhat bcrypt-hajautukset Argon2id:hen ilman, että käyttäjät joutuvat vaihtamaan salasanansa. Lopussa on kahdeksan vianmääritystilannetta, kuusi yleistä sudenkuoppaa ja parametritaulukko eri käyttötapauksiin.

Argon2id Node.js:n ytimessä – mitä muuttui lokakuussa 2026

Pitkään Node.js-kehittäjän oli pakko asentaa erillinen npm-paketti, jos halusi käyttää Argon2-algoritmia salasanojen hajauttamiseen. Node.js:n Web Crypto API -dokumentaatio vahvistaa, että Argon2-algoritmit tulivat tuetuiksi versiossa v24.8.0, ja tuki näkyy nykyisessä v26-dokumentaatiossa vakiintuneena osana globalThis.crypto-rajapintaa. Käytännössä tämä tarkoittaa, että Argon2 ei ole enää kolmannen osapuolen lisäosa vaan osa Node.js:n omaa kryptografiakerrosta, samassa sarjassa kuin AES-GCM, ECDH ja Ed25519.

Muutos on merkittävä kahdesta syystä. Ensinnäkin natiivi toteutus nojaa Node.js:n taustalla olevaan OpenSSL-kirjastoon samalla tavalla kuin muu salausfunktionaalisuus, mikä pienentää riippuvuuspintaa ja vähentää ulkoisten binääripakettien kääntämistarvetta asennuksen yhteydessä. Toiseksi se signaloi, että Argon2id on nyt osa alustan vakiotyökalupakkia, ei enää kokeellinen lisä. Tämä opas käyttää ensisijaisesti argon2-npm-pakettia, koska sen rajapinta on vakaa ja dokumentoitu jo vuosien ajan, mutta näytämme myös, miten natiivi tuki vaikuttaa valintoihisi jatkossa.

Jos sovelluksesi ajaa Node.js v24.8.0:aa tai uudempaa, kannattaa seurata Web Crypto -rajapinnan kehitystä aktiivisesti, koska natiivi tuki poistaa pitkällä aikavälillä tarpeen ulkoiselle paketille kokonaan. Tämän kirjoitushetkellä, 15. lokakuuta 2026, tuotantokäytössä turvallisin reitti on yhä kypsä npm-paketti, koska sen parametrit, virheenkäsittely ja testikattavuus ovat vuosien käytössä hioutuneet.

Node.js ei ole suinkaan ensimmäinen alusta, joka tuo Argon2id:n lähelle kehittäjää. PHP:n sisäänrakennettu password_hash()-funktio on tukenut Argon2id:tä jo vuosia, Pythonin ekosysteemissä suosittu valinta on argon2-cffi-kirjasto, ja Go-kielessä golang.org/x/crypto/argon2-paketti tarjoaa saman algoritmin. Node.js:n natiivi tuki kurottaa siis umpeen aukon, joka on ollut olemassa suhteessa muihin suosittuihin palvelinpuolen kieliin, ja tekee Argon2id:stä entistä selkeämmin kieliriippumattoman oletusvalinnan organisaatioille, joilla on useita palveluja eri teknologiapinoilla.

Mikä Argon2 on ja miksi se voitti Password Hashing Competition -kilpailun

Argon2 ei ole salausalgoritmi vaan avaimenjohtofunktio, jonka tarkoitus on tehdä salasanan murtaminen kallista sekä laskenta-ajan että muistin suhteen. Toisin kuin AES, Argon2 ei salaa eikä pura dataa vaan muuttaa salasanan yksisuuntaisesti tarkistettavaksi arvoksi. Se eroaa vanhemmista hajautusmenetelmistä yhdellä keskeisellä tavalla: se pakottaa hyökkääjän käyttämään paljon muistia jokaisessa arvausyrityksessä, mikä tekee GPU- ja ASIC-pohjaisista rinnakkaislaskentahyökkäyksistä huomattavasti kalliimpia kuin esimerkiksi PBKDF2:n tai pelkän SHA-256-kierrätyksen tapauksessa.

Algoritmin suunnittelivat Alex Biryukov, Daniel Dinu ja Dmitry Khovratovich Luxemburgin yliopistosta, ja se voitti kansainvälisen Password Hashing Competition -kilpailun vuonna 2015. Kilpailun tavoite oli löytää seuraaja bcryptille ja PBKDF2:lle, jotka molemmat ovat muistiltaan kevyitä ja siksi alttiimpia erikoisrautaa hyödyntäville hyökkäyksille. Argon2:n muisti-kova rakenne tekee siitä vaikeamman kohteen, koska jokainen rinnakkainen arvausyritys vaatii oman, usein kymmenien tai satojen megatavujen kokoisen muistilohkonsa.

Jokaisessa tallennettavassa salasanatietueessa tulee säilyttää kaikki parametrit, joilla hajautus laskettiin: suola, Argon2-variantti, muistikustannus, aikakustannus, rinnakkaisuusaste ja itse hajautusarvo. Suola on satunnainen arvo, joka generoidaan erikseen jokaiselle salasanalle, jotta kaksi identtistä salasanaa eivät koskaan tuota samaa tallennettua hajautusta. Ilman uniikkia suolaa hyökkääjä voisi käyttää valmiiksi laskettuja taulukkoja (rainbow table -hyökkäys) useiden tilien murtamiseen samanaikaisesti.

Käytännön esimerkki havainnollistaa, miksi muisti on niin tehokas puolustuskeino. Tavallinen näytönohjain pystyy ajamaan valtavan määrän rinnakkaisia SHA-256-laskuja samanaikaisesti, koska yksittäinen laskutoimitus vie vain muutaman tavun muistia. Argon2id vaatii jokaiselta rinnakkaiselta arvausyritykseltä kymmeniä megatavuja omaa muistitilaa, mikä romahduttaa näytönohjaimen rinnakkaisuuskyvyn murto-osaan alkuperäisestä: karkealla laskutoimituksella sama videomuistimäärä riittää huomattavasti pienempään määrään samanaikaisia Argon2id-laskuja kuin kevyitä SHA-256-laskuja. Juuri tämä suhde tekee muisti-kovista algoritmeista niin paljon kalliimpia murtaa erikoisraudalla kuin perinteisistä hajautusfunktioista.

Argon2i, Argon2d ja Argon2id – mikä niistä kannattaa valita

Argon2-perheessä on kolme varianttia, ja valinta niiden välillä vaikuttaa suoraan sovelluksesi turvallisuusprofiiliin. Argon2d käyttää datariippuvaista muistinkäyttöä, mikä tekee siitä erittäin tehokkaan torjumaan GPU-pohjaisia rinnakkaishyökkäyksiä, mutta se on altis sivukanavahyökkäyksille, koska muistin käyttökuvio riippuu salaisesta syötteestä. Argon2i käyttää datariippumatonta muistinkäyttöä, joka suojaa sivukanavilta mutta on hitaampi torjumaan rinnakkaislaskentaa.

Argon2id yhdistää molempien parhaat puolet kaksivaiheisella lähestymistavalla: ensimmäinen kierros käyttää datariippumatonta muistinkäyttöä suojaamaan sivukanavilta, ja loput kierrokset käyttävät datariippuvaista muistinkäyttöä torjumaan rinnakkaislaskentahyökkäyksiä. Tämän hybridirakenteen ansiosta Argon2id on käytännössä kaikkien nykyaikaisten kirjastojen ja standardien suosittelema oletusvalinta salasanojen hajautukseen. argon2-npm-paketti on vuosien ajan asettanut Argon2id:n oletusvariantiksi, ja sama linja näkyy myös siinä, miten uudemmat kirjastot ja ohjeistukset puhuvat aiheesta.

VarianttiMuistin käyttöVahvuusHeikkousSuositus
Argon2dDatariippuvainenVahva suoja rinnakkaislaskentaa vastaanAltis sivukanavahyökkäyksilleEi salasanoille (sopii esim. kryptovaluuttojen louhintaan)
Argon2iDatariippumatonSuojaa sivukanaviltaHeikompi suoja GPU-hyökkäyksiä vastaanVain jos sivukanavariski on erityisen korkea
Argon2idHybridi (molemmat)Tasapaino molempien riskien välilläEi merkittäviä tunnettuja heikkouksiaOletusvalinta salasanojen hajautukseen

Argon2id vastaan bcrypt, scrypt ja PBKDF2

Monessa olemassa olevassa Node.js-sovelluksessa pyörii edelleen bcrypt tai PBKDF2, ja syytä on hyvä ymmärtää ennen migraatiota. Bcrypt on vuodelta 1999 ja perustuu Blowfish-salakirjoitukseen. Se on osoittautunut luotettavaksi vuosikymmenten ajan, mutta sen muistinkäyttö on kiinteä ja pieni (noin 4 kilotavua), minkä takia erikoisrautaa käyttävät hyökkääjät voivat ajaa valtavan määrän rinnakkaisia arvauksia. PBKDF2 on vielä vanhempi ja vieläkin kevyempi muistiltaan, joten se on nykyisin heikoin vaihtoehto kolmikosta.

Node.js:n omassa crypto-moduulissa on sisäänrakennettu scrypt-funktio, joka on myös muisti-kova ja toimii kelvollisena vaihtoehtona, jos et halua lisätä ulkoista riippuvuutta. W3C:n kryptografiaohjeistus käsittelee avaintenpituuksia ja algoritmivalintoja yleisellä tasolla ja viittaa NIST:n suosituksiin symmetristen avainten vähimmäispituudesta, mutta Argon2id on tätä kirjoitettaessa laajalti pidetty parhaana saatavilla olevana vaihtoehtona juuri salasanojen hajautukseen, koska sen parametrit (muisti, aika, rinnakkaisuus) ovat erikseen säädettävissä toisin kuin scryptin hieman rajoittuneemmassa parametriavaruudessa.

AlgoritmiJulkaisuvuosiMuisti-kovaSäädettävät parametritTilanne 2026
PBKDF22000EiVain iteraatiomääräYhä käytössä legacy-järjestelmissä, ei suositella uusiin
bcrypt1999Osittain, kiinteäVain työkerroin (cost factor)Yhä yleinen, mutta väistyy Argon2id:n tieltä uusissa projekteissa
scrypt2009KylläMuisti, CPU, rinnakkaisuusKelvollinen vaihtoehto, sisäänrakennettu Node.js:n crypto-moduuliin
Argon2id2015Kyllä, säädettäväMuisti, aika, rinnakkaisuus, hajautuspituusSuositeltu oletusvalinta uusiin järjestelmiin

Vertailun ydin on, että Argon2id antaa sinulle suoran vaikutuskeinon kolmeen eri hyökkäyspintaan samanaikaisesti: muistiin, aikaan ja rinnakkaisuuteen. Bcrypt ja PBKDF2 antavat säädettäväksi käytännössä vain yhden parametrin, joten niiden suoja skaalautuu huonommin erikoisrautaa vastaan, vaikka työkerrointa nostaisi kuinka paljon.

Käytännön tasolla ero näkyy myös siinä, miten helppoa kirjastoa on käyttää väärin. Bcryptissä kehittäjä säätää yhtä lukua (työkerrointa), ja virhesuunta on lähes aina liian matala arvo, joka on nopea mutta heikko. Argon2id:ssä kolme säädettävää parametria antavat enemmän joustoa, mutta myös enemmän tapoja tehdä virhe: liian korkea rinnakkaisuusarvo voi kaataa pienimuistisen palvelimen, liian matala muistikustannus mitätöi koko algoritmin edun bcryptiin nähden. Tästä syystä tämän oppaan vaihe 13, kuormatestaus, on yhtä tärkeä osa käyttöönottoa kuin itse koodin kirjoittaminen. Parametrien valinta ei ole kertaluonteinen päätös, vaan arvo, jota tarkistetaan uudelleen, kun palvelinkapasiteetti tai käyttäjämäärä muuttuu merkittävästi.

Esivaatimukset ennen kuin aloitat

Tätä opasta varten tarvitset muutaman peruspalikan kunnossa. Listaa tarvittavat versiot ja työkalut läpi, ennen kuin avaat terminaalin.

  • Node.js v20.x tai uudempi (v24.8.0 tai uudempi, jos haluat kokeilla natiivia Web Crypto -Argon2-tukea rinnalla)
  • npm v10.x tai uudempi
  • argon2-npm-paketti (asennetaan vaiheessa 1, kirjoitushetken vakaa pääversiosarja)
  • Express v4.x tai v5.x, jos rakennat REST-rajapinnan tämän oppaan mukana
  • Käännösympäristö natiivipaketeille: Linuxissa build-essential, macOS:ssä Xcode Command Line Tools, Windowsissa windows-build-tools tai Visual Studio Build Tools
  • Tietokanta käyttäjätietojen tallennukseen (esimerkeissä käytämme yksinkertaistettua PostgreSQL-tyylistä skeemaa, mutta periaate pätee mihin tahansa tietokantaan)
  • Perustason tuntemus asynkronisesta JavaScriptistä (async/await)

Varmista Node.js-versiosi komennolla node --version ennen kuin etenet. Jos ajat vanhempaa kuin v18-sarjaa, päivitä ensin, koska moni tässä oppaassa käytetty API-ominaisuus (esimerkiksi tietyt crypto-moduulin async-rajapinnat) vaatii uudempaa ajonaikaa.

Kannattaa myös varata itselleen hieman ylimääräistä aikaa ensimmäiselle asennuskerralle, koska argon2-paketti kääntää natiivin laajennuksen paikallisesti eikä lataa valmiiksi käännettyä binääriä kaikille alustoille. Yritysympäristöissä, joissa kehittäjän kone on lukittu tai käännöstyökalut puuttuvat hallitusti, tämä kannattaa tarkistaa IT-osaston kanssa etukäteen, jotta asennus ei jää kiinni puuttuvaan kääntäjään kesken työpäivän. Jos käännös ei onnistu millään, varasuunnitelmana on Node.js:n sisäänrakennettu scrypt-funktio, joka ei vaadi lainkaan ulkoista kääntämistä, mutta tällöin menetät Argon2id:n hieman laajemman parametriavaruuden.

Vaihe 1-3: Projektin pohjustus ja kirjaston asennus

Vaihe 1: Alusta projekti. Luo uusi kansio ja alusta npm-projekti tavalliseen tapaan. Tässä vaiheessa kannattaa myös päättää, käytätkö CommonJS- vai ES-moduulisyntaksia, koska se vaikuttaa tuontilausekkeiden muotoon myöhemmissä esimerkeissä.

mkdir argon2-nodejs-demo
cd argon2-nodejs-demo
npm init -y
node --version
npm --version

Vaihe 2: Asenna argon2-paketti. Paketti sisältää natiivin C++-sidonnan libargon2-kirjastoon, joten asennus kääntää binäärin paikallisesti ensimmäisellä kerralla. Asenna myös Express, jos aiot rakentaa REST-rajapinnan tämän oppaan mukana.

npm install argon2
npm install express
npm install dotenv

Vaihe 3: Luo projektirakenne. Erottele hajautuslogiikka, reitit ja tietokantakutsut omiin tiedostoihin alusta alkaen. Tämä tekee myöhemmästä migraatiovaiheesta ja yksikkötestauksesta huomattavasti helpompaa.

mkdir -p src/auth src/db
touch src/auth/password.js src/auth/routes.js src/db/users.js src/server.js

Vaihe 4-6: Parametrien valinta ja hajautusfunktion rakentaminen

Vaihe 4: Päätä parametrit tietoisesti. Argon2id:n kolme keskeistä säädintä ovat muistikustannus (memoryCost, kibitavuina), aikakustannus (timeCost, kierrosten määrä) ja rinnakkaisuus (parallelism, säikeiden määrä). Mitä korkeammaksi nostat nämä, sitä kalliimpaa hyökkääjän on murtaa hajautus, mutta sitä enemmän palvelimesi joutuu kuluttamaan resursseja jokaisessa kirjautumisessa. Tasapaino kannattaa testata omalla tuotantolaitteistolla: hyvä nyrkkisääntö on, että yhden hajautuksen laskeminen saa kestää noin 250-500 millisekuntia palvelimellasi.

Vaihe 5: Kirjoita hajautusfunktio. Käärimme argon2.hash()-kutsun omaan moduuliin, jotta parametrit pysyvät yhdessä paikassa ja koko sovellus käyttää samoja asetuksia.

// src/auth/password.js
const argon2 = require('argon2');

const HASH_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: 2 ** 16, // 65536 KiB = 64 MiB
  timeCost: 3,
  parallelism: 1,
  hashLength: 32
};

async function hashPassword(plainPassword) {
  if (typeof plainPassword !== 'string' || plainPassword.length < 8) {
    throw new Error('Salasanan tulee olla vähintään 8 merkkiä pitkä merkkijono');
  }
  return argon2.hash(plainPassword, HASH_OPTIONS);
}

module.exports = { hashPassword, HASH_OPTIONS };

Vaihe 6: Kirjoita varmennusfunktio. argon2.verify() lukee tallennetusta hajautuksesta automaattisesti käytetyt parametrit ja suolan, joten sinun ei tarvitse tallentaa niitä erikseen omiin kenttiinsä. Tämä on yksi Argon2-paketin käytännöllisimmistä ominaisuuksista verrattuna moniin käsin rakennettuihin ratkaisuihin.

// src/auth/password.js (jatkoa)
async function verifyPassword(storedHash, plainPassword) {
  try {
    return await argon2.verify(storedHash, plainPassword);
  } catch (error) {
    // Virheellinen hajautusmuoto tai korruptoitunut data - käsittele turvallisesti
    console.error('Argon2-varmennus epäonnistui:', error.message);
    return false;
  }
}

module.exports.verifyPassword = verifyPassword;

Vaihe 7-9: Rekisteröinti- ja kirjautumislogiikka Expressillä

Vaihe 7: Rakenna rekisteröintireitti. Tallenna tietokantaan vain hajautettu merkkijono, jonka argon2.hash() palauttaa. Se sisältää kaiken tarvittavan: algoritmin tunnisteen, parametrit, suolan ja itse hajautuksen yhdessä merkkijonossa.

// src/auth/routes.js
const express = require('express');
const { hashPassword, verifyPassword } = require('./password');
const { createUser, findUserByEmail } = require('../db/users');

const router = express.Router();

router.post('/register', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) {
    return res.status(400).json({ error: 'Sähköposti ja salasana vaaditaan' });
  }
  const existing = await findUserByEmail(email);
  if (existing) {
    return res.status(409).json({ error: 'Käyttäjä on jo olemassa' });
  }
  const passwordHash = await hashPassword(password);
  const user = await createUser(email, passwordHash);
  return res.status(201).json({ id: user.id, email: user.email });
});

module.exports = router;

Vaihe 8: Rakenna kirjautumisreitti. Tärkeä yksityiskohta on, että virheilmoitus olemassa olevalle sähköpostille ja väärälle salasanalle pitää näyttää samanlaiselta ulospäin, jotta hyökkääjä ei voi päätellä, mitkä sähköpostiosoitteet ovat rekisteröityneitä järjestelmään.

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const genericError = { error: 'Virheellinen sähköposti tai salasana' };

  const user = await findUserByEmail(email);
  if (!user) {
    return res.status(401).json(genericError);
  }

  const isValid = await verifyPassword(user.passwordHash, password);
  if (!isValid) {
    return res.status(401).json(genericError);
  }

  return res.status(200).json({ message: 'Kirjautuminen onnistui', userId: user.id });
});

Vaihe 9: Lisää hajautuksen uudelleenlaskenta tarvittaessa. Jos nostat myöhemmin memoryCost- tai timeCost-arvoja turvallisuussyistä, vanhat käyttäjät jäävät edelleen vanhoilla, heikommilla parametreilla lasketun hajautuksen taakse, kunnes he kirjautuvat seuraavan kerran. Tarkista kirjautumisen yhteydessä, vastaako tallennettu hajautus nykyisiä HASH_OPTIONS-arvoja, ja laske se tarvittaessa uudelleen onnistuneen kirjautumisen jälkeen.

const argon2 = require('argon2');
const { HASH_OPTIONS, hashPassword } = require('./password');

async function needsRehash(storedHash) {
  return argon2.needsRehash(storedHash, HASH_OPTIONS);
}

// Kirjautumisen yhteydessä onnistuneen varmennuksen jälkeen:
if (await needsRehash(user.passwordHash)) {
  const freshHash = await hashPassword(password);
  await updateUserPasswordHash(user.id, freshHash);
}

Vaihe 10-13: Nopeusrajoitus, tilin lukitus ja kuormatestaus

Vaihe 10: Muista, että Argon2 ei pysäytä verkkohyökkäyksiä. Argon2id tekee offline-murtamisesta kallista, jos tietokanta vuotaa, mutta se ei suojaa tiliä siltä, että hyökkääjä yrittää arvata salasanaa suoraan kirjautumislomakkeen kautta verkon yli. Tätä varten tarvitaan erillinen nopeusrajoitus ja tilin lukitusmekanismi.

Vaihe 11: Lisää yksinkertainen muistinvarainen lukitus kehitysympäristöön. Tuotannossa kannattaa siirtää tämä Redikseen tai vastaavaan jaettuun tilaan, jos palvelimia on useampi.

const failedAttempts = new Map();
const MAX_ATTEMPTS = 5;
const LOCKOUT_MS = 15 * 60 * 1000; // 15 minuuttia

function isLockedOut(email) {
  const record = failedAttempts.get(email);
  if (!record) return false;
  if (record.count >= MAX_ATTEMPTS && Date.now() - record.lastAttempt < LOCKOUT_MS) {
    return true;
  }
  return false;
}

function recordFailedAttempt(email) {
  const record = failedAttempts.get(email) || { count: 0, lastAttempt: 0 };
  record.count += 1;
  record.lastAttempt = Date.now();
  failedAttempts.set(email, record);
}

function clearAttempts(email) {
  failedAttempts.delete(email);
}

Vaihe 12: Yhdistä lukitus kirjautumisreittiin. Tarkista lukitus ennen kuin kutsut kallista verifyPassword-funktiota, jotta hyökkääjä ei voi käyttää lukitun tilin yrityksiä palvelimen laskentaresurssien kuluttamiseen.

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  if (isLockedOut(email)) {
    return res.status(429).json({ error: 'Tili lukittu liian monen epäonnistuneen yrityksen takia' });
  }

  const user = await findUserByEmail(email);
  const isValid = user ? await verifyPassword(user.passwordHash, password) : false;

  if (!isValid) {
    recordFailedAttempt(email);
    return res.status(401).json({ error: 'Virheellinen sähköposti tai salasana' });
  }

  clearAttempts(email);
  return res.status(200).json({ message: 'Kirjautuminen onnistui' });
});

Vaihe 13: Kuormatesti valituille parametreille. Ennen tuotantoon viemistä, mittaa kuinka monta hajautusta palvelimesi pystyy laskemaan sekunnissa valituilla parametreilla, ja varmista, että se riittää odotettuun rekisteröinti- ja kirjautumisliikenteeseen ilman, että tapahtumasilmukka tukkeutuu.

const { hashPassword } = require('./src/auth/password');

async function benchmark(iterations = 20) {
  const start = Date.now();
  for (let i = 0; i < iterations; i++) {
    await hashPassword(`testipassword-${i}`);
  }
  const elapsed = Date.now() - start;
  console.log(`${iterations} hajautusta: ${elapsed} ms, keskiarvo ${(elapsed / iterations).toFixed(1)} ms/hajautus`);
}

benchmark();

Tyypillinen tuloste kohtuullisen tehoisella palvelimella 64 MiB muistilla ja kolmella aikakierroksella näyttää tältä:

20 hajautusta: 6840 ms, keskiarvo 342.0 ms/hajautus

Jos luku on huomattavasti alle 100 ms, parametrit ovat todennäköisesti liian kevyet tuotantokäyttöön. Jos se ylittää reilusti 500 ms, käyttäjät alkavat kokea kirjautumisen hitaana, ja kannattaa harkita rinnakkaisuuden tai muistin pienentämistä.

Migraatio bcryptistä tai PBKDF2:sta Argon2id:hen

Käytännössä mitään olemassa olevaa käyttäjäkantaa ei voi massamigratoida kerralla, koska vanhoja hajautuksia ei voi purkaa takaisin selkokieliseksi salasanaksi. Ratkaisu on asteittainen migraatio: säilytä vanha varmennuslogiikka rinnalla ja laske Argon2id-hajautus vasta, kun käyttäjä onnistuneesti kirjautuu sisään vanhalla salasanallaan. Tämä tapa on turvallinen, ei vaadi käyttäjiä vaihtamaan salasanaansa pakosta, ja siirtymä tapahtuu luonnollisesti käyttäjäkunnan aktiivisuuden tahdissa.

const bcrypt = require('bcrypt');
const { hashPassword } = require('./src/auth/password');

async function verifyAndMigrate(user, plainPassword) {
  if (user.passwordHash.startsWith('$2')) {
    // Vanha bcrypt-hajautus (tunniste alkaa $2a, $2b tai $2y)
    const isValid = await bcrypt.compare(plainPassword, user.passwordHash);
    if (!isValid) return { valid: false };

    const newHash = await hashPassword(plainPassword);
    await updateUserPasswordHash(user.id, newHash);
    return { valid: true, migrated: true };
  }

  // Jo Argon2id-muodossa
  const isValid = await require('argon2').verify(user.passwordHash, plainPassword);
  return { valid: isValid, migrated: false };
}

Käyttäjät, jotka eivät kirjaudu sisään pitkään aikaan, pysyvät vanhalla algoritmilla. Monissa organisaatioissa tämä hyväksytään tietoisena riskinä, mutta jos haluat täydellisen siirtymän, voit asettaa aikarajan (esimerkiksi 12 kuukautta), jonka jälkeen migratoitumattomat tilit pakotetaan salasanan palautukseen sähköpostilla.

Kannattaa myös seurata migraation etenemistä mittarilla, joka laskee, kuinka suuri osa käyttäjäkannasta on jo siirtynyt Argon2id:hen. Yksinkertaisin tapa on lisätä ajastettu raportti, joka laskee tietokannasta bcrypt-tunnisteella ($2) alkavien hajautusten osuuden kaikista tileistä ja kirjaa luvun viikoittain lokiin tai seurantapalveluun. Kun osuus laskee riittävän pieneksi, esimerkiksi alle viiteen prosenttiin, voit tehdä tietoisen päätöksen, pakotatko loput tilit salasanan vaihtoon vai jätätkö ne odottamaan luonnollista kirjautumista. Tämä mittari on erityisen hyödyllinen, jos organisaatiossasi on sääntely- tai auditointivaatimuksia, jotka edellyttävät näyttöä siitä, että vanhentuneista salaustavoista luovutaan hallitusti tietyn aikataulun sisällä.

Yleisimmät sudenkuopat Argon2-toteutuksessa

Useimmat Argon2-toteutuksen ongelmat eivät liity itse algoritmiin vaan siihen, miten se kytketään sovelluslogiikkaan. Algoritmi itse on hyvin tutkittu ja vakaa, mutta ympärille rakennettu sovelluslogiikka on juuri se kerros, jossa inhimilliset virheet tapahtuvat. Tässä kuusi virhettä, joita näkee toistuvasti tuotantokoodissa, eri kokoisissa tiimeissä ja eri kokemustasoilla.

  • Parametrien kopioiminen suoraan esimerkistä testaamatta omalla laitteistolla. 64 MiB ja kolme kierrosta voi olla liian raskasta pienelle palvelimelle tai liian kevyttä tehokkaalle klusterille. Mittaa aina omalla infrastruktuurillasi.
  • Salasanapituuden rajoittaminen liian tiukasti tai liian väljästi. Liian lyhyt vähimmäispituus (alle 8 merkkiä) heikentää suojaa, kun taas ylipitkien syötteiden (useita megatavuja) salliminen voi mahdollistaa palvelunestohyökkäyksen, jossa hajautusfunktio joutuu käsittelemään tarpeettoman suuria syötteitä.
  • Synkronisen hajautusfunktion käyttäminen pääsäikeessä. Jos käytät argon2.hashSync()-tyylistä kutsua Node.js:n yksisäikeisessä tapahtumasilmukassa palvelinkontekstissa, yksi raskas hajautuspyyntö voi tukkia koko palvelimen hetkellisesti. Käytä aina asynkronista rajapintaa palvelinpuolella.
  • Omien suolan- tai parametrienkäsittelyrutiinien kirjoittaminen käsin. argon2-npm-paketti hoitaa suolan generoinnin ja parametrien koodauksen hajautusmerkkijonoon automaattisesti. Käsin kirjoitettu vaihtoehto lisää virhemahdollisuuksia ilman hyötyä.
  • Virheellinen oletus, että Argon2id korvaa nopeusrajoituksen. Kuten vaiheessa 10 todettiin, Argon2id suojaa offline-murtamiselta mutta ei korvaa erillistä kirjautumisyritysten rajoitusta verkon yli tapahtuvia hyökkäyksiä vastaan.
  • Parametrien muuttaminen ilman needsRehash-tarkistusta. Jos nostat muistituksi tai aikakustannusta tuotannossa mutta unohdat lisätä vaiheen 9 kaltaisen uudelleenlaskentalogiikan, osa käyttäjäkannasta jää pysyvästi vanhoilla, heikommilla parametreilla lasketun hajautuksen taakse, vaikka koodi näyttäisi päällisin puolin päivitetyltä.

Vianmääritys: kahdeksan yleistä virhetilannetta

Seuraavat tilanteet toistuvat usein, kun Argon2id otetaan käyttöön ensimmäistä kertaa Node.js-projektissa. Osa näistä liittyy itse pakettiin, osa ympäristöön, jossa sovellus ajetaan, ja osa yksinkertaisesti siihen, että tiimi siirtyy ensimmäistä kertaa muisti-kovaan algoritmiin kevyemmästä vaihtoehdosta.

  • Asennus epäonnistuu käännösvirheeseen. argon2-paketti kääntää natiivin C++-sidonnan asennuksen aikana. Varmista, että käännösympäristö (build-essential, Xcode CLI Tools tai Visual Studio Build Tools) on asennettu, ja kokeile uudelleen npm install argon2 --build-from-source.
  • Virhe "argon2.hash is not a function". Tämä johtuu tyypillisesti siitä, että paketti on tuotu väärin ES-moduulisyntaksilla CommonJS-ympäristössä. Tarkista, että käytät require('argon2') CommonJS-projektissa tai import * as argon2 from 'argon2' ES-moduuliprojektissa.
  • Hajautus kestää yllättäen useita sekunteja. Yleisin syy on liian korkea memoryCost-arvo suhteessa palvelimen käytettävissä olevaan RAM-määrään, jolloin käyttöjärjestelmä joutuu sivuttamaan muistia levylle. Tarkista palvelimen muistin käyttö hajautuksen aikana.
  • Varmennus palauttaa aina false, vaikka salasana on oikein. Tarkista, että tietokantaan tallennettu hajautusmerkkijono ei ole katkaistu kesken (yleinen virhe, jos tietokantasarake on liian lyhyt, esimerkiksi VARCHAR(60) bcrypt-ajoilta). Argon2id-hajautukset vaativat tyypillisesti yli 90 merkin sarakkeen.
  • Muistivirhe "Argon2 memory allocation failed" kontissa. Tämä on yleinen Docker- ja Kubernetes-ympäristöissä, joissa kontille on asetettu muistiraja. Nosta kontin muistirajaa tai laske memoryCost-parametria, jos se ylittää kontin käytettävissä olevan muistin.
  • Eri tulokset kehitys- ja tuotantoympäristössä samalla koodilla. Varmista, että molemmissa ympäristöissä on sama argon2-paketin versio, koska pienet versiomuutokset libargon2-kirjastossa voivat vaikuttaa oletusarvoihin, jos et ole eksplisiittisesti määritellyt kaikkia parametreja.
  • Kirjautuminen hitaampaa kuormatestissä kuin paikallisesti. Tuotantopalvelimella saattaa olla käytössä vCPU-rajoitus (esimerkiksi pilvipalvelun pienempi instanssikoko), joka hidastaa Argon2id:n rinnakkaislaskentaa. Tarkista instanssin vCPU-määrä suhteessa asetettuun parallelism-arvoon.
  • TypeError tietokantaan tallennettaessa hajautusta. argon2.hash() palauttaa Promisen, joten unohdettu await aiheuttaa sen, että tietokantaan tallennetaan Promise-olio merkkijonon sijaan. Tarkista, että kaikki hajautuskutsut ovat async-funktioiden sisällä ja käyttävät await-avainsanaa.

Edistyneet vinkit tuotantokäyttöön ja parametritaulukko

Kun perustoteutus on pystyssä, kannattaa viilata muutama lisäasia kuntoon ennen julkaisua. Ensimmäinen vinkki: eriytä hajautusparametrit ympäristömuuttujiin, jotta voit nostaa niitä ajan myötä laitteiston kehittyessä, vertaamatta muuta koodia. Toinen vinkki: käytä tasarakenteista virheenkäsittelyä, joka ei vuoda tietoa siitä, oliko vika sähköpostiosoitteessa vai salasanassa, koska tämä on yleinen tiedustelutietovuodon lähde. Kolmas vinkki: seuraa hajautuslaskennan kestoa tuotannossa lokituksen avulla, jotta huomaat ajoissa, jos laitteistomuutos tai kuormapiikki alkaa hidastaa kirjautumista merkittävästi.

Neljäs vinkki koskee moniajopalvelimia: jos sovelluksesi ajaa useita Node.js-prosesseja klusterissa (esimerkiksi cluster-moduulilla tai PM2:lla), muista, että parallelism-parametri koskee yhden hajautuslaskennan sisäistä rinnakkaisuutta, ei prosessien määrää. Liian korkea parallelism yhdistettynä moneen samanaikaiseen prosessiin voi kuluttaa enemmän CPU-resursseja kuin odotat. Viides vinkki: jos sovelluksesi vaatii erityisen korkeaa turvallisuustasoa (esimerkiksi rahoitus- tai terveydenhuoltosektori), harkitse korkeampia parametreja kuin yleiskäytön oletusarvot, mutta testaa aina vaikutus kirjautumisviiveeseen todellisella käyttäjäliikenteellä.

Seuraava taulukko kokoaa yleisesti käytetyt parametriyhdistelmät eri käyttötapauksiin. Nämä ovat lähtökohtia, ei absoluuttisia totuuksia: paras tapa löytää sopivat arvot on aina mitata omalla tuotantolaitteistolla vaiheen 13 kaltaisella kuormatestillä. Monet tietoturva-alan ohjeistukset, kuten OWASP:n salasanojen tallennusopas, listaavat omia suositusyhdistelmiään, mutta niitäkin kohdellaan lähtökohtina, ei lopullisina arvoina, koska palvelinlaitteisto ja käyttäjämäärät vaihtelevat organisaatioittain merkittävästi.

KäyttötapausMuisti (memoryCost)Aika (timeCost)RinnakkaisuusPerustelu
Kevyt web-sovellus, rajallinen palvelinmuisti19 MiB (19456 KiB)21Sopii pienille VPS-instansseille, joissa muisti on kriittinen resurssi
Tyypillinen SaaS-kirjautuminen64 MiB (65536 KiB)31Tasapaino suojan ja kirjautumisviiveen välillä useimmille sovelluksille
Korkean turvallisuustason sovellus128 MiB (131072 KiB)42Raskaampi laskenta, vaatii tehokkaamman palvelimen ja monisäikeisen CPU:n
Rajoitettu kontti (esim. 256 MB RAM -kontti)12 MiB (12288 KiB)31Estää "memory allocation failed" -virheet tiukassa muistirajassa

Yleisesti käytetty ohjenuora on, että mitä korkeampi muisti, sitä matalampi aikakerroin riittää, ja päinvastoin: nämä kaksi parametria kompensoivat toisiaan. Jos palvelimellasi on paljon RAM-muistia mutta rajallinen CPU, kannattaa painottaa memoryCost-parametria. Jos tilanne on päinvastainen, painota timeCost-parametria.

Kuudes vinkki liittyy monivuokralaisiin (multi-tenant) järjestelmiin, joissa eri asiakassegmenteillä voi olla erilaiset turvallisuusvaatimukset. Sen sijaan että käyttäisit yhtä kiinteää parametrisarjaa koko sovellukselle, voit tallentaa käytetyt parametrit osaksi hajautusmerkkijonoa (kuten argon2-paketti tekee automaattisesti) ja antaa eri asiakassegmenteille eri oletusarvot rekisteröintihetkellä. Tämä mahdollistaa esimerkiksi sen, että yritysasiakkaiden tilit käyttävät raskaampia parametreja kuin ilmaisen tason käyttäjät, ilman että koko järjestelmän suorituskyky kärsii. Seitsemäs ja viimeinen vinkki: dokumentoi valitut parametrit ja niiden perusteet projektin sisäiseen dokumentaatioon, koska seuraava kehittäjä, joka avaa tiedoston kahden vuoden kuluttua, tarvitsee kontekstin sille, miksi juuri näihin lukuihin on päädytty.

Tausta: miten Argon2id istuu laajempaan kryptografiapinoon

Argon2id on vain yksi osa tuotantosovelluksen salausarkkitehtuuria. Node.js:n kryptografia- ja TLS-arkkitehtuurin dokumentaatio kuvaa, miten crypto-moduuli nojaa OpenSSL-kirjastoon myös muissa toiminnoissa, kuten TLS-yhteyksien salauksessa ja digitaalisissa allekirjoituksissa. Käytännössä tämä tarkoittaa, että Argon2id suojaa salasanat levossa (tietokannassa), mutta liikenteessä olevat salasanat (esimerkiksi kirjautumislomakkeen lähettämä selkokielinen salasana ennen hajautusta) suojaa erillinen kerros: TLS 1.3.

Node.js:n tls-moduulin dokumentaatio listaa vain viisi sallittua TLS 1.3 -salakirjoituspakettia, mikä on huomattavasti suppeampi valikoima kuin vanhemmissa TLS-versioissa oli tarjolla. Yksi näistä on TLS_AES_256_GCM_SHA384, joka myös mainitaan IETF:n CNSA 2.0 -profiililuonnoksessa osana Yhdysvaltain kansallisen turvallisuusviraston suosittelemaa salakirjoituspakettien joukkoa. Jos sovelluksesi käsittelee salasanoja verkon yli, varmista, että palvelimesi pakottaa TLS 1.3:n eikä salli vanhentuneita salakirjoituspaketteja, koska Argon2id:n vahva suoja tietokannassa ei auta, jos salasana vuotaa selkokielisenä siirron aikana.

NIST:n kryptografiastandardien esittelymateriaali havainnollistaa, miten symmetriset algoritmit kuten AES ja hajautusfunktiot kuten SHA-2-perhe on vuosien saatossa otettu käyttöön IETF:n protokollissa, kuten TLS:ssä. Argon2id ei (toistaiseksi) ole samalla tavalla virallinen NIST-standardi, mutta se on laajasti hyväksytty käytännön standardi alan toimijoiden, kirjastojen ja tietoturvaohjeistusten keskuudessa juuri salasanojen hajautukseen erikoistuneena ratkaisuna.

Täydellinen esimerkkiprojekti: kaikki palat yhdessä

Tähän asti olemme rakentaneet toteutuksen palasina. Seuraavaksi kootaan koko projekti yhteen, jotta näet, miten tiedostot keskustelevat toistensa kanssa käynnistyksestä lähtien. Rakenne on tarkoituksella yksinkertainen: yksi tiedosto hajautuslogiikalle, yksi reiteille, yksi tietokantakutsuille ja yksi palvelimen käynnistykselle. Tuotantosovelluksessa näitä kerroksia eriytetään tyypillisesti enemmän, mutta tämä rakenne riittää havainnollistamaan, missä kohdassa Argon2id astuu kuvaan ja mitä sen ympärille tarvitaan.

Aloitetaan ympäristömuuttujista. Hajautusparametrit kannattaa pitää konfiguroitavina, jotta voit nostaa niitä tuotannossa laitteiston parantuessa ilman, että koodia tarvitsee koskea.

# .env
PORT=3000
ARGON2_MEMORY_COST=65536
ARGON2_TIME_COST=3
ARGON2_PARALLELISM=1
DATABASE_URL=postgres://user:password@localhost:5432/argon2demo

Hajautusmoduuli lukee nyt parametrit ympäristömuuttujista ja antaa järkevät oletusarvot, jos muuttujia ei ole asetettu. Tämä on erityisen hyödyllistä, kun samaa koodia ajetaan kehitys-, testaus- ja tuotantoympäristössä eri laitteistolla.

// src/auth/password.js (täydellinen versio)
require('dotenv').config();
const argon2 = require('argon2');

const HASH_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: Number(process.env.ARGON2_MEMORY_COST) || 2 ** 16,
  timeCost: Number(process.env.ARGON2_TIME_COST) || 3,
  parallelism: Number(process.env.ARGON2_PARALLELISM) || 1,
  hashLength: 32
};

async function hashPassword(plainPassword) {
  if (typeof plainPassword !== 'string' || plainPassword.length < 8) {
    throw new Error('Salasanan tulee olla vähintään 8 merkkiä pitkä merkkijono');
  }
  if (plainPassword.length > 1024) {
    throw new Error('Salasana on epäilyttävän pitkä, hylätään');
  }
  return argon2.hash(plainPassword, HASH_OPTIONS);
}

async function verifyPassword(storedHash, plainPassword) {
  try {
    return await argon2.verify(storedHash, plainPassword);
  } catch (error) {
    console.error('Argon2-varmennus epäonnistui:', error.message);
    return false;
  }
}

async function needsRehash(storedHash) {
  return argon2.needsRehash(storedHash, HASH_OPTIONS);
}

module.exports = { hashPassword, verifyPassword, needsRehash, HASH_OPTIONS };

Huomaa yksi lisäys: tarkistamme myös, että salasana ei ylitä 1024 merkkiä. Tämä estää tilanteen, jossa joku lähettää tarkoituksella megatavujen kokoisen syötteen, jonka hajauttaminen kuluttaa kohtuuttomasti CPU-aikaa ja muuttuu käytännössä palvelunestohyökkäykseksi yhden pyynnön kautta.

Tietokantakerros tässä esimerkissä on yksinkertaistettu muistinvaraiseksi säilöksi, jotta koodia voi kokeilla suoraan ilman tietokantapalvelinta. Tuotannossa korvaat tämän oikealla tietokantakutsulla, esimerkiksi pg- tai knex-kirjastolla.

// src/db/users.js
const users = new Map();
let nextId = 1;

async function createUser(email, passwordHash) {
  const user = { id: nextId++, email, passwordHash };
  users.set(email, user);
  return user;
}

async function findUserByEmail(email) {
  return users.get(email) || null;
}

async function updateUserPasswordHash(userId, newHash) {
  for (const user of users.values()) {
    if (user.id === userId) {
      user.passwordHash = newHash;
      return user;
    }
  }
  return null;
}

module.exports = { createUser, findUserByEmail, updateUserPasswordHash };

Lopuksi palvelimen käynnistystiedosto kytkee kaiken yhteen. Tässä vaiheessa kannattaa myös lisätä perusvalvonta: lokita hajautuslaskennan kesto tuotannossa, jotta huomaat ajoissa, jos joku parametri alkaa hidastaa palvelinta odottamattomasti.

// src/server.js
require('dotenv').config();
const express = require('express');
const authRoutes = require('./auth/routes');

const app = express();
app.use(express.json());
app.use('/auth', authRoutes);

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Palvelin käynnissä portissa ${port}`);
  console.log('Argon2id-parametrit:', require('./auth/password').HASH_OPTIONS);
});

Käynnistä palvelin komennolla node src/server.js ja testaa rekisteröintiä esimerkiksi curl-komennolla. Onnistunut rekisteröinti palauttaa käyttäjän tunnisteen ja sähköpostin, ei koskaan hajautettua salasanaa.

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"TurvallinenSalasana123"}'

# Vastaus:
# {"id":1,"email":"[email protected]"}

Koko projekti koostuu näin viidestä tiedostosta: .env, src/auth/password.js, src/auth/routes.js, src/db/users.js ja src/server.js. Jokainen vastaa yhdestä selkeästä vastuualueesta, mikä tekee koodin testaamisesta ja myöhemmästä tietokantavaihdosta (esimerkiksi muistinvaraisesta säilöstä PostgreSQL:ään) huomattavasti suoraviivaisempaa kuin jos hajautuslogiikka olisi sekoitettu suoraan reittikäsittelijöihin.

Tietoturvanäkökulmat, joita Argon2id ei itsessään ratkaise

Vahvakaan hajautusalgoritmi ei tee sovelluksesta automaattisesti turvallista. Kolme asiaa kannattaa tarkistaa erikseen, kun Argon2id on jo pystyssä. Ensimmäinen on ajoitushyökkäysten (timing attack) riski vertailulogiikassa: argon2.verify() on suunniteltu vertaamaan hajautuksia tavalla, joka ei vuoda tietoa suoritusajan kautta, mutta jos lisäät omaa vertailulogiikkaa esimerkiksi sähköpostiosoitteiden tarkistukseen, käytä aina vakioaikaisia vertailufunktioita, kuten Node.js:n crypto.timingSafeEqual(), älä tavallista ===-operaattoria arkaluonteisille arvoille.

Toinen asia on lokitus. Älä koskaan kirjoita selkokielistä salasanaa lokiin, vaikka se tapahtuisi vain virheenkäsittelyn yhteydessä kehitysympäristössä. Tämä on yleinen vuoto, joka syntyy helposti, kun joku lisää console.log(req.body)-kutsun debuggausta varten ja unohtaa poistaa sen ennen tuotantoon viemistä. Kolmas asia on salasanan palautusprosessi: jos sovelluksessasi on "unohtuiko salasana" -toiminto, varmista, että palautuslinkin token lasketaan kryptografisesti turvallisella satunnaisgeneraattorilla ja että sillä on lyhyt voimassaoloaika, tyypillisesti 15–60 minuuttia, koska tämä polku ohittaa Argon2id:n kokonaan ja on usein heikoin lenkki koko kirjautumisjärjestelmässä.

Suomalaisille ja pohjoismaisille yrityksille salasanojen hajautus liittyy suoraan myös tietosuojavelvoitteisiin. EU:n yleinen tietosuoja-asetus edellyttää, että henkilötietoja käsittelevä organisaatio toteuttaa asianmukaiset tekniset ja organisatoriset toimenpiteet tietojen suojaamiseksi, ja salasanojen tallentaminen vahvalla, muisti-kovalla hajautusalgoritmilla kuuluu käytännössä aina siihen vähimmäistasoon, jota valvovat viranomaiset, kuten Suomen tietosuojavaltuutetun toimisto, odottavat tietomurtotilanteessa. Kun tietomurto tapahtuu ja tutkintaraportissa käy ilmi, että salasanat oli tallennettu heikolla tai kokonaan hajauttamattomalla menetelmällä, se usein raskauttaa organisaation vastuuta jälkikäteisessä arvioinnissa huomattavasti enemmän kuin tilanne, jossa salasanat oli suojattu ajantasaisella algoritmilla kuten Argon2id.

Yksikkötestaus Argon2id-logiikalle

Salasanalogiikka on yksi niistä sovelluksen osista, joihin kannattaa kirjoittaa testit jo ensimmäisestä päivästä lähtien, koska virhe tässä kerroksessa voi tarkoittaa, että käyttäjät lukittuvat ulos omilta tileiltään tai pahimmassa tapauksessa, että heikko hajautus päätyy tuotantoon huomaamatta. Node.js:n sisäänrakennettu node:test-moduuli riittää tähän hyvin, eikä vaadi erillistä testikehystä.

// test/password.test.js
const test = require('node:test');
const assert = require('node:assert');
const { hashPassword, verifyPassword, needsRehash } = require('../src/auth/password');

test('hashPassword tuottaa Argon2id-muotoisen hajautuksen', async () => {
  const hash = await hashPassword('TurvallinenSalasana123');
  assert.ok(hash.startsWith('$argon2id$'));
});

test('verifyPassword palauttaa true oikealle salasanalle', async () => {
  const hash = await hashPassword('TurvallinenSalasana123');
  const result = await verifyPassword(hash, 'TurvallinenSalasana123');
  assert.strictEqual(result, true);
});

test('verifyPassword palauttaa false väärälle salasanalle', async () => {
  const hash = await hashPassword('TurvallinenSalasana123');
  const result = await verifyPassword(hash, 'VääräSalasana');
  assert.strictEqual(result, false);
});

test('hashPassword hylkää liian lyhyen salasanan', async () => {
  await assert.rejects(() => hashPassword('lyhyt'));
});

test('kaksi hajautusta samasta salasanasta eivät ole identtisiä', async () => {
  const hashA = await hashPassword('SamaSalasana456');
  const hashB = await hashPassword('SamaSalasana456');
  assert.notStrictEqual(hashA, hashB);
});

Viimeinen testi on erityisen tärkeä, koska se vahvistaa suolan toimivan oikein: kaksi identtistä salasanaa eivät saa koskaan tuottaa samaa tallennettavaa hajautusta. Jos tämä testi joskus epäonnistuu, jokin suolan generoinnissa on rikki, ja se on merkki vakavasta turvallisuusongelmasta, joka kannattaa korjata välittömästi ennen tuotantoon viemistä.

Aja testit komennolla node --test projektin juuressa. Lisää tämä komento myös CI-putkeen (esimerkiksi GitHub Actionsiin), jotta mahdollinen regressio hajautuslogiikassa huomataan ennen kuin muutos päätyy pääbranchiin asti.

# .github/workflows/test.yml
name: Testit
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: node --test

Usein kysytyt kysymykset

Pitääkö minun vaihtaa Argon2id:hen, jos bcrypt toimii edelleen hyvin?
Ei ole pakko, jos sovelluksesi ei käsittele erityisen arkaluonteista dataa, mutta uusissa projekteissa Argon2id on nykyinen oletussuositus, koska sen muisti-kova rakenne suojaa paremmin erikoisrautaa käyttäviä hyökkäyksiä vastaan. Olemassa olevaa, hyvin toimivaa bcrypt-toteutusta ei tarvitse kiireellä vaihtaa, mutta asteittainen migraatio kannattaa suunnitella.

Voiko Argon2id:tä käyttää myös tiedostojen salaukseen?
Ei suoraan. Argon2 on avaimenjohtofunktio, joka muuttaa salasanan yksisuuntaisesti hajautusarvoksi tai voidaan käyttää salausavaimen johtamiseen salasanasta, mutta se ei itsessään salaa tai pura dataa. Jos tarvitset tiedostojen salausta, Argon2id:tä voidaan käyttää salausavaimen johtamiseen, mutta varsinainen salaus tehdään esimerkiksi AES-256-GCM:llä.

Mitä tapahtuu, jos Node.js:n natiivi Web Crypto -Argon2-tuki ja npm-paketti antavat eri tuloksia samoilla parametreilla?
Tämä voi johtua pienistä eroista toteutusten oletusarvoissa tai parametrien koodaustavassa. Suositus on käyttää tätä kirjoitettaessa vakiintunutta argon2-npm-pakettia tuotannossa, ja seurata natiivin Web Crypto -tuen kehitystä erikseen, ennen kuin siirryt siihen kokonaan.

Kuinka pitkä suolan tulisi olla?
argon2-npm-paketti generoi oletuksena kryptografisesti turvallisen 16 tavun (128-bittisen) suolan automaattisesti jokaiselle hajautukselle, eikä sinun tarvitse hallita sitä itse. Tämä pituus on yleisesti pidetty riittävänä käytännössä kaikkiin salasanojen hajautustarpeisiin.

Voiko Argon2id-parametreja muuttaa jälkikäteen vaikuttamatta olemassa oleviin käyttäjiin?
Kyllä. Koska parametrit tallennetaan osana itse hajautusmerkkijonoa, voit nostaa uusien rekisteröintien parametreja milloin tahansa. Vanhat käyttäjät pysyvät vanhoilla parametreilla, kunnes needsRehash()-tarkistus (vaihe 9) laskee heidän hajautuksensa uudelleen seuraavan onnistuneen kirjautumisen yhteydessä.

Miksi kirjautuminen tuntuu hitaalta kehitysympäristössä mutta ei tuotannossa?
Kehityskoneet, kuten kannettavat tietokoneet, ovat usein hitaampia rinnakkaislaskennassa kuin tuotantopalvelimet, joissa on enemmän CPU-ydintä käytettävissä. Tämä on normaalia ja yksi syy siihen, miksi parametrit kannattaa mitata ja viritellä nimenomaan tuotantoympäristössä, ei kehityskoneella.

Tarvitaanko Argon2id:n rinnalle edelleen kaksivaiheista tunnistautumista?
Kyllä. Argon2id suojaa salasanan tallennusmuotoa, mutta se ei suojaa tilannetta, jossa käyttäjän salasana on vuotanut toiselta palvelulta ja hyökkääjä kokeilee sitä suoraan oikealla salasanalla. Kaksivaiheinen tunnistautuminen lisää suojakerroksen, joka toimii riippumatta siitä, kuinka vahva hajautusalgoritmi on käytössä.

Kannattaako odottaa, että Node.js:n natiivi Web Crypto -Argon2-tuki vakiintuu, ennen kuin otan algoritmin käyttöön?
Ei. Itse Argon2id-algoritmi on ollut vakaa ja laajasti käytössä vuodesta 2015 lähtien riippumatta siitä, mistä rajapinnasta sitä kutsutaan. argon2-npm-paketti antaa saman algoritmin käyttöösi juuri nyt, tuotantokelpoisella rajapinnalla. Node.js:n natiivi Web Crypto -tuki on hyödyllinen lisäys pitkällä aikavälillä, koska se voi pienentää riippuvuuksien määrää, mutta se ei ole syy lykätä Argon2id:n käyttöönottoa.

Mitä teen, jos tietokantani sarake on jo täynnä bcrypt-hajautuksia ja vaihdan tietokantamoottoria samaan aikaan?
Käsittele kaksi muutosta erillisinä projekteina, vaikka ne tapahtuisivatkin lähekkäin kalenterissa. Tee ensin tietokantamoottorin vaihto niin, että hajautuskenttä kopioituu sellaisenaan uuteen järjestelmään, ja aja Argon2id-migraatio vasta sen jälkeen, kun uusi tietokanta on vakaa. Kahden ison muutoksen yhdistäminen samaan julkaisuun vaikeuttaa huomattavasti sen selvittämistä, kumpi muutos aiheutti mahdollisen ongelman tuotannossa.