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

PHP 標準 AES 加密類實現:PKCS7 Padding、ECB/CBC 模式與 mcrypt 遷移指南

     編輯:PHP 相關知識

早期 PHP 專案常用 mcrypt 擴充套件實現 AES 加密,但 mcrypt 已於 PHP 7.2 棄用、PHP 8.0 移除。本文提供一個符合現代標準的 AES 加密類,使用 OpenSSL 替代 mcrypt,支援 AES-128/192/256、ECB/CBC/GCM 模式、標準 PKCS7 Padding、隨機 IV 生成,並附遷移對照表。

現代 AES 加密類(OpenSSL 版)

<?php
class AesEncryption
{
    private string $key;      // 原始二進位金鑰(16/24/32 bytes)
    private string $cipher;   // OpenSSL cipher 名稱
    private bool   $useGcm;   // 是否使用 GCM 模式

    /**
     * @param string $keyBase64 Base64 編碼的金鑰
     * @param int    $keyBits   128, 192, 或 256
     * @param string $mode      'ecb', 'cbc', 'gcm'
     */
    public function __construct(string $keyBase64, int $keyBits = 256, string $mode = 'cbc')
    {
        $this->key = base64_decode($keyBase64);
        $expectedLen = $keyBits / 8;
        if (strlen($this->key) !== $expectedLen) {
            throw new InvalidArgumentException("金鑰長度錯誤:需 {$expectedLen} 位元組({$keyBits}-bit),實際 " . strlen($this->key));
        }

        $mode = strtolower($mode);
        if ($mode === 'gcm') {
            $this->cipher = "aes-{$keyBits}-gcm";
            $this->useGcm = true;
        } elseif ($mode === 'cbc') {
            $this->cipher = "aes-{$keyBits}-cbc";
            $this->useGcm = false;
        } elseif ($mode === 'ecb') {
            $this->cipher = "aes-{$keyBits}-ecb";
            $this->useGcm = false;
        } else {
            throw new InvalidArgumentException("不支援的模式: $mode");
        }
    }

    /** 加密:回傳 Base64 字串(格式:IV + 密文 + [GCM tag]) */
    public function encrypt(string $plaintext): string
    {
        if ($this->useGcm) {
            $iv = random_bytes(12); // GCM 建議 96-bit IV
            $ciphertext = openssl_encrypt($plaintext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv, $tag, '', 12);
            if ($ciphertext === false) throw new RuntimeException('加密失敗: ' . openssl_error_string());
            // 組合:IV (12) + 密文 + Tag (16)
            return base64_encode($iv . $ciphertext . $tag);
        } else {
            $ivLen = openssl_cipher_iv_length($this->cipher);
            $iv = $ivLen > 0 ? random_bytes($ivLen) : '';
            $ciphertext = openssl_encrypt($plaintext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
            if ($ciphertext === false) throw new RuntimeException('加密失敗: ' . openssl_error_string());
            // 組合:IV + 密文(ECB 模式 IV 為空)
            return base64_encode($iv . $ciphertext);
        }
    }

    /** 解密:輸入 Base64 字串,回傳明文 */
    public function decrypt(string $ciphertextB64): string
    {
        $data = base64_decode($ciphertextB64, true);
        if ($data === false) throw new InvalidArgumentException('Base64 解碼失敗');

        if ($this->useGcm) {
            if (strlen($data) < 12 + 16) throw new InvalidArgumentException('資料長度不足');
            $iv = substr($data, 0, 12);
            $tag = substr($data, -16);
            $ciphertext = substr($data, 12, -16);
            $plaintext = openssl_decrypt($ciphertext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv, $tag, '', 12);
        } else {
            $ivLen = openssl_cipher_iv_length($this->cipher);
            if ($ivLen > 0) {
                if (strlen($data) < $ivLen) throw new InvalidArgumentException('資料長度不足含 IV');
                $iv = substr($data, 0, $ivLen);
                $ciphertext = substr($data, $ivLen);
            } else {
                $iv = '';
                $ciphertext = $data;
            }
            $plaintext = openssl_decrypt($ciphertext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
        }

        if ($plaintext === false) throw new RuntimeException('解密失敗: ' . openssl_error_string());
        return $plaintext;
    }

    /** 靜態工廠:從隨機生成金鑰建立實例 */
    public static function generateKey(int $keyBits = 256): string
    {
        return base64_encode(random_bytes($keyBits / 8));
    }
}
?>

使用範例

<?php
// 1. 產生金鑰(僅首次執行,妥善保存)
$keyB64 = AesEncryption::generateKey(256); // 例如:K7gNuZs9...==
// 存入 .env 或金鑰管理系統

// 2. 實例化(建議 CBC 或 GCM)
$aes = new AesEncryption($keyB64, 256, 'cbc'); // 或 'gcm'

// 3. 加密
$plaintext = '敏感資料:身分證 A123456789、信用卡 4242-4242-4242-4242';
$encrypted = $aes->encrypt($plaintext);
echo "加密結果:$encrypted\n";

// 4. 解密
$decrypted = $aes->decrypt($encrypted);
echo "解密還原:$decrypted\n";
assert($decrypted === $plaintext);
?>

mcrypt 到 OpenSSL 遷移對照表

mcryptOpenSSL備註
MCRYPT_RIJNDAEL_128aes-128-*, aes-192-*, aes-256-*Rijndael-128 區塊大小固定 128-bit,金鑰長度可變。OpenSSL cipher 名稱含金鑰長度。
MCRYPT_MODE_ECBaes-*-ecb不需 IV,不安全,僅相容舊系統
MCRYPT_MODE_CBCaes-*-cbc需 16-byte IV,標準模式
mcrypt_create_iv()random_bytes()mcrypt 的 IV 來源較弱,OpenSSL 強制用 CSPRNG
手動 PKCS7 Padding內建自動處理OpenSSL 自動依標準填充/移除,無需手寫
mcrypt_encrypt()openssl_encrypt()參數順序、常數名稱不同,請參考官方文件

PKCS7 Padding 原理補充(理解即可,OpenSSL 已內建)

區塊大小 16 bytes。若資料長度非 16 倍數,補齊 N 個值為 N 的位元組。

  • 資料 15 bytes → 補 1 個 0x01
  • 資料 16 bytes → 補 16 個 0x10(完整區塊也要補,以便解密時區分)

解密時讀取最後一個位元組值 N,移除尾部 N 個位元組。

模式選擇建議

  1. AES-256-GCM(首選):AEAD 模式,同時提供加密與完整性驗證,免去額外 HMAC,效能佳,現代標準(TLS 1.3、JWT、WebCrypto 皆用)。
  2. AES-256-CBC + HMAC-SHA256:舊系統相容、需自行實現 Encrypt-then-MAC。
  3. AES-128-ECB:嚴禁用於新系統,僅用於解密舊資料遷移。

金鑰管理最佳實踐

  • 金鑰輪換:定期更換金鑰,舊資料用舊金鑰解密、新資料用新金鑰加密,或重新加密全量資料。
  • 金鑰分離:不同用途(資料庫加密、API 簽名、Session 加密)使用不同金鑰,透過 KDF(如 HKDF)從主金鑰衍生。
  • 金鑰不落地:運行時從 KMS/Vault 讀入記憶體,程式碼、日誌、備份均不含金鑰明文。

掌握 OpenSSL 版 AES 實現,不僅解決 mcrypt 移除帶來的遷移痛點,更能建構符合現代密碼學標準的資料保護體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved