Skip to content
目录

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],
        ];
    }
}