PHP
代码备忘录
php
<?php
// 解析PFX格式私钥文件
$pkcs12 = file_get_contents('private.pfx');
openssl_pkcs12_read($pkcs12, $certs, '123456');
file_put_contents('private_key.pem', $certs['pkey']);
file_put_contents('cert.cer', $certs['cert']);
php
<?php
// RSA加解密工具
namespace App\Security;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use RuntimeException;
class Rsa
{
public static function getPublicKey(string $publicKeyContent): OpenSSLAsymmetricKey
{
$publicKey = openssl_pkey_get_public($publicKeyContent);
if (! $publicKey) {
throw new RuntimeException('非法公钥');
}
return $publicKey;
}
public static function getPublicKeyFromCert(string $certContent): OpenSSLAsymmetricKey
{
$cert = self::getCert($certContent);
$publicKey = openssl_get_publickey($cert);
if (! $publicKey) {
throw new RuntimeException('从证书中读取公钥失败');
}
return $publicKey;
}
public static function getPublicKeyStringFromCert(string $certContent): string
{
$publicKey = self::getPublicKeyFromCert($certContent);
$detail = openssl_pkey_get_details($publicKey);
if (! $detail) {
throw new RuntimeException('获取公钥详情失败');
}
return $detail['key'];
}
public static function getCert(string $certContent): OpenSSLCertificate
{
$cert = openssl_x509_read($certContent);
if (! $cert) {
throw new RuntimeException('证书加载失败');
}
return $cert;
}
public static function getCertSn(string $certContent): string
{
$cert = self::getCert($certContent);
$info = openssl_x509_parse($cert);
if (! $info) {
throw new RuntimeException('解析证书失败');
}
return $info['serialNumberHex'];
}
public static function getPrivateKey(string $privateKeyContent): OpenSSLAsymmetricKey
{
$privateKey = openssl_pkey_get_private($privateKeyContent);
if (! $privateKey) {
throw new RuntimeException('非法私钥');
}
return $privateKey;
}
public static function encrypt(
string $plaintext,
OpenSSLAsymmetricKey $publicKey,
int $padding = OPENSSL_PKCS1_OAEP_PADDING,
): string {
if (! openssl_public_encrypt($plaintext, $encrypted, $publicKey, $padding)) {
throw new RuntimeException('加密失败');
}
return base64_encode($encrypted);
}
public static function verify(
string $message,
string $signature,
OpenSSLAsymmetricKey $publicKey,
int|string $algo,
): bool {
return (bool) openssl_verify($message, base64_decode($signature), $publicKey, $algo);
}
public static function sign(string $message, OpenSSLAsymmetricKey $privateKey, int|string $algo): string
{
if (! openssl_sign($message, $signature, $privateKey, $algo)) {
throw new RuntimeException('签名失败');
}
return base64_encode($signature);
}
public static function decrypt(
string $ciphertext,
OpenSSLAsymmetricKey $privateKey,
int $padding = OPENSSL_PKCS1_OAEP_PADDING,
): string {
if (! openssl_private_decrypt(base64_decode($ciphertext), $decrypted, $privateKey, $padding)) {
throw new RuntimeException('解密失败');
}
return $decrypted;
}
}
php
<?php
// AES加解密
namespace App\Security;
use RuntimeException;
class Aes
{
protected const ALGO_AES_256_ECB = 'aes-256-ecb';
protected const ALGO_AES_256_GCM = 'aes-256-gcm';
protected const BLOCK_SIZE = 16;
public static function ecbEncrypt(string $plaintext, string $key): string
{
$ciphertext = openssl_encrypt(
data: $plaintext,
cipher_algo: self::ALGO_AES_256_ECB,
passphrase: $key,
options: OPENSSL_RAW_DATA,
);
if (false === $ciphertext) {
throw new RuntimeException('加密失败');
}
return base64_encode($ciphertext);
}
public static function ecbDecrypt(string $ciphertext, string $key): string
{
$plaintext = openssl_decrypt(
data: base64_decode($ciphertext),
cipher_algo: self::ALGO_AES_256_ECB,
passphrase: $key,
options: OPENSSL_RAW_DATA,
);
if (false === $plaintext) {
throw new RuntimeException('解密失败');
}
return $plaintext;
}
public static function gcmEncrypt(string $plaintext, string $key, string $iv = '', string $aad = ''): string
{
$ciphertext = openssl_encrypt(
data: $plaintext,
cipher_algo: self::ALGO_AES_256_GCM,
passphrase: $key,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $tag,
aad: $aad,
tag_length: self::BLOCK_SIZE,
);
if (false === $ciphertext) {
throw new RuntimeException('加密失败');
}
return base64_encode($ciphertext.$tag);
}
public static function gcmDecrypt(string $ciphertext, string $key, string $iv = '', string $aad = ''): string
{
$ciphertext = base64_decode($ciphertext);
$authTag = substr($ciphertext, $tailLength = 0 - self::BLOCK_SIZE);
$tagLength = strlen($authTag);
if ($tagLength > self::BLOCK_SIZE || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) {
throw new RuntimeException('密文数据不完整');
}
$plaintext = openssl_decrypt(
data: substr($ciphertext, 0, $tailLength),
cipher_algo: self::ALGO_AES_256_GCM,
passphrase: $key,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $authTag,
aad: $aad,
);
if (false === $plaintext) {
throw new RuntimeException(
'解密失败'
);
}
return $plaintext;
}
}
php
<?php
// 微信APIv3签名与验签
namespace App;
use App\Security\Aes;
use App\Security\Rsa;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use OpenSSLAsymmetricKey;
use RuntimeException;
use Throwable;
class Wechat
{
protected const SIGN_ALGO = 'sha256WithRSAEncryption';
protected const SIGN_VERIFY_ALGO = OPENSSL_ALGO_SHA256;
public function __construct(
protected readonly OpenSSLAsymmetricKey $privateKey,
protected readonly string $certSN,
protected readonly string $mchtId,
protected readonly string $aesKey,
protected array $wechatCerts,
protected ?array $proxy = [],
) {
if (
! empty($this->proxy)
&& (
empty($this->proxy['host']) || empty($this->proxy['port'])
)
) {
throw new RuntimeException('代理服务器信息不正确');
}
}
/**
* @throws Throwable
*/
public function wechatCertDownload(): array
{
$url = 'https://api.mch.weixin.qq.com/v3/certificates';
$method = 'GET';
$resp = Http::withHeaders([
'Authorization' => $this->sign($url, $method, ''),
'Accept' => 'application/json;charset=utf-8',
'User-Agent' => 'HMIPP',
])->get($url);
$certs = $resp->json()['data'];
$wechatSignInfo = $this->respSignHeader($resp->headers());
$rs = $this->respVerify($wechatSignInfo, $resp->body());
throw_if(! $rs, new Exception('微信响应验签失败'));
$decryptedCerts = [];
foreach ($certs as $cert) {
$certContent = Aes::gcmDecrypt(
ciphertext: $cert['encrypt_certificate']['ciphertext'],
key: $this->aesKey,
iv: $cert['encrypt_certificate']['nonce'],
aad: $cert['encrypt_certificate']['associated_data'],
);
$sn = Rsa::getCertSn($certContent);
$decryptedCerts[$sn] = [
'effective_time' => $cert['effective_time'],
'expire_time' => $cert['expire_time'],
'content' => $certContent,
'sn' => $sn,
];
}
return $decryptedCerts;
}
/**
* @throws Throwable
*/
public function native(): array
{
$url = 'https://api.mch.weixin.qq.com/v3/pay/partner/transactions/native';
$method = 'POST';
$data = [
'sp_appid' => 'wx7b54f0a5769316b9',
'sp_mchid' => '1490937812',
'sub_mchid' => '1550552931',
'description' => 'Image形象店-深圳腾大-QQ公仔',
'out_trade_no' => '3'.date('Ymd').Str::random(),
'notify_url' => 'https://pay.uphicoo.com/notify/weixin/61000001',
'amount' => [
'total' => 1,
'currency' => 'CNY',
],
];
$resp = Http::withHeaders([
'Authorization' => $this->sign($url, $method, json_encode($data)),
'Accept' => 'application/json;charset=utf-8',
'User-Agent' => 'HMIPP',
])->post($url, $data);
$pass = $this->respVerify($this->respSignHeader($resp->headers()), $resp->body());
throw_if(! $pass, new Exception('微信响应验签失败'));
return $resp->json();
}
/**
* 签名
*
* HTTP请求方法\n
* URL\n
* 请求时间戳\n
* 请求随机串\n
* 请求报文主体\n
*
*
* @throws Throwable
*/
protected function sign(string $url, string $method, string $body): string
{
$urlInfo = parse_url($url);
$signPath = ($urlInfo['path'].(! empty($urlInfo['query']) ? "?${$urlInfo['query']}" : ''));
$time = time();
$randomStr = Str::random(32);
$data = sprintf(
"%s\n%s\n%s\n%s\n%s\n",
$method,
$signPath,
$time,
$randomStr,
$body,
);
$sign = Rsa::sign($data, $this->privateKey, self::SIGN_ALGO);
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"',
$this->mchtId,
$randomStr,
$sign,
$time,
$this->certSN,
);
}
/**
* @throws Throwable
*/
protected function respVerify(array $wechatSignInfo, string $body): bool
{
dump($wechatSignInfo['sn']);
$data = sprintf("%s\n%s\n%s\n", $wechatSignInfo['time'], $wechatSignInfo['nonce'], $body);
$cert = $this->wechatCerts[$wechatSignInfo['sn']] ?? [];
if (empty($cert)) {
$this->wechatCerts = self::wechatCertDownload();
}
$cert = $this->wechatCerts[$wechatSignInfo['sn']] ?? [];
throw_if(empty($cert), new RuntimeException('微信响应签名证书未从微信平台获取到'));
return Rsa::verify(
$data,
$wechatSignInfo['sign'],
Rsa::getPublicKeyFromCert($cert['content']),
self::SIGN_VERIFY_ALGO,
);
}
protected function respSignHeader(array $headers): array
{
return [
'nonce' => $headers['Wechatpay-Nonce'][0],
'sign' => $headers['Wechatpay-Signature'][0],
'time' => $headers['Wechatpay-Timestamp'][0],
'sn' => $headers['Wechatpay-Serial'][0],
'sign_type' => $headers['Wechatpay-Signature-Type'][0],
];
}
}