Le funzioni hash sono ovunque nello sviluppo software, anche se non te ne rendi conto. Ogni volta che scarichi un file e verifichi il checksum, accedi a un sito web, fai un commit Git o mini Bitcoin (ok, forse quest'ultimo no), le funzioni hash fanno il lavoro pesante dietro le quinte.

Cerchiamo di capire cosa sono, come si differenziano e quando usare ciascuna.

Cos'è una funzione hash?

Una funzione hash prende qualsiasi input — un singolo carattere, un romanzo, un file video da 4 GB — e produce un output a dimensione fissa chiamato "hash" o "digest". Pensala come un'impronta digitale per i dati. Indipendentemente da quanto grande o piccolo sia l'input, l'output ha sempre la stessa lunghezza.

Ecco cosa rende speciali le funzioni hash:

  • Deterministica: Lo stesso input dà sempre lo stesso output. hash("hello") restituirà lo stesso valore ogni singola volta, su qualsiasi macchina, in qualsiasi linguaggio di programmazione.
  • Unidirezionale: Non puoi risalire all'input dall'output. Dato un hash, non c'è modo di capire cosa l'ha prodotto (a parte indovinare). Questo è ciò che le rende utili per memorizzare le password.
  • Effetto valanga: Cambia un bit dell'input e l'output cambia drasticamente. Ad esempio, gli hash SHA-256 di "hello" e "Hello" sono stringhe completamente diverse.
  • Resistente alle collisioni: Dovrebbe essere praticamente impossibile trovare due input diversi che producono lo stesso output hash. Più forte è l'algoritmo, più difficile è trovare collisioni.

Vediamolo in azione:

plaintext

Output totalmente diversi da input che differiscono per un solo carattere — una "h" minuscola contro una "H" maiuscola. Questo è l'effetto valanga in azione.

Come funzionano le funzioni hash sotto il cofano

Sebbene la matematica dietro le funzioni hash sia complessa, il processo generale è semplice. La maggior parte delle funzioni hash segue questi passaggi:

1. Padding: Il messaggio di input viene riempito in modo che la sua lunghezza diventi un multiplo di una dimensione di blocco fissa (ad es. 512 bit per SHA-256). Questo assicura che l'algoritmo possa elaborare i dati in blocchi uniformi.

2. Suddivisione in blocchi: Il messaggio riempito viene diviso in blocchi di dimensione fissa.

3. Round di compressione: Ogni blocco viene elaborato attraverso molteplici round di operazioni bit a bit — XOR, AND, OR, shift di bit e addizione modulare. Queste operazioni mescolano i bit in modo approfondito cosicché ogni bit di output dipenda da ogni bit di input.

4. Concatenamento: L'output dell'elaborazione di un blocco alimenta l'elaborazione del blocco successivo. Ecco perché anche un piccolo cambiamento all'inizio dell'input si propaga a cascata attraverso ogni blocco successivo.

5. Digest finale: Dopo che tutti i blocchi sono stati elaborati, lo stato interno viene emesso come valore hash finale.

L'intuizione chiave è che queste operazioni sono facili da calcolare in avanti ma praticamente impossibili da invertire. Non puoi "demescolare" i bit per recuperare l'input originale.

MD5: Il veterano in pensione

MD5 è stato progettato da Ronald Rivest nel 1991 e produce un hash a 128 bit (32 caratteri esadecimali). È stato lo standard per decenni — vedevi checksum MD5 accanto a ogni download di file su internet.

Tuttavia, MD5 è ora considerato crittograficamente rotto. I ricercatori hanno dimostrato attacchi di collisione pratici — il che significa che possono creare due file diversi che producono lo stesso hash MD5. Nel 2008, i ricercatori hanno usato una collisione MD5 per creare un certificato SSL fraudolento, dimostrando che non era solo una debolezza teorica.

Ancora accettabile per: verifiche di integrità dei file (verificare che un download sia stato completato correttamente), checksum per la deduplicazione, tabelle hash non legate alla sicurezza e fingerprinting rapido dei dati dove la sicurezza non è una preoccupazione.

Mai usare per: archiviazione password, firme digitali, certificati di sicurezza, o qualsiasi cosa dove qualcuno potrebbe deliberatamente creare collisioni.

SHA-1: Anche lui in pensione

SHA-1 è stato progettato dalla NSA e pubblicato nel 1995. Produce un hash a 160 bit (40 caratteri esadecimali). Per anni è stato lo standard nei certificati SSL, nelle firme PGP e nei sistemi di controllo versione.

È stato deprecato dopo che Google ha dimostrato un attacco di collisione pratico nel 2017 (il famoso attacco SHAttered). Hanno creato due file PDF diversi con lo stesso hash SHA-1. L'attacco ha richiesto 9.223.372.036.854.775.808 calcoli SHA-1 — enorme, ma fattibile con le moderne risorse di cloud computing.

Git usa ancora SHA-1 internamente per gli hash dei commit, ma sta migrando a SHA-256. I browser e le autorità di certificazione hanno smesso di accettare certificati SHA-1 anni fa. Se vedi SHA-1 usato per la sicurezza in qualsiasi codebase oggi, dovrebbe essere segnalato e migrato.

SHA-256 e SHA-512: Gli standard attuali

Questi fanno parte della famiglia SHA-2, progettata dalla NSA e pubblicata nel 2001. Sono quello che dovresti usare oggi per la maggior parte degli scopi.

  • SHA-256: Output a 256 bit (64 caratteri hex). Usato in Bitcoin, certificati TLS e nella maggior parte delle applicazioni di sicurezza. È il punto ottimale tra sicurezza e prestazioni. L'intero sistema proof-of-work di Bitcoin è costruito sul doppio hashing SHA-256.
  • SHA-512: Output a 512 bit (128 caratteri hex). Output più grande significa più resistenza alle collisioni. Curiosamente, SHA-512 è spesso più veloce di SHA-256 su processori a 64 bit perché opera nativamente con parole a 64 bit, mentre SHA-256 usa parole a 32 bit.
  • SHA-384 e SHA-512/256: Queste sono varianti troncate di SHA-512. SHA-384 ti dà un output a 384 bit, mentre SHA-512/256 dà un output a 256 bit ma con i vantaggi prestazionali delle operazioni a 64 bit di SHA-512.

Confronto rapido:

plaintext

SHA-3: La prossima generazione

SHA-3 è stato standardizzato nel 2015 dopo una competizione pubblica organizzata dal NIST. A differenza di SHA-2, che usa una costruzione Merkle-Damgard, SHA-3 si basa sulla costruzione spugna Keccak — un design fondamentalmente diverso.

Perché è importante? Se una svolta matematica dovesse mai compromettere l'approccio progettuale di SHA-2, SHA-3 non ne sarebbe colpito perché funziona in modo completamente diverso. È una polizza assicurativa per la comunità crittografica.

SHA-3 è disponibile nelle stesse dimensioni di output — SHA3-256, SHA3-384, SHA3-512 — e introduce anche SHAKE128 e SHAKE256, che sono "funzioni di output estensibile" che possono produrre un hash di qualsiasi lunghezza desiderata.

In pratica, SHA-2 è ancora più ampiamente usato e più veloce sulla maggior parte dell'hardware. L'adozione di SHA-3 sta crescendo, ma è più uno standard di backup che un sostituto.

Casi d'uso reali

Controllo versione Git: Ogni commit, albero e blob in Git è identificato dal suo hash SHA-1. Quando esegui git commit, Git fa l'hash del contenuto delle tue modifiche, della struttura dell'albero, dell'hash del commit genitore, delle tue informazioni autore e del timestamp. Ecco perché gli hash dei commit sembrano a1b2c3d4e5f6... — sono letteralmente digest SHA-1.

Mining di Bitcoin: I miner competono per trovare un valore nonce che, combinato con i dati del blocco e hashato con doppio SHA-256, produce un hash sotto una soglia obiettivo. La difficoltà di trovare questo hash è ciò che protegge l'intera rete. Nel 2024, la rete Bitcoin calcola circa 500 quintilioni di hash SHA-256 al secondo.

Deduplicazione dei file: Servizi di archiviazione cloud come Dropbox fanno l'hash di ogni file che carichi. Se l'hash corrisponde a un file esistente, non archiviano un duplicato — aggiungono semplicemente un puntatore. Questo risparmia enormi quantità di spazio di archiviazione.

Firme digitali: Quando firmi un documento o un rilascio software, non stai firmando l'intero file. Invece, il file viene hashato e l'hash è ciò che viene firmato con la tua chiave privata. Il destinatario fa l'hash del file da solo e verifica la firma contro quell'hash.

Autenticazione API: HMAC (Hash-based Message Authentication Code) combina una chiave segreta con un hash del messaggio per verificare sia l'integrità che l'autenticità delle richieste API. AWS, Stripe e la maggior parte delle API principali usano HMAC-SHA256 per la firma delle richieste.

Errori comuni che gli sviluppatori fanno con l'hashing

Usare funzioni hash per le password: SHA-256 semplice è troppo veloce per l'hashing delle password. Un attaccante con una GPU può calcolare miliardi di hash SHA-256 al secondo, rendendo gli attacchi a forza bruta banali. Usa sempre funzioni di hashing delle password dedicate come bcrypt, scrypt o Argon2, che sono intenzionalmente lente e ad alto consumo di memoria.

Non usare il salt: Se fai l'hash delle password senza un salt (un valore casuale aggiunto a ogni password prima dell'hashing), password identiche producono hash identici. Un attaccante con una "rainbow table" precalcolata può trovare le password comuni istantaneamente. Aggiungi sempre un salt unico e casuale per utente.

