07 - Criptografie practică pentru dezvoltatori: AES-256-GCM, Hashing și TLS 1.3
Criptografia este fundamentul invizibil al securității cibernetice moderne. Fiecare parolă salvată într-o bază de date, fiecare dată sensibilă transmisă prin HTTPS, fiecare semnătură digitală care autentifică a actualizare software: totul depinde de primitivele criptografice implementate corect. Și totuși Criptografia este, de asemenea, unul dintre domeniile în care dezvoltatorii fac cele mai grave greșeli, adesea fără să-și dea seama.
Potrivit OWASP Top 10:2025, Eșecuri criptografice (A02) rămân printre primele trei vulnerabilități cele mai critice în aplicațiile web. Acestea nu sunt atacuri exotice care necesită calculatoare cuantice: majoritatea breșelor criptografice apar din cauza unor erori banale. MD5 sau SHA-1 folosit pentru hashing parole. AES în modul ECB care produce modele vizibile în text cifrat. IV (Initialization Vector) reutilizat. Cheile codificate în codul sursă. Erori pe care nici un pentest avansat nu este necesar să le exploateze.
Acest articol abordează criptografia din punctul de vedere al dezvoltatorului: nu teoria matematică, dar modele practice în Node.js și Web Crypto API. Veți învăța când să utilizați criptarea simetrică vs asimetric, cum să implementați corect AES-256-GCM, ce algoritm de hashing să alegeți pentru parole, cum să configurați TLS 1.3 și cum să vă pregătiți pentru criptarea post-cuantică. Fiecare secțiunea include exemple de cod gata de producție cu capcane de evitat.
Ce vei învăța
- Criptare simetrică vs asimetrică: când să folosiți ce abordare
- AES-256-GCM: Implementare corectă cu IV aleatoriu și etichetă de autentificare
- RSA vs ECC (ECDSA, Ed25519): comparație de performanță și securitate în 2025
- Hashing parole cu Argon2id și bcrypt: Parametri OWASP recomandați
- SHA-256 pentru integritatea datelor vs hashing parole: utilizări corecte
- TLS 1.3: Configurare sigură în Node.js cu suite de criptare moderne
- Gestionarea cheilor: evitați cheile codificate, utilizați variabilele de mediu și KMS
- Web Crypto API: criptare în browser fără dependențe externe
- Criptografie post-cuantică: NIST FIPS 203 (ML-KEM) și pregătirea pentru 2030
- Listă de verificare angulară: modele sigure pentru manipularea datelor sensibile în interfață
Criptare simetrică vs asimetrică
Alegerea între criptografia simetrică și asimetrică nu este o chestiune de preferință: este una decizie arhitecturală care depinde de problema de rezolvat. Confuzia celor două abordări duce sistematic la vulnerabilități sau performanțe degradate inutil.
Criptare simetrică utilizați aceeași cheie pentru a cripta și decripta. Și repede (AES-256-GCM criptează gigaocteți pe secundă pe hardware modern) și este potrivit pentru criptarea mari cantitatea de date. Problema și distribuția cheilor: cum o partajați în siguranță Cu cine ar trebui să descifreze datele?
Criptare asimetrică folosește o pereche de chei legate matematic: unul public (pe care îl puteți distribui gratuit) și unul privat (pe care îl păstrați secret). Rezolvă problemă de distribuție a cheilor, dar ordine de mărime mai lentă decât simetrică. RSA-2048 criptează aproximativ 250 de octeți pe secundă per operațiune, în timp ce AES-256-GCM ajunge la mai mulți GB/s.
Modelul profesional combină cele două abordări într-un sistem hibrid, la fel cum face TLS: criptarea asimetrică este utilizată numai pentru a schimba în siguranță o cheie de sesiune simetric; apoi toate datele sunt criptate cu acea cheie AES. Aceasta este fundamentul HTTPS, SSH și orice protocol securizat modern.
Principiul fundamental
- Simetric (AES-256-GCM): pentru a cripta în vrac datele în repaus și în tranzit
- Asimetric (RSA/ECC): pentru a schimba chei, a autentifica identități, semnături digitale
- Hibrid: pentru aproape toate cazurile reale de utilizare (TLS, PGP, Signal Protocol)
- Hashing (Argon2, SHA-256): Funcție unidirecțională, ireversibilă
AES-256-GCM: Implementare corectă
AES-256-GCM (Standard avansat de criptare cu cheie de 256 de biți în modul Galois/Counter) și standardul de facto pentru criptarea simetrică autentificată. Modul GCM are două proprietăți critice care îl fac superior alternativelor precum AES-CBC sau AES-ECB:
- Confidențialitate: datele sunt criptate și nu pot fi citite fără cheie
- Autentificare (AEAD): produce o etichetă de autentificare care garantează integritatea a datelor. Dacă cineva modifică textul cifrat, decriptarea eșuează cu o eroare explicită. AES-ECB și AES-CBC nu au această proprietate: un atacator poate modifica textul cifrat fără el pe care decriptarea îl observă (atac de tip bit-flipping).
Erori critice cu AES de evitat
- Nu reutilizați niciodată IV-ul cu aceeași cheie: în AES-GCM, reutilizați IV+cheie compromite complet confidențialitatea. Generați întotdeauna un IV aleatoriu de 12 octeți pentru fiecare operație de criptare.
- Nu utilizați niciodată AES-ECB: Modul ECB produce aceeași ieșire pentru același lucru intrare (clasicul „pingguin ECB”), făcând vizibile modelele din date.
- Nu codificați niciodată cheia: cheia trebuie să provină din variabilele de mediu, KMS (Key Management Service) sau HSM, niciodată din codul sursă.
- Verificați întotdeauna eticheta de autentificare înainte de a utiliza datele decriptate.
Iată o implementare completă și corectă a AES-256-GCM în Node.js cu modulul
crypto încorporate:
// crypto-utils.ts - Implementazione AES-256-GCM per Node.js
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
// ============================================================
// COSTANTI - non hardcodate, vengono da env/KMS in produzione
// ============================================================
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12; // 96 bit - raccomandato per GCM
const AUTH_TAG_LENGTH = 16; // 128 bit - lunghezza massima del tag
const KEY_LENGTH = 32; // 256 bit
// Interfaccia per il risultato della cifratura
interface EncryptedData {
iv: string; // base64
ciphertext: string; // base64
authTag: string; // base64
}
// ============================================================
// DERIVAZIONE CHIAVE da password (per chiavi derivate da secret)
// In produzione preferisci chiavi generate da KMS/HSM
// ============================================================
function deriveKey(password: string, salt: Buffer): Buffer {
// scrypt e memory-hard: resistente a brute-force su GPU
return scryptSync(password, salt, KEY_LENGTH, {
N: 32768, // CPU/memory cost (2^15)
r: 8,
p: 1,
});
}
// ============================================================
// CIFRATURA
// ============================================================
export function encrypt(plaintext: string, keyHex: string): EncryptedData {
// Chiave da hex string (32 byte = 64 caratteri hex)
const key = Buffer.from(keyHex, 'hex');
if (key.length !== KEY_LENGTH) {
throw new Error(`Chiave AES-256 richiede 32 byte, ricevuti ${key.length}`);
}
// IV casuale e unico per ogni operazione - CRITICO
const iv = randomBytes(IV_LENGTH);
// Crea cipher GCM
const cipher = createCipheriv(ALGORITHM, key, iv, {
authTagLength: AUTH_TAG_LENGTH,
});
// Cifra i dati
const encryptedBuffer = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
// Estrai il tag di autenticazione DOPO cipher.final()
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('base64'),
ciphertext: encryptedBuffer.toString('base64'),
authTag: authTag.toString('base64'),
};
}
// ============================================================
// DECIFRATURA
// ============================================================
export function decrypt(encrypted: EncryptedData, keyHex: string): string {
const key = Buffer.from(keyHex, 'hex');
const iv = Buffer.from(encrypted.iv, 'base64');
const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
const authTag = Buffer.from(encrypted.authTag, 'base64');
const decipher = createDecipheriv(ALGORITHM, key, iv, {
authTagLength: AUTH_TAG_LENGTH,
});
// Imposta il tag per la verifica dell'integrita
decipher.setAuthTag(authTag);
try {
// Se il tag non corrisponde, decipher.final() lancia un errore
const decryptedBuffer = Buffer.concat([
decipher.update(ciphertext),
decipher.final(), // Lancia Error se authTag non valido
]);
return decryptedBuffer.toString('utf8');
} catch (err) {
// Dati manomessi o chiave errata
throw new Error('Decifrazione fallita: dati corrotti o chiave non valida');
}
}
// ============================================================
// USO ESEMPIO
// ============================================================
// La chiave viene da una variabile d'ambiente (mai hardcodata)
const AES_KEY = process.env['AES_256_KEY']!; // 64 caratteri hex = 32 byte
const datiSensibili = JSON.stringify({
carta: '4111111111111111',
scadenza: '12/26',
cvv: '123',
});
const cifrati = encrypt(datiSensibili, AES_KEY);
console.log('Cifrato:', cifrati);
// Output: { iv: 'abc...', ciphertext: 'xyz...', authTag: 'def...' }
const decifrati = decrypt(cifrati, AES_KEY);
console.log('Decifrato:', decifrati);
// Output: {"carta":"4111111111111111","scadenza":"12/26","cvv":"123"}
RSA și ECC: Semnături digitale și criptare asimetrică
În 2025, alegerea între RSA și ECC (Elliptic Curve Cryptography) este clară: pentru sisteme noi, prefer ETC. Pentru același nivel de securitate, cheile ECC sunt drastic mai mici iar operațiunile sunt semnificativ mai rapide. O cheie ECDSA P-256 pe 256 de biți oferă aceeași securitate ca o cheie RSA pe 3072 de biți, cu operațiuni de semnare de 10-15 ori mai rapide.
| Algoritm | Dimensiunea cheii | Nivel de securitate | Performanță relativă | Recomandat 2025 |
|---|---|---|---|---|
| RSA-2048 | 2048 de biți | 112 biți | Linia de referință (1x) | Numai pentru compatibilitatea moștenită |
| RSA-3072 | 3072 biți | 128 de biți | 0,3x | Acceptabil, dar prefer ETC |
| ECDSA P-256 | 256 de biți | 128 de biți | De 10 ori mai rapid decât RSA-3072 | Da, pentru TLS și semnături |
| Ed25519 | 256 de biți | 128 de biți | De 15 ori mai rapid decât RSA-3072 | Da, de preferat pentru semnăturile digitale |
Ed25519 (Edwards-curve Digital Signature Algorithm) și alegerea modernă pentru semnături digitale: este mai rapid decât ECDSA, nu suferă de probleme de implementare legate de nu o generație și este acceptat de Node.js, OpenSSL 3.x și de toate browserele moderne. SSH, Signal și multe aplicații de înaltă securitate îl folosesc implicit.
// digital-signatures.ts - Ed25519 e RSA con Node.js crypto
import {
generateKeyPairSync,
createSign,
createVerify,
KeyObject,
} from 'crypto';
// ============================================================
// GENERAZIONE COPPIA DI CHIAVI Ed25519
// ============================================================
export function generateEd25519KeyPair(): { publicKey: string; privateKey: string } {
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
// In produzione: cifra la chiave privata con una passphrase
// cipher: 'aes-256-cbc',
// passphrase: process.env['KEY_PASSPHRASE'],
},
});
return { publicKey, privateKey };
}
// ============================================================
// FIRMA DIGITALE con Ed25519
// ============================================================
export function signData(data: string, privateKeyPem: string): string {
const signer = createSign('SHA512'); // Ed25519 usa SHA-512 internamente
signer.update(data, 'utf8');
signer.end();
const signature = signer.sign(privateKeyPem);
return signature.toString('base64');
}
// ============================================================
// VERIFICA FIRMA
// ============================================================
export function verifySignature(
data: string,
signature: string,
publicKeyPem: string
): boolean {
const verifier = createVerify('SHA512');
verifier.update(data, 'utf8');
verifier.end();
try {
return verifier.verify(publicKeyPem, Buffer.from(signature, 'base64'));
} catch {
return false;
}
}
// ============================================================
// GENERAZIONE COPPIA RSA-4096 (per sistemi legacy o interoperabilità)
// ============================================================
export function generateRSAKeyPair(): { publicKey: string; privateKey: string } {
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 4096, // 4096 bit nel 2025 per nuovi sistemi RSA
publicExponent: 0x10001, // 65537 - valore standard e sicuro
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
return { publicKey, privateKey };
}
// ============================================================
// USO COMPLETO
// ============================================================
const { publicKey, privateKey } = generateEd25519KeyPair();
const payload = JSON.stringify({
userId: 'usr_123',
action: 'transfer',
amount: 1500,
timestamp: Date.now(),
});
const firma = signData(payload, privateKey);
console.log('Firma base64:', firma);
const isValid = verifySignature(payload, firma, publicKey);
console.log('Firma valida:', isValid); // true
// Prova con dati manomessi
const payloadManomesso = payload.replace('1500', '15000');
const isValidTampered = verifySignature(payloadManomesso, firma, publicKey);
console.log('Firma valida su dati manomessi:', isValidTampered); // false
Hashing parole: Argon2id, bcrypt și SHA-256
Hashingul parolelor este unul dintre cele mai greșit înțelese aspecte ale securității aplicațiilor. The principiu fundamental și simplu: nu trebuie să poți recupera niciodată o parolă din baza de date. Dacă baza de date este compromisă, atacatorul nu trebuie să poată urmări parolele originale. Acesta este motivul pentru care hashingul parolei necesită algoritmi specifici, radical diferiți de cele utilizate pentru integritatea datelor.
SHA-256 Nu pentru parole
SHA-256 este foarte rapid (miliarde de operații pe secundă pe GPU), ceea ce este excelent pentru integritatea datelor, dar catastrofală pentru parole. Un atacator cu un RTX 4090 poate testați peste 10 miliarde SHA-256/secundă, creând un întreg dicționar de parole comune crackabil în câteva secunde. Parolele necesită algoritmi greu de memorie și costisitoare din punct de vedere computațional.
- SHA-256: pentru sume de control, HMAC, derivare de token. NICIODATĂ pentru parole.
- MD5, SHA-1: de asemenea, depreciat pentru sumele de control. Nu le folosiți în niciun cod nou.
- bcrypt: sigur, testat în luptă, folosiți-l dacă este deja integrat în sistemul dumneavoastră.
- Argon2id: Standardul de aur OWASP 2025 pentru aplicații noi.
OWASP recomandă Argon2id ca prima alegere pentru aplicatii noi. Iar cel câștigător al Concursului Password Hashing (2015) și este conceput pentru a fi rezistent la hacking Atacurile GPU și ASIC datorită naturii sale dure de memorie: necesită o cantitate semnificativă de RAM pentru a calcula hash-ul, făcând atacurile paralele mult mai costisitoare.
// password-hashing.ts - Argon2id e bcrypt per Node.js
import argon2 from 'argon2';
import bcrypt from 'bcrypt';
import { createHmac, timingSafeEqual } from 'crypto';
// ============================================================
// ARGON2ID - Raccomandato OWASP 2025 per nuove applicazioni
// ============================================================
// Parametri OWASP minimi per Argon2id
const ARGON2_OPTIONS: argon2.Options = {
type: argon2.argon2id, // id = combinazione di i (data-independent) e d (data-dependent)
memoryCost: 19456, // 19 MiB - minimo OWASP
timeCost: 2, // 2 iterazioni - minimo OWASP
parallelism: 1, // 1 thread
// Per sistemi ad alta sicurezza (es. admin, banche):
// memoryCost: 65536, // 64 MiB
// timeCost: 3,
};
export async function hashPasswordArgon2(password: string): Promise<string> {
// argon2 gestisce automaticamente il salt casuale
return argon2.hash(password, ARGON2_OPTIONS);
}
export async function verifyPasswordArgon2(
hashedPassword: string,
candidatePassword: string
): Promise<boolean> {
try {
return await argon2.verify(hashedPassword, candidatePassword);
} catch {
return false;
}
}
// Controlla se il hash necessità di essere aggiornato (rehashing)
export async function needsRehash(hash: string): Promise<boolean> {
return argon2.needsRehash(hash, ARGON2_OPTIONS);
}
// ============================================================
// BCRYPT - Per sistemi esistenti (cost factor >= 12)
// ============================================================
const BCRYPT_ROUNDS = 12; // Minimo OWASP; 14 per sistemi critici
export async function hashPasswordBcrypt(password: string): Promise<string> {
// bcrypt tronca a 72 byte - usare pre-hashing per password lunghe
if (password.length > 72) {
// Pre-hash con SHA-256 per password lunghe (prevenire DoS)
const preHashed = createHmac('sha256', process.env['BCRYPT_PEPPER']!)
.update(password)
.digest('hex');
return bcrypt.hash(preHashed, BCRYPT_ROUNDS);
}
return bcrypt.hash(password, BCRYPT_ROUNDS);
}
export async function verifyPasswordBcrypt(
hashedPassword: string,
candidatePassword: string
): Promise<boolean> {
try {
// bcrypt.compare usa timing-safe comparison internamente
return await bcrypt.compare(candidatePassword, hashedPassword);
} catch {
return false;
}
}
// ============================================================
// SHA-256 per HMAC e verifiche di integrita (NON password)
// ============================================================
export function computeHMAC(data: string, secret: string): string {
return createHmac('sha256', secret).update(data, 'utf8').digest('hex');
}
export function verifyHMAC(data: string, secret: string, expected: string): boolean {
const computed = createHmac('sha256', secret).update(data, 'utf8').digest('hex');
// timingSafeEqual previene timing attacks
const computedBuffer = Buffer.from(computed, 'hex');
const expectedBuffer = Buffer.from(expected, 'hex');
if (computedBuffer.length !== expectedBuffer.length) return false;
return timingSafeEqual(computedBuffer, expectedBuffer);
}
// ============================================================
// PATTERN DI AUTENTICAZIONE COMPLETO
// ============================================================
async function authenticationFlow() {
// Registrazione
const password = 'MyS3cur3P4ss!';
const hash = await hashPasswordArgon2(password);
// Salva hash nel DB: utente.passwordHash = hash
// Login
const isValid = await verifyPasswordArgon2(hash, password);
console.log('Login valido:', isValid); // true
// Rehashing automatico (aggiorna parametri senza invalidare le sessioni)
if (await needsRehash(hash)) {
const newHash = await hashPasswordArgon2(password);
// Aggiorna il DB con newHash
console.log('Password rehashed con parametri aggiornati');
}
// Timing-safe per ID utente (previene user enumeration timing attacks)
const userId1 = Buffer.from('user_abc123');
const userId2 = Buffer.from('user_abc123');
console.log('IDs uguali (safe):', timingSafeEqual(userId1, userId2)); // true
}
TLS 1.3: Configurare sigură în Node.js
TLS 1.3 (RFC 8446, 2018) este protocolul de transport securizat actual. În comparație cu TLS 1.2 introduce îmbunătățiri semnificative: strângere de mână mai rapidă (1-RTT în loc de 2-RTT, cu Suport 0-RTT pentru sesiunile reluate), suite de criptare simplificate și mai sigure (ștergeți toți algoritmii slabi, inclusiv RC4, DES, MD5 pentru MAC, schimbul de chei RSA) și Perfect Secretul de transmitere obligatoriu prin ECDHE.
Există doar 5 suite de criptare în TLS 1.3 (față de zeci în TLS 1.2) și toate folosesc AEAD (Criptare autentificată cu date asociate):
TLS_AES_256_GCM_SHA384- RecomandatTLS_AES_128_GCM_SHA256- AcceptabilTLS_CHACHA20_POLY1305_SHA256- De preferat pe dispozitive fără AES hardware
// tls-config.ts - Configurazione TLS 1.3 sicura per Node.js HTTPS
import https from 'https';
import fs from 'fs';
import { TLSSocket } from 'tls';
// ============================================================
// CONFIGURAZIONE HTTPS SERVER CON TLS 1.3
// ============================================================
const tlsOptions: https.ServerOptions = {
// Certificato e chiave privata (da file o KMS)
cert: fs.readFileSync('/etc/ssl/certs/server.crt'),
key: fs.readFileSync('/etc/ssl/private/server.key'),
// Forza TLS 1.3 (disabilita versioni precedenti)
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
// Cipher suite TLS 1.3 (ordine indica preferenza)
ciphers: [
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_128_GCM_SHA256',
].join(':'),
// HSTS header viene aggiunto dall'app, non da TLS
// (vedi Express middleware sotto)
// Session tickets: disabilita per PFS rigorosa
// (TLS 1.3 ha PFS by default, ma session tickets possono ridurla)
sessionTimeout: 300, // 5 minuti max
// Mutual TLS (mTLS) - opzionale per API interne
// requestCert: true,
// rejectUnauthorized: true,
// ca: fs.readFileSync('/etc/ssl/ca.crt'),
};
// ============================================================
// EXPRESS + HTTPS + SECURITY HEADERS
// ============================================================
import express from 'express';
import helmet from 'helmet';
const app = express();
// Helmet per security headers (include HSTS, CSP, etc.)
app.use(helmet({
hsts: {
maxAge: 31536000, // 1 anno
includeSubDomains: true,
preload: true, // Includi in HSTS preload list
},
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
upgradeInsecureRequests: [], // Forza HTTPS per risorse HTTP
},
},
}));
const server = https.createServer(tlsOptions, app);
server.listen(443, () => console.log('HTTPS con TLS 1.3 attivo'));
// ============================================================
// VERIFICA VERSIONE TLS PER OGNI RICHIESTA
// ============================================================
app.use((req, res, next) => {
const socket = req.socket as TLSSocket;
const tlsVersion = socket.getProtocol?.();
if (tlsVersion !== 'TLSv1.3') {
// Log per audit (non esporre info al client)
console.warn(`Connessione con ${tlsVersion} da ${req.ip} - rifiutata`);
return res.status(426).json({
error: 'TLS 1.3 richiesto',
});
}
next();
});
// ============================================================
// CERTIFICATI: rotazione automatica con Let's Encrypt + Certbot
// ============================================================
// Comando certbot per rinnovo automatico:
// certbot certonly --webroot -w /var/www/html -d example.com
// crontab: 0 2 * * 1 certbot renew --quiet --post-hook "systemctl reload nginx"
Managementul cheilor: cel mai frecvent punct slab
Cel mai robust sistem criptografic din lume și inutil dacă cheile sunt prost gestionate. Cel mai frecvent vector de atac nu este „ruperea AES-256”, ci găsirea cheii AES-256 în fișierul de mediu trimis în GitHub, în codul sursă sau în jurnalele de sistem.
În 2024, GitHub Secret Scanning a detectat peste 39 de milioane de secrete expuse în depozite public. Majoritatea erau chei API, dar o parte semnificativă erau chei criptografice. Daunele rezultate din expunerea unei chei simetrice sunt catastrofale: toate datele criptate cu cheia respectivă sunt compromise.
Ierarhia de securitate pentru managementul cheilor
-
Nivelul 1 - Minim acceptabil: variabilele de mediu (nu în .env
angajat la git). STATELE UNITE ALE AMERICII
.env.localsi adauga*.env*al.gitignore. - Nivelul 2 - Producția de aplicații mici: servicii de gestionare a secretelor cum ar fi HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager.
- Nivelul 3 - Securitate ridicată: Modul de securitate hardware (HSM) sau Servicii KMS (Key Management Service) care efectuează operațiuni criptografice în interiorul hardware inviolabil, fără a expune niciodată cheia în clar.
// key-management.ts - Pattern sicuri per gestire chiavi crittografiche
// ============================================================
// CARICAMENTO SICURO DELLE CHIAVI (da ambiente, mai hardcoded)
// ============================================================
function loadCryptoKeys(): { aesKey: string; hmacSecret: string } {
const aesKey = process.env['AES_256_KEY'];
const hmacSecret = process.env['HMAC_SECRET'];
if (!aesKey || aesKey.length !== 64) {
throw new Error('AES_256_KEY mancante o non valida (deve essere 64 hex chars = 32 byte)');
}
if (!hmacSecret || hmacSecret.length < 32) {
throw new Error('HMAC_SECRET mancante o troppo corta (minimo 32 caratteri)');
}
// Verifica che la chiave sia effettivamente hex valida
if (!/^[0-9a-fA-F]{64}$/.test(aesKey)) {
throw new Error('AES_256_KEY non e una stringa hex valida');
}
return { aesKey, hmacSecret };
}
// ============================================================
// GENERAZIONE CHIAVE SICURA (per setup iniziale)
// ============================================================
import { randomBytes } from 'crypto';
export function generateSecureKey(lengthBytes: number = 32): string {
return randomBytes(lengthBytes).toString('hex');
}
// Per generare una nuova chiave AES-256:
// node -e "const {randomBytes}=require('crypto'); console.log(randomBytes(32).toString('hex'))"
// Output: e6f7a8b9c0d1e2f3... (64 char hex)
// Salva il valore in: AWS Secrets Manager / Azure Key Vault / .env.local
// ============================================================
// KEY ROTATION PATTERN
// ============================================================
interface KeyVersion {
version: number;
key: string;
activeFrom: Date;
activeTo?: Date; // undefined = chiave attiva
}
class KeyRotationManager {
private keys: Map<number, KeyVersion> = new Map();
private currentVersion: number;
constructor(keys: KeyVersion[]) {
keys.forEach(k => this.keys.set(k.version, k));
// La versione più recente con activeTo undefined e quella attiva
this.currentVersion = Math.max(...keys.map(k => k.version));
}
// Cifra sempre con la chiave più recente
encrypt(data: string): EncryptedPayload {
const currentKey = this.keys.get(this.currentVersion)!;
const encrypted = encrypt(data, currentKey.key);
return {
...encrypted,
keyVersion: this.currentVersion, // Includi versione nel payload
};
}
// Decifra usando la versione specificata nel payload (supporta old keys)
decrypt(payload: EncryptedPayload): string {
const key = this.keys.get(payload.keyVersion);
if (!key) {
throw new Error(`Versione chiave ${payload.keyVersion} non trovata`);
}
return decrypt(payload, key.key);
}
}
interface EncryptedPayload {
iv: string;
ciphertext: string;
authTag: string;
keyVersion: number;
}
// ============================================================
// INTEGRAZIONE CON AWS SECRETS MANAGER (esempio)
// ============================================================
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
async function getKeyFromAWS(secretName: string): Promise<string> {
const client = new SecretsManagerClient({ region: 'eu-west-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
if (!response.SecretString) {
throw new Error(`Secret ${secretName} non trovato`);
}
return response.SecretString;
}
Web Crypto API: Criptare în browser
API-ul Web Crypto (disponibil în toate browserele moderne și în Node.js 15+) aduce
criptare direct în browser fără dependențe de biblioteci JavaScript externe.
Și pe baza operațiilor asincrone, folosește memoria care nu este expusă JavaScript (cheile
CryptoKey nu sunt extrase în mod implicit) și sunt implementate la nivel
nativ din browser pentru performanțe optime.
Un caz de utilizare tipic este criptarea end-to-end în browser: datele sunt criptate înainte de a fi trimis către server, care nu are niciodată acces la datele text simplu. Aceasta model și utilizat de aplicații precum managerii de parole, note securizate și clienți mesagerie criptată.
// web-crypto.service.ts - Angular service per cifratura nel browser
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class WebCryptoService {
private readonly subtle = window.crypto.subtle;
// ============================================================
// GENERAZIONE CHIAVE AES-256-GCM nel browser
// ============================================================
async generateAESKey(extractable = false): Promise<CryptoKey> {
return this.subtle.generateKey(
{
name: 'AES-GCM',
length: 256,
},
extractable, // false = chiave non estraibile (più sicuro)
['encrypt', 'decrypt']
);
}
// ============================================================
// CIFRATURA AES-256-GCM nel browser
// ============================================================
async encrypt(
data: string,
key: CryptoKey
): Promise<{ iv: string; ciphertext: string }> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
// IV casuale di 12 byte (96 bit) - CRITICO: unico per ogni cifratura
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const cipherBuffer = await this.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
dataBuffer
);
return {
iv: this.bufferToBase64(iv.buffer),
ciphertext: this.bufferToBase64(cipherBuffer),
// In AES-GCM del browser, il tag e concatenato al ciphertext
};
}
// ============================================================
// DECIFRATURA AES-256-GCM nel browser
// ============================================================
async decrypt(
encryptedData: { iv: string; ciphertext: string },
key: CryptoKey
): Promise<string> {
const iv = this.base64ToBuffer(encryptedData.iv);
const cipherBuffer = this.base64ToBuffer(encryptedData.ciphertext);
const decryptedBuffer = await this.subtle.decrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
cipherBuffer
);
const decoder = new TextDecoder();
return decoder.decode(decryptedBuffer);
}
// ============================================================
// DERIVAZIONE CHIAVE DA PASSWORD (PBKDF2)
// Per chiavi derivate da password utente
// ============================================================
async deriveKeyFromPassword(
password: string,
salt: Uint8Array,
iterations = 310000 // OWASP minimo 2025 per PBKDF2-SHA-256
): Promise<CryptoKey> {
const encoder = new TextEncoder();
// Importa la password come material grezzo
const keyMaterial = await this.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
// Deriva la chiave AES
return this.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false, // Non estraibile
['encrypt', 'decrypt']
);
}
// ============================================================
// FIRMA DIGITALE con ECDSA P-256 nel browser
// ============================================================
async generateSigningKeyPair(): Promise<CryptoKeyPair> {
return this.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256',
},
false,
['sign', 'verify']
);
}
async sign(data: string, privateKey: CryptoKey): Promise<string> {
const encoder = new TextEncoder();
const signature = await this.subtle.sign(
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
privateKey,
encoder.encode(data)
);
return this.bufferToBase64(signature);
}
// ============================================================
// UTILS
// ============================================================
private bufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
return btoa(String.fromCharCode(...bytes));
}
private base64ToBuffer(base64: string): ArrayBuffer {
const binaryStr = atob(base64);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return bytes.buffer;
}
}
Criptografie post-cuantică: pregătirea pentru 2030
Calculatoarele cuantice nu reprezintă încă o amenințare practică la adresa criptografiei de astăzi, dar NIST a publicat primele standarde post-cuantice definitive în 2024 (FIPS 203, 204, 205) tocmai pentru că timpii de migrare sunt lungi și riscul este „recoltați acum, decriptați mai târziu” si reale. Un actor rău intenționat care colectează astăzi comunicații criptate RSA ar putea descifrați-le în viitor cu un computer cuantic suficient de puternic.
Standarde post-cuantice NIST (august 2024)
- FIPS 203 - ML-KEM (Kyber): Mecanism de încapsulare cheie pentru schimb de chei. Înlocuiește schimbul de chei ECDH și RSA. Recomandat pentru hibridul TLS 1.3 (X25519+MLKEM-768).
- FIPS 204 - ML-DSA (Dilithium): Semnătură digitală post-cuantică. Înlocuiește ECDSA și RSA-PSS. Chei mai mari, dar sigure împotriva algoritmilor Shor.
- FIPS 205 - SLH-DSA (SPHINCS+): Semnătură digitală bazată pe hash, plus conservator. Util ca backup algoritmic.
- Strategia recomandată 2025-2030: abordare hibridă. Utilizați X25519+ ML-KEM-768 pentru TLS (securitate clasică + post-cuantică simultan).
Pentru dezvoltatori, pregătirea pentru criptografia post-cuantică înseamnă astăzi în principal:
-
Criptoagilitate: sisteme de proiectare pentru a putea schimba algoritmii
fără a rescrie totul. Nu codificați „AES-256” sau „RSA” în bazele de date; utilizați un câmp
algorithm_version. - Inventar criptografic: Aflați unde și cum este utilizat RSA/ECC în aplicația dvs. Primul pas este vizibilitatea.
- TLS hibrid: activați X25519+MLKEM-768 pe serverele care acceptă TLS OpenSSL 3.5+ sau BoringSSL (Chrome îl folosește din 2023).
- Fără migrare urgentă pentru datele în repaus cu taste simetrice AES-256: AES-256 și deja rezistent la computerele cuantice (Grover reduce securitatea eficientă 128 de biți, încă foarte sigur).
// crypto-agility.ts - Pattern per crypto agility
// Permette di aggiornare gli algoritmi senza riscrivere l'intera app
type AlgorithmVersion = 'v1' | 'v2' | 'v3';
interface CryptoStrategy {
version: AlgorithmVersion;
encrypt: (data: string, key: string) => Promise<string>;
decrypt: (data: string, key: string) => Promise<string>;
description: string;
}
// Registro degli algoritmi supportati
const cryptoStrategies: Record<AlgorithmVersion, CryptoStrategy> = {
v1: {
version: 'v1',
description: 'AES-256-CBC (legacy, no AEAD)',
encrypt: async (data, key) => encryptAESCBC(data, key), // vecchio
decrypt: async (data, key) => decryptAESCBC(data, key),
},
v2: {
version: 'v2',
description: 'AES-256-GCM (current standard)',
encrypt: async (data, key) => {
const result = encrypt(data, key);
return JSON.stringify(result);
},
decrypt: async (data, key) => decrypt(JSON.parse(data), key),
},
v3: {
version: 'v3',
description: 'AES-256-GCM + ML-KEM key encapsulation (post-quantum ready)',
encrypt: async (data, key) => encryptWithPQC(data, key),
decrypt: async (data, key) => decryptWithPQC(data, key),
},
};
const CURRENT_VERSION: AlgorithmVersion = 'v2';
// Struttura dati nel DB include sempre la versione dell'algoritmo
interface StoredSecret {
data: string;
algorithmVersion: AlgorithmVersion;
encryptedAt: string; // ISO date
}
async function storeEncrypted(plaintext: string, key: string): Promise<StoredSecret> {
const strategy = cryptoStrategies[CURRENT_VERSION];
const encryptedData = await strategy.encrypt(plaintext, key);
return {
data: encryptedData,
algorithmVersion: CURRENT_VERSION,
encryptedAt: new Date().toISOString(),
};
}
// La decifratura usa sempre la versione registrata nel record
async function retrieveDecrypted(stored: StoredSecret, key: string): Promise<string> {
const strategy = cryptoStrategies[stored.algorithmVersion];
if (!strategy) {
throw new Error(`Versione algoritmo '${stored.algorithmVersion}' non supportata`);
}
return strategy.decrypt(stored.data, key);
}
// Migrazione automatica: aggiorna al formato corrente al prossimo accesso
async function migrateIfNeeded(
stored: StoredSecret,
key: string
): Promise<StoredSecret | null> {
if (stored.algorithmVersion === CURRENT_VERSION) return null; // Già aggiornato
// Decifra con vecchio algoritmo, ri-cifra con il nuovo
const plaintext = await retrieveDecrypted(stored, key);
const updated = await storeEncrypted(plaintext, key);
console.log(`Migrato da ${stored.algorithmVersion} a ${CURRENT_VERSION}`);
return updated;
}
Lista de verificare a criptării pentru aplicații angulare
Aplicațiile unghiulare au cerințe criptografice specifice. Browserul și un mediu neîncrezător: orice secret din codul JavaScript este accesibil oricui îl deschide instrumente de dezvoltare. Acest lucru schimbă fundamental ceea ce are sens să faci din partea clientului.
Reguli de aur pentru criptare în Angular
- Nu codificați niciodată cheile criptografice în cod TypeScript/JavaScript. Cheile simetrice trebuie să rămână pe server.
-
Folosiți întotdeauna HTTPS - configurați Content-Security-Policy cu
upgrade-insecure-requestsși HSTS. Nu manipulați niciodată date sensibile pe HTTP, de asemenea, în dezvoltare. - Pentru criptarea E2E în browser: Utilizați API-ul Web Crypto cu chei derivat din parola utilizatorului (PBKDF2/Argon2 wasm). Cheia nu pleacă niciodată browserul.
-
Jetoane JWT: verificarea semnăturii, expirare (
exp), emitent (iss) și publicul (aud). Nu efectuați inginerie inversă fără a verifica. - localStorage vs sessionStorage: ambele accesibile din JS. Nu salvați datele ultra-sensibile (chei private, jetoane cu durată lungă de viață) în stocare accesibil prin XSS. Preferați cookie-urile HttpOnly pentru jetoanele de sesiune.
-
Aleatorie: utilizați întotdeauna
window.crypto.getRandomValues()pentru date aleatorii securizate criptografic, niciodatăMath.random().
// crypto-checklist.angular.ts - Pattern sicuri specifici per Angular
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
// ============================================================
// 1. TOKEN JWT: VERIFICA COMPLETA (lato client = solo decodifica)
// ============================================================
interface JWTPayload {
sub: string;
iss: string;
aud: string | string[];
exp: number;
iat: number;
[key: string]: unknown;
}
function decodeJWT(token: string): JWTPayload {
// Decodifica il payload (non verifica la firma - questa avviene sul server)
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Token JWT non valido');
try {
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
return payload as JWTPayload;
} catch {
throw new Error('Payload JWT non decodificabile');
}
}
function isTokenExpired(token: string): boolean {
try {
const payload = decodeJWT(token);
const nowSeconds = Math.floor(Date.now() / 1000);
return payload.exp <= nowSeconds;
} catch {
return true; // Se non decodificabile, trattalo come scaduto
}
}
// ============================================================
// 2. RANDOMNESS SICURA per CSRF token, nonce, ID temporanei
// ============================================================
function generateSecureToken(lengthBytes = 32): string {
const array = new Uint8Array(lengthBytes);
window.crypto.getRandomValues(array); // Crittograficamente sicuro
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}
// SBAGLIATO: Math.random() non e crittograficamente sicuro
// const insecureToken = Math.random().toString(36); // MAI usare per token di sicurezza
// ============================================================
// 3. HASHING SHA-256 nel browser per integrità (non password)
// ============================================================
async function sha256Browser(data: string): Promise<string> {
const encoder = new TextEncoder();
const hashBuffer = await window.crypto.subtle.digest('SHA-256', encoder.encode(data));
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Uso: verifica integrita file scaricati, PKCE code challenge
async function computePKCEChallenge(verifier: string): Promise<string> {
const hash = await window.crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(verifier)
);
// Base64URL encoding (senza padding)
return btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// ============================================================
// 4. HTTPCLIENT: intercettore per headers di sicurezza
// ============================================================
@Injectable()
export class SecurityHeadersInterceptor {
intercept(req: any, next: any) {
// Aggiungi header di sicurezza a tutte le richieste autenticate
const secureReq = req.clone({
headers: req.headers
.set('X-Requested-With', 'XMLHttpRequest') // Mitiga alcuni CSRF
.set('X-Content-Type-Options', 'nosniff'), // Lato client hint
});
return next.handle(secureReq);
}
}
// ============================================================
// 5. CIFRATURA LOCALE dati sensibili prima di sessionStorage
// ============================================================
@Injectable({ providedIn: 'root' })
export class SecureStorageService {
private sessionKey: CryptoKey | null = null;
// Inizializza con chiave di sessione in memoria (non in storage)
async initialize(): Promise<void> {
this.sessionKey = await window.crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
false, // Non estraibile
['encrypt', 'decrypt']
);
// La chiave e in memoria: sparisce al refresh della pagina
}
async setItem(key: string, value: string): Promise<void> {
if (!this.sessionKey) await this.initialize();
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.sessionKey!,
new TextEncoder().encode(value)
);
const stored = JSON.stringify({
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted)),
});
sessionStorage.setItem(key, stored);
}
}
Cele mai frecvente 10 erori criptografice
Criptografia este un domeniu în care metacunoașterea este mai importantă decât detaliile tehnice. A ști ce să NU faci este adesea mai valoroasă decât a ști cum să folosești fiecare API. Iată erorile care apar sistematic în recenziile codurilor de securitate:
| Greşeală | Impact | Soluţie |
|---|---|---|
| MD5/SHA-1 pentru hashing parole | Critic - crackable în câteva secunde | Argon2id sau bcrypt cu cost >= 12 |
| AES-ECB (nr. IV) | High - modele vizibile în textul cifrat | AES-256-GCM cu IV aleatoriu |
| IV fix sau incremental | Critic - suprascrie securitatea GCM | randomBytes(12) pentru fiecare criptare |
| Cheile codificate în cod | Critic - expune toate datele criptate | AWS/Azure KMS, variabile de mediu |
| RSA-PKCS1v1.5 pentru criptare | Ridicat - vulnerabil la Bleichenbacher | RSA-OAEP sau ECDH pentru schimbul de chei |
| JWT fără verificarea semnăturii | Critic - escaladarea privilegiilor | Verificați întotdeauna semnătura pe server |
| Compararea hashurilor cu == | Mediu - atac de timp | timingSafeEqual() de către Node.js crypto |
| Math.random() pentru jetoane | High - jetoane previzibile | crypto.randomBytes() sau getRandomValues() |
| TLS 1.0/1.1 activat | Înalt - Atacul BEAST, POODLE, CRIME | Versiune min: TLSv1.3 |
| Nicio validare a certificatului | Critic - atac MITM | Niciodată NODE_TLS_REJECT_UNAUTHORIZED=0 în producție |
Concluzii
Criptografia practică pentru dezvoltatori nu necesită să fii criptograf: necesită să știi modelele potrivite, evitați anti-modelele cunoscute și alegeți instrumentele potrivite pentru fiecare caz de utilizare. Principiile fundamentale pot fi rezumate în câteva puncte:
- AES-256-GCM pentru criptare simetrică autentificată: IV aleatoriu de 12 octeți, Etichetă de autentificare pe 128 de biți, cheie niciodată în cod.
- Ed25519 sau ECDSA P-256 pentru semnături digitale: preferați ECC decât RSA pentru sisteme noi, RSA-4096 numai pentru compatibilitate vechi.
- Argon2id pentru hashing parole (parametri OWASP: 19 MiB, 2 iterații), bcrypt cu factor de cost 12+ pentru sistemele existente. Niciodată SHA-256 sau MD5 pentru parole.
- TLS 1.3 minim pentru toate serviciile în producție, cu HSTS e suite moderne de criptare. Dezactivați TLS 1.0/1.1/1.2 dacă este posibil.
- Managementul cheilor prin KMS sau manager secret: cea mai robustă cheie devine inutil dacă este expus în depozit.
- Agilitate cripto în sisteme noi: proiectare pentru a putea migra algoritmi fără a rescrie totul, în pregătirea pentru tranziția post-cuantică până în 2030.
Criptarea nu este o caracteristică pe care o adăugați la sfârșit: este o decizie arhitectural care trebuie luat la inceput. Fiecare câmp sensibil din baza de date, fiecare API care transmite date personale, fiecare jeton de autentificare necesită o alegere deliberați asupra algoritmului, managementului cheilor și rotației.
Continuați cu seria: Web Security for Developers
- Articolul precedent: Securitatea lanțului de aprovizionare: audit npm și SBOM - cum să protejăm lanțul de dependențe
- Articolul următor: DevSecOps pentru dezvoltatori: SAST, DAST în CI/CD - integrarea securității în conductă
- Legat de: Securitate API: OAuth 2.1, JWT și Rate Limiting - Cele mai bune practici JWT în detaliu
- Vezi și: serialul DevOps Frontend pentru configurarea implementării securizate







