TripleDES (3DES) 作為 DES 的增強版,曾廣泛用於支付、金融、老系統對接。本文提供可直接落地的 PHP 實作,深度剖析 ECB 模式缺陷、金鑰派生、IV 處理、填充機制等關鍵細節,並給出遷移至 AES-GCM/ChaCha20-Poly1305 的現代化路徑。
注意:mcrypt 擴充套件已於 PHP 7.2 棄用、7.3 移除。現代專案**必須使用 openssl** 或 sodium。
<?php
/**
* TripleDES 加密解密工具類 (OpenSSL 實作,相容 PKCS#7 填充)
*/
class TripleDesCipher
{
private string $cipher = 'des-ede3'; // OpenSSL 名稱:3DES-EDE (三金鑰)
private int $keyLength = 24; // 3DES 需 24 位元組 (192 bits)
private int $ivLength = 8; // DES 區塊大小 64 bits = 8 bytes
private string $paddingMethod = 'pkcs7'; // PKCS#7 標準填充
/**
* 從任意長度密碼派生 24 位元組金鑰 (相容舊系統常見 MD5 截取)
* 生產環境建議使用 PBKDF2/Argon2/Scrypt + 隨機鹽值
*/
private function deriveKey(string $password): string
{
// 舊系統常見:md5($password) 取前 24 字元 (16 字元 hex = 8 bytes,不足 24)
// 修正:使用 hash('sha256', $password, true) 取前 24 bytes
return substr(hash('sha256', $password, true), 0, $this->keyLength);
}
/**
* 加密
* @param string $plaintext 明文
* @param string $password 密碼/金鑰
* @param bool $base64 輸出是否 Base64 編碼
* @return string 密文 (Base64 或原始二進制)
* @throws Exception
*/
public function encrypt(string $plaintext, string $password, bool $base64 = true): string
{
$key = $this->deriveKey($password);
// **關鍵:每次加密生成隨機 IV**,ECB 模式不需 IV 但極度不安全,這裡強制使用 CBC
$iv = random_bytes($this->ivLength);
// OpenSSL 自動處理 PKCS#7 填充 (OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING 需手動)
$ciphertext = openssl_encrypt(
$plaintext,
$this->cipher,
$key,
OPENSSL_RAW_DATA, // 輸出原始二進制
$iv // CBC 模式必須提供 IV
);
if ($ciphertext === false) {
throw new Exception('Encryption failed: ' . openssl_error_string());
}
// 組合:IV (8 bytes) + 密文,解密時需分離
$combined = $iv . $ciphertext;
return $base64 ? base64_encode($combined) : $combined;
}
/**
* 解密
* @param string $ciphertextBase64 Base64 編碼的密文 (含 IV 前綴)
* @param string $password 密碼/金鑰
* @return string 明文
* @throws Exception
*/
public function decrypt(string $ciphertextBase64, string $password): string
{
$combined = base64_decode($ciphertextBase64, true);
if ($combined === false) {
throw new Exception('Invalid Base64 input');
}
if (strlen($combined) < $this->ivLength) {
throw new Exception('Ciphertext too short to contain IV');
}
// 分離 IV 與密文
$iv = substr($combined, 0, $this->ivLength);
$ciphertext = substr($combined, $this->ivLength);
$key = $this->deriveKey($password);
$plaintext = openssl_decrypt(
$ciphertext,
$this->cipher,
$key,
OPENSSL_RAW_DATA,
$iv
);
if ($plaintext === false) {
throw new Exception('Decryption failed: ' . openssl_error_string());
}
return $plaintext;
}
}
// === 使用範例 ===
$cipher = new TripleDesCipher();
$password = 'MySuperSecretKey2024!';
$data = '敏感資料:身分證 A123456789、信用卡 4242-4242-4242-4242';
try {
$encrypted = $cipher->encrypt($data, $password);
echo "Encrypted (Base64): $encrypted\n";
$decrypted = $cipher->decrypt($encrypted, $password);
echo "Decrypted: $decrypted\n";
assert($decrypted === $data, 'Round-trip failed');
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>
des-ede3:對應 3-Key TripleDES (Encrypt-Decrypt-Encrypt)des-ede,**安全性較低,不推薦**| 模式 | IV 需求 | 安全性 | 適用場景 |
|---|---|---|---|
| ECB (Electronic Codebook) | 無 | **極差**:相同明文塊產生相同密文塊,圖像可見輪廓 | **嚴禁用於機密資料** |
| CBC (Cipher Block Chaining) | 隨機 IV (8 bytes) | 良好:需隨機 IV、正確填充、防填充神諭攻擊 | **標準選擇** (配合 HMAC 或改用 GCM) |
| CTR / OFB / CFB | Nonce/IV | 良好:串流模式、可並行、無填充 | 特定場景 (需同步計數器) |
| GCM / CCM | Nonce (12 bytes 推薦) | **最佳**:認證加密 (AEAD)、完整性+機密性 | **現代首選** (AES-GCM, ChaCha20-Poly1305) |
原文範例使用 ECB 且 IV 固定/隨機但未儲存,**無法正確解密**,且洩露明文模式。
OPENSSL_ZERO_PADDING 標誌需自行處理random_bytes(8)| 場景 | 舊系統 (mcrypt/Java/.NET/Python) 常見做法 | PHP OpenSSL 對應調整 |
|---|---|---|
| 金鑰派生 | MD5(password) 取 16/24 字元 hex / 直接用 password | deriveKey() 模擬邏輯,或協商改用 PBKDF2 |
| 模式 | ECB (無 IV) | **拒絕相容 ECB**,協商升級 CBC+隨機 IV |
| IV 來源 | 固定全 0、固定字串、密碼派生 | 隨機生成,前綴輸出 |
| 填充 | PKCS#5/7、Zero、ISO 10126、無填充 | OpenSSL 預設 PKCS#7,其餘需手動 |
| 輸出格式 | Base64、Hex、原始二進制、IV+密文、密文+IV | 協商格式,建議 Base64(IV||密文) |
| 字元編碼 | UTF-8、GBK、Latin1 | 明確指定 UTF-8,加密前統一編碼 |
<?php
/**
* 現代認證加密:AES-256-GCM (PHP 7.1+)
* 優點:AEAD、高效能 (AES-NI)、標準化、無填充神諭攻擊
*/
class ModernAeadCipher
{
private string $cipher = 'aes-256-gcm';
private int $keyLength = 32; // 256 bits
private int $ivLength = 12; // GCM 建議 96 bits (12 bytes)
private int $tagLength = 16; // 認證標籤 128 bits
/** 從密碼派生金鑰 (生產請用 Argon2ID/PBKDF2 + 儲存鹽值) */
private function deriveKey(string $password, string $salt = ''): string
{
if ($salt === '') $salt = random_bytes(16);
// PBKDF2: 100,000 迭代,SHA-256
return hash_pbkdf2('sha256', $password, $salt, 100_000, $this->keyLength, true);
}
public function encrypt(string $plaintext, string $password, string $associatedData = ''): string
{
$salt = random_bytes(16);
$key = $this->deriveKey($password, $salt);
$iv = random_bytes($this->ivLength);
$ciphertext = openssl_encrypt(
$plaintext,
$this->cipher,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag, // 由引用返回認證標籤
$associatedData, // AAD: 關聯資料 (如用戶ID、時間戳),不加密但驗證完整性
$this->tagLength
);
if ($ciphertext === false) throw new Exception('AES-GCM encryption failed');
// 組合:Salt(16) + IV(12) + Tag(16) + Ciphertext
$combined = $salt . $iv . $tag . $ciphertext;
return base64_encode($combined);
}
public function decrypt(string $payloadBase64, string $password, string $associatedData = ''): string
{
$combined = base64_decode($payloadBase64, true);
if ($combined === false) throw new Exception('Invalid Base64');
$minLen = 16 + 12 + 16; // Salt + IV + Tag
if (strlen($combined) < $minLen) throw new Exception('Payload too short');
$salt = substr($combined, 0, 16);
$iv = substr($combined, 16, 12);
$tag = substr($combined, 28, 16);
$ciphertext = substr($combined, 44);
$key = $this->deriveKey($password, $salt);
$plaintext = openssl_decrypt(
$ciphertext,
$this->cipher,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$associatedData
);
if ($plaintext === false) throw new Exception('Decryption failed: authentication tag mismatch or key error');
return $plaintext;
}
}
// Sodium 更簡潔 (PHP 7.2+), 內建 Argon2ID 金鑰派生 + XChaCha20-Poly1305
// sodium_crypto_aead_xchacha20poly1305_ietf_encrypt/decrypt
?>
v1|base64(salt|iv|tag|ct),支援多演算法並存平滑遷移加密是系統安全的基石,演算法選型與金鑰管理同樣重要。定期審視密碼學組件,跟進標準演進,是守護資料資產的長期策略。