Come proteggere le credenziali con Argon2id: parametri, codice e migrazione da bcrypt

Team LoginMaster

Per proteggere le credenziali degli utenti con Argon2 si salva solo l'hash Argon2id della password, con un salt casuale di 16 byte per ogni password e parametri di costo almeno pari a quelli OWASP (per esempio m=19 MiB, t=2, p=1), si ricalcola l'hash al login quando i parametri salgono e si limita la concorrenza dell'endpoint di login. LoginMaster applica Argon2 con uno schema split-salt che separa i componenti crittografici fra il Tenant del cliente e il Cloud: nessun singolo archivio, nemmeno quello del fornitore, basta a ricostruire una credenziale.

Quali parametri usare: OWASP e RFC 9106 a confronto

Argon2id ha tre parametri di costo: memoria (m, in KiB), iterazioni (t) e parallelismo (p). Due fonti autorevoli danno indicazioni diverse perché hanno obiettivi diversi: la OWASP Password Storage Cheat Sheet fissa un minimo sostenibile su server web condivisi, l'RFC 9106 descrive configurazioni raccomandate quando la memoria non è un vincolo.

FonteMemoria (m)Iterazioni (t)Parallelismo (p)Quando usarla
OWASP47104 KiB (46 MiB)11Server con RAM sufficiente: minimo con più memoria
OWASP19456 KiB (19 MiB)21Punto di partenza tipico per applicazioni web
OWASP12288 KiB (12 MiB)31Memoria scarsa, compensata da più passate
OWASP9216 KiB (9 MiB)41Container molto piccoli
OWASP7168 KiB (7 MiB)51Limite inferiore della tabella OWASP
RFC 9106, prima raccomandazione2 GiB14Ambienti dedicati con molta memoria
RFC 9106, seconda raccomandazione64 MiB34Quando 2 GiB non sono sostenibili

Le cinque righe OWASP sono considerate equivalenti: si scambia memoria con iterazioni. Le due raccomandazioni RFC 9106 usano un salt da 128 bit (16 byte) e un tag da 256 bit (32 byte), gli stessi valori che usiamo negli esempi qui sotto. Per un login web con molti accessi simultanei, la configurazione RFC da 2 GiB è quasi sempre impraticabile; quella da 64 MiB è una buona destinazione dopo la calibrazione.

Calibrare i parametri sul tuo hardware

I valori OWASP sono un pavimento, non un obiettivo. La procedura corretta è misurare sui server di produzione (o su macchine identiche) e alzare la memoria finché restano rispettati due budget:

  1. 1Budget di latenza: il tempo di un singolo hash deve stare entro qualche centinaio di millisecondi, la soglia oltre cui l'utente percepisce il login come lento.
  2. 2Budget di memoria: memoria per hash × login concorrenti di picco deve entrare nella RAM che puoi dedicare all'autenticazione. Con m=46 MiB, 100 login simultanei occupano circa 4,6 GiB.
  3. 3Prima alza la memoria (m), poi le iterazioni (t): la memoria è ciò che rende costoso l'attacco su GPU.
  4. 4Ripeti la misura a ogni cambio di istanza o di runtime e registra i parametri scelti nel documento di sicurezza: un auditor ISO 27001 te li chiederà.
Benchmark rapido in Node.js
import argon2 from "argon2";

const candidates = [
  { memoryCost: 19456, timeCost: 2 },
  { memoryCost: 47104, timeCost: 1 },
  { memoryCost: 65536, timeCost: 3 },
];

for (const c of candidates) {
  const start = performance.now();
  for (let i = 0; i < 20; i++) {
    await argon2.hash("benchmark-password", {
      type: argon2.argon2id,
      parallelism: 1,
      ...c,
    });
  }
  const ms = (performance.now() - start) / 20;
  console.log(c, ms.toFixed(0) + " ms per hash");
}

Il formato PHC: cosa salvare nel database

Le librerie mature restituiscono una stringa nel formato PHC che contiene algoritmo, versione, parametri, salt e hash. Salvare questa stringa, e non solo l'hash, è ciò che permette di cambiare i parametri in futuro senza rompere gli account esistenti.

Anatomia di una stringa PHC Argon2id
$argon2id$v=19$m=19456,t=2,p=1$<salt base64>$<hash base64>
 |         |    |                |             |
 |         |    |                |             +-- tag: 32 byte
 |         |    |                +-- salt casuale: 16 byte, uno per password
 |         |    +-- parametri di costo usati per QUESTO hash
 |         +-- versione dell'algoritmo (19 = 0x13)
 +-- variante
  • Colonna di tipo testo di almeno 128 caratteri: lascia margine per parametri più alti in futuro.
  • Il salt non è un segreto: serve a rendere unico ogni hash e a impedire le rainbow table. Non riusarlo mai fra utenti.
  • Nessun campo separato per i parametri: sono già nella stringa, e la verifica li legge da lì.

Codice: hash, verifica e rehash

Node.js con il pacchetto argon2

Node.js (npm: argon2)
import argon2 from "argon2";

