mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 21:03:17 +08:00
448 lines
14 KiB
PHP
448 lines
14 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro\library\ccblife;
|
||
|
||
use think\Exception;
|
||
|
||
/**
|
||
* 建行生活加密解密核心类
|
||
*
|
||
* ⚠️ 已废弃:请使用 CcbRSA、CcbMD5、CcbHttpClient 类替代
|
||
*
|
||
* 废弃原因:
|
||
* 1. formatKey() 方法存在密钥格式化错误(PKCS#1 vs PKCS#8 混淆)
|
||
* 2. chunk_split() 使用不当导致 OpenSSL ASN1 解析错误
|
||
* 3. 与 CcbRSA 类功能重复,维护成本高
|
||
*
|
||
* 迁移指南:
|
||
* - RSA加密/解密 → 使用 CcbRSA::encrypt() / CcbRSA::decrypt()
|
||
* - MD5签名 → 使用 CcbMD5::signApiMessage() / CcbMD5::verifyApiSignature()
|
||
* - 加密商户公钥 → 在 CcbPaymentService 中使用 encryptPublicKeyLast30()
|
||
*
|
||
* @deprecated 2025-01-21 统一使用 CcbRSA、CcbMD5 类
|
||
* @author Billy
|
||
* @date 2025-01-16
|
||
*/
|
||
class CcbEncryption
|
||
{
|
||
/**
|
||
* 配置信息
|
||
* @var array
|
||
*/
|
||
private $config;
|
||
|
||
/**
|
||
* 服务方私钥
|
||
* @var string
|
||
*/
|
||
private $privateKey;
|
||
|
||
/**
|
||
* 服务方公钥
|
||
* @var string
|
||
*/
|
||
private $publicKey;
|
||
|
||
|
||
/**
|
||
* 构造函数
|
||
*
|
||
* @param array $config 配置数组
|
||
* @throws Exception
|
||
*/
|
||
public function __construct($config = [])
|
||
{
|
||
// 如果没有传入配置,从配置文件读取
|
||
if (empty($config)) {
|
||
$config = config('ccblife');
|
||
}
|
||
|
||
$this->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->publicKey)) {
|
||
throw new Exception('服务方公钥未配置');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* RSA加密 (使用建行平台公钥加密)
|
||
*
|
||
* @param string $data 原始数据
|
||
* @return string BASE64编码的加密数据
|
||
* @throws Exception
|
||
*/
|
||
public function rsaEncrypt($data)
|
||
{
|
||
// 格式化公钥
|
||
$publicKey = $this->formatKey($this->publicKey);
|
||
|
||
// 获取公钥资源
|
||
$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->publicKey;
|
||
|
||
// 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);
|
||
}
|
||
}
|