eee
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace wchat\wx\V3;
|
||||||
|
|
||||||
|
class Config
|
||||||
|
{
|
||||||
|
|
||||||
|
protected string $appId = '';
|
||||||
|
protected string $appSecret = '';
|
||||||
|
protected string $mchId = '';
|
||||||
|
protected string $mchKey = '';
|
||||||
|
protected string $secret = '';
|
||||||
|
protected string $mchCert = '';
|
||||||
|
protected string $SerialNumber = '';
|
||||||
|
protected string $notifyUrl = '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,11 +3,36 @@
|
|||||||
namespace wchat\wx\V3;
|
namespace wchat\wx\V3;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
use wchat\wx\SmallProgram;
|
use wchat\wx\SmallProgram;
|
||||||
use wchat\wx\V3\Notify\GoodsDetail;
|
use wchat\wx\V3\Notify\GoodsDetail;
|
||||||
use wchat\wx\V3\Notify\NotifyModel;
|
use wchat\wx\V3\Notify\NotifyModel;
|
||||||
use wchat\wx\V3\Notify\PromotionDetail;
|
use wchat\wx\V3\Notify\PromotionDetail;
|
||||||
|
|
||||||
|
const KEY_LENGTH_BYTE = 32;
|
||||||
|
const AUTH_TAG_LENGTH_BYTE = 16;
|
||||||
|
const KEY_TYPE_PUBLIC = 'public';
|
||||||
|
const KEY_TYPE_PRIVATE = 'private';
|
||||||
|
|
||||||
|
const LOCAL_FILE_PROTOCOL = 'file://';
|
||||||
|
const PKEY_PEM_NEEDLE = ' KEY-';
|
||||||
|
const PKEY_PEM_FORMAT = "-----BEGIN %1\$s KEY-----\n%2\$s\n-----END %1\$s KEY-----";
|
||||||
|
const PKEY_PEM_FORMAT_PATTERN = '#-{5}BEGIN ((?:RSA )?(?:PUBLIC|PRIVATE)) KEY-{5}\r?\n([^-]+)\r?\n-{5}END \1 KEY-{5}#';
|
||||||
|
const CHR_CR = "\r";
|
||||||
|
const CHR_LF = "\n";
|
||||||
|
|
||||||
|
const RULES = [
|
||||||
|
'private.pkcs1' => [self::PKEY_PEM_FORMAT, 'RSA PRIVATE', 16],
|
||||||
|
'private.pkcs8' => [self::PKEY_PEM_FORMAT, 'PRIVATE', 16],
|
||||||
|
'public.pkcs1' => [self::PKEY_PEM_FORMAT, 'RSA PUBLIC', 15],
|
||||||
|
'public.spki' => [self::PKEY_PEM_FORMAT, 'PUBLIC', 14],
|
||||||
|
];
|
||||||
|
const ASN1_OID_RSAENCRYPTION = '300d06092a864886f70d0101010500';
|
||||||
|
const ASN1_SEQUENCE = 48;
|
||||||
|
const CHR_NUL = "\0";
|
||||||
|
const CHR_ETX = "\3";
|
||||||
|
|
||||||
|
|
||||||
class WxV3PaymentNotify extends SmallProgram
|
class WxV3PaymentNotify extends SmallProgram
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -33,7 +58,6 @@ class WxV3PaymentNotify extends SmallProgram
|
|||||||
public array $resource = []
|
public array $resource = []
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->decode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -43,17 +67,157 @@ class WxV3PaymentNotify extends SmallProgram
|
|||||||
public NotifyModel $notifyModel;
|
public NotifyModel $notifyModel;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RequestInterface $request
|
||||||
|
* @return bool
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function verify(RequestInterface $request): bool
|
||||||
|
{
|
||||||
|
$platformPublicKeyInstance = $this->rsaFrom('file:///path/to/wechatpay/inWechatpaySerial.pem', KEY_TYPE_PUBLIC);
|
||||||
|
$inWechatpaySignature = $request->getHeaderLine('Wechatpay-Signature');// 请根据实际情况获取
|
||||||
|
$inWechatpayTimestamp = $request->getHeaderLine('Wechatpay-Timestamp');// 请根据实际情况获取
|
||||||
|
$inWechatpayNonce = $request->getHeaderLine('Wechatpay-Nonce');// 请根据实际情况获取
|
||||||
|
$inBody = $request->getBody()->getContents();// 请根据实际情况获取,例如: file_get_contents('php://input');
|
||||||
|
|
||||||
|
$timeOffsetStatus = 300 >= abs(time() - (int)$inWechatpayTimestamp);
|
||||||
|
$lineFeed = $this->lineFeed([$inWechatpayTimestamp, $inWechatpayNonce, $inBody]);
|
||||||
|
$verifiedStatus = $this->notifyVerify($lineFeed, $inWechatpaySignature, $platformPublicKeyInstance);
|
||||||
|
if (!$timeOffsetStatus || !$verifiedStatus) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->decode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ...$pieces
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function lineFeed(...$pieces): string
|
||||||
|
{
|
||||||
|
return implode("\n", array_merge($pieces, ['']));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
protected function body(): string|bool
|
||||||
|
{
|
||||||
|
return json_encode(['id' => $this->id, 'create_time' => $this->create_time, 'resource_type' => $this->resource_type, 'event_type' => $this->event_type, 'summary' => $this->summary, 'resource' => $this->resource]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
* @param string $signature
|
||||||
|
* @param $publicKey
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function notifyVerify(string $message, string $signature, $publicKey): bool
|
||||||
|
{
|
||||||
|
if (($result = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256)) === false) {
|
||||||
|
throw new \UnexpectedValueException('Verified the input $message failed, please checking your $publicKey whether or nor correct.');
|
||||||
|
}
|
||||||
|
return $result === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $thing
|
||||||
|
* @param string $type
|
||||||
|
* @return \OpenSSLAsymmetricKey
|
||||||
|
*/
|
||||||
|
protected function rsaFrom($thing, string $type = KEY_TYPE_PRIVATE): \OpenSSLAsymmetricKey
|
||||||
|
{
|
||||||
|
$pkey = ($isPublic = $type === KEY_TYPE_PUBLIC)
|
||||||
|
? openssl_pkey_get_public($this->parse($thing, $type))
|
||||||
|
: openssl_pkey_get_private($this->parse($thing));
|
||||||
|
|
||||||
|
if (false === $pkey) {
|
||||||
|
throw new \UnexpectedValueException(sprintf(
|
||||||
|
'Cannot load %s from(%s), please take care about the \$thing input.',
|
||||||
|
$isPublic ? 'publicKey' : 'privateKey',
|
||||||
|
gettype($thing)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $thing
|
||||||
|
* @param string $type
|
||||||
|
* @return mixed|string
|
||||||
|
*/
|
||||||
|
protected function parse($thing, string $type = KEY_TYPE_PRIVATE): mixed
|
||||||
|
{
|
||||||
|
$src = $thing;
|
||||||
|
if (is_string($src) && is_int(strpos($src, PKEY_PEM_NEEDLE))
|
||||||
|
&& $type === KEY_TYPE_PUBLIC && preg_match(PKEY_PEM_FORMAT_PATTERN, $src, $matches)) {
|
||||||
|
[, $kind, $base64] = $matches;
|
||||||
|
$mapRules = (array)array_combine(array_column(RULES, 1/*column*/), array_keys(RULES));
|
||||||
|
$protocol = $mapRules[$kind] ?? '';
|
||||||
|
if ('public.pkcs1' === $protocol) {
|
||||||
|
$src = sprintf('%s://%s', $protocol, str_replace([CHR_CR, CHR_LF], '', $base64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_string($src) && is_bool(strpos($src, LOCAL_FILE_PROTOCOL)) && is_int(strpos($src, '://'))) {
|
||||||
|
$protocol = parse_url($src, PHP_URL_SCHEME);
|
||||||
|
[$format, $kind, $offset] = RULES[$protocol] ?? [null, null, null];
|
||||||
|
if ($format && $kind && $offset) {
|
||||||
|
$src = substr($src, $offset);
|
||||||
|
if ('public.pkcs1' === $protocol) {
|
||||||
|
$src = $this->pkcs1ToSpki($src);
|
||||||
|
[$format, $kind] = RULES['public.spki'];
|
||||||
|
}
|
||||||
|
return sprintf($format, $kind, wordwrap($src, 64, CHR_LF, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $src;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $thing
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function pkcs1ToSpki(string $thing): string
|
||||||
|
{
|
||||||
|
$raw = CHR_NUL . base64_decode($thing);
|
||||||
|
$new = pack('H*', ASN1_OID_RSAENCRYPTION) . CHR_ETX . self::encodeLength($raw) . $raw;
|
||||||
|
|
||||||
|
return base64_encode(pack('Ca*a*', ASN1_SEQUENCE, self::encodeLength($new), $new));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $thing
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function encodeLength(string $thing): string
|
||||||
|
{
|
||||||
|
$num = strlen($thing);
|
||||||
|
if ($num <= 0x7F) {
|
||||||
|
return sprintf('%c', $num);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = ltrim(pack('N', $num), CHR_NUL);
|
||||||
|
return pack('Ca*', strlen($tmp) | 0x80, $tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return void
|
* @return void
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function decode(): void
|
public function decode(): void
|
||||||
{
|
{
|
||||||
$data = $this->decryptToString($this->resource);
|
$data = $this->decrypt($this->resource['ciphertext'], $this->resource['nonce'], $this->resource['associated_data']);
|
||||||
if ($data === false) {
|
|
||||||
throw new Exception('消息体格式错误, 解码失败.');
|
|
||||||
}
|
|
||||||
$data = json_decode($data, true);
|
|
||||||
$this->notifyModel = new NotifyModel();
|
$this->notifyModel = new NotifyModel();
|
||||||
$this->notifyModel->amount = $data['amount'];
|
$this->notifyModel->amount = $data['amount'];
|
||||||
$this->notifyModel->payer = $data['payer'];
|
$this->notifyModel->payer = $data['payer'];
|
||||||
|
|||||||
@@ -5,12 +5,35 @@ namespace wchat\wx\V3;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Kiri\Client;
|
use Kiri\Client;
|
||||||
use wchat\common\Help;
|
use wchat\common\Help;
|
||||||
use function Sodium\crypto_aead_aes256gcm_decrypt;
|
|
||||||
use function Sodium\crypto_aead_aes256gcm_is_available;
|
|
||||||
|
|
||||||
const KEY_LENGTH_BYTE = 32;
|
|
||||||
|
/**
|
||||||
|
* Bytes Length of the AES block
|
||||||
|
*/
|
||||||
|
const BLOCK_SIZE = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytes length of the AES secret key.
|
||||||
|
*/
|
||||||
|
const KEY_LENGTH_BYTE = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytes Length of the authentication tag in AEAD cipher mode
|
||||||
|
* @deprecated 1.0 - As of the OpenSSL described, the `auth_tag` length may be one of 16, 15, 14, 13, 12, 8 or 4.
|
||||||
|
* Keep it only compatible for the samples on the official documentation.
|
||||||
|
*/
|
||||||
const AUTH_TAG_LENGTH_BYTE = 16;
|
const AUTH_TAG_LENGTH_BYTE = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `aes-256-gcm` algorithm string
|
||||||
|
*/
|
||||||
|
const ALGO_AES_256_GCM = 'aes-256-gcm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `aes-256-ecb` algorithm string
|
||||||
|
*/
|
||||||
|
const ALGO_AES_256_ECB = 'aes-256-ecb';
|
||||||
|
|
||||||
trait WxV3PaymentTait
|
trait WxV3PaymentTait
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -110,29 +133,26 @@ trait WxV3PaymentTait
|
|||||||
return $responseArray;
|
return $responseArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $resource
|
* @param string $ciphertext
|
||||||
* @return bool|string
|
* @param string $iv
|
||||||
|
* @param string $aad
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function decryptToString(array $resource): bool|string
|
public function decrypt(string $ciphertext, string $iv = '', string $aad = ''): array
|
||||||
{
|
{
|
||||||
[$associatedData, $nonceStr, $cipher_algo, $ciphertext] = [$resource['associated_data'], $resource['nonce'], $resource['nonce'], $resource['ciphertext']];
|
$ciphertext = base64_decode($ciphertext);
|
||||||
$ciphertext = \base64_decode($ciphertext);
|
$authTag = substr($ciphertext, $tailLength = 0 - BLOCK_SIZE);
|
||||||
if (strlen($ciphertext) <= AUTH_TAG_LENGTH_BYTE) {
|
$tagLength = strlen($authTag);
|
||||||
return FALSE;
|
|
||||||
|
/* Manually checking the length of the tag, because the `openssl_decrypt` was mentioned there, it's the caller's responsibility. */
|
||||||
|
if ($tagLength > BLOCK_SIZE || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) {
|
||||||
|
throw new \RuntimeException('The inputs `$ciphertext` incomplete, the bytes length must be one of 16, 15, 14, 13, 12, 8 or 4.');
|
||||||
}
|
}
|
||||||
if (function_exists('\sodium\crypto_aead_aes256gcm_is_available') && crypto_aead_aes256gcm_is_available()) {
|
$plaintext = openssl_decrypt(substr($ciphertext, 0, $tailLength), ALGO_AES_256_GCM, $this->getConfig()->getKey(), OPENSSL_RAW_DATA, $iv, $authTag, $aad);
|
||||||
return crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, 'XGwwZbmMXy6sD5w0IrxfaBHLl7b7jCaR');
|
if (false === $plaintext) {
|
||||||
|
throw new \UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
|
||||||
}
|
}
|
||||||
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && crypto_aead_aes256gcm_is_available()) {
|
return json_decode($plaintext, true);
|
||||||
return crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, 'XGwwZbmMXy6sD5w0IrxfaBHLl7b7jCaR');
|
|
||||||
}
|
|
||||||
if (PHP_VERSION_ID >= 70100 && in_array($cipher_algo, \openssl_get_cipher_methods())) {
|
|
||||||
$ctext = substr($ciphertext, 0, -AUTH_TAG_LENGTH_BYTE);
|
|
||||||
$authTag = substr($ciphertext, -AUTH_TAG_LENGTH_BYTE);
|
|
||||||
return \openssl_decrypt($ctext, $cipher_algo, 'XGwwZbmMXy6sD5w0IrxfaBHLl7b7jCaR', \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
|
|
||||||
}
|
|
||||||
throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user