2025-10-20 09:23:30 +08:00

287 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活URL参数解密类
* 处理ccbParamSJ参数的RSA解密
*
* 解密流程URLDecode -> BASE64解码 -> RSA解密使用服务方私钥
*/
class CcbUrlDecrypt
{
/**
* 解密建行URL参数ccbParamSJ
* 流程URLDecode -> RSA解密使用服务方私钥内部会进行BASE64解码
*
* @param string $ccbParamSJ 加密的参数字符串可能已经URLDecode
* @param string $privateKey 服务方私钥BASE64格式或PEM格式
* @return array|false 解密后的参数数组失败返回false
*/
public static function decrypt($ccbParamSJ, $privateKey)
{
try {
// 调试日志
trace('开始解密建行参数RSA方式', 'info');
trace('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
// 验证输入
if (empty($ccbParamSJ)) {
throw new \Exception('ccbParamSJ 参数为空');
}
if (empty($privateKey)) {
throw new \Exception('privateKey 为空');
}
// URLDecode如果还没有解码
$urlDecoded = urldecode($ccbParamSJ);
trace('URLDecode后长度: ' . strlen($urlDecoded), 'info');
// RSA解密CcbRSA::decrypt内部会自动进行BASE64解码
$decrypted = CcbRSA::decrypt($urlDecoded, $privateKey);
if ($decrypted === false || empty($decrypted)) {
throw new \Exception('RSA解密失败');
}
trace('RSA解密成功长度: ' . strlen($decrypted), 'info');
trace('解密内容: ' . $decrypted, 'info');
// 解析参数字符串为数组
parse_str($decrypted, $params);
if (empty($params)) {
throw new \Exception('解析参数失败,结果为空');
}
trace('参数解析成功: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info');
return $params;
} catch (\Exception $e) {
// 记录错误日志
trace('建行URL参数解密失败: ' . $e->getMessage(), 'error');
trace('错误堆栈: ' . $e->getTraceAsString(), 'error');
return false;
}
}
/**
* DES解密
* 使用ECB模式PKCS5Padding填充
*
* 兼容OpenSSL 3.x优先使用phpseclib库
*
* @param string $encryptedData 加密的数据
* @param string $key 密钥8字节
* @return string|false 解密后的数据失败返回false
*/
private static function desDecrypt($encryptedData, $key)
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
trace('DES密钥长度必须为8字节当前: ' . strlen($key), 'error');
return false;
}
// 方法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');
}
}
// 方法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');
}
trace('所有DES解密方法都失败', 'error');
return false;
}
/**
* 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 已填充的文本
* @return string|false 移除填充后的文本失败返回false
*/
private static function removePKCS5Padding($text)
{
// PHP 8 兼容性:检查空值
if (empty($text) || !is_string($text)) {
return false;
}
$textLength = strlen($text);
if ($textLength === 0) {
return false;
}
$pad = ord($text[$textLength - 1]);
// 验证填充值的合理性
if ($pad > $textLength || $pad > 8) {
return false;
}
// 验证所有填充字节是否一致
if (strspn($text, chr($pad), $textLength - $pad) != $pad) {
return false;
}
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;
}
}