Confrontare gli hash in modo non sicuro per il timing: Usare == per confrontare gli hash in codice sensibile alla sicurezza può far trapelare informazioni attraverso canali laterali di timing. Un attaccante può misurare quanto tempo impiega il confronto e dedurre l'hash carattere per carattere. Usa funzioni di confronto a tempo costante come crypto.timingSafeEqual() in Node.js o hmac.compare_digest() in Python.

Troncare gli hash: Alcuni sviluppatori troncano gli hash per risparmiare spazio (ad es. memorizzando solo i primi 16 caratteri di un hash SHA-256). Questo riduce drasticamente la resistenza alle collisioni. Un hash SHA-256 completo ha 2^256 valori possibili; troncare a 16 caratteri esadecimali lascia solo 2^64 — un numero che l'hardware moderno può forzare con forza bruta.

Quale funzione hash dovresti usare?

  • Integrità dei file (non-sicurezza): SHA-256 o anche MD5 va bene. Stai verificando la corruzione accidentale, non la manomissione dolosa.
  • Archiviazione password: Nessuna di queste! Usa bcrypt, scrypt o Argon2 — sono deliberatamente lenti, il che rende gli attacchi a forza bruta impraticabili. Le funzioni hash regolari sono troppo veloci per l'hashing delle password.
  • Firme digitali e certificati: SHA-256 o SHA-512.
  • HMAC (autenticazione dei messaggi): SHA-256 o SHA-512.
  • Indirizzamento del contenuto in stile Git: SHA-256 (dove sta andando Git).
  • A prova di futuro: Se stai costruendo un sistema che deve durare decenni e vuoi un piano di riserva nel caso in cui SHA-2 venga mai compromesso, considera SHA-3.
  • Checksum nelle pipeline di dati: SHA-256 per la verifica dell'integrità dei dati tra le fasi della pipeline. CRC32 è più veloce ma cattura solo errori accidentali, non manomissioni intenzionali.

