Vai al contenuto

Laboratorio: hashing e cifratura in Python

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 come pycryptodome.

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))