Vai al contenuto
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Password e hashing con Argon2id

Perché gli hash veloci non vanno bene per le password, a cosa servono salt e work factor e come calcolare e verificare un hash con Argon2id in Python usando argon2-cffi.

Intermedio10 min di letturaDi NotHoly

Di cosa parla questo articolo

I database di password trapelano più spesso di quanto i team mettano in conto. Quando succede, l'attaccante ottiene degli hash, non delle password. Quanto lavoro serva per passare dagli uni alle altre dipende da due scelte: la funzione che hai usato e le impostazioni che le hai dato. Questo articolo spiega perché per conservare le password serve un tipo diverso di hash, a cosa servono un salt e un work factor e perché le indicazioni attuali puntano su Argon2id. Calcolerai e verificherai l'hash di una password in Python, leggerai ogni parte dell'hash codificato e rifarai l'hash delle password salvate quando cambiano le impostazioni.

Ti servono un terminale e Python 3. Il laboratorio installa un solo pacchetto, argon2-cffi.

Perché un hash veloce è lo strumento sbagliato

Una funzione di hash trasforma un input in un output di dimensione fissa. SHA-256 lo fa molto in fretta, ed è quello che vuoi per checksum e integrità dei file. Per le password, la velocità è il problema. Una tabella di password rubata viene attaccata tirando a indovinare: prendi un candidato, calcoli l'hash, confronti, ripeti. Se ogni tentativo costa poco, l'attaccante può farne miliardi sulle schede grafiche.

L'OWASP Password Storage Cheat Sheet è diretto su questo punto. SHA-256 è veloce, ed è esattamente quello che serve a un attaccante quando l'input è una password, cioè una stringa breve scelta da una persona.

Puoi misurare la differenza sulla tua macchina. Il benchmark del laboratorio calcola l'hash di una password con SHA-256 con salt e con Argon2id, poi stampa la velocità di ciascuno.

A cosa serve un salt

Un salt è un valore casuale, diverso per ogni password, salvato accanto all'hash. Ha due compiti.

  • Due utenti con la stessa password finiscono con valori salvati diversi. Una tabella trapelata non mostra quali account condividono una password.
  • Rende inutili le tabelle precalcolate. Senza salt, un attaccante calcola una volta sola gli hash di un elenco di password comuni e cerca ogni hash trapelato. Con un salt unico per utente, ogni password va attaccata per conto suo.

Un salt non è un segreto e non rende forte una password debole. Il suo compito è impedire all'attaccante di riusare il lavoro su più utenti. RFC 9106 raccomanda un salt di 128 bit e ammette 64 bit quando lo spazio è poco. NIST SP 800-63B richiede almeno 32 bit. Il valore predefinito di argon2-cffi è 16 byte, cioè 128 bit.

A cosa serve un work factor

Un work factor è un'impostazione che rende costoso di proposito ogni tentativo. Gli schemi più vecchi come PBKDF2 contano soprattutto le iterazioni. Argon2 ha tre parametri.

  • Costo di memoria (m): la RAM, in KiB, usata da ogni hash.
  • Costo di tempo (t): quante passate fa la funzione su quella memoria.
  • Parallelismo (p): quanti lane possono girare contemporaneamente.

La memoria è il parametro che pesa di più. Una GPU ha migliaia di piccoli core, e ogni core ha poca memoria propria. Se ogni tentativo richiede decine di megabyte, l'attaccante non può fare in parallelo tanti tentativi quanti ne farebbe contro una funzione che chiede pochi kilobyte. Questa proprietà si chiama memory-hardness.

Il work factor è una manopola, non una costante. Alzalo man mano che l'hardware diventa più veloce e il budget del login lo permette. Salva le impostazioni con ogni hash, così gli hash più vecchi si possono riconoscere e aggiornare in seguito. Gli ultimi passi del laboratorio fanno proprio questo.

Perché Argon2id

