首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP AES 加密解密完全指南:從 MCrypt 遷移到 OpenSSL 的現代化實踐

     編輯:PHP 相關知識

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 PaddingPKCS#7
加密模式ECB/CBC/CFB/OFB/CTR/GCM/CCMGCM (認證加密) / 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密鑰必須為精確字節長度,避免截斷/補零

安全最佳實踐總結

  1. 優先 AES-GCM:一步到位獲得機密性+完整性,避免手動 HMAC 易出錯。
  2. 密鑰管理分離:主密鑰存 KMS/環境變量,代碼只讀取;定期輪換,支持版本號。
  3. IV 唯一性:每次加密 random_bytes(),同一密鑰下 IV 絕不重複(GCM 重複 IV 會災難性破解)。
  4. AAD 綁定上下文:API 版本、用戶 ID、時間戳作為 AAD,防止密文跨上下文重放。
  5. 恒時比較:驗證 MAC/Tag 時使用 hash_equals() 防時序攻擊(OpenSSL GCM 內部已處理)。
  6. 棄用 ECB、棄用 MCrypt、棄用自定義加密。
加密庫的選擇不應成為業務代碼的負擔。封裝為 AesGcm::encrypt/decrypt 這樣的單方法調用,讓開發者專注於「什麼數據需要加密」而非「如何加密」。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved