Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalLa crittografia è la scienza che studia le tecniche per proteggere le informazioni da accessi non autorizzati, garantendo riservatezza, integrità e autenticità dei dati.
In informatica, si basa sull’uso di algoritmi matematici che trasformano i dati leggibili (testo in chiaro) in dati illeggibili (testo cifrato), e viceversa, attraverso chiavi.
INFO
Obiettivo della crittografia: rendere i dati comprensibili solo a chi possiede la chiave giusta per decifrarli.
| Tipo | Descrizione | Esempio |
|---|---|---|
| Simmetrica | Usa la stessa chiave per cifrare e decifrare | AES, Fernet |
| Asimmetrica | Usa una coppia di chiavi: pubblica (per cifrare) e privata (per decifrare) | RSA, ECC |
| Hashing | Trasforma i dati in un’impronta digitale unidirezionale, non reversibile | SHA256, bcrypt |
INFO
Strumento utile: CyberChef - Tool di crittografia online
Simmetrica: usa la stessa chiave per cifrare e decifrare. Esempio: AES.

Asimmetrica: usa una chiave pubblica per cifrare e una privata per decifrare. Esempio: RSA.

Immagina una scatola con lucchetto:
L’hashing è un processo che prende un input di lunghezza arbitraria e produce un output (hash o digest) di lunghezza fissa. Un hash non può essere decifrato: serve per verificare integrità o autenticare dati, non per nasconderli.
import hashlib
text = "Hello World"
hash_object = hashlib.sha256(text.encode())
hash_digest = hash_object.hexdigest()
print(f"SHA256 Hash di '{text}' è:\n{hash_digest}")
Output:
SHA256 Hash di 'Hello World' è:
a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
def hash_file(file_path):
h = hashlib.new("sha256")
with open(file_path, "rb") as file:
for chunk in iter(lambda: file.read(4096), b""):
h.update(chunk)
return h.hexdigest()
INFO
Gli hash di file si usano per verificare integrità (es. controllare che un file scaricato non sia stato modificato).
import hashlib
def hash_file(file_path):
h = hashlib.new("sha256")
with open(file_path, "rb") as file:
for chunk in iter(lambda: file.read(4096), b""):
h.update(chunk)
return h.hexdigest()
# ipotizziamo di avere un file hashato e voler vedere se è cambiato nel corso del tempo:
def controllo_integrita(file1, file2):
hash1 = hash_file(file1)
hash2 = hash_file(file2)
print(f"Controllo integrità tra {file1} e {file2}:")
if hash1 == hash2:
return "Il file è intatto. Nessuna modifica rilevata."
else:
return "Il file è stato modificato o corrotto!"
Esempio d’uso:
# Per testare la funzione controllo_integrita, prendere 1 file immagine, duplicarlo 2 volte (averne 3)
# su uno dei 3 applicare una modifica, per esempio ruotare l'immagine
print(controllo_integrita("immagine_nomrale1.svg", "immagine_nomrale2.svg"))
print(controllo_integrita("immagine_nomrale1.svg", "immagine_ruotata.svg"))
L’hashing semplice (es. SHA256) non basta per le password, perché è vulnerabile ad attacchi di tipo “rainbow table”.
La soluzione è usare un salt, una stringa casuale univoca aggiunta alla password prima di eseguire l’hash.
In Python possiamo usare hashlib.pbkdf2_hmac per derivare chiavi sicure.
import hashlib, os
password = b"mypass123"
salt = os.urandom(16)
key = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000) # 100_000 sono il numero delle iterazioni (possibile modificare)
print(f"Salt: {salt.hex()}")
print(f"Hash derivato: {key.hex()}")
INFO
Di norma, più alto è il numero di iterazioni, più la chiave risultante sarà difficile da forzare (più sicura), ma anche più tempo servirà per generarla.
Esempio pratico con hashlib.pbkdf2_hmac:
(importante: evita valori troppo grandi se esegui su macchine poco performanti)
import hashlib, os, time
password = b"mypass123"
salt = os.urandom(16)
print(f"Salt: {salt.hex()}")
start = time.time()
key = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000)
end = time.time()
print(f"Hash derivato (100.000 iterazioni): {key.hex()} in {end - start:.4f} secondi")
start = time.time()
key = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000_000)
end = time.time()
print(f"Hash derivato (1.000.000 iterazioni): {key.hex()} in {end - start:.4f} secondi")
TIP
Le funzioni di derivazione come PBKDF2, bcrypt o scrypt rallentano il calcolo dell’hash, rendendo più difficile un attacco brute-force.
La crittografia simmetrica usa la stessa chiave sia per cifrare che per decifrare. È veloce ed efficiente, ma richiede un canale sicuro per condividere la chiave.
import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def aes_encrypt_decrypt(message):
key = secrets.token_bytes(32)
nonce = secrets.token_bytes(12)
aes = AESGCM(key)
ciphertext = aes.encrypt(nonce, message.encode(), None)
plaintext = aes.decrypt(nonce, ciphertext, None)
return key.hex(), ciphertext.hex(), plaintext.decode()
print(aes_encrypt_decrypt("Ciao AES!"))
Per casi pratici, la libreria cryptography offre un’interfaccia più facile: Fernet, che integra automaticamente AES + HMAC.
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(b"Messaggio segreto")
print(token)
print(f.decrypt(token))
INFO
Fernet gestisce automaticamente nonce, padding e autenticazione, garantendo sicurezza “out of the box”.
La crittografia asimmetrica utilizza due chiavi:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
# Generazione chiavi
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Cifratura
message = b"Messaggio segreto RSA"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Decifratura
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(plaintext.decode())
La firma digitale consente di garantire l’autenticità e l’integrità di un messaggio.
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
message = b"Messaggio firmato"
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# Verifica
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
INFO
Se la verifica fallisce, significa che il messaggio è stato modificato o che la firma non proviene dal mittente previsto.
La libreria zxcvbn consente di stimare la forza di una password.
from zxcvbn import zxcvbn
def controllo_password(password):
result = zxcvbn(password)
score = result["score"]
if score >= 3:
return f"Password efficace (score {score})"
else:
return f"Password debole (score {score}) - {result['feedback']['suggestions']}"
import bcrypt
def hash_pw(password):
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode(), salt)
def verifica_password(pw_attempt, hashed):
return bcrypt.checkpw(pw_attempt.encode(), hashed)
TIP
Buone pratiche per la crittografia:
1) Qual è l'obiettivo principale della crittografia?
2) Qual è la differenza principale tra crittografia simmetrica e asimmetrica?
3) Quale tra i seguenti algoritmi è di tipo asimmetrico?
4) Che cos'è l'hashing?
5) Perché si utilizza il salt negli hash delle password?
6) Quale libreria Python fornisce un'interfaccia semplice per la crittografia simmetrica con Fernet?
7) Quale algoritmo si usa comunemente per creare hash sicuri e lenti per le password?
8) Nella crittografia asimmetrica RSA, quale chiave viene usata per cifrare un messaggio?
9) A cosa serve la firma digitale?
10) Quale delle seguenti è una buona pratica di sicurezza?