07 - 開発者のための実践的な暗号化: AES-256-GCM、ハッシュ、TLS 1.3
暗号化は現代のサイバーセキュリティの目に見えない基盤です。保存されたすべてのパスワード データベース内、HTTPS 経由で送信されるすべての機密データ、ユーザーを認証するすべてのデジタル署名 ソフトウェアのアップデート: すべては正しく実装された暗号化プリミティブに依存します。それなのに 暗号化は、開発者が最も重大な間違いを犯す分野の 1 つでもあります。 気づかないうちにしばしば。
OWASP Top 10:2025 によると、 暗号化の失敗 (A02) 上位を維持 Web アプリケーションの 3 つの最も重大な脆弱性。これらは特殊な攻撃ではありません。 量子コンピューターが必要: ほとんどの暗号侵害は、些細なエラーが原因で発生します。 パスワードのハッシュ化に使用される MD5 または SHA-1。 ECB モードの AES。目に見えるパターンを生成します。 暗号文。 IV (初期化ベクトル) が再利用されます。ソースコードにハードコーディングされたキー。 悪用するために高度な侵入テストが必要ないエラー。
この記事では、数学理論ではなく、開発者の観点から暗号化について説明します。 ただし、Node.js と Web Crypto API の実用的なパターンです。対称暗号化をいつ使用するかを学びます 対非対称、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 チェックリスト: フロントエンドで機密データを処理するための安全なパターン
対称暗号化と非対称暗号化
対称暗号化と非対称暗号化のどちらを選択するかは好みの問題ではありません。 解決すべき問題に応じたアーキテクチャ上の決定。 2 つのアプローチを混同すると、 脆弱性や不必要なパフォーマンスの低下を体系的に防止します。
対称暗号化 暗号化と復号化に同じキーを使用します。そして素早く (AES-256-GCM は、最新のハードウェアでは 1 秒あたりギガバイト単位で暗号化します) で、大規模な暗号化に適しています。 データ量。問題と鍵の配布: 安全に共有するにはどうすればよいですか データを解読するのは誰ですか?
非対称暗号化 数学的に関連したキーのペアを使用します。 1 つは公開 (自由に配布できる)、もう 1 つは非公開 (秘密にしておく) です。を解決します 鍵の配布に問題がありますが、対称よりも桁違いに遅いです。 RSA-2048 は操作ごとに 1 秒あたり約 250 バイトを暗号化しますが、AES-256-GCM は最大数 GB/秒に達します。
プロフェッショナル パターンは、TLS と同様に、ハイブリッド システムで 2 つのアプローチを組み合わせます。 非対称暗号化は、セッションキーを安全に交換するためにのみ使用されます。 対称的。すべてのデータはその AES キーで暗号化されます。これが基礎です HTTPS、SSH、その他の最新の安全なプロトコル。
基本原則
- 対称 (AES-256-GCM): 保存中データと転送中のデータを一括で暗号化する
- 非対称 (RSA/ECC): キーを交換し、身元を認証し、デジタル署名を行うため
- ハイブリッド: ほぼすべての実際のユースケース (TLS、PGP、シグナルプロトコル) に対応
- ハッシュ (Argon2、SHA-256): 一方向関数、非可逆
AES-256-GCM: 正しい実装
AES-256-GCM (ガロア/カウンター モードの 256 ビット キーを使用した高度な暗号化標準) 認証された対称暗号化の事実上の標準です。 GCM モードには 2 つのモードがあります 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 (エドワーズ曲線デジタル署名アルゴリズム) と現代の選択肢 デジタル署名: ECDSA よりも高速で、電子署名に関連する実装上の問題が発生しません。 nonce 生成であり、Node.js、OpenSSL 3.x、およびすべての最新のブラウザーでサポートされています。 SSH、 Sign および多くの高セキュリティ アプリケーションは、デフォルトでこれを使用します。
// 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
パスワードのハッシュ化は、アプリケーションのセキュリティにおいて最も誤解されている側面の 1 つです。の 基本的かつ単純な原則: データベースからパスワードを決して回復できてはなりません。 データベースが侵害された場合、攻撃者は元のパスワードを追跡できてはなりません。 このため、パスワードのハッシュ化には、従来とは根本的に異なる特定のアルゴリズムが必要になります。 データの整合性のために使用されるもの。
SHA-256 パスワードには使用できません
SHA-256 は非常に高速 (GPU 上で 1 秒あたり数十億回の演算) なので、 データの整合性は保たれますが、パスワードに関しては致命的です。 RTX 4090 を使用する攻撃者は、 1 秒あたり 100 億以上の SHA-256 をテストし、一般的なパスワードの辞書全体を作成します 数秒で割れます。パスワードにはアルゴリズムが必要です メモリが厳しく、計算コストがかかる.
- SHA-256: チェックサム、HMAC、トークン導出用。パスワードには決して使用しないでください。
- MD5、SHA-1: チェックサムについても非推奨になりました。新しいコードではこれらを使用しないでください。
- bcrypt: 安全で、実戦テスト済みなので、すでにシステムに組み込まれている場合には使用してください。
- アルゴン 2id: 新しいアプリケーションの OWASP 2025 ゴールド スタンダード。
OWASP が推奨する Argon2id 新しいアプリケーションの最初の選択肢として。そして、 パスワード ハッシュ コンペティション (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 サポート)、簡素化されより安全な暗号スイート(削除 RC4、DES、MAC 用の MD5、RSA キー交換を含むすべての弱いアルゴリズム)、および Perfect ECDHE による必須の Forward Secrecy。
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 Service) サービス キーを透明な場所にさらすことなく、改ざん防止ハードウェアの内部に保管できます。
// 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 (Kyber): 交換のためのキーのカプセル化メカニズム キーの。 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 アプリケーションには特定の暗号化要件があります。ブラウザと環境 untrusted: JavaScript コード内のシークレットは、それを開いた人なら誰でもアクセスできます。 開発ツール。これにより、クライアント側で行う意味が根本的に変わります。
Angular での暗号化の黄金律
- 暗号キーをハードコーディングしないでください TypeScript/JavaScript コード内。 対称キーはサーバー上に残しておく必要があります。
-
常にHTTPSを使用する - Content-Security-Policy を設定します
upgrade-insecure-requestsそしてHSTS。機密データは決して扱わないでください HTTP 上でも開発中です。 - ブラウザでの E2E 暗号化の場合: キーを使用して Web Crypto API を使用する ユーザーのパスワード (PBKDF2/Argon2 wasm) から派生します。鍵が離れない 決してブラウザではありません。
-
JWT トークン: 署名検証、有効期限 (
exp)、発行者 (iss) と視聴者 (aud)。確認せずにリバースエンジニアリングを行わないでください。 - localStorage と sessionStorage の比較: どちらも JS からアクセスできます。そうではない 超機密データ (秘密鍵、有効期間の長いトークン) をストレージに保存する XSS経由でアクセス可能。セッション トークンには HttpOnly Cookie を優先します。
-
ランダム性: いつも使う
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 | 高 - ブライヘンバッハーに対して脆弱 | 鍵交換用の 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 はレガシー互換性のみを目的としています。
- Argon2id パスワードハッシュの場合 (OWASP パラメータ: 19 MiB、2 回の反復)、 既存システム向けのコスト係数 12+ の bcrypt。パスワードに SHA-256 または MD5 を使用しないでください。
- TLS1.3 実稼働環境のすべてのサービスの最低条件として、HSTS e 最新の暗号スイート。可能であれば、TLS 1.0/1.1/1.2 を無効にします。
- 鍵の管理 KMS またはシークレット マネージャー経由: 最も堅牢なキー リポジトリに公開すると役に立たなくなります。
- 暗号化のアジリティ 新しいシステム: アルゴリズムを移行できるように設計する 2030 年までのポスト量子移行に備えて、すべてを書き換えることなく。
暗号化は最後に追加する機能ではありません。それは決定です。 最初に取得する必要がある建築。データベース内のすべての機密フィールド、すべての API 個人データを送信する場合、各認証トークンには選択が必要です アルゴリズム、鍵管理、ローテーションについて慎重に検討します。
シリーズを続ける: 開発者のための Web セキュリティ
- 前の記事: サプライ チェーン セキュリティ: npm 監査と SBOM - 依存関係の連鎖を保護する方法
- 次の記事: 開発者向けの DevSecOps: CI/CD の SAST、DAST - セキュリティをパイプラインに統合する
- リンク先: API セキュリティ: OAuth 2.1、JWT、レート制限 - JWT のベスト プラクティスの詳細
- 関連項目: シリーズ DevOps フロントエンド 安全な展開を構成するため







