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: 尝试使用 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) { 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); } }