360 lines
12 KiB
PHP
Raw Normal View History

2025-10-17 17:18:15 +08:00
<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活URL参数解密类
2025-10-20 09:23:30 +08:00
* 处理ccbParamSJ参数的RSA解密
*
* 解密流程URLDecode -> BASE64解码 -> RSA解密使用服务方私钥
2025-10-17 17:18:15 +08:00
*/
class CcbUrlDecrypt
{
/**
* 解密建行URL参数ccbParamSJ
2025-10-20 09:58:32 +08:00
*
* 建行加密流程参考Java demo
* 1. 原文 RSA公钥加密 BASE64编码第一次
* 2. 再把结果转UTF-8字节 BASE64编码第二次
*
* 解密流程:
* 1. URLDecode BASE64解码第一次 BASE64解码第二次
* 2. RSA私钥解密 原文
2025-10-17 17:18:15 +08:00
*
2025-10-20 09:23:30 +08:00
* @param string $ccbParamSJ 加密的参数字符串可能已经URLDecode
* @param string $privateKey 服务方私钥BASE64格式或PEM格式
2025-10-17 17:18:15 +08:00
* @return array|false 解密后的参数数组失败返回false
*/
2025-10-20 09:23:30 +08:00
public static function decrypt($ccbParamSJ, $privateKey)
2025-10-17 17:18:15 +08:00
{
try {
2025-10-20 09:23:30 +08:00
// 调试日志
2025-10-20 09:58:32 +08:00
trace('开始解密建行参数双重BASE64 + RSA', 'info');
2025-10-20 09:23:30 +08:00
trace('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
// 验证输入
if (empty($ccbParamSJ)) {
throw new \Exception('ccbParamSJ 参数为空');
2025-10-17 17:18:15 +08:00
}
2025-10-20 09:23:30 +08:00
if (empty($privateKey)) {
throw new \Exception('privateKey 为空');
2025-10-17 17:18:15 +08:00
}
2025-10-20 09:58:32 +08:00
// 步骤1: URLDecode如果还没有解码
2025-10-20 09:23:30 +08:00
$urlDecoded = urldecode($ccbParamSJ);
trace('URLDecode后长度: ' . strlen($urlDecoded), 'info');
2025-10-17 17:18:15 +08:00
2025-10-20 09:58:32 +08:00
// 步骤2: 第一次 BASE64 解码
$firstDecode = base64_decode($urlDecoded);
if ($firstDecode === false || empty($firstDecode)) {
throw new \Exception('第一次 BASE64 解码失败');
}
trace('第一次BASE64解码成功长度: ' . strlen($firstDecode), 'info');
// 步骤3: 第二次 BASE64 解码建行特殊之处双重BASE64编码
$secondDecode = base64_decode($firstDecode);
if ($secondDecode === false || empty($secondDecode)) {
throw new \Exception('第二次 BASE64 解码失败');
}
trace('第二次BASE64解码成功长度: ' . strlen($secondDecode), 'info');
// 步骤4: RSA 私钥解密(直接处理二进制数据,不再用 CcbRSA::decrypt
$decrypted = self::rsaPrivateDecrypt($secondDecode, $privateKey);
2025-10-20 09:23:30 +08:00
if ($decrypted === false || empty($decrypted)) {
throw new \Exception('RSA解密失败');
2025-10-17 17:18:15 +08:00
}
2025-10-20 09:23:30 +08:00
trace('RSA解密成功长度: ' . strlen($decrypted), 'info');
trace('解密内容: ' . $decrypted, 'info');
2025-10-20 09:58:32 +08:00
// 步骤5: 解析参数字符串为数组
2025-10-17 17:18:15 +08:00
parse_str($decrypted, $params);
2025-10-20 09:23:30 +08:00
if (empty($params)) {
throw new \Exception('解析参数失败,结果为空');
}
trace('参数解析成功: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info');
2025-10-17 17:18:15 +08:00
return $params;
} catch (\Exception $e) {
// 记录错误日志
trace('建行URL参数解密失败: ' . $e->getMessage(), 'error');
2025-10-20 09:23:30 +08:00
trace('错误堆栈: ' . $e->getTraceAsString(), 'error');
2025-10-17 17:18:15 +08:00
return false;
}
}
2025-10-20 09:58:32 +08:00
/**
* RSA 私钥分块解密(不做 BASE64 解码)
*
* @param string $encryptedData 加密的二进制数据
* @param string $privateKey 私钥BASE64或PEM格式
* @return string|false 解密后的数据
*/
private static function rsaPrivateDecrypt($encryptedData, $privateKey)
{
try {
// 格式化私钥为 PEM 格式
$pemPrivateKey = $privateKey;
if (strpos($pemPrivateKey, '-----BEGIN') === false) {
$pemPrivateKey = preg_replace('/\s+/', '', $pemPrivateKey);
$pemPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
chunk_split($pemPrivateKey, 64, "\n") .
"-----END RSA PRIVATE KEY-----\n";
}
// 加载私钥资源
$priKey = openssl_pkey_get_private($pemPrivateKey);
if (!$priKey) {
throw new \Exception('私钥格式错误: ' . openssl_error_string());
}
// 获取密钥大小
$keyDetails = openssl_pkey_get_details($priKey);
$keySize = $keyDetails['bits'] / 8; // 1024位 = 128字节
// 分块解密
$blocks = str_split($encryptedData, $keySize);
$decrypted = '';
foreach ($blocks as $block) {
$decryptedBlock = '';
$success = openssl_private_decrypt($block, $decryptedBlock, $priKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('RSA分块解密失败: ' . openssl_error_string());
}
$decrypted .= $decryptedBlock;
}
openssl_free_key($priKey);
return $decrypted;
} catch (\Exception $e) {
trace('RSA私钥解密错误: ' . $e->getMessage(), 'error');
return false;
}
}
2025-10-17 17:18:15 +08:00
/**
* DES解密
* 使用ECB模式PKCS5Padding填充
*
2025-10-20 09:23:30 +08:00
* 兼容OpenSSL 3.x优先使用phpseclib库
*
2025-10-17 17:18:15 +08:00
* @param string $encryptedData 加密的数据
* @param string $key 密钥8字节
* @return string|false 解密后的数据失败返回false
*/
private static function desDecrypt($encryptedData, $key)
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
2025-10-20 09:23:30 +08:00
trace('DES密钥长度必须为8字节当前: ' . strlen($key), 'error');
2025-10-17 17:18:15 +08:00
return false;
}
2025-10-20 09:23:30 +08:00
// 方法1: 尝试使用 phpseclib推荐兼容OpenSSL 3.x
if (class_exists('\phpseclib3\Crypt\DES')) {
try {
$cipher = new \phpseclib3\Crypt\DES('ecb');
$cipher->setKey($key);
$cipher->disablePadding(); // 手动处理填充
$decrypted = $cipher->decrypt($encryptedData);
if ($decrypted !== false && !empty($decrypted)) {
// 移除PKCS5填充
$result = self::removePKCS5Padding($decrypted);
if ($result !== false) {
trace('使用phpseclib解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
trace('phpseclib解密失败: ' . $e->getMessage(), 'error');
}
2025-10-17 17:18:15 +08:00
}
2025-10-20 09:23:30 +08:00
// 方法2: 尝试使用 OpenSSLOpenSSL 3.x可能不支持
try {
$decrypted = @openssl_decrypt(
$encryptedData,
'DES-ECB',
$key,
OPENSSL_RAW_DATA
);
if ($decrypted !== false && !empty($decrypted)) {
// 移除PKCS5填充
$result = self::removePKCS5Padding($decrypted);
if ($result !== false) {
trace('使用OpenSSL解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
trace('OpenSSL解密失败: ' . $e->getMessage(), 'error');
}
2025-10-17 17:18:15 +08:00
2025-10-20 09:23:30 +08:00
trace('所有DES解密方法都失败', 'error');
return false;
2025-10-17 17:18:15 +08:00
}
/**
* DES加密用于测试
* 使用ECB模式PKCS5Padding填充
*
* @param string $data 待加密的数据
* @param string $key 密钥8字节
* @return string|false 加密后的数据失败返回false
*/
public static function desEncrypt($data, $key)
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
return false;
}
// 添加PKCS5填充
$data = self::addPKCS5Padding($data, 8);
// 使用openssl进行DES加密
$encrypted = openssl_encrypt(
$data,
'DES-ECB',
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
);
return $encrypted;
}
/**
* 生成建行URL参数ccbParamSJ用于测试
*
* @param array $params 参数数组
* @param string $serviceId 服务ID
* @return string 加密后的ccbParamSJ参数
*/
public static function encrypt($params, $serviceId)
{
// 将参数数组转换为查询字符串
$queryString = http_build_query($params);
// 获取DES密钥服务ID前8位
$desKey = substr($serviceId, 0, 8);
// DES加密
$encrypted = self::desEncrypt($queryString, $desKey);
// 双层BASE64编码
$firstEncode = base64_encode($encrypted);
$secondEncode = base64_encode($firstEncode);
return $secondEncode;
}
/**
* 添加PKCS5填充
*
* @param string $text 待填充的文本
* @param int $blocksize 块大小
* @return string 填充后的文本
*/
private static function addPKCS5Padding($text, $blocksize)
{
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
/**
* 移除PKCS5填充
*
* @param string $text 已填充的文本
2025-10-20 09:23:30 +08:00
* @return string|false 移除填充后的文本失败返回false
2025-10-17 17:18:15 +08:00
*/
private static function removePKCS5Padding($text)
{
2025-10-20 09:23:30 +08:00
// PHP 8 兼容性:检查空值
if (empty($text) || !is_string($text)) {
2025-10-17 17:18:15 +08:00
return false;
}
2025-10-20 09:23:30 +08:00
$textLength = strlen($text);
if ($textLength === 0) {
2025-10-17 17:18:15 +08:00
return false;
}
2025-10-20 09:23:30 +08:00
$pad = ord($text[$textLength - 1]);
// 验证填充值的合理性
if ($pad > $textLength || $pad > 8) {
return false;
}
// 验证所有填充字节是否一致
if (strspn($text, chr($pad), $textLength - $pad) != $pad) {
return false;
}
2025-10-17 17:18:15 +08:00
return substr($text, 0, -1 * $pad);
}
/**
* 解析建行跳转URL中的所有参数
* 处理URL中的ccbParamSJ和其他参数
*
* @param string $url 完整的URL或查询字符串
* @param string $serviceId 服务ID
* @return array 包含所有参数的数组
*/
public static function parseUrl($url, $serviceId)
{
// 解析URL获取查询参数
$urlParts = parse_url($url);
$queryString = isset($urlParts['query']) ? $urlParts['query'] : $url;
// 解析查询字符串
parse_str($queryString, $params);
// 如果存在ccbParamSJ参数进行解密
if (isset($params['ccbParamSJ']) && !empty($params['ccbParamSJ'])) {
$decryptedParams = self::decrypt($params['ccbParamSJ'], $serviceId);
if ($decryptedParams !== false) {
// 合并解密后的参数
$params = array_merge($params, $decryptedParams);
}
}
return $params;
}
/**
* 生成测试URL
* 用于生成包含加密参数的完整URL
*
* @param string $baseUrl 基础URL
* @param array $encryptedParams 需要加密的参数
* @param array $plainParams 明文参数
* @param string $serviceId 服务ID
* @return string 完整的URL
*/
public static function generateUrl($baseUrl, $encryptedParams, $plainParams, $serviceId)
{
// 加密参数
$ccbParamSJ = self::encrypt($encryptedParams, $serviceId);
// 合并所有参数
$allParams = array_merge($plainParams, ['ccbParamSJ' => $ccbParamSJ]);
// 构建查询字符串
$queryString = http_build_query($allParams);
// 拼接URL
$separator = strpos($baseUrl, '?') === false ? '?' : '&';
return $baseUrl . $separator . $queryString;
}
}