2025-10-21 10:17:40 +08:00

448 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}