Passed
Push — master ( 7ff004...a83eae )
by Songda
01:54
created

get_wechat_authorization()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 15
c 1
b 0
f 1
dl 0
loc 25
rs 9.7666
cc 3
nc 3
nop 4
1
<?php
2
3
declare(strict_types=1);
4
5
use Psr\Http\Message\MessageInterface;
6
use Psr\Http\Message\ServerRequestInterface;
7
use Yansongda\Pay\Contract\ConfigInterface;
8
use Yansongda\Pay\Exception\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
9
use Yansongda\Pay\Exception\InvalidConfigException;
10
use Yansongda\Pay\Exception\InvalidResponseException;
11
use Yansongda\Pay\Parser\NoHttpRequestParser;
12
use Yansongda\Pay\Pay;
13
use Yansongda\Pay\Plugin\ParserPlugin;
14
use Yansongda\Pay\Plugin\Wechat\PreparePlugin;
15
use Yansongda\Pay\Plugin\Wechat\SignPlugin;
16
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
17
use Yansongda\Pay\Provider\Wechat;
18
use Yansongda\Supports\Config;
19
use Yansongda\Supports\Str;
20
21
if (!function_exists('should_do_http_request')) {
22
    function should_do_http_request(?string $direction): bool
23
    {
24
        return is_null($direction) ||
25
            (NoHttpRequestParser::class !== $direction &&
26
            !in_array(NoHttpRequestParser::class, class_parents($direction)));
27
    }
28
}
29
30
if (!function_exists('get_alipay_config')) {
31
    /**
32
     * @throws \Yansongda\Pay\Exception\ContainerException
33
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
34
     */
35
    function get_alipay_config(array $params = []): Config
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
36
    {
37
        $alipay = Pay::get(ConfigInterface::class)->get('alipay');
38
39
        $config = $params['_config'] ?? 'default';
40
41
        return new Config($alipay[$config] ?? []);
42
    }
43
}
44
45
if (!function_exists('get_public_cert')) {
46
    function get_public_cert(string $key): string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
47
    {
48
        return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
49
    }
50
}
51
52
if (!function_exists('get_private_cert')) {
53
    function get_private_cert(string $key): string
54
    {
55
        if (Str::endsWith($key, ['.crt', '.pem'])) {
56
            return file_get_contents($key);
57
        }
58
59
        return "-----BEGIN RSA PRIVATE KEY-----\n".
60
            wordwrap($key, 64, "\n", true).
61
            "\n-----END RSA PRIVATE KEY-----";
62
    }
63
}
64
65
if (!function_exists('verify_alipay_sign')) {
66
    /**
67
     * @param string $sign base64decode 之后的
68
     *
69
     * @throws \Yansongda\Pay\Exception\ContainerException
70
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
71
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
72
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
73
     */
74
    function verify_alipay_sign(array $params, string $contents, string $sign): void
75
    {
76
        $public = get_alipay_config($params)->get('alipay_public_cert_path');
77
78
        if (empty($public)) {
79
            throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
80
        }
81
82
        $result = 1 === openssl_verify(
83
            $contents,
84
            $sign,
85
            get_public_cert($public),
86
            OPENSSL_ALGO_SHA256);
87
88
        if (!$result) {
89
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', func_get_args());
90
        }
91
    }
92
}
93
94
if (!function_exists('get_wechat_config')) {
95
    /**
96
     * @throws \Yansongda\Pay\Exception\ContainerException
97
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
98
     */
99
    function get_wechat_config(array $params): Config
100
    {
101
        $wechat = Pay::get(ConfigInterface::class)->get('wechat');
102
103
        $config = $params['_config'] ?? 'default';
104
105
        return new Config($wechat[$config] ?? []);
106
    }
107
}
108
109
if (!function_exists('get_wechat_base_uri')) {
110
    /**
111
     * @throws \Yansongda\Pay\Exception\ContainerException
112
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
113
     */
114
    function get_wechat_base_uri(array $params): string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
115
    {
116
        $config = get_wechat_config($params);
117
118
        return Wechat::URL[$config->get('mode', Pay::MODE_NORMAL)];
119
    }
120
}
121
122
if (!function_exists('get_wechat_sign')) {
123
    /**
124
     * @throws \Yansongda\Pay\Exception\ContainerException
125
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
126
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
127
     */
128
    function get_wechat_sign(array $params, string $contents): string
129
    {
130
        $privateKey = get_wechat_config($params)->get('mch_secret_cert');
131
132
        if (empty($privateKey)) {
133
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
134
        }
135
136
        $privateKey = get_private_cert($privateKey);
137
138
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
139
140
        return base64_encode($sign);
141
    }
142
}
143
144
if (!function_exists('verify_wechat_sign')) {
145
    /**
146
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
147
     *
148
     * @throws \Yansongda\Pay\Exception\ContainerException
149
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
150
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
151
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
152
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
153
     */
154
    function verify_wechat_sign(MessageInterface $message, array $params): void
155
    {
156
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
157
            return;
158
        }
159
160
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
161
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
162
        $random = $message->getHeaderLine('Wechatpay-Nonce');
163
        $sign = $message->getHeaderLine('Wechatpay-Signature');
164
        $body = (string) $message->getBody();
165
166
        $content = $timestamp."\n".$random."\n".$body."\n";
167
        $public = get_wechat_config($params)->get('wechat_public_cert_path.'.$wechatSerial);
168
169
        if (empty($sign)) {
170
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
171
        }
172
173
        $public = get_public_cert(
174
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
175
        );
176
177
        $result = 1 === openssl_verify(
178
            $content,
179
            base64_decode($sign),
180
            $public,
181
            'sha256WithRSAEncryption'
182
        );
183
184
        if (!$result) {
185
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
186
        }
187
    }
188
}
189
190
if (!function_exists('encrypt_wechat_contents')) {
191
    function encrypt_wechat_contents(string $contents, string $publicKey): ?string
192
    {
193
        if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
194
            return base64_encode($encrypted);
195
        }
196
197
        return null;
198
    }
199
}
200
201
if (!function_exists('reload_wechat_public_certs')) {
202
    /**
203
     * @throws \Yansongda\Pay\Exception\ContainerException
204
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
205
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
206
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
207
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
208
     */
209
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
210
    {
211
        $data = Pay::wechat()->pay(
212
            [PreparePlugin::class, WechatPublicCertsPlugin::class, SignPlugin::class, ParserPlugin::class],
213
            $params
214
        )->get('data', []);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Yansongda\Pay\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

214
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
215
216
        foreach ($data as $item) {
217
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
218
        }
219
220
        $wechatConfig = get_wechat_config($params);
221
        $wechatConfig['wechat_public_cert_path'] = ((array) $wechatConfig['wechat_public_cert_path']) + ($certs ?? []);
222
223
        Pay::set(ConfigInterface::class, Pay::get(ConfigInterface::class)->merge([
224
            'wechat' => [$params['_config'] ?? 'default' => $wechatConfig->all()],
225
        ]));
226
227
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
228
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
229
        }
230
231
        return $certs[$serialNo] ?? '';
232
    }
233
}
234
235
if (!function_exists('decrypt_wechat_resource')) {
236
    /**
237
     * @throws \Yansongda\Pay\Exception\ContainerException
238
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
239
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
240
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
241
     */
242
    function decrypt_wechat_resource(array $resource, array $params): array
243
    {
244
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
245
        $secret = get_wechat_config($params)->get('mch_secret_key');
246
247
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
248
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
249
        }
250
251
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
252
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
253
        }
254
255
        switch ($resource['algorithm'] ?? '') {
256
            case 'AEAD_AES_256_GCM':
257
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
258
                break;
259
            default:
260
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
261
        }
262
263
        return $resource;
264
    }
265
}
266
267
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
268
    /**
269
     * @return array|string
270
     *
271
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
272
     */
273
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
274
    {
275
        $decrypted = openssl_decrypt(
276
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
277
            'aes-256-gcm',
278
            $secret,
279
            OPENSSL_RAW_DATA,
280
            $nonce,
281
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
282
            $associatedData
283
        );
284
285
        if ('certificate' !== $associatedData) {
286
            $decrypted = json_decode($decrypted, true);
287
288
            if (JSON_ERROR_NONE !== json_last_error()) {
289
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
290
            }
291
        }
292
293
        return $decrypted;
294
    }
295
}
296