Argon2 ha vinto la Password Hashing Competition nel 2015. Ha tre varianti.

  • Argon2d sceglie gli indirizzi di memoria in base ai dati. Resiste bene agli attacchi con GPU, ma quel modello di accesso dipendente dai dati può far trapelare informazioni di timing.
  • Argon2i usa un modello di accesso che non dipende dall'input. Protegge dalle fughe di informazioni di timing, ma perde parte della resistenza agli attacchi che scambiano memoria con tempo.
  • Argon2id esegue il modello indipendente dai dati nella prima metà della prima passata, poi passa a quello dipendente dai dati.

Argon2id copre entrambi i problemi. RFC 9106 la raccomanda come variante da usare in generale, e OWASP la mette al primo posto tra le sue opzioni per l'hashing delle password.

RFC 9106 dà due configurazioni raccomandate. La prima, per i sistemi con memoria in abbondanza, è 2 GiB, 1 passata e 4 lane. La seconda, per gli ambienti con poca memoria, è 64 MiB, 3 passate e 4 lane. Il minimo di OWASP è 19 MiB, 2 iterazioni e 1 lane. OWASP elenca anche alternative che scambiano memoria con passate, come 12 MiB con 3 passate o 7 MiB con 5 passate, tutte a 1 lane. I valori predefiniti di argon2-cffi corrispondono alla seconda configurazione della RFC.

Considerali soglie minime e punti di partenza, non obiettivi. Una regola empirica diffusa è regolare le impostazioni finché un hash non richiede qualche centinaio di millisecondi sul tuo hardware di produzione. Poi controlla che la memoria per hash, moltiplicata per il numero di login che possono arrivare nello stesso momento, entri ancora nei tuoi server. Quella regola viene dalla pratica, non da nessuno dei due standard.

Il NIST ragiona diversamente. SP 800-63B richiede che le password abbiano un salt e passino per uno schema di hash adatto, e dice che andrebbe usato uno schema approvato di SP 800-132, che riguarda PBKDF2. Non nomina Argon2id. Se un programma di conformità richiede algoritmi approvati dal NIST, verifica con il tuo assessor prima di scegliere Argon2id.

Come funziona l'hash salvato, passo per passo

Quando una libreria di hashing salva una password, succedono cinque cose.

  1. La libreria genera un salt casuale.
  2. Esegue Argon2id su password, salt e parametri di costo.
  3. Codifica in una sola stringa variante, versione, parametri, salt e output.
  4. Al login legge la stringa, ne estrae salt e parametri, ricalcola l'output e confronta i due.
  5. Se i parametri nella stringa sono più vecchi delle tue impostazioni attuali, dopo un login riuscito rifai l'hash.

Il passo 3 è il motivo per cui non ti serve una colonna separata per il salt. La stringa codificata ha questo formato.

text
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
  • argon2id è la variante.
  • v=19 è la versione di Argon2, 1.3, scritta in decimale. In esadecimale è 0x13.
  • m, t e p sono memoria in KiB, costo di tempo e parallelismo.
  • <salt> e <hash> sono in base64 senza padding. L'hash è l'output derivato, 32 byte per impostazione predefinita.

La stringa del laboratorio è lunga 97 caratteri. Usa una colonna da almeno 255 caratteri o un tipo text. Non dividerla in colonne, a meno che non ti piaccia scrivere il tuo parser.

Laboratorio pratico

Il laboratorio usa un ambiente virtuale locale e non richiede servizi di rete. L'output seguente viene da una macchina Windows 11 con Python 3.12.10, argon2-cffi 25.1.0 e argon2-cffi-bindings 26.1.0. Su quella macchina python3 non era nel PATH di Git Bash, quindi i comandi sono stati eseguiti come python. Usa python3 se da te funziona. Le tue stringhe di hash saranno diverse da quelle mostrate, perché il salt è casuale.

1. Crea l'ambiente

bash
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffi

Su Windows, attivalo con .venv\Scripts\activate in PowerShell o cmd, oppure con . .venv/Scripts/activate in Git Bash.

