Passed
Pull Request — master (#733)
by Songda
01:44
created

get_wechat_public_certs()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yansongda\Pay;
6
7
use Psr\Http\Message\MessageInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Yansongda\Pay\Contract\ConfigInterface;
10
use Yansongda\Pay\Exception\Exception;
11
use Yansongda\Pay\Exception\InvalidConfigException;
12
use Yansongda\Pay\Exception\InvalidResponseException;
13
use Yansongda\Pay\Parser\NoHttpRequestParser;
14
use Yansongda\Pay\Plugin\ParserPlugin;
15
use Yansongda\Pay\Plugin\Wechat\PreparePlugin;
16
use Yansongda\Pay\Plugin\Wechat\RadarSignPlugin;
17
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
18
use Yansongda\Pay\Provider\Wechat;
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_tenant')) {
31
    function get_tenant(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...
32
    {
33
        return strval($params['_config'] ?? 'default');
34
    }
35
}
36
37
if (!function_exists('get_alipay_config')) {
38
    /**
39
     * @throws \Yansongda\Pay\Exception\ContainerException
40
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
41
     */
42
    function get_alipay_config(array $params = []): array
43
    {
44
        $alipay = Pay::get(ConfigInterface::class)->get('alipay');
45
46
        return $alipay[get_tenant($params)] ?? [];
47
    }
48
}
49
50
if (!function_exists('get_public_cert')) {
51
    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...
52
    {
53
        return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
54
    }
55
}
56
57
if (!function_exists('get_private_cert')) {
58
    function get_private_cert(string $key): string
59
    {
60
        if (Str::endsWith($key, ['.crt', '.pem'])) {
61
            return file_get_contents($key);
62
        }
63
64
        return "-----BEGIN RSA PRIVATE KEY-----\n".
65
            wordwrap($key, 64, "\n", true).
66
            "\n-----END RSA PRIVATE KEY-----";
67
    }
68
}
69
70
if (!function_exists('verify_alipay_sign')) {
71
    /**
72
     * @throws \Yansongda\Pay\Exception\ContainerException
73
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
74
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
75
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
76
     */
77
    function verify_alipay_sign(array $params, string $contents, string $sign): void
78
    {
79
        $public = get_alipay_config($params)['alipay_public_cert_path'] ?? null;
80
81
        if (empty($public)) {
82
            throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
83
        }
84
85
        $result = 1 === openssl_verify(
86
            $contents,
87
            base64_decode($sign),
88
            get_public_cert($public),
89
            OPENSSL_ALGO_SHA256);
90
91
        if (!$result) {
92
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Alipay Response Sign Failed', func_get_args());
93
        }
94
    }
95
}
96
97
if (!function_exists('get_wechat_config')) {
98
    /**
99
     * @throws \Yansongda\Pay\Exception\ContainerException
100
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
101
     */
102
    function get_wechat_config(array $params): array
103
    {
104
        $wechat = Pay::get(ConfigInterface::class)->get('wechat');
105
106
        return $wechat[get_tenant($params)] ?? [];
107
    }
108
}
109
110
if (!function_exists('get_wechat_base_uri')) {
111
    /**
112
     * @throws \Yansongda\Pay\Exception\ContainerException
113
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
114
     */
115
    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...
116
    {
117
        $config = get_wechat_config($params);
118
119
        return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL];
120
    }
121
}
122
123
if (!function_exists('get_wechat_sign')) {
124
    /**
125
     * @throws \Yansongda\Pay\Exception\ContainerException
126
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
127
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
128
     */
129
    function get_wechat_sign(array $params, string $contents): string
130
    {
131
        $privateKey = get_wechat_config($params)['mch_secret_cert'] ?? null;
132
133
        if (empty($privateKey)) {
134
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
135
        }
136
137
        $privateKey = get_private_cert($privateKey);
138
139
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
140
141
        return base64_encode($sign);
142
    }
143
}
144
145
if (!function_exists('verify_wechat_sign')) {
146
    /**
147
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
148
     *
149
     * @throws \Yansongda\Pay\Exception\ContainerException
150
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
151
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
152
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
153
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
154
     */
155
    function verify_wechat_sign(MessageInterface $message, array $params): void
156
    {
157
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
158
            return;
159
        }
160
161
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
162
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
163
        $random = $message->getHeaderLine('Wechatpay-Nonce');
164
        $sign = $message->getHeaderLine('Wechatpay-Signature');
165
        $body = (string) $message->getBody();
166
167
        $content = $timestamp."\n".$random."\n".$body."\n";
168
        $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
169
170
        if (empty($sign)) {
171
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
172
        }
173
174
        $public = get_public_cert(
175
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
176
        );
177
178
        $result = 1 === openssl_verify(
179
            $content,
180
            base64_decode($sign),
181
            $public,
182
            'sha256WithRSAEncryption'
183
        );
184
185
        if (!$result) {
186
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
187
        }
188
    }