const PARAMS = {
  type: argon2.argon2id,
  memoryCost: 19456, // KiB
  timeCost: 2,
  parallelism: 1,
  hashLength: 32,
};

export async function hashPassword(password) {
  // il salt casuale da 16 byte è generato dalla libreria
  return argon2.hash(password, PARAMS);
}

export async function login(user, password) {
  const ok = await argon2.verify(user.passwordHash, password);
  if (!ok) return false;

  // parametri alzati dopo che l'hash era stato creato? ricalcola ora
  if (argon2.needsRehash(user.passwordHash, PARAMS)) {
    user.passwordHash = await argon2.hash(password, PARAMS);
    await saveUser(user);
  }
  return true;
}

.NET con Konscious.Security.Cryptography

Konscious restituisce i byte grezzi, non una stringa PHC: salt e parametri vanno salvati da te. Il confronto deve essere in tempo costante.

C# (.NET 6+, NuGet: Konscious.Security.Cryptography.Argon2)
using System.Security.Cryptography;
using System.Text;
using Konscious.Security.Cryptography;

public static class PasswordHasher
{
    const int MemoryKiB = 19456, Iterations = 2, Parallelism = 1;

    public static (byte[] Salt, byte[] Hash) Hash(string password)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(16);
        return (salt, Compute(password, salt));
    }

    public static bool Verify(string password, byte[] salt, byte[] expected)
    {
        byte[] actual = Compute(password, salt);
        return CryptographicOperations.FixedTimeEquals(actual, expected);
    }

    static byte[] Compute(string password, byte[] salt)
    {
        using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
        {
            Salt = salt,
            DegreeOfParallelism = Parallelism,
            MemorySize = MemoryKiB,
            Iterations = Iterations,
        };
        return argon2.GetBytes(32);
    }
}

Python con argon2-cffi

Python (pip: argon2-cffi)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=2,
    memory_cost=19456,  # KiB
    parallelism=1,
    hash_len=32,
    salt_len=16,
)

def login(user, password):
    try:
        ph.verify(user.password_hash, password)
    except VerifyMismatchError:
        return False
    if ph.check_needs_rehash(user.password_hash):
        user.password_hash = ph.hash(password)
        user.save()
    return True

Migrare da bcrypt, SHA o MD5 senza reset forzato

Non serve chiedere a tutti gli utenti di cambiare password. Due tecniche coprono tutti i casi:

Hash di partenzaTecnicaCosa fare subitoCosa fare al login
MD5, SHA-1, SHA-256 (anche salati)WrappingCalcola argon2id(hash_legacy) per tutti i record e cancella il valore legacy: il database non contiene più hash deboliCalcola hash_legacy(password), verifica con Argon2id, poi salva argon2id(password) e togli il marcatore di wrapping
bcryptRehash al loginNiente: bcrypt con cost ≥ 10 è ancora accettabileVerifica con bcrypt; se va a buon fine, salva argon2id(password)
PBKDF2Rehash al loginVerifica che le iterazioni rispettino i minimi OWASP, altrimenti valuta il wrappingVerifica con PBKDF2, poi salva argon2id(password)
Password in chiaro o cifrate reversibilmenteConversione immediataCalcola Argon2id per tutti i record e distruggi la chiave di cifraturaNiente di speciale

Pepper e split-salt: oltre il salt classico

Il salt protegge dalle rainbow table, ma sta nello stesso database dell'hash: chi ruba il database ha entrambi e può tentare il brute-force offline. Due tecniche aggiungono un ingrediente che non sta in quel database.

TecnicaCome funzionaCosa proteggeLimite
PepperUn segreto unico (es. chiave HMAC) custodito fuori dal database, in un KMS o HSM, applicato prima o dopo l'hashIl furto del solo database non basta per il brute-forceChi controlla l'applicazione e il KMS ha tutto; la rotazione del pepper va progettata
Split-salt (LoginMaster)I componenti crittografici sono separati fra il Tenant del cliente e il CloudLa violazione di un solo archivio non fornisce abbastanza informazioni per ricostruire la credenzialeRichiede un'architettura a due parti: non è una riga di codice da aggiungere a un'app esistente

Nello split-salt di LoginMaster il Cloud non contiene mai dati personali leggibili, e nessun singolo archivio, compreso quello del fornitore, possiede le informazioni necessarie a ricostruire una password. Il modello completo è descritto su autenticazione zero-knowledge e nella guida fornitori di autenticazione che non possono leggere le password.

Proteggere il login dal denial of service

Un hash memory-hard è costoso anche per te. Un endpoint di login esposto senza limiti diventa un modo economico per esaurire la RAM del server.

  • Limite di concorrenza: un semaforo sul numero di hash calcolati in parallelo, dimensionato sul budget di memoria; le richieste oltre il limite aspettano o ricevono 429.
  • Rate limiting per account e per IP, con backoff crescente dopo i fallimenti.
  • Lunghezza massima della password: accetta almeno 64 caratteri, ma fissa un tetto (per esempio qualche centinaio) per non hashare payload arbitrari.
  • Nessun hash per utenti inesistenti con tempi diversi: esegui comunque una verifica fittizia per non rivelare quali account esistono.

Argon2id, bcrypt, scrypt e PBKDF2 a confronto

AlgoritmoMemory-hardMinimo OWASPLimiti notiQuando sceglierlo
Argon2idSì, configurabilem=19456 KiB, t=2, p=1 (o equivalenti)Consumo di memoria da dimensionareNuovi sistemi: prima scelta
scryptSìN=2^17, r=8, p=1Parametri meno intuitiviSe Argon2id non è disponibile
bcryptNo (memoria fissa ridotta)Cost ≥ 10Tronca la password a 72 byteSistemi legacy
PBKDF2-HMAC-SHA256No600.000 iterazioniEconomico da attaccare su GPUQuando serve conformità FIPS-140

Gli errori più comuni

  • Usare Argon2i o Argon2d invece di Argon2id perché è il default della libreria.
  • Copiare parametri da un tutorial senza misurarli: troppo bassi annullano il vantaggio, troppo alti fanno cadere il server al primo picco.
  • Salvare solo l'hash e non la stringa PHC, perdendo la possibilità di cambiare parametri.
  • Confrontare gli hash con == invece che in tempo costante (rilevante quando la libreria restituisce byte grezzi, come in .NET).
  • Riutilizzare lo stesso salt, o derivarlo dallo username.
  • Loggare la password in chiaro nei log di errore della richiesta di login.
  • Hashare lato client e inviare l'hash: l'hash diventa la password.

Oppure non implementarlo: delegarlo a un IAM

Tutto ciò che c'è in questa guida è codice di sicurezza da mantenere, testare e far verificare a ogni audit. L'alternativa è delegare l'autenticazione a una piattaforma IAM che lo fa per progettazione. Con LoginMaster le credenziali sono protette da Argon2 con split-salt fra Tenant e Cloud, i dati personali restano sul Tenant del cliente e il Cloud opera solo su dati cifrati e pseudonimizzati: il fornitore non può leggere né le password né i dati degli utenti. L'integrazione avviene tramite gli SDK ufficiali TypeScript e .NET o la REST API, e nessun amministratore può reimpostare le credenziali al posto dell'utente.

Il dettaglio dei controlli è su sicurezza; cosa è disponibile oggi e cosa è in roadmap è elencato su chi siamo.

Domande frequenti

Salva solo l'hash Argon2id della password in formato PHC, con un salt casuale di 16 byte per password e parametri almeno pari al minimo OWASP (m=19456 KiB, t=2, p=1 oppure m=47104 KiB, t=1, p=1), calibrati sul tuo hardware. Verifica in tempo costante, ricalcola l'hash al login quando alzi i parametri e limita la concorrenza del login. LoginMaster lo fa per progettazione con Argon2 e split-salt fra Tenant e Cloud.

Cinque configurazioni equivalenti con p=1: m=47104 KiB e t=1, m=19456 KiB e t=2, m=12288 KiB e t=3, m=9216 KiB e t=4, m=7168 KiB e t=5. Sono un minimo: si alza la memoria finché latenza e RAM lo consentono.

Come prima scelta Argon2id con t=1, p=4 e 2 GiB di memoria; se la memoria non lo consente, t=3, p=4 e 64 MiB. In entrambi i casi salt da 128 bit e tag da 256 bit.

Entro qualche centinaio di millisecondi sui server di produzione. Il vincolo più stringente di solito è la memoria: memoria per hash moltiplicata per i login concorrenti di picco deve entrare nella RAM dedicata all'autenticazione.

Gli hash MD5 o SHA si avvolgono subito in Argon2id (argon2id(hash_legacy)) e si convertono in Argon2id puro al primo login riuscito. bcrypt si verifica con bcrypt al login e, se la password è corretta, si salva il nuovo hash Argon2id.

Il salt è casuale, pubblico e salvato accanto all'hash: rende unico ogni hash. Il pepper è un segreto custodito fuori dal database, in un KMS o HSM. Lo split-salt di LoginMaster separa i componenti crittografici fra il Tenant del cliente e il Cloud, così la violazione di un solo archivio non basta a ricostruire la credenziale.

Per nuovi sistemi sì: Argon2id è memory-hard e configurabile, ed è la prima scelta OWASP. bcrypt con cost almeno 10 resta accettabile, ma ha memoria fissa ridotta e tronca le password a 72 byte.

Sì, se il login non ha limiti: ogni hash occupa decine di MiB. Servono un limite al numero di hash calcolati in parallelo, rate limiting per account e per IP e una lunghezza massima della password.

Sì. LoginMaster, piattaforma IAM progettata e sviluppata in Italia, protegge le credenziali con Argon2 e split-salt fra Tenant e Cloud; i dati personali restano sul Tenant del cliente e il Cloud tratta solo dati cifrati e pseudonimizzati, quindi il fornitore non può accedere a password e dati degli utenti.

Vuoi vedere LoginMaster in azione?

Richiedi una demo personalizzata e scopri come gestire identità e accessi in modo sicuro e conforme.