242 lines
7.9 KiB
PHP
Raw Normal View History

2025-10-17 17:18:15 +08:00
<?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;
}
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;
}
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;
}
2025-10-21 10:17:40 +08:00
// ✅ 修复: chunk_split()会在末尾添加换行符需要用rtrim()去除
// 否则会导致PEM格式中密钥内容和尾部之间有多余空行OpenSSL解析失败
2025-10-17 17:18:15 +08:00
$pem = "-----BEGIN PUBLIC KEY-----\n";
2025-10-21 10:17:40 +08:00
$pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n";
2025-10-17 17:18:15 +08:00
$pem .= "-----END PUBLIC KEY-----\n";
return $pem;
}
/**
* 格式化私钥字符串
* 将BASE64字符串格式化为PEM格式
2025-10-22 11:34:32 +08:00
* 自动识别PKCS#1和PKCS#8格式
2025-10-17 17:18:15 +08:00
*
* @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;
}
2025-10-22 11:34:32 +08:00
// ✅ 自动识别密钥格式
// 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";
}
2025-10-17 17:18:15 +08:00
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
];
}
}