
本文详解如何在 php 中准确复现 java 版 samsung pay 加密载荷(jwt-like 格式)的解密流程,涵盖 pkcs#8 私钥加载、rsa 解密加密密钥、aes-128-gcm 解密主数据三步核心逻辑,并指出 phpseclib v3 的关键用法陷阱与最佳实践。
本文详解如何在 php 中准确复现 java 版 samsung pay 加密载荷(jwt-like 格式)的解密流程,涵盖 pkcs#8 私钥加载、rsa 解密加密密钥、aes-128-gcm 解密主数据三步核心逻辑,并指出 phpseclib v3 的关键用法陷阱与最佳实践。
Samsung Pay 服务端返回的加密载荷通常采用类似 JWT 的分段 Base64URL 编码格式(header.payload.iv.ciphertext.tag),其解密需严格遵循两层嵌套流程:先用 RSA 私钥解出 AES 密钥,再用该密钥 + IV + Tag 执行 AES-128-GCM 解密。Java 示例代码逻辑清晰,但迁移到 PHP 时极易因密钥加载方式、填充模式或 GCM 参数配置差异导致 Decryption error。以下为基于 phpseclib 3.x 的生产级实现方案。
✅ 正确加载私钥:避免常见路径陷阱
Java 中通过 FileInputStream 读取 PEM 文件字节流,而 phpseclib v3 的 RSA::loadPrivateKeyFormat() 不接受文件路径字符串,仅接收密钥内容(string)。若直接传入路径(如 "./rsapriv.pem"),库会尝试将其解析为密钥文本,必然失败。
✅ 正确做法是使用 file_get_contents() 读取文件内容,并推荐使用更智能的 PublicKeyLoader(自动识别 PEM/DER/PKCS#1/PKCS#8 等格式):
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
private function loadPrivateKey(string $keyPath): RSA
{
$keyContent = file_get_contents($keyPath);
if ($keyContent === false) {
throw new RuntimeException("Failed to read private key file: {$keyPath}");
}
return PublicKeyLoader::load($keyContent)
->withPadding(RSA::ENCRYPTION_PKCS1); // 必须显式指定 PKCS#1 填充
}⚠️ 注意:RSA::ENCRYPTION_PKCS1 是 Java RSA/ECB/PKCS1Padding 的等效填充,不可省略或误用 NO_PADDING / OAEP。
立即学习“PHP免费学习笔记(深入)”;
✅ 分段解码与 RSA 解密加密密钥
Samsung Pay 载荷格式为 A.B.C.D.E,其中:
- B → Base64URL 编码的 RSA 加密 AES 密钥(16 字节)
- C → IV(12 字节,GCM 标准)
- D → 密文主体
- E → 认证标签(16 字节)
需使用 base64url_decode(非 base64_decode)处理 URL 安全 Base64(补 = 并替换 -/_):
private function base64url_decode(string $input): string
{
$remainder = strlen($input) % 4;
if ($remainder) {
$input .= str_repeat('=', 4 - $remainder);
}
return base64_decode(strtr($input, '-_', '+/'));
}
private function decryptPayload(string $encPayload, string $privateKeyPath): string
{
$tokens = explode('.', $encPayload);
if (count($tokens) !== 5) {
throw new InvalidArgumentException('Invalid encrypted payload format');
}
$encKey = $this->base64url_decode($tokens[1]); // B
$iv = $this->base64url_decode($tokens[2]); // C
$cipher = $this->base64url_decode($tokens[3]); // D
$tag = $this->base64url_decode($tokens[4]); // E
// Step 1: RSA decrypt the AES key
$rsa = $this->loadPrivateKey($privateKeyPath);
$aesKey = $rsa->decrypt($encKey); // Returns raw binary key (e.g., 16 bytes)
if ($aesKey === false) {
throw new RuntimeException('RSA decryption failed — check key format, padding, and key length');
}✅ AES-128-GCM 解密:精确匹配 Java GCMParameterSpec
Java 使用 new GCMParameterSpec(128, iv)(即 tag length = 128 bits = 16 bytes),PHP 中需确保:
- 使用 openssl_decrypt() 且 cipher 为 'aes-128-gcm'
- iv 长度必须为 12 字节(GCM 最佳实践)
- tag 必须作为独立参数传入(PHP 7.1+ 支持)
// Step 2: AES-128-GCM decrypt
$cipherMethod = 'aes-128-gcm';
$tagLength = 16;
// Verify IV length (critical!)
if (strlen($iv) !== 12) {
throw new InvalidArgumentException('IV must be exactly 12 bytes for AES-GCM');
}
$decrypted = openssl_decrypt(
$cipher,
$cipherMethod,
$aesKey,
OPENSSL_RAW_DATA,
$iv,
$tag // ← PHP requires explicit tag param
);
if ($decrypted === false) {
throw new RuntimeException('AES-GCM decryption failed: ' . openssl_error_string());
}
return $decrypted;
}✅ 完整调用示例与关键检查清单
try {
$payload = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9...'; // A.B.C.D.E
$result = $this->decryptPayload($payload, '/path/to/rsapriv.pem');
echo "Decrypted: " . $result . "\n";
} catch (Exception $e) {
error_log('Decryption failed: ' . $e->getMessage());
}务必验证以下 5 项,否则 90% 的 Decryption error 源于此:
- ✅ 私钥文件为 PKCS#8 格式 PEM(可用 openssl pkcs8 -in key.pem -inform PEM -noout -text 检查)
- ✅ 使用 file_get_contents() 读取密钥内容,而非路径字符串
- ✅ RSA 解密时明确设置 ->withPadding(RSA::ENCRYPTION_PKCS1)
- ✅ IV 长度严格为 12 字节(非 16);Tag 长度为 16 字节
- ✅ openssl_decrypt() 的 $tag 参数不可省略,且必须是原始二进制(非 hex/base64)
掌握此流程后,即可无缝对接 Samsung Pay、Google Pay 等采用相同混合加密规范的支付平台。建议将解密逻辑封装为独立服务类,并添加单元测试覆盖边界情况(空密钥、损坏 Base64、IV 长度异常等)。











