config = $config; // 加载密钥 $this->loadKeys(); } /** * 加载密钥 * * @throws Exception */ private function loadKeys() { $this->privateKey = $this->config['private_key'] ?? ''; $this->publicKey = $this->config['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('建行平台公钥格式错误: ' . openssl_error_string()); } // ⚠️ 动态获取RSA密钥大小,而非写死117字节 $keyDetails = openssl_pkey_get_details($pubKeyId); if (!$keyDetails || !isset($keyDetails['bits'])) { throw new Exception('无法获取RSA密钥详情'); } $keySize = $keyDetails['bits'] / 8; // 密钥字节数: 1024位=128字节, 2048位=256字节 $chunkSize = $keySize - 11; // PKCS1填充需要预留11字节 // RSA加密 (分段加密) $encrypted = ''; $dataLen = strlen($data); for ($i = 0; $i < $dataLen; $i += $chunkSize) { $chunk = substr($data, $i, $chunkSize); $encryptedChunk = ''; if (!openssl_public_encrypt($chunk, $encryptedChunk, $pubKeyId, OPENSSL_PKCS1_PADDING)) { throw new Exception('RSA加密失败: ' . openssl_error_string()); } $encrypted .= $encryptedChunk; } // PHP 8+ 资源自动释放 if (PHP_VERSION_ID < 80000) { openssl_free_key($pubKeyId); } // 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('服务方私钥格式错误: ' . openssl_error_string()); } // ⚠️ 动态获取RSA密钥大小 $keyDetails = openssl_pkey_get_details($privKeyId); if (!$keyDetails || !isset($keyDetails['bits'])) { throw new Exception('无法获取RSA密钥详情'); } $keySize = $keyDetails['bits'] / 8; // 密钥字节数: 1024位=128字节, 2048位=256字节 // BASE64解码 $encrypted = base64_decode($data); // RSA解密 (分段解密,每段密文长度等于密钥字节数) $decrypted = ''; $encryptedLen = strlen($encrypted); for ($i = 0; $i < $encryptedLen; $i += $keySize) { $chunk = substr($encrypted, $i, $keySize); $decryptedChunk = ''; if (!openssl_private_decrypt($chunk, $decryptedChunk, $privKeyId, OPENSSL_PKCS1_PADDING)) { throw new Exception('RSA解密失败: ' . openssl_error_string()); } $decrypted .= $decryptedChunk; } // PHP 8+ 资源自动释放 if (PHP_VERSION_ID < 80000) { openssl_free_key($privKeyId); } 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字段) * * ⚠️ 已废弃:建行要求只加密公钥后30位,请使用 encryptMerchantPublicKeyLast30() * * @return string BASE64编码的加密公钥 * @throws Exception * @deprecated 使用 encryptMerchantPublicKeyLast30() 替代 */ public function encryptMerchantPublicKey() { if (empty($this->publicKey)) { throw new Exception('服务方公钥未配置'); } // 使用建行平台公钥加密商户公钥 return $this->rsaEncrypt($this->publicKey); } /** * 加密商户公钥后30位 (用于支付串的ENCPUB字段) * * 根据建行文档v2.2规范: * "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" * * @return string BASE64编码的加密密文 * @throws Exception */ public function encryptMerchantPublicKeyLast30() { if (empty($this->publicKey)) { throw new Exception('服务方公钥未配置'); } // 取商户公钥的后30位 $publicKeyContent = $this->publicKey; // 如果是PEM格式,去除头尾和换行符 $publicKeyContent = str_replace([ '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\r", "\n", " " ], '', $publicKeyContent); // 取后30位 $last30Chars = substr($publicKeyContent, -30); if (strlen($last30Chars) < 30) { throw new Exception('商户公钥长度不足30位'); } // 使用建行平台公钥加密这30位字符 return $this->rsaEncrypt($last30Chars); } }