Table of Contents
Senza una corretta crittografia, i dati sono vulnerabili all'intercettazione, alla modifica e al furto durante la trasmissione. JavaScript, a lungo confinato alle interazioni basate sul browser, offre ora strumenti maturi, pronti alla produzione per l'implementazione di forti canali di crittografia e di comunicazione. Questa guida fornisce una guida completa e pratica dell'utilizzo di JavaScript per la crittografia dei dati e le librerie di sicurezza sicure, coprendo le pratiche di implementazione dei dati.
Comprendere i Fondamenti di Crittografia dei dati
La crittografia è il processo di trasformazione del testo chiaro leggibile in un testo cifrato illeggibile utilizzando un algoritmo e una chiave segreta. Solo le parti autorizzate che possiedono la chiave di decrittografia corretta possono invertire la trasformazione e recuperare i dati originali. La forza di un sistema di crittografia dipende dalle proprietà matematiche dell'algoritmo, dalla lunghezza della chiave e dalla casualità, e dalla sicurezza delle pratiche di gestione chiave.
La cripografia distingue tra due ampie categorie di crittografia:
- Crezione simmetrica[[] – utilizza una singola chiave condivisa per la crittografia e la decrittografia. È veloce ed efficiente per i dati di massa, ma la chiave deve essere scambiata in modo sicuro tra le parti. Esempi: AES, ChaCha20.
- Codifica asimmetrica[] – utilizza una coppia di chiavi pubblico-privato. La chiave pubblica crittografa i dati; solo la chiave privata corrispondente può decifrarlo. Questo elimina la necessità di un segreto condiviso ma è computazionalmente più lento. Esempi: RSA, ECC (cripografia a curva ellittica).
Inoltre, hashing[] (ad esempio, SHA‐256) è una funzione a senso unico che produce una digerenza a dimensione fissa dai dati di input. Non è crittografia (non si può invertire), ma è essenziale per i controlli di integrità, l'archiviazione password e le firme digitali. HMAC verifica a autentica] (i dati chiave codifica basata su Hash-
Crittografia nel Browser vs. Node.js
JavaScript funziona in due ambienti principali: il browser e Node.js (server-side). Ogni ambiente fornisce diverse interfacce native:
- Browser — L'interfaccia standard, crittograficamente sonora. Supporta AES‐CBC, AES‐GCM, RSA‐OAEP, ECDH e molte altre operazioni. L'API è progettata per essere sicura per impostazione predefinita, utilizzando l'algoritmo nativo del provider JavaScript del sistema.
- Node.js[ — Il modulo crittografico offre un'ampia serie di funzioni crittografiche, tra cui primitivi di basso livello e classi di alto livello come , ], supporta il flusso, e
librerie di terze parti come ]CryptoJS, libsodium.js, e Forge]] possono integrare le API native, ma gli sviluppatori dovrebbero preferire implementazioni native ogni volta possibile per evitare algoritmi scarsamente implementati o deprecati.
Algoritmi di crittografia comune in JavaScript
AES (Standard di crittografia avanzata)
AES è il cifrario simmetrico de facto utilizzato in tutto il mondo, che opera su blocchi a 128 bit e supporta le dimensioni chiave di 128, 192, o 256 bit.
- AES‐GCM[] (Modalità Gallo/Paese) — fornisce una crittografia autenticata; crittografa e produce un tag di autenticazione che rileva la manomissione.
- AES‐CBC[] (Cintura blocco cifrario) – richiede un vettore di inizializzazione (IV) e un'imbottitura. Non fornisce l'integrità da solo, quindi dovrebbe essere combinato con HMAC o utilizzato in un protocollo che aggiunge l'autenticazione.
Esempio utilizzando l'API Web Crypto (browser) per crittografare con AES‐GCM:
async function encryptAESGCM(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(data);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoded
);
return { iv, ciphertext: new Uint8Array(encrypted) };
}
// Generate a 256‑bit AES key
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
RSA (Rivest–Shamir–Adleman)
RSA è un algoritmo asimmetrico comunemente usato per lo scambio di chiavi e le firme digitali. In JavaScript, è disponibile tramite il Web Crypto API (browser) e il modulo Node.js . RSA-OAEP (Optimal Asymmetric Encryption Padding) è lo schema di crittografia raccomandato, in quanto include l'imbottitura casuale per proteggere dagli attacchi prescelti.
RSA è molto più lento della crittografia simmetrica ed è limitato nella dimensione dei dati che può crittografare (proporzionale alla dimensione chiave). In pratica, RSA è usato per crittografare una chiave AES (crittografia ibrida), e poi AES crittografa il carico reale.
SHA‐256 / SHA‐3 (Secure Hash Algorithms)
SHA‐256 è l'hash più utilizzato nella moderna sicurezza del web (ad esempio, per i certificati TLS, HMAC e password che si schiantano con il sale). L'API Web Crypto fornisce per SHA‐1, SHA‐256, SHA‐384, e SHA‐512. Nota che SHA‐1 è un attacco di collisione.
Cripografia della curva ellittica (ECC)
ECC offre una sicurezza equivalente a RSA con chiavi molto più corte, rendendolo ideale per applicazioni mobili e ad alte prestazioni. L'API Web Crypto supporta ECDH (Elliptic Curve Diffie‐Hellman) per un accordo chiave e ECDSA (Elliptic Curve Digital Signature Algorithm) per la firma. Il modulo Node.js [[[‐FLT:7] supporta anche queste curve (e.g4).
Implementazione della crittografia in JavaScript: una guida pratica
Qui di seguito sono riportati esempi completi, copia-e-paste sia per il browser che per Node.js. Preferisci sempre il modulo Web Crypto API o Node.js su librerie di terze parti quando possibile, come sono controllati e eseguire operazioni crittografiche nella memoria sicura del sistema.
Browser: Crittografia simmetrica con AES‐GCM (Web Crypto API)
// Encrypt a message with a given password
async function encryptWithPassword(password, plaintext) {
// Derive a key from the password using PBKDF2
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 600000, // OWASP recommended min
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(plaintext)
);
// Return salt, iv, ciphertext as base64 for transport
return {
salt: btoa(String.fromCharCode(...salt)),
iv: btoa(String.fromCharCode(...iv)),
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
};
}
// Decrypt
async function decryptWithPassword(password, { salt, iv, ciphertext }) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const saltBytes = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: saltBytes,
iterations: 600000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: ivBytes },
key,
data
);
return decoder.decode(decrypted);
}
Node.js: AES‐256‐GCM con il modulo di crittografia
const crypto = require('crypto');
function encrypt(text, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return { iv: iv.toString('hex'), encrypted, authTag };
}
function decrypt(encryptedData, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Usage: generate a 256‑bit key
const key = crypto.randomBytes(32);
const message = 'This is secret.';
const ciphertext = encrypt(message, key);
console.log(ciphertext);
console.log('Decrypted:', decrypt(ciphertext, key));
Migliori Pratiche di Comunicazione Sicuro
La crittografia da sola non garantisce una comunicazione sicura. Le seguenti pratiche devono essere integrate nell'intero flusso di dati:
1. Utilizzare sempre HTTPS con TLS 1.2 o superiore
HTTPS non è facoltativo. Crittografa l’intera conversazione HTTP, prevenendo attacchi di intercettazione e di dominio. Configura il server da usare TLS 1.2] o TLS 1.3] e disabilitare le suite di cifratura deboli. Servizi come
2. Gestione chiave corretta di implementazione
- Utilizzare variabili di ambiente o volte sicure (ad esempio, HashiCorp Vault, AWS Secrets Manager) sul server.
- Ruotare le chiavi regolarmente e immediatamente revocare le chiavi compromesse.
- Utilizzare le funzioni di derivazione chiave (PBKDF2, scrypt, Argon2) per la crittografia basata su password.
- Quando si scambiano le chiavi, utilizzare un protocollo di accordo chiave sicuro come ECDH o RSA‐OAEP, e combinare con una fase di conferma chiave.
3. Autenticazione e autorizzazione
La crittografia protegge i dati in transito, ma il sistema deve anche verificare l'identità. Utilizzare l'autenticazione basata su gettoni (JWT, OAuth 2.0) e garantire che i gettoni vengano trasmessi solo su HTTPS. Per le API, richiedono un accesso valido per ogni richiesta e implementano le vite a token corto con gettoni di aggiornamento.
4. Proteggere contro attacchi comuni Web
- Cross‐Site Scripting (XSS) – Gli attaccanti possono iniettare script che rubano dati o chiavi crittografati.
- Cross-Site Request Forgery (CSRF)[ – Utilizzare i token anti-CSRF o i cookie SameSite per evitare richieste forgiate.
- Clickjacking[] – Impostare l'intestazione ] o .
- In-the-Middle[[] – Enforce HSTS (HTTP Strict Transport Security) e considerare la pinning del certificato per applicazioni di alta sicurezza.
5. Convalida tutti gli ingressi e utilizza Confronti a tempo costante
Quando si confrontano HMAC, tag di autenticazione o password, utilizzare sempre una funzione di confronto costante-tempo per prevenire gli attacchi di temporizzazione. Node.js fornisce ; per il browser, è possibile implementare un confronto costante-tempo o utilizzare una libreria come .
Argomenti avanzati: Firme digitali, Crittografia ibrida e Avviso di attesa
Firme digitali
Le firme digitali forniscono non-repudiazione e dimostrano l'autenticità di un messaggio. Utilizzare ECDSA (con curva P‐256) o RSA‐PSS. Nel browser, il Web Crypto API supporta e operazioni. Esempio snippet (browser):
// Generate signing key pair
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
);
// Sign a message
const data = new TextEncoder().encode('Important contract');
const signature = await crypto.subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
keyPair.privateKey,
data
);
Crittografia ibrida per grandi carichi di pagamento
La crittografia asimmetrica non può gestire in modo efficiente i grandi dati. L'approccio standard è la crittografia ibrida:
- Generare una chiave simmetrica casuale (ad esempio AES a 256 bit).
- Crittografare la chiave simmetrica con la chiave RSA pubblica del destinatario (RSA‐OAEP).
- Crittografare il messaggio reale con AES‐GCM utilizzando la chiave simmetrica.
- Trasmettere sia la chiave simmetrica crittografata che il testo cifrato (con IV e tag auth).
Il destinatario decifra la chiave simmetrica con la chiave privata, quindi decifra il messaggio con AES‐GCM.
Certificato di pinza
Per proteggere ulteriormente dalle autorità di certificazione compromessa, è possibile pernottare il certificato di server previsto o il relativo hash chiave pubblica. Questo può essere fatto utilizzando l'intestazione [[ (deprecato ma ancora utilizzato in alcuni contesti) o tramite l'intestazione [] (Certificate Transparency).
Pitfalls comune e come evitare di loro
- Utilizzando un IV debole o un nonce[] – Il IV deve essere casuale e unico per ogni operazione di crittografia con la stessa chiave.
- Implementazione della crittografia personalizzata[[] – Non scrivere mai il tuo algoritmo di crittografia.
- Clicca distorsione in cookie o storage locale[[] – Lo storage del browser è accessibile a JavaScript e quindi agli attacchi XSS. Per la crittografia lato client, considerare l'utilizzo della Web Crypto API per generare chiavi che non sono mai esposte alla memoria JavaScript.
- Autenticazione di autenticazione[] – Crittografia senza un tag di autenticazione (ad esempio, utilizzando AES‐CBC senza HMAC) consente agli aggressori di modificare il testo cifrato non registrato.
- Ignorando attacchi side-channel[[] – In ambienti ad alta sicurezza, essere consapevoli degli attacchi di tempismo, cache e analisi di potenza.
Conclusioni
JavaScript è pienamente in grado di implementare una crittografia robusta e di garantire la comunicazione quando utilizzato correttamente. Il modulo Web Crypto API e Node.js fornisce funzioni crittografiche di livello di produzione, di base che seguono gli standard del settore. Combinando la crittografia simmetrica e asimmetrica con le migliori pratiche come HTTPS, la gestione delle chiavi corretta, i confronti di tempo costante e gli aggiornamenti di input, è possibile costruire applicazioni web che proteggono i dati degli utenti