Funzioni hash nel codice: Esempi pratici

Ok, basta teoria — scriviamo un po' di codice. Perché onestamente, il modo migliore per capire l'hashing è semplicemente... farlo. Ecco come calcoli gli hash nei linguaggi che probabilmente usi ogni giorno.

Node.js — Il modulo crypto integrato lo rende semplicissimo:

javascript

E la parte bella — fare l'hash di un file è quasi la stessa cosa:

javascript

Python — Il hashlib di Python è altrettanto semplice. In realtà penso che Python abbia l'API più bella per questo:

python

Go — La libreria standard di Go è incredibilmente ben progettata per questo:

go

Java — Un po' più verboso (perché... Java), ma funziona benissimo:

java

Verificare un download di file: Questo è uno degli usi più pratici dell'hashing. Diciamo che scarichi una ISO di Linux e il sito web dice che il checksum SHA-256 dovrebbe essere abc123.... Ecco come verificarlo:

bash

Lo so che sembra banale, ma rimarresti sorpreso da quanti sviluppatori saltano questo passaggio. Un singolo byte corrotto in un download da 4 GB può rovinarti tutto il pomeriggio.

Rainbow Table e perché sono terrificanti

Ok, questa è la parte che mi ha fatto esplodere la testa quando l'ho imparata per la prima volta. Immagina che qualcuno precalcoli l'hash per ogni possibile password fino a, diciamo, 8 caratteri. Ogni combinazione di lettere, numeri e simboli. Archiviano tutte quelle corrispondenze hash-password in una gigantesca tabella di ricerca.

