07 - 개발자를 위한 실용적인 암호화: AES-256-GCM, 해싱 및 TLS 1.3
암호화는 현대 사이버 보안의 보이지 않는 기초입니다. 모든 비밀번호가 저장됨 데이터베이스에는 HTTPS를 통해 전송되는 모든 민감한 데이터, 인증하는 모든 디지털 서명 소프트웨어 업데이트: 모든 것은 올바르게 구현된 암호화 기본 요소에 달려 있습니다. 그럼에도 불구하고 암호화는 개발자가 가장 심각한 실수를 저지르는 영역 중 하나이기도 합니다. 깨닫지 못하는 경우가 많습니다.
OWASP Top 10:2025에 따르면, 암호화 실패 (A02) 상위권 유지 웹 애플리케이션의 세 가지 가장 심각한 취약점. 이것은 이국적인 공격이 아닙니다. 양자 컴퓨터가 필요합니다. 대부분의 암호화 위반은 사소한 오류로 인해 발생합니다. 비밀번호 해싱에 MD5 또는 SHA-1이 사용됩니다. 눈에 보이는 패턴을 생성하는 ECB 모드의 AES 암호문. IV(초기화 벡터)가 재사용되었습니다. 소스 코드에 하드코딩된 키입니다. 악용하는 데 고급 침투 테스트가 필요하지 않은 오류입니다.
이 문서에서는 수학적 이론이 아닌 개발자의 관점에서 암호화를 다루고 있습니다. 하지만 Node.js와 Web Crypto API의 실용적인 패턴입니다. 대칭 암호화를 사용하는 경우를 배우게 됩니다. vs 비대칭, 선택할 해싱 알고리즘인 AES-256-GCM을 올바르게 구현하는 방법 비밀번호, TLS 1.3 구성 방법, 포스트퀀텀 암호화 준비 방법을 알아보세요. 매 섹션에는 피해야 할 함정이 있는 프로덕션 준비 코드 예제가 포함되어 있습니다.
무엇을 배울 것인가
- 대칭 암호화와 비대칭 암호화: 언제 어떤 접근 방식을 사용해야 할까요?
- AES-256-GCM: 무작위 IV 및 인증 태그를 사용한 올바른 구현
- RSA와 ECC(ECDSA, Ed25519): 2025년 성능 및 보안 비교
- Argon2id 및 bcrypt를 사용한 비밀번호 해싱: 권장 OWASP 매개변수
- 데이터 무결성과 비밀번호 해싱을 위한 SHA-256: 올바른 사용
- TLS 1.3: 최신 암호화 제품군을 사용한 Node.js의 보안 설정
- 키 관리: 하드코딩된 키 방지, 환경 변수 및 KMS 사용
- Web Crypto API: 외부 종속성이 없는 브라우저 내 암호화
- 포스트 양자 암호화: NIST FIPS 203(ML-KEM) 및 2030년 준비
- Angular 체크리스트: 프런트엔드에서 민감한 데이터를 처리하기 위한 안전한 패턴
대칭 암호화와 비대칭 암호화
대칭 암호화와 비대칭 암호화 사이의 선택은 선호의 문제가 아닙니다. 해결해야 할 문제에 따라 달라지는 아키텍처 결정. 두 가지 접근 방식을 혼동하면 취약점이나 불필요하게 저하된 성능을 체계적으로 제거합니다.
대칭 암호화 동일한 키를 사용하여 암호화하고 복호화합니다. 그리고 빨리 (AES-256-GCM은 최신 하드웨어에서 초당 기가바이트를 암호화합니다.) 대규모 암호화에 적합합니다. 데이터의 양. 문제 및 키 배포: 어떻게 안전하게 공유합니까? 누가 데이터를 해독해야 합니까?
비대칭 암호화 수학적으로 관련된 키 쌍을 사용합니다. 하나는 공개(자유롭게 배포할 수 있음)이고 다른 하나는 비공개(비밀로 유지)입니다. 해결하다 키 분배 문제가 있지만 대칭보다 훨씬 느립니다. RSA-2048은 작업당 초당 약 250바이트를 암호화하는 반면, AES-256-GCM은 최대 수 GB/s까지 암호화합니다.
전문 패턴은 TLS와 마찬가지로 하이브리드 시스템에서 두 가지 접근 방식을 결합합니다. 비대칭 암호화는 세션 키를 안전하게 교환하는 데에만 사용됩니다. 대칭; 그러면 모든 데이터가 해당 AES 키로 암호화됩니다. 이것이 기초이다 HTTPS, SSH 및 모든 최신 보안 프로토콜.
기본원리
- 대칭형(AES-256-GCM): 미사용 데이터와 전송 중인 데이터를 대량으로 암호화
- 비대칭(RSA/ECC): 키 교환, 신원 인증, 디지털 서명
- 잡종: 거의 모든 실제 사용 사례(TLS, PGP, 신호 프로토콜)
- 해싱(Argon2, SHA-256): 단방향 기능, 되돌릴 수 없음
AES-256-GCM: 올바른 구현
AES-256-GCM(갈루아/카운터 모드에서 256비트 키를 사용하는 고급 암호화 표준) 인증된 대칭 암호화를 위한 사실상의 표준입니다. GCM 모드에는 두 가지가 있습니다. AES-CBC 또는 AES-ECB와 같은 대안보다 우수하게 만드는 중요한 특성:
- 기밀성: 데이터는 암호화되어 키 없이는 읽을 수 없습니다.
- 인증(AEAD): 무결성을 보장하는 인증 태그를 생성합니다. 데이터의. 누군가 암호문을 수정하면 명시적인 오류와 함께 암호 해독이 실패합니다. AES-ECB 및 AES-CBC에는 이 속성이 없습니다. 공격자는 이 속성 없이 암호문을 수정할 수 있습니다. 암호 해독 시 이를 알 수 있습니다(비트 반전 공격).
피해야 할 AES의 심각한 오류
- 동일한 키로 IV를 재사용하지 마십시오. AES-GCM에서는 IV+키를 재사용합니다. 기밀성을 완전히 손상시킵니다. 각각에 대해 항상 임의의 12바이트 IV를 생성합니다. 암호화 작업.
- AES-ECB를 사용하지 마십시오. ECB 모드는 동일한 항목에 대해 동일한 출력을 생성합니다. 입력(고전적인 "ECB 펭귄")을 사용하여 데이터의 패턴을 표시합니다.
- 키를 하드코딩하지 마세요. 키는 환경 변수에서 나와야 합니다. KMS(키 관리 서비스) 또는 HSM(소스 코드에서는 절대 사용되지 않음)
- 항상 인증태그를 확인하세요 해독된 데이터를 사용하기 전에
다음은 모듈을 사용하여 Node.js에서 AES-256-GCM을 완전하고 올바르게 구현한 것입니다.
crypto 내장:
// 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 및 ECC: 디지털 서명 및 비대칭 암호화
2025년에는 RSA와 ECC(타원곡선 암호화) 사이의 선택이 명확해집니다. 새로운 시스템의 경우 ETC를 선호합니다. 동일한 수준의 보안을 위해 ECC 키는 훨씬 더 작습니다. 작업 속도가 훨씬 빨라졌습니다. 256비트 ECDSA P-256 키는 다음을 제공합니다. 3072비트 RSA 키와 동일한 보안을 제공하며 서명 작업 속도가 10~15배 더 빠릅니다.
| 연산 | 키 크기 | 보안 수준 | 상대 성능 | 2025년 추천 |
|---|---|---|---|---|
| RSA-2048 | 2048비트 | 112비트 | 기준선(1x) | 레거시 호환성에만 해당 |
| RSA-3072 | 3072비트 | 128비트 | 0.3배 | 허용되지만 ETC를 선호합니다. |
| ECDSA P-256 | 256비트 | 128비트 | RSA-3072보다 10배 빠름 | 예, TLS 및 서명의 경우 |
| Ed25519 | 256비트 | 128비트 | RSA-3072보다 15배 빠름 | 예, 디지털 서명에 선호됩니다. |
Ed25519 (Edwards-curve Digital Signature Algorithm) 및 현대적인 선택 디지털 서명: ECDSA보다 빠르며 다음과 관련된 구현 문제를 겪지 않습니다. Nonce 생성을 지원하며 Node.js, OpenSSL 3.x 및 모든 최신 브라우저에서 지원됩니다. SSH, Signal 및 많은 보안 수준이 높은 애플리케이션에서는 기본적으로 이를 사용합니다.
// 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
비밀번호 해싱: Argon2id, bcrypt 및 SHA-256
비밀번호 해싱은 애플리케이션 보안에 대해 가장 잘못 이해되는 측면 중 하나입니다. 는 기본적이고 간단한 원칙: 데이터베이스에서 비밀번호를 절대 복구할 수 없어야 합니다. 데이터베이스가 손상된 경우 공격자는 원래 비밀번호를 추적할 수 없어야 합니다. 이것이 바로 비밀번호 해싱에 기존 알고리즘과 근본적으로 다른 특정 알고리즘이 필요한 이유입니다. 데이터 무결성에 사용되는 것.
SHA-256은 비밀번호용이 아님
SHA-256은 매우 빠릅니다(GPU에서 초당 수십억 개의 작업). 데이터 무결성은 있지만 비밀번호에는 치명적입니다. RTX 4090을 사용하는 공격자는 다음을 수행할 수 있습니다. 초당 100억 개 이상의 SHA-256을 테스트하여 전체 공통 비밀번호 사전을 만듭니다. 몇 초 안에 깨질 수 있습니다. 비밀번호에는 알고리즘이 필요합니다 메모리 하드 및 계산 비용이 많이 듭니다..
- SHA-256: 체크섬, HMAC, 토큰 파생용. 비밀번호는 절대 사용하지 마세요.
- MD5, SHA-1: 체크섬에 대해서도 더 이상 사용되지 않습니다. 새로운 코드에서는 사용하지 마세요.
- 암호화: 안전하고 전투 테스트를 거쳤으므로 이미 시스템에 통합되어 있는 경우 사용하십시오.
- Argon2id: 새로운 애플리케이션을 위한 OWASP 2025 골드 표준입니다.
OWASP가 권장하는 아르곤2id 새로운 애플리케이션을 위한 첫 번째 선택입니다. 그리고 2015년 비밀번호 해싱 대회 우승자이며 해킹에 강하도록 설계되었습니다. 메모리 하드 특성으로 인한 GPU 및 ASIC 공격: 상당한 양의 공격 필요 해시를 계산하기 위해 RAM을 사용하므로 병렬 공격에 훨씬 더 많은 비용이 듭니다.
// 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: Node.js의 보안 구성
TLS 1.3(RFC 8446, 2018)은 현재의 보안 전송 프로토콜입니다. TLS 1.2와 비교 상당한 개선이 이루어졌습니다. 더 빠른 핸드셰이크(2-RTT 대신 1-RTT, 재개된 세션에 대한 0-RTT 지원), 단순화되고 더욱 안전한 암호화 제품군(삭제 MAC용 RC4, DES, MD5, RSA 키 교환을 포함한 모든 취약한 알고리즘) 및 Perfect ECDHE를 통한 필수 순방향 비밀성.
TLS 1.3에는 단 5개의 암호화 제품군이 있으며(TLS 1.2에는 수십 개가 있음) 모두 AEAD를 사용합니다. (관련 데이터로 인증된 암호화):
TLS_AES_256_GCM_SHA384- 추천TLS_AES_128_GCM_SHA256- 허용됨TLS_CHACHA20_POLY1305_SHA256- 하드웨어 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"
키 관리: 가장 일반적인 약점
세계에서 가장 강력한 암호화 시스템이며 키가 잘못 관리되면 쓸모가 없습니다. 가장 빈번한 공격 벡터는 "AES-256 침입"이 아니라 AES-256 키를 찾는 것입니다. GitHub에 커밋된 환경 파일, 소스 코드 또는 시스템 로그에 있습니다.
2024년 GitHub Secret Scanning은 저장소에 노출된 3,900만 개 이상의 비밀을 감지했습니다. 공개. 대부분은 API 키였지만 상당 부분은 암호화 키였습니다. 대칭 키 노출로 인한 피해는 치명적입니다. 모든 데이터 해당 키로 암호화된 정보가 손상되었습니다.
키 관리를 위한 보안 계층
-
레벨 1 - 허용되는 최소값: 환경 변수(.env에 없음)
git에 전념). 미국
.env.local그리고 추가하세요*.env*al.gitignore. - 레벨 2 - 소규모 앱 제작: 다음과 같은 비밀 관리자 서비스 HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager.
- 레벨 3 - 높은 보안: 하드웨어 보안 모듈(HSM) 또는 암호화 작업을 수행하는 KMS(키 관리 서비스) 서비스 키를 투명하게 노출시키지 않고 변조 방지 하드웨어 내부에 보관할 수 있습니다.
// 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: 브라우저의 암호화
Web Crypto API(모든 최신 브라우저 및 Node.js 15+에서 사용 가능)는 다음을 제공합니다.
외부 JavaScript 라이브러리에 의존하지 않고 브라우저에서 직접 암호화합니다.
그리고 비동기 작업을 기반으로 JavaScript에 노출되지 않은 메모리를 사용합니다(키
CryptoKey 기본적으로 추출할 수 없음) 수준에서 구현됩니다.
최적의 성능을 위해 브라우저에서 기본으로 제공됩니다.
일반적인 사용 사례는 브라우저의 종단 간 암호화입니다. 데이터는 암호화됩니다. 일반 텍스트 데이터에 액세스할 수 없는 서버로 전송되기 전에. 이 패턴을 사용하며 비밀번호 관리자, 보안 메모, 클라이언트와 같은 애플리케이션에서 사용됩니다. 암호화된 메시징.
// 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;
}
}
포스트 양자 암호화: 2030년을 준비하다
양자 컴퓨터는 아직 오늘날의 암호화에 실질적인 위협이 되지는 않습니다. 그러나 NIST는 2024년에 최초의 최종 양자 후 표준을 발표했습니다(FIPS 203, 204, 205). 마이그레이션 시간이 길고 "지금 수확하고 나중에 해독"할 위험이 있기 때문입니다. 그리고 진짜. 오늘날 RSA로 암호화된 통신을 수집하는 악의적인 행위자는 충분히 강력한 양자 컴퓨터를 사용하여 미래에 이를 해독할 수 있습니다.
NIST 포스트 양자 표준(2024년 8월)
- FIPS 203 - ML-KEM(카이버): 교환을 위한 키 캡슐화 메커니즘 열쇠. ECDH 및 RSA 키 교환을 대체합니다. TLS 1.3 하이브리드에 권장됨 (X25519+MLKEM-768).
- FIPS 204 - ML-DSA(디리튬): 포스트퀀텀 디지털 서명. 대체 ECDSA 및 RSA-PSS. Shor 알고리즘에 비해 더 크지만 안전한 키입니다.
- FIPS 205 - SLH-DSA(SPHINCS+): 해시 기반 디지털 서명 및 보수적이다. 알고리즘 백업으로 유용합니다.
- 권장 전략 2025-2030: 하이브리드 접근 방식. X25519+ 사용 TLS용 ML-KEM-768(클래식 + 포스트퀀텀 보안 동시).
개발자에게 오늘날 포스트퀀텀 암호화를 준비한다는 것은 주로:
-
암호화 민첩성: 알고리즘을 변경할 수 있는 시스템 설계
모든 것을 다시 쓰지 않고. "AES-256" 또는 "RSA"를 데이터베이스에 하드코딩하지 마십시오. 필드를 사용하다
algorithm_version. - 암호화폐 인벤토리: RSA/ECC가 사용되는 위치와 방법을 파악합니다. 귀하의 신청서에. 첫 번째 단계는 가시성입니다.
- 하이브리드 TLS: TLS를 지원하는 서버에서 X25519+MLKEM-768을 활성화합니다. OpenSSL 3.5+ 또는 BoringSSL(Chrome은 2023년부터 사용해 왔습니다).
- 미사용 데이터에 대한 긴급 마이그레이션 없음 대칭 AES-256 키 사용: AES-256 및 이미 양자 컴퓨터에 대한 저항성(Grover는 효과적인 보안을 감소시킵니다. 128비트, 여전히 매우 안전함).
// 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;
}
Angular 애플리케이션의 암호화 체크리스트
Angular 애플리케이션에는 특정 암호화 요구 사항이 있습니다. 브라우저와 환경 신뢰할 수 없음: JavaScript 코드의 모든 비밀은 그것을 여는 모든 사람이 액세스할 수 있습니다. 개발 도구. 이는 클라이언트 측에서 수행하는 작업을 근본적으로 변경합니다.
Angular의 암호화에 대한 황금률
- 암호화 키를 하드코딩하지 마세요. TypeScript/JavaScript 코드에서. 대칭 키는 서버에 남아 있어야 합니다.
-
항상 HTTPS를 사용하세요 - 다음을 사용하여 콘텐츠 보안 정책을 구성합니다.
upgrade-insecure-requests그리고 HSTS. 민감한 데이터를 절대 처리하지 마세요 HTTP에서도 개발 중입니다. - 브라우저에서 E2E 암호화의 경우: 키와 함께 Web Crypto API 사용 사용자의 비밀번호(PBKDF2/Argon2 wasm)에서 파생됩니다. 열쇠는 떠나지 않는다 결코 브라우저가 아닙니다.
-
JWT 토큰: 서명 확인, 만료(
exp), 발급자 (iss) 및 청중(aud). 확인하지 않고 리버스 엔지니어링하지 마십시오. - localStorage와 sessionStorage 비교: 둘 다 JS에서 액세스할 수 있습니다. 아니다 매우 민감한 데이터(개인 키, 장기 토큰)를 스토리지에 저장 XSS를 통해 액세스할 수 있습니다. 세션 토큰에는 HttpOnly 쿠키를 선호합니다.
-
무작위성: 항상 사용
window.crypto.getRandomValues()암호화된 무작위 데이터의 경우 절대로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);
}
}
가장 일반적인 10가지 암호화 오류
암호화폐는 기술적 세부사항보다 메타 지식이 더 중요한 분야입니다. 하지 말아야 할 일을 아는 것이 각 API를 사용하는 방법을 아는 것보다 더 가치 있는 경우가 많습니다. 다음은 오류입니다. 보안 코드 검토에 체계적으로 나타납니다.
| 실수 | 영향 | 해결책 |
|---|---|---|
| 비밀번호 해싱을 위한 MD5/SHA-1 | 심각 - 몇 초 만에 깨질 수 있음 | 비용이 12 이상인 Argon2id 또는 bcrypt |
| AES-ECB(IV 없음) | 높음 - 암호문에 보이는 패턴 | 무작위 IV가 포함된 AES-256-GCM |
| 고정 또는 증분 IV | 중요 - GCM 보안을 재정의합니다. | 각 암호화에 대한 randomBytes(12) |
| 코드에 하드코딩된 키 | 중요 - 암호화된 모든 데이터를 노출합니다. | AWS/Azure KMS, 환경 변수 |
| 암호화용 RSA-PKCS1v1.5 | 높음 - Bleichenbacher에 취약 | 키 교환을 위한 RSA-OAEP 또는 ECDH |
| 서명 확인이 없는 JWT | 심각 - 권한 에스컬레이션 | 항상 서버의 서명을 확인하세요. |
| 해시를 ==와 비교 | 중간 - 타이밍 공격 | Node.js 암호화의 timingSafeEqual() |
| 토큰용 Math.random() | 높음 - 예측 가능한 토큰 | crypto.randomBytes() 또는 getRandomValues() |
| TLS 1.0/1.1 활성화됨 | 높음 - BEAST, POODLE, CRIME 공격 | 최소 버전: TLSv1.3 |
| 인증서 유효성 검사 없음 | 심각 - MITM 공격 | 프로덕션에서는 절대 NODE_TLS_REJECT_UNAUTHORIZED=0이 아닙니다. |
결론
개발자를 위한 실용적인 암호화에는 암호화 전문가가 필요하지 않습니다. 올바른 패턴을 찾고, 알려진 안티 패턴을 피하고, 각 패턴에 적합한 도구를 선택하세요. 사용 사례. 기본 원칙은 몇 가지 사항으로 요약될 수 있습니다.
- AES-256-GCM 인증된 대칭 암호화의 경우: 12바이트 무작위 IV, 128비트 인증 태그, 코드에 키가 포함되지 않음.
- Ed25519 또는 ECDSA P-256 디지털 서명의 경우: RSA보다 ECC를 선호합니다. 새로운 시스템, 레거시 호환성만을 위한 RSA-4096.
- 아르곤2id 비밀번호 해싱(OWASP 매개변수: 19MiB, 2회 반복) 기존 시스템의 경우 비용 요소가 12 이상인 bcrypt. 비밀번호에 SHA-256 또는 MD5를 사용하지 마세요.
- TLS 1.3 HSTS e를 사용하여 프로덕션의 모든 서비스에 대해 최소한 현대 암호 제품군. 가능하면 TLS 1.0/1.1/1.2를 비활성화하십시오.
- 키 관리 KMS 또는 비밀 관리자를 통해: 가장 강력한 키 저장소에 노출되면 쓸모가 없게 됩니다.
- 암호화폐 민첩성 새로운 시스템: 알고리즘을 마이그레이션할 수 있도록 설계 모든 것을 다시 작성하지 않고 2030년까지 포스트퀀텀 전환을 준비합니다.
암호화는 마지막에 추가하는 기능이 아니라 결정입니다. 처음에 꼭 잡아야 하는 건축. 데이터베이스의 모든 민감한 필드, 모든 API 개인 데이터를 전송하는 각 인증 토큰에는 선택이 필요합니다. 알고리즘, 키 관리 및 순환에 대해 심의합니다.
시리즈 계속하기: 개발자를 위한 웹 보안
- 이전 기사: 공급망 보안: npm 감사 및 SBOM - 종속성 체인을 보호하는 방법
- 다음 기사: 개발자를 위한 DevSecOps: CI/CD의 SAST, DAST - 보안을 파이프라인에 통합
- 연결됨: API 보안: OAuth 2.1, JWT 및 속도 제한 - JWT 모범 사례에 대한 자세한 내용
- 참조: 시리즈 DevOps 프론트엔드 보안 배포 구성용







