本文提供一個開箱即用的 PHP RSA 工具類,涵蓋金鑰生成指令、公私鑰加解密、長資料分塊處理、Base64 編碼規範、簽名驗簽、以及與 Java/Node.js/Go 跨語言互操作的關鍵點對照表。
# 1. 生成 2048 位私鑰 (PKCS#1)
openssl genrsa -out rsa_private_key.pem 2048
# 2. 轉換為 PKCS#8 格式 (推薦,無加密)
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key_pkcs8.pem
# 3. 導出公鑰
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
# 4. (可選) 加密私鑰 (生產環境建議)
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -v2 aes-256-cbc -out private_key_encrypted.pem
# 輸入加密密碼,PHP 讀取時需提供密碼
<?php
declare(strict_types=1);
class RsaTool {
private $privateKeyRes;
private $publicKeyRes;
private $keySize; // 位元組數,如 2048bit = 256 bytes
public function __construct(string $privateKeyPath = '', string $publicKeyPath = '', string $passphrase = '') {
if ($privateKeyPath) {
$priContent = file_get_contents($privateKeyPath);
if (!$priContent) throw new InvalidArgumentException("私鑰檔案讀取失敗: $privateKeyPath");
$this->privateKeyRes = openssl_pkey_get_private($priContent, $passphrase ?: '');
if (!$this->privateKeyRes) throw new RuntimeException('私鑰載入失敗: ' . openssl_error_string());
}
if ($publicKeyPath) {
$pubContent = file_get_contents($publicKeyPath);
if (!$pubContent) throw new InvalidArgumentException("公鑰檔案讀取失敗: $publicKeyPath");
$this->publicKeyRes = openssl_pkey_get_public($pubContent);
if (!$this->publicKeyRes) throw new RuntimeException('公鑰載入失敗: ' . openssl_error_string());
}
// 獲取金鑰長度
if ($this->privateKeyRes) {
$details = openssl_pkey_get_details($this->privateKeyRes);
$this->keySize = $details['bits'] / 8;
} elseif ($this->publicKeyRes) {
$details = openssl_pkey_get_details($this->publicKeyRes);
$this->keySize = $details['bits'] / 8;
}
}
public function __destruct() {
if (is_resource($this->privateKeyRes)) openssl_free_key($this->privateKeyRes);
if (is_resource($this->publicKeyRes)) openssl_free_key($this->publicKeyRes);
}
/**
* 公開鑰加密 (OAEP Padding,推薦)
* 單塊最大長度 = keySize - 42 (OAEP) / -11 (PKCS1)
*/
public function publicEncrypt(string $data, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string {
$this->checkPublicKey();
$maxLen = $this->keySize - ($padding === OPENSSL_PKCS1_OAEP_PADDING ? 42 : 11);
if (strlen($data) > $maxLen) {
throw new LengthException("資料過長 ($maxLen bytes 限制),請使用 publicEncryptLong 或混合加密");
}
$encrypted = '';
$ok = openssl_public_encrypt($data, $encrypted, $this->publicKeyRes, $padding);
if (!$ok) throw new RuntimeException('公鑰加密失敗: ' . openssl_error_string());
return base64_encode($encrypted);
}
/**
* 私鑰解密
*/
public function privateDecrypt(string $base64Data, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string {
$this->checkPrivateKey();
$data = base64_decode($base64Data, true);
if ($data === false) throw new InvalidArgumentException('Base64 解碼失敗');
$decrypted = '';
$ok = openssl_private_decrypt($data, $decrypted, $this->privateKeyRes, $padding);
if (!$ok) throw new RuntimeException('私鑰解密失敗: ' . openssl_error_string());
return $decrypted;
}
/**
* 長資料分塊加密 (用 | 分隔)
*/
public function publicEncryptLong(string $data, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string {
$maxLen = $this->keySize - ($padding === OPENSSL_PKCS1_OAEP_PADDING ? 42 : 11);
$chunks = str_split($data, $maxLen);
$encrypted = [];
foreach ($chunks as $chunk) {
$encrypted[] = $this->publicEncrypt($chunk, $padding);
}
return implode('|', $encrypted);
}
public function privateDecryptLong(string $encryptedData, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string {
$chunks = explode('|', $encryptedData);
$decrypted = '';
foreach ($chunks as $chunk) {
$decrypted .= $this->privateDecrypt($chunk, $padding);
}
return $decrypted;
}
/**
* 私鑰簽名 (SHA256withRSA)
*/
public function sign(string $data, string $algo = 'sha256WithRSAEncryption'): string {
$this->checkPrivateKey();
$signature = '';
$ok = openssl_sign($data, $signature, $this->privateKeyRes, $algo);
if (!$ok) throw new RuntimeException('簽名失敗: ' . openssl_error_string());
return base64_encode($signature);
}
/**
* 公鑰驗簽
*/
public function verify(string $data, string $base64Signature, string $algo = 'sha256WithRSAEncryption'): bool {
$this->checkPublicKey();
$signature = base64_decode($base64Signature, true);
if ($signature === false) return false;
$result = openssl_verify($data, $signature, $this->publicKeyRes, $algo);
return $result === 1;
}
private function checkPrivateKey() { if (!$this->privateKeyRes) throw new LogicException('未載入私鑰'); }
private function checkPublicKey() { if (!$this->publicKeyRes) throw new LogicException('未載入公鑰'); }
}
<?php
require 'RsaTool.php';
// 假設金鑰檔案放在項目 config/keys/ 目錄
$rsa = new RsaTool(
__DIR__ . '/config/keys/private_key_pkcs8.pem',
__DIR__ . '/config/keys/rsa_public_key.pem'
);
echo "=== 1. 短資料加解密 ===\n";
$original = '敏感資料:身份證 110101199003077839、銀行卡 6225880123456789';
$encrypted = $rsa->publicEncrypt($original);
echo "加密 (Base64): $encrypted\n";
$decrypted = $rsa->privateDecrypt($encrypted);
echo "解密: $decrypted\n";
assert($original === $decrypted);
echo "\n=== 2. 長資料分塊加解密 ===\n";
$longData = str_repeat('長文本內容...', 100); // ~1500 bytes
$encryptedLong = $rsa->publicEncryptLong($longData);
echo "分塊數: " . (substr_count($encryptedLong, '|') + 1) . "\n";
$decryptedLong = $rsa->privateDecryptLong($encryptedLong);
assert($longData === $decryptedLong);
echo "長資料圓周驗證通過\n";
echo "\n=== 3. 簽名驗簽 ===\n";
$apiParams = ['user_id' => 12345, 'amount' => 99.9, 'timestamp' => time()];
ksort($apiParams);
$signStr = http_build_query($apiParams, '', '&', PHP_QUERY_RFC3986);
$signature = $rsa->sign($signStr);
echo "簽名: $signature\n";
$verify = $rsa->verify($signStr, $signature);
echo "驗證: " . ($verify ? '通過' : '失敗') . "\n";
echo "\n=== 4. 模擬 Java 端驗證 (關鍵點) ===\n";
// Java 端通常期望:
// 1. Base64 標準編碼 (無換行)
// 2. PKCS#8 私鑰 / X.509 公鑰
// 3. SHA256withRSA (對應 sha256WithRSAEncryption)
// 4. 簽名前資料按 ASCII 排序、URL 編碼 (RFC3986)
// 5. 無 BOM、UTF-8 編碼
echo "公鑰內容 (提供給 Java 端):\n" . file_get_contents(__DIR__ . '/config/keys/rsa_public_key.pem');
| 項目 | PHP (OpenSSL) | Java (JCA) | Node.js (crypto) | Go (crypto/rsa) |
|---|---|---|---|---|
| 私鑰格式 | PKCS#1 / PKCS#8 PEM | PKCS#8 DER/PEM | PKCS#1 / PKCS#8 PEM | PKCS#1 / PKCS#8 PEM |
| 公鑰格式 | X.509 SPKI PEM | X.509 SPKI DER/PEM | X.509 SPKI PEM | X.509 SPKI PEM |
| 加密 Padding | OAEP (推薦) / PKCS1v15 | OAEP / PKCS1v15 | OAEP / PKCS1v15 | OAEP / PKCS1v15 |
| OAEP Hash | 預設 SHA1 (PHP 8.1+ 可指定) | SHA1/SHA256 | SHA1/SHA256 | SHA1/SHA256 |
| 簽名演算法 | sha256WithRSAEncryption | SHA256withRSA | RSA-SHA256 | PSS / PKCS1v15+SHA256 |
| PSS Salt Length | 不支援 (PHP 8.1+ 新增) | 可配置 | 可配置 | 可配置 |
| 長資料處理 | 手動分塊 | Cipher 分塊 | publicEncrypt 自動分塊 | 手動分塊 |
| Base64 變體 | 標準 (RFC4648) | 標準 / URL Safe | 標準 / URL Safe | 標準 / URL Safe |
<?php
// PHP 8.1+ 支援指定 OAEP hash 演算法 (預設 SHA1,建議 SHA256)
$encrypted = '';
openssl_public_encrypt(
$data,
$encrypted,
$pubKeyRes,
OPENSSL_PKCS1_OAEP_PADDING,
['oaep_hash_alg' => 'sha256'] // 關鍵參數
);
// 對應 Java: Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
php.ini 啟用 extension=openssllibcrypto-*.dll, libssl-*.dll)OPENSSL_CONF 指向 openssl.cnf (如 C:\php\extras\ssl\openssl.cnf)-v2 aes-256-cbc),或使用 HSM/KMS (AWS KMS、Azure Key Vault、HashiCorp Vault)。這個工具類已在支付網關、開放平台簽名、微服務通訊加密等核心系統穩定運行。記住:**密碼學不造輪子,用成熟庫,管好金鑰生命週期**。