Quella è una rainbow table. E sono assolutamente terrificanti.

Ecco perché: se hai memorizzato le password come semplici hash SHA-256 (senza salt), un attaccante che ottiene il tuo database non deve "craccare" nulla. Cerca semplicemente ogni hash nella sua rainbow table. Boom — recupero password istantaneo. La ricerca richiede microsecondi.

Quanto sono grandi queste tabelle? Una rainbow table che copre tutte le password alfanumeriche fino a 8 caratteri può essere di circa 100-200 GB. Sembra tanto, ma sta su un singolo SSD. Siti come CrackStation hanno tabelle con miliardi di hash precalcolati e craccano hash di password comuni in secondi gratuitamente.

Ora ecco la buona notizia: il salting sconfigge completamente le rainbow table. Un salt è semplicemente una stringa casuale che aggiungi alla password prima dell'hashing:

plaintext

Vedi cosa è successo? La stessa password ("password123") produce hash completamente diversi a causa dei diversi salt. Un attaccante dovrebbe costruire una rainbow table separata per ogni possibile salt, il che è computazionalmente impossibile.

Ogni libreria moderna di hashing delle password (bcrypt, Argon2, scrypt) gestisce il salting automaticamente. Se sei mai tentato di creare il tuo sistema di hashing delle password — non farlo. Seriamente. Usa bcrypt e vai avanti con la tua vita.

HMAC: Hashing con un segreto

HMAC sta per Hash-based Message Authentication Code, e lo so, lo so, sembra intimidatorio. Ma resta con me — in realtà è un concetto abbastanza semplice che probabilmente hai già usato senza rendertene conto.

L'hashing regolare prende un messaggio e produce un hash. HMAC prende un messaggio E una chiave segreta, e produce un hash. La differenza chiave (gioco di parole intenzionale) è che solo chi conosce la chiave segreta può produrre o verificare l'HMAC. Dimostra due cose contemporaneamente: il messaggio non è stato manomesso, E proviene da qualcuno che conosce il segreto.

Dove lo vedi nel mondo reale? Firme dei webhook. Quando GitHub o Stripe invia un webhook al tuo server, includono una firma HMAC-SHA256 negli header. Il tuo server può verificare che il webhook sia effettivamente arrivato da GitHub (e non sia stato falsificato da un attaccante qualsiasi) calcolando l'HMAC da solo e confrontando.

Ecco un esempio pratico di verifica di una firma webhook di GitHub in Node.js:

javascript

Hai notato la chiamata timingSafeEqual? È cruciale. Un confronto regolare con === restituisce false non appena trova il primo carattere non corrispondente, il che significa che un attaccante può misurare il tempo di risposta e dedurre la firma byte per byte. Il confronto sicuro per il timing impiega sempre lo stesso tempo indipendentemente da dove si verifica la discrepanza.