2. Calcola l'hash, verificalo e controlla i parametri

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h); print(ph.verify(h,'correct horse battery')); print(ph.check_needs_rehash(h))"

Output osservato:

text
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
False

verify restituisce True per la password giusta. check_needs_rehash restituisce False perché questo hash è stato creato con i valori predefiniti attuali. Quei valori sono un costo di tempo di 3, un costo di memoria di 65536 KiB (64 MiB), parallelismo 4, un hash di 32 byte, un salt di 16 byte e la variante ID.

3. Rileva una password sbagliata

bash
python3 -c "from argon2 import PasswordHasher; from argon2.exceptions import VerifyMismatchError; ph=PasswordHasher(); h=ph.hash('a');
try: ph.verify(h,'b')
except VerifyMismatchError: print('mismatch detected')"

Output osservato:

text
mismatch detected

Una password sbagliata non restituisce False. verify solleva VerifyMismatchError, quindi il controllo va in un blocco except. Una stringa salvata malformata solleva un'eccezione diversa, InvalidHashError. Gestiscila come un problema di dati lato server e non farla arrivare allo schermo dell'utente.

4. Stessa password, salt diversi

Calcola due volte l'hash della stessa password.

bash
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"

Output osservato:

text
$argon2id$v=19$m=65536,t=3,p=4$p8OU0plYOCO2/v70mjYTFA$FwhMW3ECJ0F/8rulKrJ158HjyNekURguaCIfoVk0i/o
$argon2id$v=19$m=65536,t=3,p=4$MTgAgrtBHJmc1eL2iKy8nA$h/7+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYg

Una password, due stringhe diverse. I parametri coincidono perché entrambe le esecuzioni hanno usato gli stessi valori predefiniti. Cambiano solo il salt e l'output. Chi legge la tabella non può capire che questi due account usano la stessa password.

5. Leggi le parti dell'hash codificato

Salva questo codice come parts.py.

python
from argon2 import PasswordHasher

h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))

Eseguilo con python3 parts.py. Output osservato:

text
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']

Il primo elemento vuoto viene dal $ iniziale. Dopo ci sono variante, versione, parametri, salt e hash. Per la verifica non serve altro, ed è per questo che basta una colonna.

6. Rifai l'hash quando cambiano i parametri

Le impostazioni cambiano nel tempo. Questo script crea un hash con impostazioni più deboli e vecchie, poi lo controlla rispetto ai valori predefiniti attuali. Salvalo come rehash.py.

python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

old = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = old.hash('correct horse battery')
print('stored:', stored)

new = PasswordHasher()  # current defaults
print('needs rehash under new params:', new.check_needs_rehash(stored))
try:
    new.verify(stored, 'correct horse battery')
    print('verify ok')
    if new.check_needs_rehash(stored):
        stored = new.hash('correct horse battery')
        print('rehashed:', stored)
except VerifyMismatchError:
    print('wrong password')
print('after rehash needs rehash:', new.check_needs_rehash(stored))

Esegui python3 rehash.py. Output osservato:

text
stored: $argon2id$v=19$m=19456,t=2,p=1$EWji+p6+3HbAKY+wkykcXg$NxPq/zGPsx9okK4vQCKfZ4BVoSOqeF1QkHxd3f2g2uE
needs rehash under new params: True
verify ok
rehashed: $argon2id$v=19$m=65536,t=3,p=4$FkifxeWUHV9eU7d8JFt6mg$ZFoMkNjYqo4D/bax/jPz80Hvr4mjLWRi4TVoqCoR6ng
after rehash needs rehash: False

L'ordine conta. Prima verifica, poi rifai l'hash, così all'aggiornamento arriva solo una password corretta. In un'applicazione reale, riscrivi la nuova stringa nella stessa riga subito dopo un login riuscito. Se salti questo passo, le vecchie impostazioni restano nella tabella a tempo indefinito.