189
}
190
191
if (!function_exists('encrypt_wechat_contents')) {
192
    function encrypt_wechat_contents(string $contents, string $publicKey): ?string
193
    {
194
        if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
195
            return base64_encode($encrypted);
196
        }
197
198
        return null;
199
    }
200
}
201
202
if (!function_exists('reload_wechat_public_certs')) {
203
    /**
204
     * @throws \Yansongda\Pay\Exception\ContainerException
205
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
206
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
207
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
208
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
209
     */
210
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
211
    {
212
        $data = Pay::wechat()->pay(
213
            [PreparePlugin::class, WechatPublicCertsPlugin::class, RadarSignPlugin::class, ParserPlugin::class],
214
            $params
215
        )->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

215
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
216
217
        foreach ($data as $item) {
218
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
219
        }
220
221
        $wechatConfig = get_wechat_config($params);
222
223
        Pay::get(ConfigInterface::class)->set(
224
            'wechat.'.get_tenant($params).'.wechat_public_cert_path',
225
            ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
226
        );
227
228
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
229
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
230
        }
231
232
        return $certs[$serialNo] ?? '';
233
    }
234
}
235
236
if (!function_exists('get_wechat_public_certs')) {
237
    /**
238
     * @throws \Yansongda\Pay\Exception\ContainerException
239
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
240
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
241
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
242
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
243
     */
244
    function get_wechat_public_certs(array $params = [], ?string $path = null): void
245
    {
246
        reload_wechat_public_certs($params);
247
248
        $config = get_wechat_config($params);
249
250
        if (empty($path)) {
251
            var_dump($config['wechat_public_cert_path']);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($config['wechat_public_cert_path']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
252
253
            return;
254
        }
255
256
        foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
257
            file_put_contents($path.'/'.$serialNo.'.crt', $cert);
258
        }
259
    }
260
}
261
262
if (!function_exists('decrypt_wechat_resource')) {
263
    /**
264
     * @throws \Yansongda\Pay\Exception\ContainerException
265
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
266
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
267
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
268
     */
269
    function decrypt_wechat_resource(array $resource, array $params): array
270
    {
271
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
272
        $secret = get_wechat_config($params)['mch_secret_key'] ?? null;
273
274
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
275
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
276
        }
277
278
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
279
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
280
        }
281
282
        switch ($resource['algorithm'] ?? '') {
283
            case 'AEAD_AES_256_GCM':
284
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
285
                break;
286
            default:
287
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
288
        }
289
290
        return $resource;
291
    }
292
}
293
294
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
295
    /**
296
     * @return array|string
297
     *
298
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
299
     */
300
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
301
    {
302
        $decrypted = openssl_decrypt(
303
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
304
            'aes-256-gcm',
305
            $secret,
306
            OPENSSL_RAW_DATA,
307
            $nonce,
308
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
309
            $associatedData
310
        );
311
312
        if ('certificate' !== $associatedData) {
313
            $decrypted = json_decode(strval($decrypted), true);
314
315
            if (JSON_ERROR_NONE !== json_last_error()) {
316
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
317
            }
318
        }
319
320
        return $decrypted;
321
    }
322
}
323
324
if (!function_exists('get_unipay_config')) {
325
    /**
326
     * @throws \Yansongda\Pay\Exception\ContainerException
327
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
328
     */
329
    function get_unipay_config(array $params): array
330
    {
331
        $unipay = Pay::get(ConfigInterface::class)->get('unipay');
332
333
        return $unipay[get_tenant($params)] ?? [];
334
    }
335
}
336
337
if (!function_exists('verify_unipay_sign')) {
338
    /**
339
     * @throws \Yansongda\Pay\Exception\ContainerException
340
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
341
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
342
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
343
     */
344
    function verify_unipay_sign(array $params, string $contents, string $sign): void
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...
345
    {
346
        if (empty($params['signPubKeyCert'])
347
            && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
348
            throw new InvalidConfigException(Exception::UNIPAY_CONFIG_ERROR, 'Missing Unipay Config -- [unipay_public_cert_path]');
349
        }
350
351
        $result = 1 === openssl_verify(
352
            hash('sha256', $contents),
353
            base64_decode($sign),
354
            get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
355
            'sha256');
356
357
        if (!$result) {
358
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Unipay Response Sign Failed', func_get_args());
359
        }
360
    }
361
}
362