In questa lezione impari a
- installare e usare la libreria pycryptodome
- calcolare l'hash di una stringa e di un file
- capire perché MD5 non è più sicuro e preferire SHA-256
- cifrare e decifrare dati con AES
Mettiamo in pratica la teoria. Useremo pycryptodome, una libreria Python per la crittografia.
pip install pycryptodome
I nomi dei moduli iniziano con
Crypto(con la C maiuscola), anche se il pacchetto si installa comepycryptodome.
Hashing: l'impronta di un dato
Partiamo da MD5, lo storico algoritmo di hash, per capire il meccanismo:
from Crypto.Hash import MD5
h = MD5.new()
h.update(b"ciao")
print("MD5:", h.hexdigest())
Prova a cambiare una sola lettera (b"Ciao"): l'impronta cambia completamente. Questo è il effetto valanga.
Perché MD5 non va più usato
MD5 è rotto: sono note tecniche per costruire due file diversi con lo stesso hash (una collisione). Per questo oggi, per la sicurezza, si usa SHA-256:
from Crypto.Hash import SHA256
h = SHA256.new()
h.update(b"ciao")
print("SHA-256:", h.hexdigest())
Regola pratica: MD5 va bene solo per capire il concetto o per controlli non di sicurezza. Per password, firme e integrità si usa SHA-256 (o superiore).
Verificare l'integrità di un file
Un uso classico dell'hash: controllare che un file scaricato non sia stato alterato. Si legge il file a blocchi (così funziona anche con file enormi) e si calcola l'impronta.
from Crypto.Hash import SHA256
def impronta_file(percorso):
h = SHA256.new()
with open(percorso, "rb") as f:
for blocco in iter(lambda: f.read(8192), b""):
h.update(blocco)
return h.hexdigest()
print("Impronta:", impronta_file("documento.txt"))
Se cambi anche un solo carattere del file e rilanci, l'impronta sarà diversa: è così che ci si accorge di una manomissione.
Cifratura simmetrica con AES
AES è lo standard attuale. Useremo la modalità CBC, che richiede tre ingredienti:
- una chiave segreta (16, 24 o 32 byte);
- un IV (vettore di inizializzazione) casuale, lungo come un blocco (16 byte);
- un padding, perché AES lavora a blocchi: il messaggio va «riempito» fino a un multiplo di 16 byte.
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
chiave = get_random_bytes(16) # la chiave segreta
iv = get_random_bytes(16) # casuale a ogni cifratura
messaggio = "Ciao, questo è un messaggio segreto!".encode("utf-8")
# --- CIFRATURA ---
cifrario = AES.new(chiave, AES.MODE_CBC, iv)
testo_cifrato = cifrario.encrypt(pad(messaggio, AES.block_size))
print("Cifrato:", testo_cifrato.hex())
# --- DECIFRATURA ---
decifrario = AES.new(chiave, AES.MODE_CBC, iv)
testo_in_chiaro = unpad(decifrario.decrypt(testo_cifrato), AES.block_size)
print("Decifrato:", testo_in_chiaro.decode("utf-8"))
Punti importanti:
- la stessa chiave serve per cifrare e decifrare (è simmetrica);
- l'IV non è segreto, ma deve essere diverso ogni volta: così lo stesso messaggio, cifrato due volte, dà risultati diversi;
- chi non ha la chiave vede solo byte senza senso.
I vecchi DES e 3DES (modulo
DES3) funzionano in modo simile ma usano chiavi troppo corte: oggi si usano solo per compatibilità con sistemi datati. Per qualsiasi cosa nuova, AES.
Esercizi
Esercizio 1 – Le password si confrontano, non si decifrano. Scrivi un programma che chiede una password con input(), ne calcola l'hash SHA-256 e lo confronta con l'hash di una password «corretta» salvata nel codice. Stampa «Accesso consentito» o «Password errata». Nota che in nessun punto conservi la password in chiaro.
Mostra la soluzione
from Crypto.Hash import SHA256
def hash_pw(p):
return SHA256.new(p.encode("utf-8")).hexdigest()
# hash della password corretta (es. "Primo2026!")
HASH_CORRETTO = hash_pw("Primo2026!")
tentativo = input("Password: ")
if hash_pw(tentativo) == HASH_CORRETTO:
print("Accesso consentito")
else:
print("Password errata")
Esercizio 2 – Cifrare username e password. Cifra con AES due stringhe (username e password), stampa i testi cifrati, poi decifrali e verifica che tornino uguali agli originali. Simula così il lato client (cifra) e il lato server (decifra) che condividono la stessa chiave.
Mostra la soluzione
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
chiave = get_random_bytes(16)
def cifra(testo, iv):
c = AES.new(chiave, AES.MODE_CBC, iv)
return c.encrypt(pad(testo.encode("utf-8"), AES.block_size))
def decifra(dato, iv):
d = AES.new(chiave, AES.MODE_CBC, iv)
return unpad(d.decrypt(dato), AES.block_size).decode("utf-8")
iv_user = get_random_bytes(16)
iv_pwd = get_random_bytes(16)
c_user = cifra("mario.rossi", iv_user)
c_pwd = cifra("Primo2026!", iv_pwd)
print("User cifrato:", c_user.hex())
print("Password cifrata:", c_pwd.hex())
print("Lato server ->", decifra(c_user, iv_user), "/", decifra(c_pwd, iv_pwd))