Benchmark delle prestazioni delle funzioni hash

Guarda, capisco — le prestazioni contano. Soprattutto se stai hashando milioni di file in una pipeline di build o elaborando un flusso massiccio di dati. Ecco come si confrontano le principali funzioni hash in termini di velocità (benchmark approssimativi su hardware x86_64 moderno):

plaintext

Aspetta, hai notato? BLAKE3 è 10 volte più veloce di SHA-256 pur essendo crittograficamente sicuro. Non è un errore di battitura.

BLAKE3 è l'ultima novità nel mondo dell'hashing, e a buon ragione. Si basa sulla famiglia BLAKE2 (che aveva già superato SHA-3 nella competizione NIST) ma è stato riprogettato per sfruttare il parallelismo SIMD e il multithreading. Può hashare dati praticamente alla velocità di memcpy.

Perché dovrebbe interessarti? Ai tool di build interessa. Eccome. Strumenti come Bazel, Buck e vari sistemi di archiviazione indirizzati per contenuto passano una quantità sorprendente di tempo a hashare file. Passare da SHA-256 a BLAKE3 può velocizzare il controllo delle dipendenze di un ordine di grandezza. L'ecosistema Rust sta adottando BLAKE3 in modo aggressivo, e sta comparendo in sempre più posti.

Detto questo, SHA-256 e SHA-512 rimangono la scelta giusta quando hai bisogno di ampia compatibilità o conformità con standard come FIPS. Non tutto supporta ancora BLAKE3, e in molti casi d'uso, la velocità di hashing non è comunque il collo di bottiglia.

Blockchain e alberi di Merkle: Hashing su larga scala

Ok, qui diventa davvero interessante. Sai come Git può dirti esattamente quale file è cambiato in un repository enorme? E come Bitcoin può verificare una transazione senza scaricare l'intera blockchain? Il segreto è una struttura dati chiamata albero di Merkle (dal nome di Ralph Merkle, che lo brevettò nel 1979).

Un albero di Merkle è fondamentalmente un albero di hash. Ecco come funziona — immagina di avere quattro blocchi di dati:

plaintext

Ogni nodo foglia è l'hash di un blocco di dati. Ogni nodo genitore è l'hash dei suoi due figli concatenati. L'hash radice (a volte chiamato "Merkle root") è un singolo hash che rappresenta TUTTI i dati nell'albero.

Ecco la parte genuinamente elegante: se anche un solo bit di Data C cambia, Hash(C) cambia, il che significa che Hash(CD) cambia, il che significa che il Root Hash cambia. Puoi rilevare la manomissione istantaneamente controllando solo la radice.

Ma migliora ancora. Supponiamo che tu voglia dimostrare che Data C fa parte dell'albero senza rivelare Data A, B o D. Devi solo fornire: Data C, Hash(D) e Hash(AB). Il verificatore può ricostruire il percorso fino alla radice e controllare che corrisponda. Questa si chiama "prova di Merkle", ed è incredibilmente efficiente — per un albero con un milione di foglie, la prova è lunga solo circa 20 hash (log2 di 1.000.000).

Dove viene usato nella pratica?

  • Git: L'intero tuo repository è un albero di Merkle. I commit puntano ad alberi, gli alberi puntano a blob, e tutto è identificato dal suo hash SHA-1. Ecco perché Git può sapere istantaneamente se qualcosa è cambiato.
  • Bitcoin: Ogni blocco contiene un Merkle root di tutte le transazioni. I client leggeri (come i wallet mobili) possono verificare una specifica transazione usando una prova di Merkle senza scaricare l'intero blocco.
  • IPFS: L'InterPlanetary File System spezza i file in chunk, costruisce un Merkle DAG (grafo aciclico diretto) e usa l'hash radice come identificatore di contenuto (CID) del file.
  • Certificate Transparency: I log di Certificate Transparency di Google usano alberi di Merkle in modo che chiunque possa verificare efficientemente se un certificato è stato (o non è stato) registrato.

Il futuro: Funzioni hash post-quantistiche

