AES (Advanced Encryption Standard) 是對稱加密的行業標準。PHP 7.2 移除 MCrypt 擴展後,OpenSSL 成為唯一原生選擇。本文系統講解 AES 模式選擇、密鑰派生、IV 生成、認證加密 (GCM/CCM)、兼容性遷移,提供生產級封裝類。
核心概念速查
| 概念 | 說明 | 推薦值 |
| 密鑰長度 | AES-128/192/256 對應 16/24/32 字節 | 32 字節 (AES-256) |
| 填充模式 | PKCS#7 (OpenSSL 默認) / Zero Padding | PKCS#7 |
| 加密模式 | ECB/CBC/CFB/OFB/CTR/GCM/CCM | GCM (認證加密) / CBC (兼容舊系統) |
| IV/Nonce | 初始化向量,CBC 需 16 字節隨機,GCM 建議 12 字節 | random_bytes() 生成 |
| 認證標簽 | GCM/CCM 產生的完整性校驗碼 (Tag),解密時必須驗證 | 16 字節 |
模式選擇決策樹
需要認證加密 (防篡改)?
├─ 是 → AES-GCM (PHP 7.1+) / AES-CCM (PHP 8.1+)
└─ 否 → 兼容舊系統 (Java/C#/Python 既有 CBC 實現)?
├─ 是 → AES-CBC + HMAC-SHA256 (Encrypt-then-MAC)
└─ 否 → 仍建議用 GCM,除非極端性能敏感且無完整性需求
生產級封裝:AES-256-GCM (推薦標準)
<?php
/**
* 現代化 AES-256-GCM 加密工具
* 特性:認證加密、隨機 IV、AAD 支持、Base64 安全傳輸
* 需 PHP 7.1+ OpenSSL
*/
class AesGcm
{
private const CIPHER = 'aes-256-gcm';
private const KEY_BYTES = 32;
private const IV_BYTES = 12; // NIST 推薦 96 bits
private const TAG_BYTES = 16;
/** 加密
* @param string $plaintext 明文
* @param string $keyBase64 Base64 編碼的 32 字節主密鑰
* @param string|null $aad 附加認證數據 (可選)
* @return string Base64 編碼密文 (IV || Ciphertext || Tag)
*/
public static function encrypt(string $plaintext, string $keyBase64, ?string $aad = null): string
{
$key = base64_decode($keyBase64, true);
if ($key === false || strlen($key) !== self::KEY_BYTES) {
throw new \InvalidArgumentException('密鑰必須為 32 字節 Base64 編碼字符串');
}
$iv = random_bytes(self::IV_BYTES);
$tag = null;
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad ?? '',
self::TAG_BYTES
);
if ($ciphertext === false) {
throw new \RuntimeException('加密失敗: ' . openssl_error_string());
}
// 組合格式:IV (12) + Ciphertext + Tag (16)
return base64_encode($iv . $ciphertext . $tag);
}
/** 解密
* @param string $payloadBase64 Base64 編碼密文
* @param string $keyBase64 Base64 編碼主密鑰
* @param string|null $aad 附加認證數據 (需與加密時一致)
* @return string 明文
*/
public static function decrypt(string $payloadBase64, string $keyBase64, ?string $aad = null): string
{
$key = base64_decode($keyBase64, true);
$payload = base64_decode($payloadBase64, true);
if ($payload === false) throw new \InvalidArgumentException('無效的 Base64 密文');
$iv = substr($payload, 0, self::IV_BYTES);
$tag = substr($payload, -self::TAG_BYTES);
$ciphertext = substr($payload, self::IV_BYTES, -self::TAG_BYTES);
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad ?? ''
);
if ($plaintext === false) {
throw new \RuntimeException('解密失敗:密文被篡改、密鑰錯誤或 AAD 不匹配');
}
return $plaintext;
}
/** 生成隨機主密鑰 (僅執行一次,安全存儲) */
public static function generateKey(): string
{
return base64_encode(random_bytes(self::KEY_BYTES));
}
}
// === 使用示例 ===
// 1. 生成密鑰 → 存入 .env / Vault / KMS
// $masterKey = AesGcm::generateKey(); // 例: "jX9f2K8LmP3Q7RtVwYzAcDeFgHiJkLmN="
$masterKey = 'jX9f2K8LmP3Q7RtVwYzAcDeFgHiJkLmN='; // 從配置讀取
// 2. 加密敏感數據
$plaintext = '{"user_id":12345,"id_card":"110101199001011234","phone":"13800001111"}';
$encrypted = AesGcm::encrypt($plaintext, $masterKey, 'api_v1_user_profile');
echo "加密: $encrypted\n";
// 3. 解密
$decrypted = AesGcm::decrypt($encrypted, $masterKey, 'api_v1_user_profile');
echo "解密: $decrypted\n";
?>
兼容舊系統:AES-256-CBC + HMAC-SHA256 (Encrypt-then-MAC)
<?php
/**
* 兼容模式:AES-CBC + HMAC (Encrypt-then-MAC)
* 適用於需與 Java/C#/Go/Python 既有 CBC 實現互通的場景
*/
class AesCbcHmac
{
private const CIPHER = 'aes-256-cbc';
private const KEY_BYTES = 32;
private const IV_BYTES = 16;
private const MAC_ALGO = 'sha256';
/** 派生加密鑰和 MAC 鑰 */
private static function deriveKeys(string $masterKeyBase64): array
{
$master = base64_decode($masterKeyBase64, true);
// HKDF 派生 (需 PHP 7.1+ hash_hkdf)
$encKey = hash_hkdf('sha256', $master, 32, '', 'encryption');
$macKey = hash_hkdf('sha256', $master, 32, '', 'authentication');
return [hex2bin($encKey), hex2bin($macKey)];
}
public static function encrypt(string $plaintext, string $masterKeyBase64): string
{
[$encKey, $macKey] = self::deriveKeys($masterKeyBase64);
$iv = random_bytes(self::IV_BYTES);
$ciphertext = openssl_encrypt($plaintext, self::CIPHER, $encKey, OPENSSL_RAW_DATA, $iv);
// Encrypt-then-MAC: HMAC(IV || Ciphertext)
$mac = hash_hmac(self::MAC_ALGO, $iv . $ciphertext, $macKey, true);
return base64_encode($iv . $ciphertext . $mac);
}
public static function decrypt(string $payloadBase64, string $masterKeyBase64): string
{
[$encKey, $macKey] = self::deriveKeys($masterKeyBase64);
$payload = base64_decode($payloadBase64, true);
$iv = substr($payload, 0, self::IV_BYTES);
$mac = substr($payload, -32); // SHA256 = 32 bytes
$ciphertext = substr($payload, self::IV_BYTES, -32);
// 驗證 MAC (恒時比較防時序攻擊)
$expectedMac = hash_hmac(self::MAC_ALGO, $iv . $ciphertext, $macKey, true);
if (!hash_equals($expectedMac, $mac)) {
throw new \RuntimeException('MAC 驗證失敗:密文被篡改');
}
$plaintext = openssl_decrypt($ciphertext, self::CIPHER, $encKey, OPENSSL_RAW_DATA, $iv);
if ($plaintext === false) throw new \RuntimeException('解密失敗');
return $plaintext;
}
}
?>
遷移檢查清單:從 MCrypt 到 OpenSSL
| MCrypt 代碼 | OpenSSL 替代 | 注意事項 |
MCRYPT_RIJNDAEL_128 | 'aes-128-cbc' 等 | Rijndael-256 ≠ AES-256 (塊大小不同),遷移需確認算法 |
mcrypt_encrypt(..., $iv) | openssl_encrypt(..., OPENSSL_RAW_DATA, $iv) | OpenSSL 默認 PKCS#7 填充,MCrypt 需手動填充 |
mcrypt_create_iv() | random_bytes() | MCrypt 的 MCRYPT_RAND/DEV_URANDOM 不再推薦 |
| ECB 模式 | 嚴禁使用 | ECB 無 IV,相同明文產生相同密文,嚴重洩露模式 |
| 密鑰直接使用字符串 | hash('sha256', $pwd, true) 或 HKDF | 密鑰必須為精確字節長度,避免截斷/補零 |
安全最佳實踐總結
- 優先 AES-GCM:一步到位獲得機密性+完整性,避免手動 HMAC 易出錯。
- 密鑰管理分離:主密鑰存 KMS/環境變量,代碼只讀取;定期輪換,支持版本號。
- IV 唯一性:每次加密
random_bytes(),同一密鑰下 IV 絕不重複(GCM 重複 IV 會災難性破解)。
- AAD 綁定上下文:API 版本、用戶 ID、時間戳作為 AAD,防止密文跨上下文重放。
- 恒時比較:驗證 MAC/Tag 時使用
hash_equals() 防時序攻擊(OpenSSL GCM 內部已處理)。
- 棄用 ECB、棄用 MCrypt、棄用自定義加密。
加密庫的選擇不應成為業務代碼的負擔。封裝為 AesGcm::encrypt/decrypt 這樣的單方法調用,讓開發者專注於「什麼數據需要加密」而非「如何加密」。