07 – Praktická kryptografie pro vývojáře: AES-256-GCM, hašování a TLS 1.3
Kryptografie je neviditelným základem moderní kybernetické bezpečnosti. Každé heslo bylo uloženo v databázi, všechna citlivá data přenášená přes HTTPS, každý digitální podpis, který ověřuje a aktualizace softwaru: vše závisí na správně implementovaných kryptografických primitivech. A přesto kryptografie je také jednou z oblastí, ve kterých vývojáři dělají nejzávažnější chyby, často aniž by si to uvědomovali.
Podle OWASP Top 10:2025 je Kryptografická selhání (A02) zůstávají mezi nejlepšími tři nejzávažnější zranitelnosti webových aplikací. Nejsou to žádné exotické útoky vyžadují kvantové počítače: většina kryptografických porušení se vyskytuje v důsledku triviálních chyb. MD5 nebo SHA-1 používané pro hashování hesel. AES v režimu ECB, který vytváří viditelné vzory v šifrovaný text. IV (Initialization Vector) znovu použit. Klíče pevně zakódované do zdrojového kódu. Chyby, k jejichž zneužití není potřeba žádný pokročilý pentest.
Tento článek se zabývá kryptografií z pohledu vývojáře: ne matematickou teorií, ale praktické vzory v Node.js a Web Crypto API. Naučíte se, kdy používat symetrické šifrování vs asymetrické, jak správně implementovat AES-256-GCM, který hashovací algoritmus zvolit pro hesla, jak nakonfigurovat TLS 1.3 a jak se připravit na postkvantové šifrování. Každý sekce obsahuje příklady kódu připraveného pro výrobu s nástrahami, kterým je třeba se vyhnout.
Co se naučíte
- Symetrické vs asymetrické šifrování: kdy použít jaký přístup
- AES-256-GCM: Správná implementace s náhodnou IV a autentizační značkou
- RSA vs ECC (ECDSA, Ed25519): srovnání výkonu a zabezpečení v roce 2025
- Hash hesla pomocí Argon2id a bcrypt: Doporučené parametry OWASP
- SHA-256 pro integritu dat vs hašování hesel: správné použití
- TLS 1.3: Zabezpečené nastavení v Node.js s moderními šifrovacími sadami
- Správa klíčů: vyhněte se pevně zakódovaným klíčům, používejte proměnné prostředí a KMS
- Web Crypto API: Šifrování v prohlížeči bez externích závislostí
- Post-kvantová kryptografie: NIST FIPS 203 (ML-KEM) a příprava na rok 2030
- Úhlový kontrolní seznam: Bezpečné vzory pro manipulaci s citlivými daty ve frontendu
Symetrické vs asymetrické šifrování
Volba mezi symetrickou a asymetrickou kryptografií není záležitostí preference: je to jedna architektonické rozhodnutí, které závisí na řešeném problému. Záměna těchto dvou přístupů vede systematicky na zranitelnosti nebo zbytečně snížený výkon.
Symetrické šifrování k šifrování a dešifrování použijte stejný klíč. A rychle (AES-256-GCM šifruje gigabajty za sekundu na moderním hardwaru) a je vhodný pro šifrování velkých množství dat. Problém a distribuce klíčů: jak je bezpečně sdílet S kým by měl data dešifrovat?
Asymetrické šifrování používá dvojici matematicky souvisejících klíčů: jeden veřejný (který můžete volně šířit) a jeden soukromý (který uchováte v tajnosti). Řeší klíčový distribuční problém, ale řádově pomalejší než symetrický. RSA-2048 šifruje přibližně 250 bajtů za sekundu na operaci, zatímco AES-256-GCM jde až do několika GB/s.
Profesionální vzor kombinuje dva přístupy v hybridním systému, stejně jako TLS: asymmetric encryption is only used to securely exchange a session key symetrický; pak jsou všechna data šifrována tímto klíčem AES. Toto je základ HTTPS, SSH a jakýkoli moderní zabezpečený protokol.
Základní princip
- Symetrické (AES-256-GCM): k hromadnému šifrování dat v klidu a během přenosu
- Asymetrické (RSA/ECC): k výměně klíčů, ověřování identit, digitálních podpisů
- Hybridní: pro téměř všechny skutečné případy použití (TLS, PGP, Signal Protocol)
- Hašování (Argon2, SHA-256): jednosměrná funkce, nevratná
AES-256-GCM: Správná implementace
AES-256-GCM (pokročilý standard šifrování s 256bitovým klíčem v režimu Galois/Counter) a de facto standard pro ověřené symetrické šifrování. Režim GCM má dva kritické vlastnosti, díky nimž je lepší než alternativy, jako je AES-CBC nebo AES-ECB:
- Důvěrnost: data jsou zašifrovaná a bez klíče nečitelná
- Autentizace (AEAD): vytváří ověřovací značku, která zaručuje integritu dat. Pokud někdo upraví šifrovaný text, dešifrování se nezdaří s explicitní chybou. AES-ECB a AES-CBC tuto vlastnost nemají: útočník může změnit šifrovaný text bez ní kterého si dešifrování všimne (bit-flipping attack).
Kritické chyby s AES, kterým je třeba se vyhnout
- Nikdy znovu nepoužívejte IV se stejným klíčem: v AES-GCM znovu použijte IV+klíč zcela narušuje důvěrnost. Vždy vygenerujte náhodnou 12 bajtovou IV pro každého šifrovací operace.
- Nikdy nepoužívejte AES-ECB: ECB mode produces the same output for the same vstup (klasický "ECB tučňák"), díky čemuž jsou vzory v datech viditelné.
- Nikdy nekódujte klíč pevně: klíč musí pocházet z proměnných prostředí, KMS (Key Management Service) nebo HSM, nikdy ze zdrojového kódu.
- Vždy zkontrolujte autentizační značku před použitím dešifrovaných dat.
Zde je úplná a správná implementace AES-256-GCM v Node.js s modulem
crypto vestavěné:
// 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 a ECC: Digitální podpisy a asymetrické šifrování
V roce 2025 je volba mezi RSA a ECC (Elliptic Curve Cryptography) jasná: pro nové systémy, preferuji ETC. Pro stejnou úroveň zabezpečení jsou klíče ECC výrazně menší a operace jsou výrazně rychlejší. 256bitový klíč ECDSA P-256 nabízí stejné zabezpečení jako 3072bitový klíč RSA, s 10-15x rychlejšími operacemi podepisování.
| Algoritmus | Velikost klíče | Úroveň zabezpečení | Relativní výkon | Doporučeno 2025 |
|---|---|---|---|---|
| RSA-2048 | 2048 bitů | 112 bitů | Základní linie (1x) | Pouze pro starší kompatibilitu |
| RSA-3072 | 3072 bitů | 128 bitů | 0,3x | Přijatelné, ale preferuji ETC |
| ECDSA P-256 | 256 bitů | 128 bitů | 10x rychlejší než RSA-3072 | Ano, pro TLS a podpisy |
| Ed25519 | 256 bitů | 128 bitů | 15x rychlejší než RSA-3072 | Ano, preferováno pro digitální podpisy |
Ed25519 (Edwards-curve Digital Signature Algorithm) a moderní volba pro digitální podpisy: je rychlejší než ECDSA, netrpí problémy souvisejícími s implementací generace nonce a je podporována Node.js, OpenSSL 3.xa všemi moderními prohlížeči. SSH, Signal a mnoho vysoce zabezpečených aplikací jej používá ve výchozím nastavení.
// 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
Hašování hesel: Argon2id, bcrypt a SHA-256
Hašování hesel je jedním z nejvíce nepochopených aspektů zabezpečení aplikací. The základní a jednoduchý princip: nikdy nesmíte být schopni obnovit heslo z databáze. Pokud je databáze kompromitována, útočník nesmí být schopen vysledovat původní hesla. To je důvod, proč hašování hesel vyžaduje specifické algoritmy, které se radikálně liší od ty, které se používají pro integritu dat.
SHA-256 není pro hesla
SHA-256 je velmi rychlý (miliardy operací za sekundu na GPU), což je skvělé integrita dat, ale katastrofální pro hesla. Útočník s RTX 4090 může otestujte 10+ miliard SHA-256/s, čímž vytvoříte celý slovník běžných hesel prolomitelné během několika sekund. Hesla vyžadují algoritmy paměťově náročné a výpočetně drahé.
- SHA-256: pro kontrolní součty, HMAC, odvození tokenu. NIKDY pro hesla.
- MD5, SHA-1: také zastaralé pro kontrolní součty. Nepoužívejte je v žádném novém kódu.
- bcrypt: bezpečný, otestovaný v bitvě, použijte jej, pokud je již integrován do vašeho systému.
- Argon2id: Zlatý standard OWASP 2025 pro nové aplikace.
OWASP doporučuje Argon2id jako první volba pro nové aplikace. A vítěz soutěže Password Hashing Competition (2015) a je navržen tak, aby byl odolný vůči hackování Útoky GPU a ASIC kvůli své povaze náročné na paměť: vyžadují značné množství RAM pro výpočet hashe, takže paralelní útoky jsou mnohem nákladnější.
// 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: Zabezpečená konfigurace v Node.js
TLS 1.3 (RFC 8446, 2018) je aktuální protokol zabezpečeného přenosu. Ve srovnání s TLS 1.2 přináší významná vylepšení: rychlejší handshake (1-RTT místo 2-RTT, s Podpora 0-RTT pro obnovené relace), zjednodušené a bezpečnější šifrovací sady (smazat všechny slabé algoritmy včetně RC4, DES, MD5 pro MAC, výměna klíčů RSA) a Perfect Povinné dopředné utajení prostřednictvím ECDHE.
V TLS 1.3 je pouze 5 šifrovacích sad (oproti desítkám v TLS 1.2) a všechny používají AEAD (Ověřené šifrování s přidruženými daty):
TLS_AES_256_GCM_SHA384- DoporučenoTLS_AES_128_GCM_SHA256- PřijatelnéTLS_CHACHA20_POLY1305_SHA256- Preferováno na zařízeních bez hardwarového AES
// 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"
Key Management: Nejčastější slabé místo
Nejrobustnější kryptografický systém na světě a nepoužitelný, pokud jsou klíče špatně spravovány. Nejčastějším vektorem útoku není „prolomení AES-256“, ale nalezení klíče AES-256 v souboru prostředí odevzdaném GitHubu, ve zdrojovém kódu nebo v systémových protokolech.
V roce 2024 odhalilo GitHub Secret Scanning více než 39 milionů tajemství odhalených v úložištích veřejné. Většinou šlo o klíče API, ale významnou část tvořily klíče kryptografické. Poškození způsobené vystavením symetrického klíče je katastrofální: všechna data zašifrované tímto klíčem jsou kompromitovány.
Hierarchie zabezpečení pro správu klíčů
-
Úroveň 1 – Minimálně přijatelné: proměnné prostředí (ne v .env
oddaný git). USA
.env.locala přidat*.env*al.gitignore. - Úroveň 2 – Produkce malých aplikací: služby správce tajemství jako HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager.
- Úroveň 3 – Vysoká bezpečnost: Hardwarový bezpečnostní modul (HSM) popř Služby KMS (Key Management Service), které provádějí kryptografické operace uvnitř hardwaru odolného proti neoprávněné manipulaci, aniž by byl klíč odhalen.
// 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: Šifrování v prohlížeči
Web Crypto API (dostupné ve všech moderních prohlížečích a v Node.js 15+) přináší
šifrování přímo v prohlížeči bez závislostí na externích JavaScriptových knihovnách.
A na základě asynchronních operací používá paměť, která není vystavena JavaScriptu (klíče
CryptoKey nejsou ve výchozím nastavení extrahovatelné) a je implementován na úrovni
nativní z prohlížeče pro optimální výkon.
Typickým případem použití je end-to-end šifrování v prohlížeči: data jsou šifrována před odesláním na server, který nikdy nemá přístup k datům v prostém textu. Toto vzor a používané aplikacemi, jako jsou správci hesel, bezpečné poznámky a klienti zašifrované zprávy.
// 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;
}
}
Post-kvantová kryptografie: Příprava na rok 2030
Kvantové počítače ještě nejsou praktickou hrozbou pro dnešní kryptografii, ale NIST zveřejnil první definitivní postkvantové standardy v roce 2024 (FIPS 203, 204, 205) právě proto, že doba migrace je dlouhá a riziko je „sklízejte nyní, dešifrujte později“ a skutečné. Zákeřný hráč, který dnes shromažďuje komunikaci šifrovanou RSA, by mohl dešifrovat je v budoucnu dostatečně výkonným kvantovým počítačem.
NIST Post-Quantum Standards (srpen 2024)
- FIPS 203 – ML-KEM (Kyber): Klíčový zapouzdřovací mechanismus pro výměnu klíčů. Nahrazuje výměnu klíčů ECDH a RSA. Doporučeno pro TLS 1.3 hybrid (X25519+MLKEM-768).
- FIPS 204 – ML-DSA (Dilithium): Postkvantový digitální podpis. Nahrazuje ECDSA a RSA-PSS. Větší, ale bezpečné klíče proti algoritmům Shor.
- FIPS 205 – SLH-DSA (SPHINCS+): Digitální podpis založený na hash, plus konzervativní. Užitečné jako algoritmická záloha.
- Doporučená strategie 2025–2030: hybridní přístup. Použijte X25519+ ML-KEM-768 pro TLS (klasické + post-kvantové zabezpečení současně).
Pro vývojáře dnes znamená příprava na postkvantovou kryptografii hlavně:
-
Crypto agility: navrhovat systémy tak, aby byly schopny měnit algoritmy
aniž bych vše přepsal. Nekódujte „AES-256“ nebo „RSA“ do databází; použít pole
algorithm_version. - Inventář kryptoměn: Vědět, kde a jak se RSA/ECC používá ve vaší aplikaci. Prvním krokem je viditelnost.
- Hybridní TLS: povolit X25519+MLKEM-768 na serverech, které podporují TLS OpenSSL 3.5+ nebo BoringSSL (Chrome jej používá od roku 2023).
- Žádná naléhavá migrace pro data v klidu with symmetric AES-256 keys: AES-256 a již odolný vůči kvantovým počítačům (Grover snižuje efektivní zabezpečení 128 bit, still very secure).
// 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;
}
Kontrolní seznam šifrování pro úhlové aplikace
Úhlové aplikace mají specifické kryptografické požadavky. Prohlížeč a prostředí nedůvěryhodný: Jakékoli tajemství v kódu JavaScript je přístupné každému, kdo jej otevře vývojové nástroje. To zásadně mění to, co má smysl dělat na straně klienta.
Zlatá pravidla pro šifrování v Angular
- Nikdy nekódujte kryptografické klíče napevno v kódu TypeScript/JavaScript. Symetrické klíče musí zůstat na serveru.
-
Vždy používejte HTTPS - nakonfigurujte zásady zabezpečení obsahu pomocí
upgrade-insecure-requestsa HSTS. Nikdy nemanipulujte s citlivými daty na HTTP také ve vývoji. - Pro E2E šifrování v prohlížeči: Použijte Web Crypto API s klíči odvozené od hesla uživatele (PBKDF2/Argon2 wasm). Klíč neopouští nikdy prohlížeč.
-
Tokeny JWT: ověření podpisu, expirace (
exp), vydavatel (iss) a publikum (aud). Bez kontroly neprovádějte zpětnou analýzu. - localStorage vs sessionStorage: oba přístupné z JS. Ne uložte do úložiště velmi citlivá data (soukromé klíče, tokeny s dlouhou životností). přístupné přes XSS. Upřednostňovat soubory cookie HttpOnly pro tokeny relace.
-
Náhodnost: vždy používat
window.crypto.getRandomValues()u kryptograficky bezpečných náhodných dat nikdyMath.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);
}
}
10 nejčastějších kryptografických chyb
Kryptografie je obor, kde jsou metaznalosti důležitější než technické detaily. Vědět, co NEDĚLAT, je často cennější než vědět, jak používat jednotlivé API. Tady jsou ty chyby se systematicky objevují v recenzích bezpečnostních kódů:
| Chyba | Dopad | Řešení |
|---|---|---|
| MD5/SHA-1 pro hash hesla | Kritické – prolomitelné během několika sekund | Argon2id nebo bcrypt s cenou >= 12 |
| AES-ECB (ne IV) | Vysoká - vzory viditelné v šifrovém textu | AES-256-GCM s náhodnou IV |
| Pevná nebo přírůstková IV | Kritické – přepíše zabezpečení GCM | randomBytes(12) pro každé šifrování |
| Klíče pevně zakódované do kódu | Kritické – zpřístupní všechna zašifrovaná data | AWS/Azure KMS, proměnné prostředí |
| RSA-PKCS1v1.5 pro šifrování | Vysoká – zranitelná vůči Bleichenbacherovi | RSA-OAEP nebo ECDH pro výměnu klíčů |
| JWT bez ověření podpisu | Kritické – eskalace oprávnění | Vždy ověřte podpis na serveru |
| Porovnání hashů s == | Středně načasovaný útok | timingSafeEqual() od krypto Node.js |
| Math.random() pro tokeny | Vysoké - předvídatelné tokeny | crypto.randomBytes() nebo getRandomValues() |
| TLS 1.0/1.1 povoleno | Vysoká - BEAST, PODLE, CRIME útok | minVersion: TLSv1.3 |
| Žádné ověření certifikátu | Kritický - útok MITM | Nikdy NODE_TLS_REJECT_UNAUTHORIZED=0 ve výrobě |
Závěry
Praktická kryptografie pro vývojáře nevyžaduje, abyste byli kryptografem: vyžaduje to, abyste věděli správné vzory, vyhněte se známým anti-vzorům a pro každý z nich vyberte vhodné nástroje případ použití. Základní principy lze shrnout do několika bodů:
- AES-256-GCM pro ověřené symetrické šifrování: 12bajtové náhodné IV, 128bitová autentizační značka, klíč nikdy v kódu.
- Ed25519 nebo ECDSA P-256 pro digitální podpisy: preferujte ECC před RSA nové systémy, RSA-4096 pouze pro starší kompatibilitu.
- Argon2id pro hashování hesla (parametry OWASP: 19 MiB, 2 iterace), bcrypt s nákladovým faktorem 12+ pro stávající systémy. Nikdy ne SHA-256 nebo MD5 pro hesla.
- TLS 1.3 jako minimum pro všechny služby ve výrobě, s HSTS e moderní šifrovací soupravy. Pokud je to možné, zakažte TLS 1.0/1.1/1.2.
- Správa klíčů přes KMS nebo tajného správce: nejrobustnější klíč stane se zbytečným, pokud je vystaven v úložišti.
- Crypto agility v nových systémech: design, aby bylo možné migrovat algoritmy aniž bychom vše přepisovali, v rámci přípravy na postkvantový přechod do roku 2030.
Šifrování není funkce, kterou přidáváte na konci: je to rozhodnutí architektonické, které je třeba vzít na začátku. Každé citlivé pole v databázi, každé API který přenáší osobní údaje, každý autentizační token vyžaduje volbu uvažovat o algoritmu, správě klíčů a rotaci.
Pokračujte v sérii: Web Security pro vývojáře
- Předchozí článek: Zabezpečení dodavatelského řetězce: audit npm a SBOM - jak chránit řetězec závislostí
- Další článek: DevSecOps pro vývojáře: SAST, DAST v CI/CD - integrovat zabezpečení do potrubí
- Propojeno s: Zabezpečení API: OAuth 2.1, JWT a Rate Limiting - Podrobně osvědčené postupy JWT
- Viz také: seriál Frontend DevOps pro konfiguraci zabezpečeného nasazení







