Sostieni AppuntiFacili con una piccola donazione su PayPal

Dona con PayPal
AppuntiFacili
Torna Indietro Segnala errore

Crittografia

✍️ Dennis Turco 🏷️ Informatica 📘 Python
Ultima modifica:
#python#programmazione#crittografia#hashing#security

1.Introduzione

La 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.

1.1 Tipi di crittografia

TipoDescrizioneEsempio
SimmetricaUsa la stessa chiave per cifrare e decifrareAES, Fernet
AsimmetricaUsa una coppia di chiavi: pubblica (per cifrare) e privata (per decifrare)RSA, ECC
HashingTrasforma i dati in un’impronta digitale unidirezionale, non reversibileSHA256, bcrypt

1.1 Crittografia simmetrica e asimmetrica

  • Simmetrica: usa la stessa chiave per cifrare e decifrare. Esempio: AES.

    simmetrica

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

    asimmetrica

    Immagina una scatola con lucchetto:

    • 🔑 La chiave pubblica chiude la scatola (cifratura)
    • 🔒 Solo la chiave privata la può riaprire (decifratura)

Hashing (Crittografia unidirezionale)

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.

2.1 Esempio con SHA256

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

2.2 Hash di un file

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

2.3 Hashare un file e controllare l’integrità

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

3. Hash sicuri con salt (protezione contro attacchi dizionario)

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.

4. Crittografia simmetrica

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.

4.1 Esempio con AES-GCM

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

4.2 Fernet - interfaccia semplificata

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”.

5. Crittografia asimmetrica (RSA)

La crittografia asimmetrica utilizza due chiavi:

  • una pubblica, per cifrare;
  • una privata, per decifrare.
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())

6. Firma digitale

La firma digitale consente di garantire l’autenticità e l’integrità di un messaggio.

  • Il mittente firma il messaggio con la chiave privata.
  • Il destinatario verifica la firma con la chiave pubblica.
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.

7. Crittografia e password

7.1 Controllo della robustezza

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']}"

7.2 Hash e verifica password con bcrypt

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)

8. Best Practice di Sicurezza

TIP

Buone pratiche per la crittografia:

  • Usa librerie collaudate (es. cryptography, bcrypt, hashlib), mai algoritmi fatti a mano.
  • Non riutilizzare mai le stesse chiavi o nonce.
  • Non memorizzare password in chiaro.
  • Proteggi sempre le chiavi private.
  • Aggiungi sempre un salt agli hash delle password.
  • Usa algoritmi aggiornati (AES-256, RSA-2048+, SHA-256).

9. Quiz a risposta multipla

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?

Prenota una lezione