Guida implementazione AES-GCM: crittografia autenticata fatta bene
Implementa correttamente la crittografia AES-GCM. Gestione nonce, chiavi e trappole comuni da evitare.
AES-GCM (Galois/Counter Mode) combina la cifratura AES-CTR con l'autenticazione GHASH per produrre Authenticated Encryption with Associated Data (AEAD). Un'implementazione corretta usa una chiave a 256 bit, un nonce a 96 bit (12 byte) unico per chiave (mai riutilizzato), un tag di autenticazione a 128 bit e, opzionalmente, dati associati autenticati (AAD) verificati ma non cifrati. NIST SP 800-38D specifica la costruzione esatta. Sbagliare uno qualsiasi di questi elementi — in particolare il riutilizzo del nonce — fa collassare la sicurezza di GCM: una singola coppia (chiave, nonce) ripetuta permette agli attaccanti di recuperare la chiave di autenticazione e forgiare ciphertext arbitrari. Questa guida copre il modo corretto di usare AES-GCM nei contesti browser, Node e server.
Cosa garantisce effettivamente GCM
Due proprietà fondamentali:
Confidenzialità: il plaintext non può essere recuperato senza la chiave. La cifratura in modalità CTR di AES-GCM fornisce questa garanzia.
Integrità e autenticità: qualsiasi modifica al ciphertext, al nonce o ai dati associati causa il fallimento della decrittografia. GHASH produce un tag a 128 bit verificato in tempo costante al momento della decrittografia.
Cosa GCM non garantisce: non-ripudio (è simmetrica, quindi chiunque abbia la chiave può produrre ciphertext validi), protezione contro i replay (è una preoccupazione di livello superiore) o ordinamento (per gli stream, devi concatenare in qualche modo).
Il punto chiave: GCM rimane sicuro solo quando i nonce sono unici per chiave. Non quasi unici, non di solito unici: effettivamente unici. La prova di sicurezza collassa al primo riutilizzo.
Gestione del nonce: l'aspetto che conta di più
Un nonce a 96 bit può essere generato in due modi:
Casuale: crypto.getRandomValues(new Uint8Array(12)). Con nonce casuali a 96 bit sotto una singola chiave, le collisioni per birthday bound compaiono intorno a 2^48 cifrature. NIST suggerisce un margine di sicurezza, quindi limita gli usi a 2^32 per chiave.
Contatore: incrementa un intero a 96 bit. Garantisce l'unicità fino a 2^96 messaggi. Richiede uno stato monotonico affidabile, difficile nei sistemi distribuiti.
Per il trasferimento file con una chiave fresca per file, i nonce casuali sono perfettamente sicuri — non raggiungerai mai 2^32 cifrature con una singola chiave. Per la cifratura a blocchi sotto una singola chiave file, usa un contatore in cui il nonce codifica l'indice del blocco:
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));
Il caso disastroso: più processi che cifrano con la stessa chiave condivisa e nonce casuali, scalando a milioni di cifrature al secondo. Le collisioni per birthday bound diventano probabili. Se devi condividere chiavi tra processi, usa un contatore coordinato con un prefisso che identifica il processo.
Non usare nonce a 64 bit
AES-GCM supporta lunghezze variabili di nonce, ma solo i nonce a 96 bit usano la costruzione ottimizzata specificata in NIST 800-38D. Altre lunghezze (tipicamente 64 o 128 bit) attivano un passaggio di pre-elaborazione GHASH che riduce le prestazioni e aumenta la complessità. La Web Crypto API accetta IV non a 96 bit, ma la specifica raccomanda 96. Usa sempre 96.
Lunghezza del tag: non ridurla
Il tag di GCM arriva fino a 128 bit. Alcune specifiche consentono la troncatura a 96, 64 o perfino 32 bit. Non farlo. I tag troncati rendono più facili gli attacchi di contraffazione, e il risparmio (4–12 byte per messaggio) è irrilevante per il trasferimento file. La AES-GCM di Web Crypto usa tag a 128 bit tramite il parametro tagLength (default 128). Non modificarlo.
Dati associati (AAD)
L'AAD è dati che vengono autenticati ma non cifrati. Usalo per metadati da legare al ciphertext: nome file, tipo di contenuto, timestamp di scadenza, ID mittente.
await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(JSON.stringify({
filename: "report.pdf",
contentType: "application/pdf",
expires: 1712345678,
})),
},
key,
plaintext
);
Se un attaccante modifica l'AAD, la decrittografia fallisce. Questo previene gli attacchi di sostituzione in cui qualcuno sostituisce il nome file su un ciphertext conservato senza essere rilevato. Il destinatario deve conoscere esattamente l'AAD per decifrare, quindi conservalo insieme al ciphertext.
Generazione e derivazione delle chiavi
Per chiavi per-file:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
256 bit è lo standard nel 2026. AES a 128 bit è ancora sicuro, ma ha meno margine post-quantum (l'algoritmo di Grover dimezza la forza effettiva).
Per chiavi derivate da password:
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Conserva il salt insieme al ciphertext. Non è segreto; deve solo essere unico per ogni password.
Il percorso di codice critico
Una funzione di cifratura minimale:
async function encrypt(key, plaintext, aad = new Uint8Array()) {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
plaintext
)
);
return { nonce, ciphertext, aad };
}
Decrittografia con gestione degli errori appropriata:
async function decrypt(key, { nonce, ciphertext, aad }) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
ciphertext
);
} catch (e) {
// Fallimento di autenticazione
throw new Error("Decrittografia fallita: ciphertext manomesso o chiave errata");
}
}
La chiamata decrypt lancia OperationError in caso di mancata corrispondenza del tag, ciphertext troppo corto o chiave errata. Tratta qualsiasi eccezione come un fallimento di integrità; non cercare di distinguerle.
File di grandi dimensioni a blocchi
Per file di alcune centinaia di megabyte, suddividili in blocchi per evitare pressione sulla memoria:
async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce }, key, chunk
);
chunks.push(new Uint8Array(ct));
}
return chunks;
}
Attenzione: AES-GCM a blocchi non rileva la troncatura. Un attaccante potrebbe eliminare i blocchi finali e ogni blocco superstite decifra correttamente. Come difesa, includi il conteggio totale dei blocchi nell'AAD di ogni blocco, oppure usa crypto_secretstream di libsodium che gestisce questo problema.
Decrittografia lato server (Node.js)
Il modulo crypto di Node può decifrare dati cifrati nel browser:
const { createDecipheriv } = require('crypto');
function decrypt(key, nonce, ciphertextWithTag) {
const tag = ciphertextWithTag.slice(-16);
const ct = ciphertextWithTag.slice(0, -16);
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
Web Crypto aggiunge il tag a 128 bit al ciphertext; l'API di Node li vuole separati. Suddividi di conseguenza.
Numeri sulle prestazioni
Su hardware tipico 2024–2026 con AES-NI:
- Nativo (OpenSSL, AES-NI): 3–5 GB/s per core
- Web Crypto (browser con accelerazione hardware): 1–2 GB/s
- libsodium.js WASM AES-GCM: 400–800 MB/s
- Pure JS (@noble/ciphers): 50–150 MB/s
Per un file da 1 GB, la cifratura con Web Crypto richiede 0,5–1 secondo. Il pure JS impiega 7–20 secondi. Scegli le implementazioni in base a questa realtà; per UX di trasferimento file grandi, Web Crypto è la scelta pratica.
Riepilogo degli errori comuni
- Riutilizzo del nonce: catastrofico. La causa di fallimento più grave in assoluto.
- Usare
Math.random()invece dicrypto.getRandomValues(). - Dimenticare di autenticare i metadati associati con AAD.
- Usare la modalità CBC "perché siamo abituati". CBC richiede un MAC separato per eguagliare l'integrità di GCM; una costruzione HMAC-CBC corretta è complessa da implementare correttamente, GCM evita la trappola.
- Catturare silenziosamente gli errori di decrittografia e restituire dati corrotti. Fai sempre fallire in modo esplicito.
- Reimplementare GCM da zero. Usa Web Crypto, libsodium o node:crypto. L'implementazione GHASH ha insidie legate ai side channel che gli esperti hanno impiegato anni a risolvere.
HexaTransfer usa AES-256-GCM di Web Crypto con nonce casuali a 96 bit, tag a 128 bit e nessun AAD perché la chiave è per-file e il nome file è conservato separatamente in metadati protetti da AEAD. Semplice, corretto, veloce.
Quando scegliere altro
AES-GCM è ottimale per il trasferimento file, ma considera alternative in casi specifici:
- XChaCha20-Poly1305: nonce a 192 bit rendono la sicurezza dei nonce casuali banale a qualsiasi scala. Leggermente più lento su hardware con AES-NI, più veloce su ARM più vecchi senza AES-NI. libsodium lo fornisce.
- AES-GCM-SIV: resistente all'uso improprio; il riutilizzo del nonce non fa trapelare la chiave, rivela solo se i plaintext erano uguali. Utile quando non puoi garantire l'unicità del nonce.
Per la maggior parte dei carichi di lavoro di trasferimento file in uno stack web standard, AES-256-GCM con una chiave fresca per file e nonce casuali a 96 bit è la scelta giusta e quella più semplice da implementare correttamente.
Provalo su hexatransfer.com — gratis, senza registrazione, fino a 10 GB.
Invia file di grandi dimensioni in modo sicuro con crittografia end-to-end
Trasferisci file fino a 10 GB gratuitamente con crittografia end-to-end. Nessun account necessario. I tuoi file vengono crittografati nel browser prima del caricamento — nessun altro può leggerli.
Invia un file