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

PHP 與 C# 互通的可逆加密:AES-256-GCM 標準實作、金鑰衍生、跨平台相容性驗證

     編輯:PHP 綜合知識

舊文使用 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/IV12 位元組 (96-bit) 隨機產生,每次加密唯一
關聯資料 (AAD)可選,用於綁定上下文 (如用戶 ID、API 版本)
輸出格式Base64( version(1) || salt(16) || nonce(12) || ciphertext || tag(16) )

二、PHP 實作 (openssl)

<?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";
?>

三、C# 實作 (.NET 6+, AesGcm)

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")}");

四、跨平台驗證清單

  1. 版本位元組:PHP `chr(1)` == C# `byte 1`。
  2. 鹽值長度/位置:16 位元組,緊接版本後。
  3. Nonce 長度/位置:12 位元組,鹽值後。
  4. 標籤長度/位置:16 位元組,密文末尾。
  5. PBKDF2 參數完全一致:SHA256、100,000 迭代、32 位元組輸出。
  6. AAD 編碼:雙方均用 UTF-8。
  7. Base64 標準:RFC 4648 標準編碼 (無換行)。

五、安全最佳實踐

  • 主金鑰 $masterKey 應從環境變數/密鑰管理系統 (Vault/Azure Key Vault) 讀取,**勿硬編碼**。
  • 定期輪換主金鑰:保留版本號,解密時依版本選擇對應主金鑰。
  • Nonce **絕不重複使用** (配合隨機產生機率極低),若需確定性加密改用 AES-SIV。
  • 大量資料加密改用串流模式 openssl_encrypt_stream / AesGcm.EncryptStream。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved