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.
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.
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.
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.
Un salt è un valore casuale, diverso per ogni password, salvato accanto all'hash. Ha due compiti.
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.
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.
m): la RAM, in KiB, usata da ogni hash.t): quante passate fa la funzione su quella memoria.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.
Argon2 ha vinto la Password Hashing Competition nel 2015. Ha tre varianti.
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.
Quando una libreria di hashing salva una password, succedono cinque cose.
Il passo 3 è il motivo per cui non ti serve una colonna separata per il salt. La stringa codificata ha questo formato.
$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.
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.
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffiSu Windows, attivalo con .venv\Scripts\activate in PowerShell o cmd, oppure con . .venv/Scripts/activate in Git 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:
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
Falseverify 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.
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:
mismatch detectedUna 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.
Calcola due volte l'hash della stessa password.
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:
$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+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYgUna 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.
Salva questo codice come parts.py.
from argon2 import PasswordHasher
h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))Eseguilo con python3 parts.py. Output osservato:
['', '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.
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.
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:
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: FalseL'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.
Salva questo codice come bench.py.
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:
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hashSono 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.
verify. Non confrontare le stringhe con ==.PasswordHasher, verify e check_needs_rehash.Crittografia
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.
Crittografia
Chiavi pubbliche e private da zero: cosa dimostra una firma, perché la chiave privata resta segreta e un lab con Ed25519 in cui un file modificato non passa la verifica.
Crittografia
La crittografia da zero: cifratura, chiavi, hash, firme e perché un cifrario di Cesare insegna i concetti ma non protegge nulla.