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