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:23:30 +08:00
|
|
|
|
* 流程:URLDecode -> RSA解密(使用服务方私钥,内部会进行BASE64解码)
|
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
|
|
|
|
// 调试日志
|
|
|
|
|
|
trace('开始解密建行参数(RSA方式)', 'info');
|
|
|
|
|
|
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:23:30 +08:00
|
|
|
|
// URLDecode(如果还没有解码)
|
|
|
|
|
|
$urlDecoded = urldecode($ccbParamSJ);
|
|
|
|
|
|
trace('URLDecode后长度: ' . strlen($urlDecoded), 'info');
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
2025-10-20 09:23:30 +08:00
|
|
|
|
// RSA解密(CcbRSA::decrypt内部会自动进行BASE64解码)
|
|
|
|
|
|
$decrypted = CcbRSA::decrypt($urlDecoded, $privateKey);
|
|
|
|
|
|
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-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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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: 尝试使用 OpenSSL(OpenSSL 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|