Passed
Pull Request — master (#772)
by Songda
01:55
created

get_wechat_sign_v2()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

247
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
248
249
        foreach ($data as $item) {
250
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
251
        }
252
253
        $wechatConfig = get_wechat_config($params);
254
255
        Pay::get(ConfigInterface::class)->set(
256
            'wechat.'.get_tenant($params).'.wechat_public_cert_path',
257
            ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
258
        );
259
260
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
261
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
262
        }
263
264
        return $certs[$serialNo] ?? '';
265
    }
266
}
267
268
if (!function_exists('get_wechat_public_certs')) {
269
    /**
270
     * @throws ContainerException
271
     * @throws InvalidConfigException
272
     * @throws ServiceNotFoundException
273
     * @throws InvalidParamsException
274
     * @throws InvalidResponseException
275
     */
276
    function get_wechat_public_certs(array $params = [], ?string $path = null): void
277
    {
278
        reload_wechat_public_certs($params);
279
280
        $config = get_wechat_config($params);
281
282
        if (empty($path)) {
283
            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...
284
285
            return;
286
        }
287
288
        foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
289
            file_put_contents($path.'/'.$serialNo.'.crt', $cert);
290
        }
291
    }
292
}
293
294
if (!function_exists('decrypt_wechat_resource')) {
295
    /**
296
     * @throws ContainerException
297
     * @throws InvalidConfigException
298
     * @throws InvalidResponseException
299
     * @throws ServiceNotFoundException
300
     */
301
    function decrypt_wechat_resource(array $resource, array $params): array
302
    {
303
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
304
        $secret = get_wechat_config($params)['mch_secret_key'] ?? null;
305
306
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
307
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
308
        }
309
310
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
311
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
312
        }
313
314
        switch ($resource['algorithm'] ?? '') {
315
            case 'AEAD_AES_256_GCM':
316
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
317
318
                break;
319
320
            default:
321
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
322
        }
323
324
        return $resource;
325
    }
326
}
327
328
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
329
    /**
330
     * @return array|string
331
     *
332
     * @throws InvalidResponseException
333
     */
334
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
335
    {
336
        $decrypted = openssl_decrypt(
337
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
338
            'aes-256-gcm',
339
            $secret,
340
            OPENSSL_RAW_DATA,
341
            $nonce,
342
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
343
            $associatedData
344
        );
345
346
        if ('certificate' !== $associatedData) {
347
            $decrypted = json_decode(strval($decrypted), true);
348
349
            if (JSON_ERROR_NONE !== json_last_error()) {
350
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
351
            }
352
        }
353
354
        return $decrypted;
355
    }
356
}
357
358
if (!function_exists('get_unipay_config')) {
359
    /**
360
     * @throws ContainerException
361
     * @throws ServiceNotFoundException
362
     */
363
    function get_unipay_config(array $params): array
364
    {
365
        $unipay = Pay::get(ConfigInterface::class)->get('unipay');
366
367
        return $unipay[get_tenant($params)] ?? [];
368
    }
369
}
370
371
if (!function_exists('verify_unipay_sign')) {
372
    /**
373
     * @throws ContainerException
374
     * @throws InvalidConfigException
375
     * @throws InvalidResponseException
376
     * @throws ServiceNotFoundException
377
     */
378
    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...
379
    {
380
        if (empty($params['signPubKeyCert'])
381
            && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
382
            throw new InvalidConfigException(Exception::UNIPAY_CONFIG_ERROR, 'Missing Unipay Config -- [unipay_public_cert_path]');
383
        }
384
385
        $result = 1 === openssl_verify(
386
            hash('sha256', $contents),
387
            base64_decode($sign),
388
            get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
389
            'sha256'
390
        );
391
392
        if (!$result) {
393
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Unipay Response Sign Failed', func_get_args());
394
        }
395
    }
396
}
397