Potresti aver sentito che i computer quantistici romperanno tutta la nostra crittografia. E sì, è parzialmente vero — RSA, ECC e Diffie-Hellman sono tutti spacciati una volta che arriveranno i computer quantistici su larga scala. L'algoritmo di Shor può fattorizzare numeri grandi e calcolare logaritmi discreti in modo efficiente, ed è su questo che quei sistemi si basano.

Ma ecco la notizia sorprendentemente buona: le funzioni hash sono in realtà abbastanza sicure contro i computer quantistici. La principale minaccia quantistica alle funzioni hash è l'algoritmo di Grover, che può cercare in uno spazio non strutturato quadraticamente più veloce. In pratica, questo significa che dimezza i bit di sicurezza — SHA-256 passa da 2^256 a 2^128 di forza contro gli attacchi quantistici.

2^128 è ancora assolutamente enorme. È approssimativamente il numero di atomi nell'universo osservabile al quadrato. Nessuno lo forzerà con forza bruta, computer quantistico o meno.

Quindi, mentre il NIST sta lavorando attivamente agli standard di crittografia post-quantistica (e ne ha finalizzati diversi nel 2024), l'urgenza riguarda principalmente la crittografia a chiave pubblica e le firme — non le funzioni hash. Se stai usando SHA-256 oggi, puoi dormire tranquillo sapendo che i computer quantistici non lo renderanno inutile.

Detto questo, se sei veramente paranoico (e in crittografia, la paranoia è una virtù), passare a SHA-512 o SHA3-256 ti dà un margine di sicurezza extra. Alcuni schemi di firma post-quantistici come SPHINCS+ sono costruiti interamente su funzioni hash, il che è un bel voto di fiducia nella loro resistenza quantistica.

Collisioni hash: Attacchi del compleanno spiegati

Parliamo di una delle cose più controintuitive in tutta l'informatica: l'attacco del compleanno. Prende il nome dal paradosso del compleanno, ed è il motivo per cui le funzioni hash devono essere più grandi di quanto ci si aspetterebbe intuitivamente.

Ecco il paradosso del compleanno: in una stanza con sole 23 persone, c'è il 50% di probabilità che due di loro condividano lo stesso compleanno. Non un compleanno specifico — qualsiasi coppia corrispondente. Con 70 persone, la probabilità sale al 99,9%. La maggior parte delle persone indovinerebbe che servono circa 183 persone (la metà di 365), ma il numero reale è molto più basso perché stiamo cercando QUALSIASI collisione, non una specifica.

La stessa identica matematica si applica alle funzioni hash. Se una funzione hash produce N output possibili, non servono N hash per trovare una collisione — ne servono solo circa la radice quadrata di N.

Per un hash a 256 bit come SHA-256, ci sono 2^256 output possibili. Trovare una collisione richiede circa 2^128 operazioni (la radice quadrata di 2^256). È ancora un numero impossibilmente grande — ma è il motivo per cui non possiamo semplicemente usare un hash a 64 bit e chiuderla lì.

plaintext

Questo è esattamente il motivo per cui MD5 (128 bit) è crollato. La sua resistenza alle collisioni era solo 2^64 fin dall'inizio, e le debolezze strutturali nell'algoritmo l'hanno ridotta ulteriormente. I ricercatori alla fine hanno trovato collisioni in secondi su un normale laptop.

La conclusione pratica? Usa sempre almeno una funzione hash a 256 bit per qualsiasi cosa legata alla sicurezza. SHA-256, SHA3-256 o BLAKE3 sono tutte scelte eccellenti. E se qualcuno suggerisce di usare un hash a 64 o 128 bit per scopi di sicurezza, ora sai esattamente perché è un'idea terribile.

Provalo tu stesso

Curioso di vedere l'hash dei tuoi dati? Usa il nostro Generatore di hash MD5, Generatore di hash SHA-256 o Generatore di hash SHA-512. Incolla del testo e osserva come anche piccole modifiche producono hash completamente diversi — è il modo migliore per costruire l'intuizione su come si comportano questi algoritmi.