From 59456a47597d23cd47b372583fa7ddbc7fff8920 Mon Sep 17 00:00:00 2001 From: xl Date: Mon, 13 Nov 2023 22:13:44 +0800 Subject: [PATCH] eee --- wchat/wx/V3/Config.php | 20 ++++ wchat/wx/V3/WxV3PaymentNotify.php | 176 +++++++++++++++++++++++++++++- wchat/wx/V3/WxV3PaymentTait.php | 64 +++++++---- 3 files changed, 232 insertions(+), 28 deletions(-) create mode 100644 wchat/wx/V3/Config.php diff --git a/wchat/wx/V3/Config.php b/wchat/wx/V3/Config.php new file mode 100644 index 0000000..9ac5a04 --- /dev/null +++ b/wchat/wx/V3/Config.php @@ -0,0 +1,20 @@ + [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']; diff --git a/wchat/wx/V3/WxV3PaymentTait.php b/wchat/wx/V3/WxV3PaymentTait.php index 78bc8cd..3bce3a2 100644 --- a/wchat/wx/V3/WxV3PaymentTait.php +++ b/wchat/wx/V3/WxV3PaymentTait.php @@ -5,12 +5,35 @@ namespace wchat\wx\V3; use Exception; use Kiri\Client; 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; +/** + * 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 { @@ -110,29 +133,26 @@ trait WxV3PaymentTait return $responseArray; } - /** - * @param array $resource - * @return bool|string + * @param string $ciphertext + * @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); - if (strlen($ciphertext) <= AUTH_TAG_LENGTH_BYTE) { - return FALSE; + $ciphertext = base64_decode($ciphertext); + $authTag = substr($ciphertext, $tailLength = 0 - BLOCK_SIZE); + $tagLength = strlen($authTag); + + /* 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()) { - return crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, 'XGwwZbmMXy6sD5w0IrxfaBHLl7b7jCaR'); + $plaintext = openssl_decrypt(substr($ciphertext, 0, $tailLength), ALGO_AES_256_GCM, $this->getConfig()->getKey(), OPENSSL_RAW_DATA, $iv, $authTag, $aad); + 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 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'); + return json_decode($plaintext, true); } }