舊文使用 DES/ECB/PKCS5 已不符合現代安全標準 (鍵長 56-bit、無完整性保護、ECB 模式洩露模式)。本文提供 AES-256-GCM 經認證加密 (AEAD) 實作,PHP 使用 openssl_encrypt,C# 使用 System.Security.Cryptography.AesGcm (.NET 6+),雙向驗證向量、標籤、金鑰衍生 (PBKDF2) 完全相容。
| 項目 | 規格 |
|---|---|
| 演算法 | AES-256-GCM (AEAD) |
| 金鑰衍生 | PBKDF2-HMAC-SHA256, 100,000 迭代, 32 位元組金鑰 |
| Nonce/IV | 12 位元組 (96-bit) 隨機產生,每次加密唯一 |
| 關聯資料 (AAD) | 可選,用於綁定上下文 (如用戶 ID、API 版本) |
| 輸出格式 | Base64( version(1) || salt(16) || nonce(12) || ciphertext || tag(16) ) |
<?php
declare(strict_types=1);
namespace App\Security;
class AesGcmCipher {
private const VERSION = 1;
private const ITERATIONS = 100_000;
private const KEY_LEN = 32; // 256-bit
private const SALT_LEN = 16;
private const NONCE_LEN = 12;
private const TAG_LEN = 16;
public function __construct(private string $masterKey) {} // 32+ 字元高熵密碼
/** @return string Base64 編碼的完整密文包 */
public function encrypt(string $plaintext, string $aad = ''): string {
$salt = random_bytes(self::SALT_LEN);
$key = hash_pbkdf2('sha256', $this->masterKey, $salt, self::ITERATIONS, self::KEY_LEN, true);
$nonce = random_bytes(self::NONCE_LEN);
$ciphertext = '';
$tag = '';
$len = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad, 16);
if ($len === false) throw new \RuntimeException('加密失敗: '.openssl_error_string());
$packet = self::VERSION . $salt . $nonce . $ciphertext . $tag;
return base64_encode($packet);
}
public function decrypt(string $b64Packet, string $aad = ''): string {
$packet = base64_decode($b64Packet, true);
if ($packet === false || strlen($packet) < 1 + self::SALT_LEN + self::NONCE_LEN + self::TAG_LEN) {
throw new \InvalidArgumentException('密文封包格式錯誤');
}
$version = ord($packet[0]);
if ($version !== self::VERSION) throw new \RuntimeException("不支援的版本 $version");
$offset = 1;
$salt = substr($packet, $offset, self::SALT_LEN); $offset += self::SALT_LEN;
$nonce = substr($packet, $offset, self::NONCE_LEN); $offset += self::NONCE_LEN;
$tag = substr($packet, -self::TAG_LEN);
$ciphertext = substr($packet, $offset, -self::TAG_LEN);
$key = hash_pbkdf2('sha256', $this->masterKey, $salt, self::ITERATIONS, self::KEY_LEN, true);
$plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
if ($plaintext === false) throw new \RuntimeException('解密失敗: 驗證標籤不符或金鑰錯誤');
return $plaintext;
}
}
// 使用
$cipher = new AesGcmCipher('super-secret-master-key-32-chars!!');
$encrypted = $cipher->encrypt('敏感資料: 信用卡 4242-4242-4242-4242', 'user_12345');
echo "Encrypted: $encrypted\n";
$decrypted = $cipher->decrypt($encrypted, 'user_12345');
echo "Decrypted: $decrypted\n";
?>
using System;
using System.Security.Cryptography;
using System.Text;
public static class AesGcmCipher
{
private const byte Version = 1;
private const int Iterations = 100_000;
private const int KeySize = 32; // 256-bit
private const int SaltSize = 16;
private const int NonceSize = 12;
private const int TagSize = 16;
public static string Encrypt(string plaintext, string masterKey, string aad = "")
{
var masterKeyBytes = Encoding.UTF8.GetBytes(masterKey);
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var key = Rfc2898DeriveBytes.Pbkdf2(masterKeyBytes, salt, Iterations, HashAlgorithmName.SHA256, KeySize);
var nonce = RandomNumberGenerator.GetBytes(NonceSize);
var ciphertext = new byte[plaintext.Length];
var tag = new byte[TagSize];
var aadBytes = Encoding.UTF8.GetBytes(aad);
AesGcm.Encrypt(key, nonce, Encoding.UTF8.GetBytes(plaintext), ciphertext, tag, aadBytes);
var packet = new byte[1 + SaltSize + NonceSize + ciphertext.Length + TagSize];
packet[0] = Version;
Buffer.BlockCopy(salt, 0, packet, 1, SaltSize);
Buffer.BlockCopy(nonce, 0, packet, 1 + SaltSize, NonceSize);
Buffer.BlockCopy(ciphertext, 0, packet, 1 + SaltSize + NonceSize, ciphertext.Length);
Buffer.BlockCopy(tag, 0, packet, packet.Length - TagSize, TagSize);
return Convert.ToBase64String(packet);
}
public static string Decrypt(string b64Packet, string masterKey, string aad = "")
{
var packet = Convert.FromBase64String(b64Packet);
if (packet.Length < 1 + SaltSize + NonceSize + TagSize)
throw new ArgumentException("Invalid packet");
if (packet[0] != Version)
throw new NotSupportedException($"Unsupported version {packet[0]}");
var salt = packet[1..(1+SaltSize)];
var nonce = packet[(1+SaltSize)..(1+SaltSize+NonceSize)];
var tag = packet[^TagSize..];
var ciphertext = packet[(1+SaltSize+NonceSize)..^TagSize];
var masterKeyBytes = Encoding.UTF8.GetBytes(masterKey);
var key = Rfc2898DeriveBytes.Pbkdf2(masterKeyBytes, salt, Iterations, HashAlgorithmName.SHA256, KeySize);
var aadBytes = Encoding.UTF8.GetBytes(aad);
var plaintext = new byte[ciphertext.Length];
AesGcm.Decrypt(key, nonce, ciphertext, plaintext, tag, aadBytes);
return Encoding.UTF8.GetString(plaintext);
}
}
// 測試互通性
var master = "super-secret-master-key-32-chars!!";
var encrypted = AesGcmCipher.Encrypt("敏感資料: 信用卡 4242-4242-4242-4242", master, "user_12345");
Console.WriteLine($"C# Encrypted: {encrypted}");
var decrypted = AesGcmCipher.Decrypt(encrypted, master, "user_12345");
Console.WriteLine($"C# Decrypted: {decrypted}");
// 驗證 PHP 產出的密文
var phpEncrypted = "..."; // 從 PHP 端複製
Console.WriteLine($"PHP -> C#: {AesGcmCipher.Decrypt(phpEncrypted, master, "user_12345")}");
$masterKey 應從環境變數/密鑰管理系統 (Vault/Azure Key Vault) 讀取,**勿硬編碼**。openssl_encrypt_stream / AesGcm.EncryptStream。