mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 21:03:17 +08:00
360 lines
12 KiB
PHP
360 lines
12 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro\library\ccblife;
|
||
|
||
/**
|
||
* 建行生活URL参数解密类
|
||
* 处理ccbParamSJ参数的RSA解密
|
||
*
|
||
* 解密流程:URLDecode -> BASE64解码 -> RSA解密(使用服务方私钥)
|
||
*/
|
||
class CcbUrlDecrypt
|
||
{
|
||
/**
|
||
* 解密建行URL参数ccbParamSJ
|
||
*
|
||
* 建行加密流程(参考Java demo):
|
||
* 1. 原文 → RSA公钥加密 → BASE64编码(第一次)
|
||
* 2. 再把结果转UTF-8字节 → BASE64编码(第二次)
|
||
*
|
||
* 解密流程:
|
||
* 1. URLDecode → BASE64解码(第一次)→ BASE64解码(第二次)
|
||
* 2. RSA私钥解密 → 原文
|
||
*
|
||
* @param string $ccbParamSJ 加密的参数字符串(可能已经URLDecode)
|
||
* @param string $privateKey 服务方私钥(BASE64格式或PEM格式)
|
||
* @return array|false 解密后的参数数组,失败返回false
|
||
*/
|
||
public static function decrypt($ccbParamSJ, $privateKey)
|
||
{
|
||
try {
|
||
// 调试日志
|
||
trace('开始解密建行参数(双重BASE64 + RSA)', 'info');
|
||
trace('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
|
||
|
||
// 验证输入
|
||
if (empty($ccbParamSJ)) {
|
||
throw new \Exception('ccbParamSJ 参数为空');
|
||
}
|
||
|
||
if (empty($privateKey)) {
|
||
throw new \Exception('privateKey 为空');
|
||
}
|
||
|
||
// 步骤1: URLDecode(如果还没有解码)
|
||
$urlDecoded = urldecode($ccbParamSJ);
|
||
trace('URLDecode后长度: ' . strlen($urlDecoded), 'info');
|
||
|
||
// 步骤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);
|
||
if ($decrypted === false || empty($decrypted)) {
|
||
throw new \Exception('RSA解密失败');
|
||
}
|
||
|
||
trace('RSA解密成功,长度: ' . strlen($decrypted), 'info');
|
||
trace('解密内容: ' . $decrypted, 'info');
|
||
|
||
// 步骤5: 解析参数字符串为数组
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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: 尝试使用 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');
|
||
}
|
||
|
||
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;
|
||
}
|
||
} |