07 - Praktyczna kryptografia dla programistów: AES-256-GCM, hashowanie i TLS 1.3
Kryptografia to niewidzialny fundament współczesnego cyberbezpieczeństwa. Każde hasło zapisane w bazie danych, każde wrażliwe dane przesyłane za pośrednictwem protokołu HTTPS, każdy podpis cyfrowy uwierzytelniający aktualizacja oprogramowania: wszystko zależy od poprawnie zaimplementowanych prymitywów kryptograficznych. A jednak kryptografia to także jeden z obszarów, w którym programiści popełniają najpoważniejsze błędy, często nie zdając sobie z tego sprawy.
Według OWASP Top 10:2025, Awarie kryptograficzne (A02) pozostają w czołówce trzy najbardziej krytyczne luki w aplikacjach internetowych. To nie są egzotyczne ataki wymagają komputerów kwantowych: większość naruszeń kryptograficznych ma miejsce w wyniku trywialnych błędów. MD5 lub SHA-1 używane do mieszania haseł. AES w trybie EBC, który tworzy widoczne wzory w szyfrogram. IV (wektor inicjujący) ponownie wykorzystany. Klucze zakodowane na stałe w kodzie źródłowym. Błędy, których wykorzystanie nie wymaga zaawansowanego pentestu.
W tym artykule omówiono kryptografię z punktu widzenia programisty: nie teorii matematycznej, ale praktyczne wzorce w Node.js i Web Crypto API. Dowiesz się, kiedy stosować szyfrowanie symetryczne vs asymetryczny, jak poprawnie zaimplementować AES-256-GCM, jaki algorytm mieszający wybrać dla haseł, jak skonfigurować TLS 1.3 i jak przygotować się do szyfrowania post-kwantowego. Każdy sekcja zawiera gotowe do produkcji przykłady kodu z pułapkami, których należy unikać.
Czego się nauczysz
- Szyfrowanie symetryczne a asymetryczne: kiedy zastosować jakie podejście
- AES-256-GCM: Poprawna implementacja z losowym tagiem IV i uwierzytelniającym
- RSA vs ECC (ECDSA, Ed25519): porównanie wydajności i bezpieczeństwa w 2025 roku
- Hashowanie haseł za pomocą Argon2id i bcrypt: Zalecane parametry OWASP
- SHA-256 dla integralności danych a hashowania haseł: prawidłowe zastosowania
- TLS 1.3: Bezpieczna konfiguracja w Node.js dzięki nowoczesnym zestawom szyfrów
- Zarządzanie kluczami: unikaj kluczy zakodowanych na stałe, używaj zmiennych środowiskowych i KMS
- Web Crypto API: szyfrowanie w przeglądarce bez zewnętrznych zależności
- Kryptografia postkwantowa: NIST FIPS 203 (ML-KEM) i przygotowania do roku 2030
- Angular Checklist: Bezpieczne wzorce obsługi wrażliwych danych w frontendzie
Szyfrowanie symetryczne i asymetryczne
Wybór pomiędzy kryptografią symetryczną i asymetryczną nie jest kwestią preferencji: jest nią decyzja architektoniczna zależna od problemu do rozwiązania. Mylenie tych dwóch podejść prowadzi systematycznie na luki w zabezpieczeniach lub niepotrzebnie obniżoną wydajność.
Szyfrowanie symetryczne użyj tego samego klucza do szyfrowania i deszyfrowania. I szybko (AES-256-GCM szyfruje gigabajty na sekundę na nowoczesnym sprzęcie) i nadaje się do szyfrowania dużych ilość danych. Problem i dystrybucja klucza: jak go bezpiecznie udostępnić Z kim należy odszyfrować dane?
Szyfrowanie asymetryczne wykorzystuje parę powiązanych matematycznie kluczy: jeden publiczny (który możesz swobodnie rozpowszechniać) i jeden prywatny (który trzymasz w tajemnicy). Rozwiązuje problem dystrybucji klucza, ale o rzędy wielkości wolniejszy niż symetryczny. RSA-2048 szyfruje około 250 bajtów na sekundę na operację, podczas gdy AES-256-GCM osiąga prędkość wielu GB/s.
Wzorzec profesjonalny łączy oba podejścia w systemie hybrydowym, podobnie jak robi to TLS: szyfrowanie asymetryczne służy wyłącznie do bezpiecznej wymiany klucza sesji symetryczny; następnie wszystkie dane są szyfrowane za pomocą tego klucza AES. To jest podstawa HTTPS, SSH i dowolny nowoczesny bezpieczny protokół.
Główna zasada
- Symetryczny (AES-256-GCM): do masowego szyfrowania danych przechowywanych i przesyłanych
- Asymetryczny (RSA/ECC): do wymiany kluczy, uwierzytelniania tożsamości, podpisów cyfrowych
- Hybrydowy: dla prawie wszystkich rzeczywistych przypadków użycia (TLS, PGP, protokół sygnału)
- Haszowanie (Argon2, SHA-256): funkcja jednokierunkowa, nieodwracalna
AES-256-GCM: Prawidłowa implementacja
AES-256-GCM (zaawansowany standard szyfrowania z 256-bitowym kluczem w trybie Galois/licznika) i de facto standardem uwierzytelnionego szyfrowania symetrycznego. Tryb GCM ma dwa krytyczne właściwości, które czynią go lepszym od alternatyw, takich jak AES-CBC lub AES-ECB:
- Poufność: dane są zaszyfrowane i nieczytelne bez klucza
- Uwierzytelnianie (AEAD): tworzy znacznik uwierzytelniający, który gwarantuje integralność danych. Jeśli ktoś zmodyfikuje zaszyfrowany tekst, odszyfrowanie zakończy się niepowodzeniem i wystąpi wyraźny błąd. AES-ECB i AES-CBC nie mają tej właściwości: atakujący może bez niej zmodyfikować tekst zaszyfrowany zauważone podczas deszyfrowania (atak typu bit-flip).
Błędy krytyczne w AES, których należy unikać
- Nigdy nie używaj ponownie IV z tym samym kluczem: w AES-GCM użyj ponownie klucza IV+ całkowicie narusza poufność. Zawsze generuj losowe 12 bajtów IV dla każdego operacja szyfrowania.
- Nigdy nie używaj AES-ECB: Tryb EBC generuje ten sam wynik dla tego samego input (the classic "ECB penguin"), making patterns in the data visible.
- Nigdy nie koduj klucza na stałe: klucz musi pochodzić ze zmiennych środowiskowych, KMS (Key Management Service) lub HSM, nigdy z kodu źródłowego.
- Zawsze sprawdzaj znacznik uwierzytelniający przed użyciem odszyfrowanych danych.
Oto kompletna i poprawna implementacja AES-256-GCM w Node.js z modułem
crypto wbudowane:
// 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: podpisy cyfrowe i szyfrowanie asymetryczne
W 2025 r. wybór między RSA a ECC (kryptografią krzywych eliptycznych) jest jasny: w przypadku nowych systemów wolę itp. Aby zachować ten sam poziom bezpieczeństwa, klucze ECC są znacznie mniejsze a operacje są znacznie szybsze. 256-bitowy klucz ECDSA P-256 oferuje takie same zabezpieczenia jak 3072-bitowy klucz RSA, z 10-15 razy szybszymi operacjami podpisywania.
| Algorytm | Rozmiar klucza | Poziom bezpieczeństwa | Względna wydajność | Polecane 2025 |
|---|---|---|---|---|
| RSA-2048 | 2048 bitów | 112 bitów | Linia bazowa (1x) | Tylko dla zgodności ze starszymi wersjami |
| RSA-3072 | 3072 bity | 128 bitów | 0,3x | Dopuszczalne, ale preferowane ETC |
| ECDSA P-256 | 256 bitów | 128 bitów | 10 razy szybszy niż RSA-3072 | Tak, dla TLS i podpisów |
| Ed25519 | 256 bitów | 128 bitów | 15 razy szybszy niż RSA-3072 | Tak, preferowane w przypadku podpisów cyfrowych |
Ed25519 (algorytm podpisu cyfrowego według krzywej Edwardsa) i nowoczesny wybór dla podpisy cyfrowe: jest szybszy niż ECDSA, nie boryka się z problemami związanymi z wdrażaniem nonce i jest obsługiwany przez Node.js, OpenSSL 3.x i wszystkie nowoczesne przeglądarki. SSH, Domyślnie używa go Signal i wiele aplikacji o wysokim poziomie bezpieczeństwa.
// 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
Hashowanie hasła: Argon2id, bcrypt i SHA-256
Hashowanie haseł to jeden z najbardziej źle rozumianych aspektów bezpieczeństwa aplikacji. The podstawowa i prosta zasada: nigdy nie wolno odzyskać hasła z bazy danych. Jeśli baza danych zostanie naruszona, osoba atakująca nie może być w stanie wyśledzić oryginalnych haseł. Dlatego mieszanie haseł wymaga określonych algorytmów, radykalnie odmiennych od te wykorzystywane do zapewnienia integralności danych.
SHA-256 Nie dla haseł
SHA-256 jest bardzo szybki (miliardy operacji na sekundę na GPU), co jest świetne integralność danych, ale katastrofalne dla haseł. Atakujący z puszką RTX 4090 przetestuj ponad 10 miliardów SHA-256/sekundę, tworząc cały słownik popularnych haseł pękać w ciągu kilku sekund. Hasła wymagają algorytmów wymagające dużej pamięci i kosztowne obliczeniowo.
- SHA-256: dla sum kontrolnych, HMAC, wyprowadzania tokenów. NIGDY dla haseł.
- MD5, SHA-1: również przestarzałe w przypadku sum kontrolnych. Nie używaj ich w żadnym nowym kodzie.
- szyfrować: bezpieczny, sprawdzony w boju, użyj go, jeśli jest już zintegrowany z twoim systemem.
- Argon2id: Złoty standard OWASP 2025 dla nowych zastosowań.
OWASP rekomenduje Argon2id jako pierwszy wybór w przypadku nowych zastosowań. I zwycięzca konkursu hashowania haseł (2015) i został zaprojektowany tak, aby był odporny na ataki hakerskie Ataki na procesory graficzne i ASIC ze względu na ich naturę obciążającą pamięć: wymagają znacznej ilości pamięci RAM do obliczenia skrótu, co sprawia, że ataki równoległe są znacznie droższe.
// 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: Bezpieczna konfiguracja w Node.js
TLS 1.3 (RFC 8446, 2018) to aktualny protokół bezpiecznego transportu. W porównaniu z TLS 1.2 wprowadza znaczące ulepszenia: szybsze uzgadnianie (1-RTT zamiast 2-RTT, z Obsługa 0-RTT dla wznowionych sesji), uproszczone i bezpieczniejsze zestawy szyfrów (usuń wszystkie słabe algorytmy, w tym RC4, DES, MD5 dla MAC, wymiana kluczy RSA) i Perfect Obowiązkowa tajemnica przekazywana za pośrednictwem ECDHE.
W TLS 1.3 jest tylko 5 zestawów szyfrów (w porównaniu z dziesiątkami w TLS 1.2) i wszystkie używają AEAD (Uwierzytelnione szyfrowanie z powiązanymi danymi):
TLS_AES_256_GCM_SHA384- ZalecaneTLS_AES_128_GCM_SHA256- Do przyjęciaTLS_CHACHA20_POLY1305_SHA256- Preferowane na urządzeniach bez sprzętowego 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"
Zarządzanie kluczami: najczęstszy słaby punkt
Najsolidniejszy system kryptograficzny na świecie i bezużyteczny, jeśli klucze są źle zarządzane. Najczęstszym wektorem ataku nie jest „łamanie AES-256”, ale znalezienie klucza AES-256 w pliku środowiska przekazanym do GitHuba, w kodzie źródłowym lub w dziennikach systemowych.
W 2024 r. GitHub Secret Scanning wykrył ponad 39 milionów sekretów ujawnionych w repozytoriach publiczne. Większość z nich stanowiły klucze API, ale znaczną część stanowiły klucze kryptograficzne. Szkody wynikające z odsłonięcia klucza symetrycznego są katastrofalne: wszystkie dane zaszyfrowane tym kluczem są zagrożone.
Hierarchia zabezpieczeń dla zarządzania kluczami
-
Poziom 1 – Minimalne akceptowalne: zmienne środowiskowe (nie w .env
zaangażowany w git). USA
.env.locali dodaj*.env*al.gitignore. - Poziom 2 – Produkcja małych aplikacji: usługi menedżera tajemnic, takie jak HashiCorp Vault, Menedżer sekretów AWS, Azure Key Vault, Menedżer sekretów GCP.
- Poziom 3 – Wysokie bezpieczeństwo: Sprzętowy moduł zabezpieczeń (HSM) lub Usługi KMS (Key Management Service) wykonujące operacje kryptograficzne wewnątrz sprzętu odpornego na manipulacje, bez konieczności wystawiania klucza na światło dzienne.
// 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: Szyfrowanie w przeglądarce
Web Crypto API (dostępne we wszystkich nowoczesnych przeglądarkach oraz w Node.js 15+) zapewnia
szyfrowanie bezpośrednio w przeglądarce bez zależności od zewnętrznych bibliotek JavaScript.
Opierając się na operacjach asynchronicznych, wykorzystuje pamięć niewidoczną dla JavaScript (klucze
CryptoKey domyślnie nie można ich wyodrębnić) i jest zaimplementowany na poziomie
natywnie z przeglądarki, aby zapewnić optymalną wydajność.
Typowym przypadkiem użycia jest kompleksowe szyfrowanie w przeglądarce: dane są szyfrowane przed wysłaniem na serwer, który nigdy nie ma dostępu do danych w postaci zwykłego tekstu. To wzorzec i używany przez aplikacje, takie jak menedżery haseł, bezpieczne notatki i klienci szyfrowane wiadomości.
// 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;
}
}
Kryptografia postkwantowa: przygotowania do roku 2030
Komputery kwantowe nie stanowią jeszcze praktycznego zagrożenia dla dzisiejszej kryptografii, ale NIST opublikował pierwsze ostateczne standardy postkwantowe w 2024 r. (FIPS 203, 204, 205) właśnie dlatego, że czas migracji jest długi, a ryzyko polega na tym, że „zebierz teraz, odszyfruj później” i prawdziwe. Złośliwy aktor zbierający obecnie wiadomości zaszyfrowane RSA mógłby to zrobić rozszyfrować je w przyszłości za pomocą wystarczająco wydajnego komputera kwantowego.
Standardy postkwantowe NIST (sierpień 2024 r.)
- FIPS 203 – ML-KEM (Kyber): Kluczowy mechanizm hermetyzacji wymiany kluczy. Zastępuje wymianę kluczy ECDH i RSA. Zalecany do hybrydy TLS 1.3 (X25519+MLKEM-768).
- FIPS 204 – ML-DSA (dylit): Postkwantowy podpis cyfrowy. Zastępuje ECDSA i RSA-PSS. Większe, ale bezpieczne klucze przed algorytmami Shora.
- FIPS 205 – SLH-DSA (SPHINCS+): Podpis cyfrowy oparty na skrótach plus konserwatywny. Przydatne jako algorytmiczna kopia zapasowa.
- Rekomendowana strategia na lata 2025-2030: podejście hybrydowe. Użyj X25519+ ML-KEM-768 dla TLS (jednocześnie bezpieczeństwo klasyczne i postkwantowe).
Dla programistów przygotowanie do kryptografii postkwantowej oznacza dziś głównie:
-
Elastyczność kryptowalut: projektować systemy tak, aby móc zmieniać algorytmy
bez przepisywania wszystkiego. Nie zapisuj na stałe „AES-256” ani „RSA” w bazach danych; użyj pola
algorithm_version. - Inwentarz kryptowalut: Dowiedz się, gdzie i jak stosuje się RSA/ECC w Twojej aplikacji. Pierwszym krokiem jest widoczność.
- Hybrydowy TLS: włącz X25519+MLKEM-768 na serwerach obsługujących TLS OpenSSL 3.5+ lub BoringSSL (Chrome używa go od 2023 roku).
- Brak pilnej migracji danych w spoczynku z kluczami symetrycznymi AES-256: AES-256 i już odporny na komputery kwantowe (Grover zmniejsza skuteczne bezpieczeństwo 128-bitowy, nadal bardzo bezpieczny).
// 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 kontrolna szyfrowania dla aplikacji Angular
Aplikacje Angular mają specyficzne wymagania kryptograficzne. Przeglądarka i środowisko niezaufane: każdy sekret w kodzie JavaScript jest dostępny dla każdego, kto go otworzy narzędzia rozwojowe. To zasadniczo zmienia to, co ma sens robić po stronie klienta.
Złote zasady szyfrowania w Angular
- Nigdy nie koduj kluczy kryptograficznych na stałe w kodzie TypeScript/JavaScript. Klucze symetryczne muszą pozostać na serwerze.
-
Zawsze używaj protokołu HTTPS - skonfiguruj Politykę bezpieczeństwa treści za pomocą
upgrade-insecure-requestsi HSTS. Nigdy nie obsługuj wrażliwych danych na HTTP również w fazie rozwoju. - Do szyfrowania E2E w przeglądarce: Użyj Web Crypto API z kluczami pochodzi z hasła użytkownika (PBKDF2/Argon2 wasm). Klucz nie opuszcza nigdy przeglądarka.
-
Tokeny JWT: weryfikacja podpisu, wygaśnięcie (
exp), emitent (iss) i publiczność (aud). Nie przeprowadzaj inżynierii wstecznej bez sprawdzenia. - localStorage vs sessionStorage: oba dostępne z JS. Nie zapisuj w pamięci bardzo wrażliwe dane (klucze prywatne, tokeny o długim czasie życia). dostępne poprzez XSS. Preferuj pliki cookie HttpOnly dla tokenów sesji.
-
Losowość: zawsze używaj
window.crypto.getRandomValues()w przypadku kryptograficznie bezpiecznych danych losowych – nigdyMath.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 najczęstszych błędów kryptograficznych
Kryptografia to dziedzina, w której metawiedza jest ważniejsza niż szczegóły techniczne. Wiedza o tym, czego NIE robić, jest często warta więcej niż wiedza, jak korzystać z każdego interfejsu API. Oto błędy, które pojawiają się systematycznie w przeglądach kodów bezpieczeństwa:
| Błąd | Uderzenie | Rozwiązanie |
|---|---|---|
| MD5/SHA-1 do mieszania haseł | Krytyczny - można go złamać w ciągu kilku sekund | Argon2id lub bcrypt z kosztem >= 12 |
| AES-ECB (nr IV) | Wysoki - wzory widoczne w zaszyfrowanym tekście | AES-256-GCM z losowym IV |
| Stałe lub przyrostowe IV | Krytyczny — zastępuje zabezpieczenia GCM | randomBytes(12) dla każdego szyfrowania |
| Klucze zakodowane na stałe w kodzie | Krytyczny — ujawnia wszystkie zaszyfrowane dane | AWS/Azure KMS, zmienne środowiskowe |
| RSA-PKCS1v1.5 do szyfrowania | Wysoki - podatny na Bleichenbachera | RSA-OAEP lub ECDH do wymiany kluczy |
| JWT bez weryfikacji podpisu | Krytyczny — eskalacja uprawnień | Zawsze sprawdzaj podpis na serwerze |
| Porównywanie skrótów z == | Średni - atak w czasie | TimingSafeEqual() przez kryptografię Node.js |
| Math.random() dla tokenów | Wysokie - przewidywalne tokeny | crypto.randomBytes() lub getRandomValues() |
| Włączono protokół TLS 1.0/1.1 | Wysoki - BEAST, POODLE, Atak ZBRODNICZY | min Wersja: TLSv1.3 |
| Brak weryfikacji certyfikatu | Krytyczny - atak MITM | Nigdy NODE_TLS_REJECT_UNAUTHORIZED=0 w produkcji |
Wnioski
Praktyczna kryptografia dla programistów nie wymaga bycia kryptografem: wymaga wiedzy odpowiednie wzorce, unikaj znanych anty-wzorców i wybieraj odpowiednie narzędzia dla każdego z nich przypadek użycia. Podstawowe zasady można podsumować w kilku punktach:
- AES-256-GCM dla uwierzytelnionego szyfrowania symetrycznego: 12-bajtowe losowe IV, 128-bitowy znacznik uwierzytelniający, klucz nigdy nie jest w kodzie.
- Ed25519 lub ECDSA P-256 w przypadku podpisów cyfrowych: preferuj ECC zamiast RSA nowe systemy, RSA-4096 tylko dla zgodności ze starszymi.
- Argon2id do haszowania haseł (parametry OWASP: 19 MiB, 2 iteracje), bcrypt ze współczynnikiem kosztów 12+ dla istniejących systemów. Nigdy nie używaj haseł SHA-256 ani MD5.
- TLS 1.3 jako minimum dla wszystkich usług w produkcji, z HSTS e nowoczesne zestawy szyfrów. Jeśli to możliwe, wyłącz TLS 1.0/1.1/1.2.
- Zarządzanie kluczami przez KMS lub tajnego menedżera: najbardziej niezawodny klucz staje się bezużyteczny, jeśli zostanie ujawniony w repozytorium.
- Elastyczność kryptowalut w nowych systemach: projektowanie umożliwiające migrację algorytmów bez przepisywania wszystkiego, w ramach przygotowań do przejścia postkwantowego do roku 2030.
Szyfrowanie nie jest funkcją, którą dodajesz na końcu: to decyzja architektoniczne, które trzeba podjąć na początku. Każde wrażliwe pole w bazie danych, każde API przesyłający dane osobowe, każdy token uwierzytelniający wymaga wyboru przemyślane podejście do algorytmu, zarządzania kluczami i rotacji.
Kontynuuj serię: Bezpieczeństwo sieciowe dla programistów
- Poprzedni artykuł: Bezpieczeństwo łańcucha dostaw: audyt npm i SBOM - jak chronić łańcuch zależności
- Następny artykuł: DevSecOps dla programisty: SAST, DAST w CI/CD - zintegrować zabezpieczenia z rurociągiem
- Powiązane z: Bezpieczeństwo API: OAuth 2.1, JWT i ograniczanie szybkości - Szczegóły najlepszych praktyk JWT
- Zobacz także: serial Frontend DevOps do konfigurowania bezpiecznego wdrożenia







