2025-10-20 10:37:59 +08:00

413 lines
14 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
*
* 建行加密流程参考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 {
// 调试日志
self::log('开始解密建行参数双重BASE64 + RSA', 'info');
self::log('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info');
// 验证输入
if (empty($ccbParamSJ)) {
throw new \Exception('ccbParamSJ 参数为空');
}
if (empty($privateKey)) {
throw new \Exception('privateKey 为空');
}
// 步骤1: URLDecode如果还没有解码
$urlDecoded = urldecode($ccbParamSJ);
self::log('URLDecode后长度: ' . strlen($urlDecoded), 'info');
// 步骤2: 第一次 BASE64 解码
$firstDecode = base64_decode($urlDecoded);
if ($firstDecode === false || empty($firstDecode)) {
throw new \Exception('第一次 BASE64 解码失败');
}
self::log('第一次BASE64解码成功长度: ' . strlen($firstDecode), 'info');
// 步骤3: 第二次 BASE64 解码建行特殊之处双重BASE64编码
$secondDecode = base64_decode($firstDecode);
if ($secondDecode === false || empty($secondDecode)) {
throw new \Exception('第二次 BASE64 解码失败');
}
self::log('第二次BASE64解码成功长度: ' . strlen($secondDecode), 'info');
// 步骤4: RSA 私钥解密(直接处理二进制数据,不再用 CcbRSA::decrypt
$decrypted = self::rsaPrivateDecrypt($secondDecode, $privateKey);
if ($decrypted === false || empty($decrypted)) {
throw new \Exception('RSA解密失败');
}
self::log('RSA解密成功长度: ' . strlen($decrypted), 'info');
self::log('解密内容: ' . $decrypted, 'info');
// 步骤5: 解析参数字符串为数组
$params = [];
parse_str($decrypted, $params);
if (empty($params)) {
throw new \Exception('解析参数失败,结果为空');
}
// 验证解析结果是否为有效数组
if (!is_array($params)) {
throw new \Exception('解析参数失败,结果不是数组: ' . gettype($params));
}
self::log('参数解析成功: ' . json_encode($params, JSON_UNESCAPED_UNICODE), 'info');
return $params;
} catch (\Exception $e) {
// 记录错误日志
self::log('建行URL参数解密失败: ' . $e->getMessage(), 'error');
self::log('错误堆栈: ' . $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 {
self::log('rsaPrivateDecrypt 开始', 'info');
self::log('加密数据长度: ' . strlen($encryptedData), 'info');
self::log('加密数据MD5: ' . md5($encryptedData), 'info');
self::log('私钥长度: ' . strlen($privateKey), 'info');
// 格式化私钥为 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) {
$openssl_error = openssl_error_string();
self::log('openssl_pkey_get_private 失败', 'error');
self::log('OpenSSL错误: ' . ($openssl_error ?: '无错误信息'), 'error');
self::log('PEM私钥前200字符: ' . substr($pemPrivateKey, 0, 200), 'error');
throw new \Exception('私钥格式错误: ' . ($openssl_error ?: '未知错误'));
}
// 获取密钥大小
$keyDetails = openssl_pkey_get_details($priKey);
if (!$keyDetails || !isset($keyDetails['bits'])) {
self::log('openssl_pkey_get_details 失败', 'error');
self::log('priKey 类型: ' . gettype($priKey), 'error');
throw new \Exception('无法获取密钥详情');
}
self::log('密钥加载成功,大小: ' . ($keyDetails['bits'] / 8) . ' 字节', 'info');
$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;
}
// PHP 8+ openssl_free_key() 已废弃,资源会自动释放
if (PHP_VERSION_ID < 80000) {
openssl_free_key($priKey);
}
return $decrypted;
} catch (\Exception $e) {
self::log('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) {
self::log('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) {
self::log('使用phpseclib解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
self::log('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) {
self::log('使用OpenSSL解密成功', 'info');
return $result;
}
}
} catch (\Exception $e) {
self::log('OpenSSL解密失败: ' . $e->getMessage(), 'error');
}
self::log('所有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;
}
/**
* 日志记录辅助方法
* 兼容 trace() 函数和独立使用
*
* @param string $message 日志消息
* @param string $level 日志级别
* @return void
*/
private static function log($message, $level = 'info')
{
// 如果 trace() 函数存在,使用它
if (function_exists('trace')) {
trace($message, $level);
return;
}
// 否则输出到标准错误流CLI模式和 error_log
$prefix = strtoupper($level);
$logMessage = "[{$prefix}] {$message}";
if (PHP_SAPI === 'cli') {
fwrite(STDERR, $logMessage . PHP_EOL);
}
error_log($logMessage);
}
}