7. Misura il costo sul tuo hardware

Salva questo codice come bench.py.

python
import hashlib, time, os
from argon2 import PasswordHasher

pw = b'correct horse battery'
salt = os.urandom(16)
n = 1_000_000
t = time.perf_counter()
for _ in range(n):
    hashlib.sha256(salt + pw).digest()
dt = time.perf_counter() - t
print(f"sha256 salted: {n} hashes in {dt:.2f} s, {n/dt:,.0f} per second")

ph = PasswordHasher()
m = 10
t = time.perf_counter()
for _ in range(m):
    ph.hash('correct horse battery')
dt = time.perf_counter() - t
print(f"argon2id default: {m} hashes in {dt:.2f} s, {dt/m*1000:.0f} ms per hash")

Esegui python3 bench.py. Output osservato:

text
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hash

Sono singole esecuzioni su una sola macchina Windows. La riga di SHA-256 misura un ciclo Python e non codice ottimizzato, quindi sottostima quello che l'hardware dedicato fa con SHA-256. La riga di Argon2id è il costo pagato a ogni login. I valori esatti contano meno della differenza. Qui una chiamata a SHA-256 con salt richiede circa 4 microsecondi. Una chiamata ad Argon2id richiede circa 640 millisecondi, ed è voluto. Regola m, t e p su hardware simile a quello di produzione, finché un login non costa qualche centinaio di millisecondi.

Errori comuni

  • Hash veloci. Usare SHA-256, SHA-512 o MD5 per le password. Una password non è il checksum di un file.
  • Salt condivisi o prevedibili. Un solo salt per tutti gli utenti, o un salt ricavato dal nome utente. Genera un salt casuale per ogni password.
  • Copiare le impostazioni senza misurare. I valori predefiniti della libreria sono un punto di partenza ragionevole, non un verdetto sul tuo server. Misura prima.
  • Memoria senza un budget di login simultanei. 64 MiB per login sembrano pochi finché non arrivano 100 login insieme, cioè circa 6.4 GB. Dimensiona la memoria sul picco di login simultanei.
  • Confrontare da solo le stringhe di hash. Chiama verify. Non confrontare le stringhe con ==.
  • Password nei log. Log, report di errore e analytics non dovrebbero mai contenere la password, l'hash o il corpo completo di una richiesta di login.
  • Nessun percorso di aggiornamento. Tieni i parametri dentro la stringa salvata e rifai l'hash al login riuscito.
  • Considerare un pepper un rimedio. OWASP suggerisce un pepper, un segreto tenuto fuori dal database, come difesa in profondità. Il cheat sheet precisa anche che un pepper da solo aggiunge poco. Se ne usi uno, tienilo in un archivio di segreti, e non usarlo al posto di un hash adeguato.

Per andare oltre

  • Leggi le sezioni su parametri e sicurezza di RFC 9106 e confrontale con il tuo threat model.
  • Leggi la documentazione di argon2-cffi per PasswordHasher, verify e check_needs_rehash.
  • Se il tuo framework web ha un modulo per le password, controlla se usa già Argon2id prima di scrivere un wrapper.
  • Prima di progettare un flusso di login, chiarisci la differenza tra hashing, codifica e cifratura. La tratta l'articolo precedente: cifratura, codifica e hashing a confronto. Il prossimo passa a coppie di chiavi e firme: chiavi pubbliche e private spiegate.

Fonti

Fonti

  1. RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
  2. OWASP Cheat Sheet Series: Password Storage Cheat Sheet
  3. NIST SP 800-63B-4: Authentication and Authenticator Management
  4. argon2-cffi documentation

Altro su Crittografia

Crittografia

Cifratura, codifica e hashing: che cosa fa ciascuno

Distingui codifica, cifratura e hashing con laboratori su Base64, SHA-256 e OpenSSL. Impara perché Base64 non protegge niente e perché gli hash sono unidirezionali.

Base10 min di lettura