config = $config; // 加载密钥 $this->loadKeys(); } /** * 加载密钥 * * @throws Exception */ private function loadKeys() { $this->privateKey = $this->config['private_key'] ?? ''; $this->publicKey = $this->config['public_key'] ?? ''; $this->platformPublicKey = $this->config['platform_public_key'] ?? ''; if (empty($this->privateKey)) { throw new Exception('服务方私钥未配置'); } if (empty($this->platformPublicKey)) { throw new Exception('建行平台公钥未配置'); } } /** * RSA加密 (使用建行平台公钥加密) * * @param string $data 原始数据 * @return string BASE64编码的加密数据 * @throws Exception */ public function rsaEncrypt($data) { // 格式化公钥 $publicKey = $this->formatKey($this->platformPublicKey, 'PUBLIC'); // 获取公钥资源 $pubKeyId = openssl_pkey_get_public($publicKey); if (!$pubKeyId) { throw new Exception('建行平台公钥格式错误'); } // RSA加密 (分段加密,每段117字节) $encrypted = ''; $dataLen = strlen($data); $chunkSize = 117; // 1024位RSA密钥,每次最多加密117字节 for ($i = 0; $i < $dataLen; $i += $chunkSize) { $chunk = substr($data, $i, $chunkSize); $encryptedChunk = ''; if (!openssl_public_encrypt($chunk, $encryptedChunk, $pubKeyId)) { throw new Exception('RSA加密失败: ' . openssl_error_string()); } $encrypted .= $encryptedChunk; } // BASE64编码并去除换行符 return str_replace(["\r", "\n"], '', base64_encode($encrypted)); } /** * RSA解密 (使用服务方私钥解密) * * @param string $data BASE64编码的加密数据 * @return string 解密后的原始数据 * @throws Exception */ public function rsaDecrypt($data) { // 格式化私钥 $privateKey = $this->formatKey($this->privateKey, 'PRIVATE'); // 获取私钥资源 $privKeyId = openssl_pkey_get_private($privateKey); if (!$privKeyId) { throw new Exception('服务方私钥格式错误'); } // BASE64解码 $encrypted = base64_decode($data); // RSA解密 (分段解密,每段128字节) $decrypted = ''; $encryptedLen = strlen($encrypted); $chunkSize = 128; // 1024位RSA密钥,密文长度为128字节 for ($i = 0; $i < $encryptedLen; $i += $chunkSize) { $chunk = substr($encrypted, $i, $chunkSize); $decryptedChunk = ''; if (!openssl_private_decrypt($chunk, $decryptedChunk, $privKeyId)) { throw new Exception('RSA解密失败: ' . openssl_error_string()); } $decrypted .= $decryptedChunk; } return $decrypted; } /** * 生成MD5签名 * * @param string $data 原始数据(JSON字符串) * @return string 32位大写MD5签名 (与Java保持一致) */ public function generateSign($data) { // 签名规则: MD5(原始数据 + 服务方私钥) $signString = $data . $this->privateKey; return strtoupper(md5($signString)); // 转为大写,与Java一致 } /** * 验证MD5签名 * * @param string $data 原始数据 * @param string $sign 签名 * @return bool 验证结果 */ public function verifySign($data, $sign) { $expectedSign = $this->generateSign($data); return $expectedSign === $sign; } /** * 构造完整加密报文 * * @param string $txCode 交易代码 (如: A3341TP01) * @param array $bodyData 业务数据 * @return array 加密后的完整报文 ['cnt' => '...', 'mac' => '...', 'svcid' => '...'] * @throws Exception */ public function buildEncryptedMessage($txCode, $bodyData) { // 1. 构造原始报文 $txSeq = $this->generateTransSeq(); $txTime = date('YmdHis'); $message = [ 'CLD_HEADER' => [ 'CLD_TX_CHNL' => $this->config['service_id'], 'CLD_TX_TIME' => $txTime, 'CLD_TX_CODE' => $txCode, 'CLD_TX_SEQ' => $txSeq, ], 'CLD_BODY' => $bodyData, ]; // 2. 转换为JSON (不转义中文和斜杠) $jsonData = json_encode($message, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($jsonData === false) { throw new Exception('JSON编码失败: ' . json_last_error_msg()); } // 3. RSA加密 $encryptedData = $this->rsaEncrypt($jsonData); // 4. 生成MD5签名 $sign = $this->generateSign($jsonData); // 5. 组装最终报文 return [ 'cnt' => $encryptedData, 'mac' => $sign, 'svcid' => $this->config['service_id'], ]; } /** * 解析响应报文 * * @param string $response 响应JSON字符串 * @return array 解析后的业务数据 * @throws Exception */ public function parseResponse($response) { // 1. 解析JSON $result = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('响应不是有效的JSON: ' . json_last_error_msg()); } // 2. 检查是否包含加密字段 if (!isset($result['cnt']) || !isset($result['mac'])) { // 可能是错误响应,直接返回 return $result; } // 3. 验证签名 (如果启用) if ($this->config['security']['verify_sign'] ?? true) { // 解密后验证签名 $decryptedData = $this->rsaDecrypt($result['cnt']); if (!$this->verifySign($decryptedData, $result['mac'])) { throw new Exception('响应签名验证失败'); } // 解析业务数据 $businessData = json_decode($decryptedData, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('业务数据解析失败: ' . json_last_error_msg()); } return $businessData; } else { // 不验证签名,直接解密 $decryptedData = $this->rsaDecrypt($result['cnt']); return json_decode($decryptedData, true); } } /** * 生成唯一交易流水号 * * 格式: YmdHis + 微秒 + 4位随机数 * 示例: 202501161200001234567890 * * @return string 24位交易流水号 */ public function generateTransSeq() { $date = date('YmdHis'); // 14位 $microtime = substr(microtime(), 2, 6); // 6位微秒 $random = str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT); // 4位随机数 return $date . $microtime . $random; // 24位 } /** * 生成支付流水号 * * 格式: PAY + YmdHis + 8位随机数 * 示例: PAY2025011612000012345678 * * @return string 支付流水号 */ public function generatePayFlowId() { $prefix = 'PAY'; $date = date('YmdHis'); // 14位 $random = str_pad(mt_rand(0, 99999999), 8, '0', STR_PAD_LEFT); // 8位随机数 return $prefix . $date . $random; // 3 + 14 + 8 = 25位 } /** * 格式化密钥 (添加PEM头尾) * * 密钥格式说明: * - 公钥: X.509格式 (BEGIN PUBLIC KEY) * - 私钥: PKCS#8格式 (BEGIN PRIVATE KEY) - 与Java保持一致 * * @param string $key 密钥内容 (BASE64字符串,不含头尾) * @param string $type 类型: PUBLIC 或 PRIVATE * @return string 格式化后的PEM密钥 */ private function formatKey($key, $type = 'PUBLIC') { // 如果已经包含头尾,直接返回 if (strpos($key, '-----BEGIN') !== false) { return $key; } // 添加头尾 (注意: 私钥使用PKCS#8格式,与Java的PKCS8EncodedKeySpec一致) if ($type === 'PUBLIC') { $header = "-----BEGIN PUBLIC KEY-----\n"; $footer = "\n-----END PUBLIC KEY-----"; } else { // 使用PKCS#8格式 (不是RSA PRIVATE KEY) $header = "-----BEGIN PRIVATE KEY-----\n"; $footer = "\n-----END PRIVATE KEY-----"; } // 每64个字符换行 $key = chunk_split($key, 64, "\n"); return $header . $key . $footer; } /** * 生成支付串签名 * * 用于生成建行支付串的MAC签名 * * @param array $params 支付参数数组 * @return string 32位大写MD5签名 (与Java保持一致) */ public function generatePaymentSign($params) { // 1. 按参数名ASCII排序 ksort($params); // 2. 拼接成字符串: key1=value1&key2=value2&... $signString = http_build_query($params); // 3. 追加平台公钥 $signString .= '&PLATFORMPUB=' . $this->platformPublicKey; // 4. 生成MD5签名 (转为大写,与Java一致) return strtoupper(md5($signString . $this->privateKey)); } /** * 加密商户公钥 (用于支付串的ENCPUB字段) * * @return string BASE64编码的加密公钥 * @throws Exception */ public function encryptMerchantPublicKey() { if (empty($this->publicKey)) { throw new Exception('服务方公钥未配置'); } // 使用建行平台公钥加密商户公钥 return $this->rsaEncrypt($this->publicKey); } }