This commit is contained in:
xl
2023-11-13 22:13:44 +08:00
parent 0d1cb506aa
commit 59456a4759
3 changed files with 232 additions and 28 deletions
+170 -6
View File
@@ -3,11 +3,36 @@
namespace wchat\wx\V3;
use Exception;
use Psr\Http\Message\RequestInterface;
use wchat\wx\SmallProgram;
use wchat\wx\V3\Notify\GoodsDetail;
use wchat\wx\V3\Notify\NotifyModel;
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
{
@@ -33,7 +58,6 @@ class WxV3PaymentNotify extends SmallProgram
public array $resource = []
)
{
$this->decode();
}
@@ -43,17 +67,157 @@ class WxV3PaymentNotify extends SmallProgram
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
* @throws Exception
*/
public function decode(): void
{
$data = $this->decryptToString($this->resource);
if ($data === false) {
throw new Exception('消息体格式错误, 解码失败.');
}
$data = json_decode($data, true);
$data = $this->decrypt($this->resource['ciphertext'], $this->resource['nonce'], $this->resource['associated_data']);
$this->notifyModel = new NotifyModel();
$this->notifyModel->amount = $data['amount'];
$this->notifyModel->payer = $data['payer'];