Passed
Pull Request — master (#579)
by Songda
02:28
created

get_public_cert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 2
nc 2
nop 1
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
            $key = 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_authorization')) {
123
    /**
124
     * @throws \Yansongda\Pay\Exception\ContainerException
125
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
126
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
127
     */
128
    function get_wechat_authorization(array $params, int $timestamp, string $random, string $contents): string
129
    {
130
        $config = get_wechat_config($params);
131
        $mchPublicCertPath = $config->get('mch_public_cert_path');
132
133
        if (empty($mchPublicCertPath)) {
134
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_public_cert_path]');
135
        }
136
137
        $ssl = openssl_x509_parse(get_public_cert($mchPublicCertPath));
138
139
        if (empty($ssl['serialNumberHex'])) {
140
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Parse [mch_public_cert_path] Serial Number Error');
141
        }
142
143
        $auth = sprintf(
144
            'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
145
            $config->get('mch_id', ''),
146
            $random,
147
            $timestamp,
148
            $ssl['serialNumberHex'],
149
            get_wechat_sign($params, $contents),
150
        );
151
152
        return 'WECHATPAY2-SHA256-RSA2048 '.$auth;
153
    }
154
}
155
156
if (!function_exists('get_wechat_sign')) {
157
    /**
158
     * @throws \Yansongda\Pay\Exception\ContainerException
159
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
160
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
161
     */
162
    function get_wechat_sign(array $params, string $contents): string
163
    {
164
        $privateKey = get_wechat_config($params)->get('mch_secret_cert');
165
166
        if (empty($privateKey)) {
167
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
168
        }
169
170
        $privateKey = get_private_cert($privateKey);
171
172
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
173
174
        $sign = base64_encode($sign);
175
176
        !is_resource($privateKey) ?: openssl_free_key($privateKey);
0 ignored issues
show
introduced by
The condition is_resource($privateKey) is always false.
Loading history...
177
178
        return $sign;
179
    }
180
}
181
182
if (!function_exists('verify_wechat_sign')) {
183
    /**
184
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
185
     *
186
     * @throws \Yansongda\Pay\Exception\ContainerException
187
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
188
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
189
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
190
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
191
     */
192
    function verify_wechat_sign(MessageInterface $message, array $params): void
193
    {
194
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
195
            return;
196
        }
197
198
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
199
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
200
        $random = $message->getHeaderLine('Wechatpay-Nonce');
201
        $sign = $message->getHeaderLine('Wechatpay-Signature');
202
        $body = $message->getBody()->getContents();
203
204
        $content = $timestamp."\n".$random."\n".$body."\n";
205
        $public = get_wechat_config($params)->get('wechat_public_cert_path.'.$wechatSerial);
206
207
        if (empty($sign)) {
208
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
209
        }
210
211
        $public = get_public_cert(
212
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
213
        );
214
215
        $result = 1 === openssl_verify(
216
            $content,
217
            base64_decode($sign),
218
            $public,
219
            'sha256WithRSAEncryption'
220
        );
221
222
        if (!$result) {
223
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
224
        }
225
    }
226
}
227
228
if (!function_exists('encrypt_wechat_contents')) {
229
    function encrypt_wechat_contents(string $contents, string $publicKey): ?string
230
    {
231
        if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
232
            return base64_encode($encrypted);
233
        }
234
235
        return null;
236
    }
237
}
238
239
if (!function_exists('reload_wechat_public_certs')) {
240
    /**
241
     * @throws \Yansongda\Pay\Exception\ContainerException
242
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
243
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
244
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
245
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
246
     */
247
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
248
    {
249
        $data = Pay::wechat()->pay(
250
            [PreparePlugin::class, WechatPublicCertsPlugin::class, SignPlugin::class, ParserPlugin::class],
251
            $params
252
        )->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

252
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
253
254
        foreach ($data as $item) {
255
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
256
        }
257
258
        $wechatConfig = get_wechat_config($params);
259
        $wechatConfig['wechat_public_cert_path'] = ((array) $wechatConfig['wechat_public_cert_path']) + ($certs ?? []);
260
261
        Pay::set(ConfigInterface::class, Pay::get(ConfigInterface::class)->merge([
262
            'wechat' => [$params['_config'] ?? 'default' => $wechatConfig->all()],
263
        ]));
264
265
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
266
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
267
        }
268
269
        return $certs[$serialNo] ?? '';
270
    }
271
}
272
273
if (!function_exists('decrypt_wechat_resource')) {
274
    /**
275
     * @throws \Yansongda\Pay\Exception\ContainerException
276
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
277
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
278
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
279
     */
280
    function decrypt_wechat_resource(array $resource, array $params): array
281
    {
282
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
283
        $secret = get_wechat_config($params)->get('mch_secret_key');
284
285
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
286
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
287
        }
288
289
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
290
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
291
        }
292
293
        switch ($resource['algorithm'] ?? '') {
294
            case 'AEAD_AES_256_GCM':
295
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
296
                break;
297
            default:
298
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
299
        }
300
301
        return $resource;
302
    }
303
}
304
305
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
306
    /**
307
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
308
     *
309
     * @return array|string
310
     */
311
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
312
    {
313
        $decrypted = openssl_decrypt(
314
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
315
            'aes-256-gcm',
316
            $secret,
317
            OPENSSL_RAW_DATA,
318
            $nonce,
319
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
320
            $associatedData
321
        );
322
323
        if ('certificate' !== $associatedData) {
324
            $decrypted = json_decode($decrypted, true);
325
326
            if (JSON_ERROR_NONE !== json_last_error()) {
327
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
328
            }
329
        }
330
331
        return $decrypted;
332
    }
333
}
334