| 模式 | 機密性 | 完整性/認證 | 性能 | 推薦度 |
| ECB | ✅ | ❌ (無 IV、塊獨立) | 快 | 🚫 禁用 |
| CBC | ✅ | ❌ (需額外 HMAC) | 中 | ⚠️ 僅存量遷移 |
| CTR | ✅ | ❌ (需額外 HMAC) | 快 (並行) | ⚠️ 僅特定場景 |
| GCM | ✅ | ✅ (AEAD: Associated Data + Tag) | 快 (硬件加速 AES-NI) | ✅ 首選 |
核心參數規範
- 密鑰長度:128 位 (足夠安全) / 256 位 (長期機密、合規要求)
- IV/Nonce 長度:12 字節 (96 位) (GCM 標準、性能最佳),**每次加密必須唯一** (隨機生成或計數器)。
- 認證標簽 Tag:16 字節 (128 位),驗證密文完整性與關聯數據。
- 關聯數據 AAD:不加密但需認證的數據 (如协議版本、用戶 ID、時間戳)。
生產級工具類 (AES-GCM + PBKDF2 密鑰派生)
public final class AesGcmCrypto {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12; // 96 bits
private static final int TAG_LENGTH = 16; // 128 bits
private static final int KEY_LENGTH = 256; // 256 bits
private static final int PBKDF2_ITERATIONS = 100_000; // OWASP 建議 ≥ 100k
private static final int SALT_LENGTH = 16; // 128 bits
private AesGcmCrypto() {}
/**
* 從密碼派生密鑰 (PBKDF2WithHmacSHA256)
* @param password 用戶密碼/主密鑰
* @param salt 隨機鹽值 (存儲與密文一起)
* @return SecretKey
*/
public static SecretKey deriveKey(char[] password, byte[] salt) {
try {
PBEKeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new CryptoException("密鑰派生失敗", e);
}
}
/**
* 加密:返回 Base64 編碼的 [Salt(16) + IV(12) + CipherText + Tag(16)]
*/
public static String encrypt(String plaintext, char[] password) {
try {
// 1. 生成隨機鹽值
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(SALT_LENGTH);
// 2. 派生密鑰
SecretKey key = deriveKey(password, salt);
// 3. 生成隨機 IV
byte[] iv = SecureRandom.getInstanceStrong().generateSeed(IV_LENGTH);
// 4. 初始化 Cipher
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
// 5. 可選 AAD (如版本號)
byte[] aad = "v1".getBytes(StandardCharsets.UTF_8);
cipher.updateAAD(aad);
// 6. 加密 (輸出包含 Tag)
byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// 7. 組裝: Salt + IV + CipherText
ByteBuffer buffer = ByteBuffer.allocate(salt.length + iv.length + cipherText.length);
buffer.put(salt).put(iv).put(cipherText);
return Base64.getEncoder().encodeToString(buffer.array());
} catch (Exception e) {
throw new CryptoException("加密失敗", e);
}
}
/**
* 解密:輸入 Base64 [Salt + IV + CipherText]
*/
public static String decrypt(String ciphertextBase64, char[] password) {
try {
byte[] decoded = Base64.getDecoder().decode(ciphertextBase64);
ByteBuffer buffer = ByteBuffer.wrap(decoded);
// 1. 解析 Salt
byte[] salt = new byte[SALT_LENGTH];
buffer.get(salt);
// 2. 解析 IV
byte[] iv = new byte[IV_LENGTH];
buffer.get(iv);
// 3. 剩餘為密文 (含 Tag)
byte[] cipherText = new byte[buffer.remaining()];
buffer.get(cipherText);
// 4. 派生密鑰
SecretKey key = deriveKey(password, salt);
// 5. 初始化 Cipher
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD("v1".getBytes(StandardCharsets.UTF_8));
// 6. 解密驗證 (Tag 驗證失敗拋 AEADBadTagException)
byte[] plainBytes = cipher.doFinal(cipherText);
return new String(plainBytes, StandardCharsets.UTF_8);
} catch (AEADBadTagException e) {
throw new CryptoException("認證失敗:密文被篡改或密鑰錯誤", e);
} catch (Exception e) {
throw new CryptoException("解密失敗", e);
}
}
/** 高級:使用隨機生成的數據加密密鑰 (DEK) + 密鑰加密密鑰 (KEK) 分層 */
public static EncryptedData encryptWithDek(String plaintext, SecretKey kek) {
// 1. 生成隨機 DEK
SecretKey dek = generateRandomKey();
// 2. DEK 加密數據
String encryptedData = encryptWithKey(plaintext, dek);
// 3. KEK 加密 DEK (Key Wrap: AESWrap 或 RSA-OAEP)
String encryptedDek = wrapKey(dek, kek);
return new EncryptedData(encryptedData, encryptedDek);
}
// ... 內部輔助方法省略
}
密鑰管理最佳實踐
- 分層加密:數據加密密鑰 (DEK) 隨機生成、定期輪換;密鑰加密密鑰 (KEK) 存儲於 HSM/KMS/Vault,極少輪換。
- 密鑰輪換:定期 (如 90 天) 生成新 DEK 重新加密熱數據;舊 DEK 保留解密歷史數據,標記過期。
- 存儲隔離:密鑰**絕不**硬編碼、配置文件、代碼庫。使用 HashiCorp Vault、AWS KMS、Azure Key Vault、Google Cloud KMS、阿里云 KMS。
- 訪問控制:最小權限原則,應用只能請求解密特定上下文的密鑰,審計所有解密操作。
- 銷毀機制:內存中密鑰用完即清零 (
Arrays.fill(keyBytes, (byte)0)),JDK 9+ 用 SecretKey.destroy()。
Spring Boot 整合配置
# application.yml
crypto:
aes-gcm:
password: ${CRYPTO_PASSWORD:} # 從環境變量/密管平台注入,非配置文件
pbkdf2-iterations: 200000 # 生產環境建議 ≥ 200k
aad: "myapp-v1" # 關聯數據
@Component
@ConfigurationProperties(prefix = "crypto.aes-gcm")
@Data
public class CryptoProperties {
private char[] password;
private int pbkdf2Iterations = 100_000;
private String aad;
}
@Service
@RequiredArgsConstructor
public class CryptoService {
private final CryptoProperties props;
public String encrypt(String plaintext) {
return AesGcmCrypto.encrypt(plaintext, props.getPassword());
}
public String decrypt(String ciphertext) {
return AesGcmCrypto.decrypt(ciphertext, props.getPassword());
}
}
常見安全陷阱
| 陷阱 | 後果 | 修正 |
| 硬編碼密鑰/IV | 代碼洩露即數據洩露 | KMS/Vault/環境變量注入 |
| 固定 IV (如全 0) | 相同明文產生相同密文,語義洩露、重放攻擊 | 每次加密 **SecureRandom 生成 12 字節隨機 IV** |
| ECB 模式 | 模式可見、塊可重排 | **禁用 ECB**,用 GCM |
| CBC 無 HMAC | 填充預言機攻擊、密文可篡改 | 改 GCM 或 CBC+HMAC (Encrypt-then-MAC) |
| 密碼直接作密鑰 | 熵不足、字典攻擊 | **PBKDF2/Argon2/bcrypt/scrypt** 派生,高迭代次數 |
| Base64 當加密 | 零安全性 | Base64 僅編碼,非加密 |
| Tag 截斷/忽略 | 失去完整性保護 | **完整 16 字節 Tag**,驗證失敗拒絕解密 |
性能優化
- AES-NI 硬件加速:現代 CPU (Intel Westmere+、AMD Bulldozer+) 內建 AES 指令集,JDK 自動啟用,GCM 吞吐可達 **GB/s 級別**。
- 複用 Cipher 對象:
Cipher 非線程安全,但可用 ThreadLocal 或對象池複用 (需 init 重置)。
- 批量加密:大文件分塊流式加密 (
CipherInputStream/CipherOutputStream),避免全量載入內存。
總結
**AES-GCM + PBKDF2 + KMS 密鑰管理** 是現代 Java 對稱加密的黃金標準。核心原則:**AEAD 模式 (GCM)、隨機 IV、完整 Tag、密鑰派生、分層密鑰、硬件加速、審計日誌、零硬編碼**。封裝 AesGcmCrypto 工具類,業務層零感知安全,定期輪換 DEK,接入 KMS 管理 KEK,即可構建合規、高性能的加密體系。
- 上一篇文章: Java 字符串內存分配深度解析:字符串常量池、堆、直接內存、JVM 參數與優化實戰
- 下一篇文章: JVM 垃圾回收核心原理:分代假設、GC 算法、收集器演進與調優實戰
Copyright ©
程式碼之家 All Rights Reserved