2025-10-27 14:20:37 +08:00

325 lines
11 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;
/**
* 建行生活RSA加密解密类
* 支持1024位RSA密钥117字节分段加密128字节分段解密
*/
class CcbRSA
{
/**
* RSA公钥加密
* 对于长数据使用117字节分段加密1024位RSA密钥
*
* @param string $data 待加密数据
* @param string $publicKey BASE64编码的公钥字符串
* @return string 加密后的BASE64字符串
* @throws \Exception
*/
public static function encrypt($data, $publicKey)
{
// 格式化公钥
$publicKey = self::formatPublicKey($publicKey);
// 加载公钥资源
$pubKey = openssl_pkey_get_public($publicKey);
if (!$pubKey) {
throw new \Exception('公钥格式错误: ' . openssl_error_string());
}
// 获取密钥详情
$keyDetails = openssl_pkey_get_details($pubKey);
$keySize = $keyDetails['bits'] / 8; // 密钥字节数
$maxEncryptBlock = $keySize - 11; // RSA_PKCS1_PADDING 模式下最大加密块大小
// 将数据分段加密
$dataBytes = str_split($data, $maxEncryptBlock);
$encrypted = '';
foreach ($dataBytes as $block) {
$encryptedBlock = '';
$success = openssl_public_encrypt($block, $encryptedBlock, $pubKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('RSA加密失败: ' . openssl_error_string());
}
$encrypted .= $encryptedBlock;
}
// PHP 8+ 资源自动释放
if (PHP_VERSION_ID < 80000) {
openssl_free_key($pubKey);
}
// 返回BASE64编码的结果
return base64_encode($encrypted);
}
/**
* RSA私钥解密
* 对于长数据使用128字节分段解密1024位RSA密钥
*
* @param string $encryptedData BASE64编码的加密数据
* @param string $privateKey BASE64编码的私钥字符串
* @return string 解密后的原始数据
* @throws \Exception
*/
public static function decrypt($encryptedData, $privateKey)
{
// 格式化私钥
$privateKey = self::formatPrivateKey($privateKey);
// 加载私钥资源
$priKey = openssl_pkey_get_private($privateKey);
if (!$priKey) {
throw new \Exception('私钥格式错误: ' . openssl_error_string());
}
// 获取密钥详情
$keyDetails = openssl_pkey_get_details($priKey);
$keySize = $keyDetails['bits'] / 8; // 密钥字节数128字节
// BASE64解码
$encryptedData = base64_decode($encryptedData);
// 将数据分段解密
$dataBytes = str_split($encryptedData, $keySize);
$decrypted = '';
foreach ($dataBytes as $block) {
$decryptedBlock = '';
$success = openssl_private_decrypt($block, $decryptedBlock, $priKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('RSA解密失败: ' . openssl_error_string());
}
$decrypted .= $decryptedBlock;
}
// PHP 8+ 资源自动释放
if (PHP_VERSION_ID < 80000) {
openssl_free_key($priKey);
}
return $decrypted;
}
/**
* 使用建行公钥加密数据API请求
*
* @param string $data 待加密数据
* @param string $ccbPublicKey 建行提供的公钥
* @return string 加密后的BASE64字符串
* @throws \Exception
*/
public static function encryptForCcb($data, $ccbPublicKey)
{
return self::encrypt($data, $ccbPublicKey);
}
/**
* 使用商户私钥解密数据API响应
*
* @param string $encryptedData 加密的数据
* @param string $merchantPrivateKey 商户私钥
* @return string 解密后的数据
* @throws \Exception
*/
public static function decryptFromCcb($encryptedData, $merchantPrivateKey)
{
return self::decrypt($encryptedData, $merchantPrivateKey);
}
/**
* 格式化公钥字符串
* 将BASE64字符串格式化为PEM格式
*
* @param string $publicKey BASE64格式的公钥
* @return string PEM格式的公钥
*/
private static function formatPublicKey($publicKey)
{
// 移除可能存在的空格和换行
$publicKey = preg_replace('/\s+/', '', $publicKey);
// 如果已经是PEM格式直接返回
if (strpos($publicKey, '-----BEGIN PUBLIC KEY-----') !== false) {
return $publicKey;
}
// ✅ 修复: chunk_split()会在末尾添加换行符需要用rtrim()去除
// 否则会导致PEM格式中密钥内容和尾部之间有多余空行OpenSSL解析失败
$pem = "-----BEGIN PUBLIC KEY-----\n";
$pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n";
$pem .= "-----END PUBLIC KEY-----\n";
return $pem;
}
/**
* 格式化私钥字符串
* 将BASE64字符串格式化为PEM格式
* 自动识别PKCS#1和PKCS#8格式
*
* @param string $privateKey BASE64格式的私钥
* @return string PEM格式的私钥
*/
private static function formatPrivateKey($privateKey)
{
// 移除可能存在的空格和换行
$privateKey = preg_replace('/\s+/', '', $privateKey);
// 如果已经是PEM格式直接返回
if (strpos($privateKey, '-----BEGIN RSA PRIVATE KEY-----') !== false ||
strpos($privateKey, '-----BEGIN PRIVATE KEY-----') !== false) {
return $privateKey;
}
// ✅ 自动识别密钥格式
// PKCS#8格式特征以 MIICdwIBADANBgkqhkiG9w0BAQEFAASC 开头包含ASN.1结构标识)
// PKCS#1格式特征以 MIICXAIBAAKBgQC 或类似开头直接是RSA私钥参数
// 解码BASE64看前几个字节
$decoded = base64_decode($privateKey);
$isPkcs8 = false;
if ($decoded !== false && strlen($decoded) > 20) {
// PKCS#8格式的特征包含OID标识 (0x06 0x09 0x2a 0x86 0x48 0x86 0xf7 0x0d 0x01 0x01 0x01)
// 简单判断:检查是否包含 "0609" (DER编码的OID标识)
$hex = bin2hex(substr($decoded, 0, 30));
if (strpos($hex, '06092a864886f70d010101') !== false) {
$isPkcs8 = true;
}
}
// 格式化为PEM
$keyContent = rtrim(chunk_split($privateKey, 64, "\n"), "\n");
if ($isPkcs8) {
// PKCS#8格式 (建行使用的格式)
$pem = "-----BEGIN PRIVATE KEY-----\n";
$pem .= $keyContent . "\n";
$pem .= "-----END PRIVATE KEY-----\n";
} else {
// PKCS#1格式 (传统格式)
$pem = "-----BEGIN RSA PRIVATE KEY-----\n";
$pem .= $keyContent . "\n";
$pem .= "-----END RSA PRIVATE KEY-----\n";
}
return $pem;
}
/**
* 生成RSA密钥对用于测试
*
* @param int $bits 密钥长度默认1024
* @return array 包含public_key和private_key的数组
* @throws \Exception
*/
public static function generateKeyPair($bits = 1024)
{
$config = [
'private_key_bits' => $bits,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
// 生成密钥对
$resource = openssl_pkey_new($config);
if (!$resource) {
throw new \Exception('生成密钥对失败: ' . openssl_error_string());
}
// 导出私钥
openssl_pkey_export($resource, $privateKey);
// 获取公钥
$details = openssl_pkey_get_details($resource);
$publicKey = $details['key'];
// 转换为BASE64格式去除PEM头尾
$privateKey = str_replace(['-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----', "\n"], '', $privateKey);
$publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n"], '', $publicKey);
return [
'public_key' => $publicKey,
'private_key' => $privateKey
];
}
/**
* RSA公钥验签用于建行回调通知
*
* 建行回调通知中的 SIGN 字段是使用商户私钥签名的,
* 服务方需要使用建行公钥进行验签
*
* @param string $data 待验签的原始数据
* @param string $signature 签名字符串(十六进制)
* @param string $publicKey 建行公钥BASE64编码
* @return bool 验签是否成功
* @throws \Exception
*/
public static function verify($data, $signature, $publicKey)
{
// 格式化公钥
$publicKey = self::formatPublicKey($publicKey);
// 加载公钥资源
$pubKey = openssl_pkey_get_public($publicKey);
if (!$pubKey) {
throw new \Exception('公钥格式错误: ' . openssl_error_string());
}
// 将十六进制签名转换为二进制
$signatureBinary = hex2bin($signature);
if ($signatureBinary === false) {
throw new \Exception('签名格式错误:无法从十六进制转换');
}
// 使用公钥验签SHA256算法
$result = openssl_verify($data, $signatureBinary, $pubKey, OPENSSL_ALGO_SHA256);
// PHP 8+ 资源自动释放
if (PHP_VERSION_ID < 80000) {
openssl_free_key($pubKey);
}
if ($result === 1) {
return true; // 验签成功
} elseif ($result === 0) {
return false; // 验签失败
} else {
throw new \Exception('验签过程出错: ' . openssl_error_string());
}
}
/**
* 建行通知验签(针对回调通知)
*
* 用于验证建行支付通知和退款通知的签名
*
* @param array $params 通知参数不包含SIGN字段
* @param string $signature SIGN字段的值
* @param string $ccbPublicKey 建行公钥
* @return bool 验签是否成功
* @throws \Exception
*/
public static function verifyNotify($params, $signature, $ccbPublicKey)
{
// 移除 SIGN 字段(如果存在)
unset($params['SIGN']);
// 按照建行规范拼接验签字符串
// 格式将参数按字典序排列后拼接key=value&key=value
ksort($params);
$signStr = '';
foreach ($params as $key => $value) {
if ($value !== '' && $value !== null) {
$signStr .= $key . '=' . $value . '&';
}
}
$signStr = rtrim($signStr, '&');
// 调用验签方法
return self::verify($signStr, $signature, $ccbPublicKey);
}
}