在 PHP 開發中,對稱加密是保護敏感數據(如 API 金鑰、用戶 Token、配置文件密碼)的常用手段。早期開發者多依賴 mcrypt 擴展,但自 PHP 7.1 起已棄用,PHP 7.2 完全移除。本文將梳理經典 mcrypt 代碼遷移至現代 openssl 的最佳實踐,並提供一個開箱即用的加密工具類。
hash('sha256', $key, true) 產生 32 字節金鑰,而非直接使用任意長度字串。| mcrypt 函數 | OpenSSL 替代方案 |
|---|---|
mcrypt_encrypt() | openssl_encrypt() |
mcrypt_decrypt() | openssl_decrypt() |
mcrypt_create_iv() | openssl_random_pseudo_bytes() 或 random_bytes() |
mcrypt_get_iv_size() | openssl_cipher_iv_length() |
<?php
namespace App\Security;
class SymmetricCrypt
{
private const CIPHER = 'AES-256-CBC';
private const KEY_LENGTH = 32; // 256 bits
private const IV_LENGTH = 16; // 128 bits
/** @var string 原始金鑰(建議從環境變數讀取) */
private string $rawKey;
public function __construct(string $rawKey)
{
$this->rawKey = $rawKey;
}
/** 取得派生後的 32 字節金鑰 */
private function getKey(): string
{
return hash('sha256', $this->rawKey, true);
}
/** 加密並回傳 Base64(含 IV 前綴) */
public function encrypt(string $plaintext): string
{
$iv = random_bytes(self::IV_LENGTH);
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$this->getKey(),
OPENSSL_RAW_DATA,
$iv
);
if ($ciphertext === false) {
throw new \RuntimeException('加密失敗:' . openssl_error_string());
}
// IV + 密文 → Base64,解密時先切割 IV
return base64_encode($iv . $ciphertext);
}
/** 解密 Base64 字串 */
public function decrypt(string $payload): string
{
$data = base64_decode($payload, true);
if ($data === false) {
throw new \InvalidArgumentException('非法的 Base64 載荷');
}
if (strlen($data) < self::IV_LENGTH) {
throw new \InvalidArgumentException('載荷長度不足');
}
$iv = substr($data, 0, self::IV_LENGTH);
$ciphertext = substr($data, self::IV_LENGTH);
$plaintext = openssl_decrypt(
$ciphertext,
self::CIPHER,
$this->getKey(),
OPENSSL_RAW_DATA,
$iv
);
if ($plaintext === false) {
throw new \RuntimeException('解密失敗:資料可能已竄改或金鑰錯誤');
}
return $plaintext;
}
}
<?php
require_once 'vendor/autoload.php';
$key = getenv('APP_ENCRYPTION_KEY') ?: 'your-32-char-secret-key-here!!';
$crypt = new App\Security\SymmetricCrypt($key);
$original = '敏感資料:API_SECRET=sk_live_xxx...';
$encrypted = $crypt->encrypt($original);
$decrypted = $crypt->decrypt($encrypted);
echo "原文:$original\n";
echo "密文:$encrypted\n";
echo "解密:$decrypted\n";
assert($original === $decrypted);
OPENSSL_RAW_DATA 模式輸出二進位,務必自行 Base64 編碼;預設模式會雙重 Base64 導致解密失敗。openssl_encrypt/decrypt 失敗回傳 false,需搭配 openssl_error_string() 診斷。若專案需相容舊版 DES / AES-128 / AES-256 並支援 CBC/ECB/OFB/CFB 模式,可參考下列重構版(已移除 mcrypt 依賴):
<?php
namespace App\Security;
class Xcrypt
{
private string $cipher;
private string $key;
private string $iv;
/** 支援 key 長度:16(DES/AES-128), 24(AES-192), 32(AES-256) */
public function __construct(string $key, string $mode = 'cbc', string $iv = 'auto')
{
$klen = strlen($key);
$this->key = hash('sha256', $key, true); // 統一派生 32 字節
$this->cipher = match ($klen) {
8 => 'DES-CBC', // 僅相容舊資料,新專案勿用
16 => 'AES-128-CBC',
24 => 'AES-192-CBC',
32 => 'AES-256-CBC',
default => throw new \InvalidArgumentException('金鑰長度需為 8/16/24/32 字節'),
};
// 模式映射(OpenSSL 命名慣例)
$this->cipher = str_replace('CBC', strtoupper($mode), $this->cipher);
// IV 處理
$ivLen = openssl_cipher_iv_length($this->cipher);
if ($iv === 'off') {
$this->iv = '';
} elseif ($iv === 'auto') {
$this->iv = random_bytes($ivLen);
} else {
if (strlen($iv) !== $ivLen) {
throw new \InvalidArgumentException("IV 長度需為 $ivLen 字節");
}
$this->iv = $iv;
}
}
public function getIV(string $encoding = 'base64'): string
{
return match ($encoding) {
'base64' => base64_encode($this->iv),
'hex' => bin2hex($this->iv),
'bin' => $this->iv,
default => throw new \InvalidArgumentException('不支援的編碼'),
};
}
public function encrypt(string $data, string $encoding = 'base64'): string
{
$ct = openssl_encrypt($data, $this->cipher, $this->key, OPENSSL_RAW_DATA, $this->iv);
if ($ct === false) throw new \RuntimeException('加密失敗');
return match ($encoding) {
'base64' => base64_encode($ct),
'hex' => bin2hex($ct),
'bin' => $ct,
};
}
public function decrypt(string $data, string $encoding = 'base64'): string
{
$bin = match ($encoding) {
'base64' => base64_decode($data, true),
'hex' => hex2bin($data),
'bin' => $data,
};
if ($bin === false) throw new \InvalidArgumentException('編碼格式錯誤');
$pt = openssl_decrypt($bin, $this->cipher, $this->key, OPENSSL_RAW_DATA, $this->iv);
if ($pt === false) throw new \RuntimeException('解密失敗');
return $pt;
}
}
淘汰 mcrypt、擁抱 OpenSSL 是 PHP 專案現代化的必經之路。掌握金鑰派生、IV 管理、編碼規範三大核心,即可構建安全、可維護的對稱加密體系。建議將金鑰存放於環境變數或密碼管理系統(如 Vault),切勿硬編碼於代碼庫中。