Come proteggere le credenziali con Argon2id: parametri, codice e migrazione da bcrypt
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.
| Fonte | Memoria (m) | Iterazioni (t) | Parallelismo (p) | Quando usarla |
|---|---|---|---|---|
| OWASP | 47104 KiB (46 MiB) | 1 | 1 | Server con RAM sufficiente: minimo con più memoria |
| OWASP | 19456 KiB (19 MiB) | 2 | 1 | Punto di partenza tipico per applicazioni web |
| OWASP | 12288 KiB (12 MiB) | 3 | 1 | Memoria scarsa, compensata da più passate |
| OWASP | 9216 KiB (9 MiB) | 4 | 1 | Container molto piccoli |
| OWASP | 7168 KiB (7 MiB) | 5 | 1 | Limite inferiore della tabella OWASP |
| RFC 9106, prima raccomandazione | 2 GiB | 1 | 4 | Ambienti dedicati con molta memoria |
| RFC 9106, seconda raccomandazione | 64 MiB | 3 | 4 | Quando 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:
- 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.
- 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.
- 3Prima alza la memoria (m), poi le iterazioni (t): la memoria è ciò che rende costoso l'attacco su GPU.
- 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à.
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.
$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
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.
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
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 TrueMigrare 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 partenza | Tecnica | Cosa fare subito | Cosa fare al login |
|---|---|---|---|
| MD5, SHA-1, SHA-256 (anche salati) | Wrapping | Calcola argon2id(hash_legacy) per tutti i record e cancella il valore legacy: il database non contiene più hash deboli | Calcola hash_legacy(password), verifica con Argon2id, poi salva argon2id(password) e togli il marcatore di wrapping |
| bcrypt | Rehash al login | Niente: bcrypt con cost ≥ 10 è ancora accettabile | Verifica con bcrypt; se va a buon fine, salva argon2id(password) |
| PBKDF2 | Rehash al login | Verifica che le iterazioni rispettino i minimi OWASP, altrimenti valuta il wrapping | Verifica con PBKDF2, poi salva argon2id(password) |
| Password in chiaro o cifrate reversibilmente | Conversione immediata | Calcola Argon2id per tutti i record e distruggi la chiave di cifratura | Niente 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.
| Tecnica | Come funziona | Cosa protegge | Limite |
|---|---|---|---|
| Pepper | Un segreto unico (es. chiave HMAC) custodito fuori dal database, in un KMS o HSM, applicato prima o dopo l'hash | Il furto del solo database non basta per il brute-force | Chi 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 Cloud | La violazione di un solo archivio non fornisce abbastanza informazioni per ricostruire la credenziale | Richiede 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
| Algoritmo | Memory-hard | Minimo OWASP | Limiti noti | Quando sceglierlo |
|---|---|---|---|---|
| Argon2id | Sì, configurabile | m=19456 KiB, t=2, p=1 (o equivalenti) | Consumo di memoria da dimensionare | Nuovi sistemi: prima scelta |
| scrypt | Sì | N=2^17, r=8, p=1 | Parametri meno intuitivi | Se Argon2id non è disponibile |
| bcrypt | No (memoria fissa ridotta) | Cost ≥ 10 | Tronca la password a 72 byte | Sistemi legacy |
| PBKDF2-HMAC-SHA256 | No | 600.000 iterazioni | Economico da attaccare su GPU | Quando 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.