Passed
Pull Request — master (#912)
by Songda
02:01
created

decrypt_wechat_contents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yansongda\Pay;
6
7
use Closure;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Yansongda\Pay\Contract\ConfigInterface;
11
use Yansongda\Pay\Contract\DirectionInterface;
12
use Yansongda\Pay\Direction\NoHttpRequestDirection;
13
use Yansongda\Pay\Exception\ContainerException;
14
use Yansongda\Pay\Exception\DecryptException;
15
use Yansongda\Pay\Exception\Exception;
16
use Yansongda\Pay\Exception\InvalidConfigException;
17
use Yansongda\Pay\Exception\InvalidParamsException;
18
use Yansongda\Pay\Exception\InvalidSignException;
19
use Yansongda\Pay\Exception\ServiceNotFoundException;
20
use Yansongda\Pay\Plugin\ParserPlugin;
21
use Yansongda\Pay\Plugin\Wechat\AddPayloadBodyPlugin;
22
use Yansongda\Pay\Plugin\Wechat\AddPayloadSignaturePlugin;
23
use Yansongda\Pay\Plugin\Wechat\AddRadarPlugin;
24
use Yansongda\Pay\Plugin\Wechat\ResponsePlugin;
25
use Yansongda\Pay\Plugin\Wechat\StartPlugin;
26
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
27
use Yansongda\Pay\Provider\Wechat;
28
use Yansongda\Supports\Collection;
29
use Yansongda\Supports\Str;
30
31
function should_do_http_request(string $direction): bool
32
{
33
    return NoHttpRequestDirection::class !== $direction
34
        && !in_array(NoHttpRequestDirection::class, class_parents($direction));
35
}
36
37
function get_tenant(array $params = []): string
38
{
39
    return strval($params['_config'] ?? 'default');
40
}
41
42
/**
43
 * @throws InvalidConfigException
44
 */
45
function get_direction(mixed $direction): DirectionInterface
46
{
47
    try {
48
        $direction = Pay::get($direction);
49
50
        $direction = is_string($direction) ? Pay::get($direction) : $direction;
51
    } catch (ContainerException|ServiceNotFoundException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
52
    }
53
54
    if (!$direction instanceof DirectionInterface) {
55
        throw new InvalidConfigException(Exception::CONFIG_DIRECTION_INVALID, '配置异常: 配置的 DirectionInterface 未实现 `DirectionInterface`');
56
    }
57
58
    return $direction;
59
}
60
61
function get_public_cert(string $key): string
62
{
63
    return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
64
}
65
66
function get_private_cert(string $key): string
67
{
68
    if (Str::endsWith($key, ['.crt', '.pem'])) {
69
        return file_get_contents($key);
70
    }
71
72
    return "-----BEGIN RSA PRIVATE KEY-----\n".
73
        wordwrap($key, 64, "\n", true).
74
        "\n-----END RSA PRIVATE KEY-----";
75
}
76
77
function filter_params(array $params, ?Closure $closure = null): array
78
{
79
    return array_filter($params, static fn ($v, $k) => !Str::startsWith($k, '_') && !is_null($v) && (empty($closure) || $closure($k, $v)), ARRAY_FILTER_USE_BOTH);
80
}
81
82
/**
83
 * @throws ContainerException
84
 * @throws ServiceNotFoundException
85
 */
86
function get_alipay_config(array $params = []): array
87
{
88
    $alipay = Pay::get(ConfigInterface::class)->get('alipay');
89
90
    return $alipay[get_tenant($params)] ?? [];
91
}
92
93
/**
94
 * @throws InvalidConfigException
95
 * @throws InvalidSignException
96
 */
97
function verify_alipay_sign(array $config, string $contents, string $sign): void
98
{
99
    $public = $config['alipay_public_cert_path'] ?? null;
100
101
    if (empty($public)) {
102
        throw new InvalidConfigException(Exception::CONFIG_ALIPAY_INVALID, '配置异常: 缺少支付宝配置 -- [alipay_public_cert_path]');
103
    }
104
105
    $result = 1 === openssl_verify(
106
        $contents,
107
        base64_decode($sign),
108
        get_public_cert($public),
109
        OPENSSL_ALGO_SHA256
110
    );
111
112
    if (!$result) {
113
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证支付宝签名失败', func_get_args());
114
    }
115
}
116
117
/**
118
 * @throws ContainerException
119
 * @throws ServiceNotFoundException
120
 */
121
function get_wechat_config(array $params = []): array
122
{
123
    $wechat = Pay::get(ConfigInterface::class)->get('wechat');
124
125
    return $wechat[get_tenant($params)] ?? [];
126
}
127
128
function get_wechat_method(?Collection $payload): string
129
{
130
    return strtoupper($payload?->get('_method') ?? 'POST');
131
}
132
133
/**
134
 * @throws InvalidParamsException
135
 */
136
function get_wechat_url(array $config, ?Collection $payload): string
137
{
138
    $url = $payload?->get('_url') ?? null;
139
140
    if (Pay::MODE_SERVICE === ($config['mode'] ?? Pay::MODE_NORMAL)) {
141
        $url = $payload?->get('_service_url') ?? $url ?? null;
142
    }
143
144
    if (empty($url)) {
145
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_URL_MISSING, '参数异常: 微信 `_url` 或 `_service_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件`');
146
    }
147
148
    if (str_starts_with($url, 'http')) {
149
        return $url;
150
    }
151
152
    return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
153
}
154
155
/**
156
 * @throws InvalidParamsException
157
 */
158
function get_wechat_body(?Collection $payload): string
159
{
160
    $body = $payload?->get('_body') ?? null;
161
162
    if (is_null($body)) {
163
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_BODY_MISSING, '参数异常: 微信 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
164
    }
165
166
    return $body;
167
}
168
169
function get_wechat_type_key(array $params): string
170
{
171
    $key = ($params['_type'] ?? 'mp').'_app_id';
172
173
    if ('app_app_id' === $key) {
174
        $key = 'app_id';
175
    }
176
177
    return $key;
178
}
179
180
/**
181
 * @throws InvalidConfigException
182
 */
183
function get_wechat_sign(array $config, string $contents): string
184
{
185
    $privateKey = $config['mch_secret_cert'] ?? null;
186
187
    if (empty($privateKey)) {
188
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_cert]');
189
    }
190
191
    $privateKey = get_private_cert($privateKey);
192
193
    openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
194
195
    return base64_encode($sign);
196
}
197
198
/**
199
 * @throws ContainerException
200
 * @throws ServiceNotFoundException
201
 * @throws InvalidConfigException
202
 */
203
function get_wechat_sign_v2(array $params, array $payload, bool $upper = true): string
204
{
205
    $key = get_wechat_config($params)['mch_secret_key_v2'] ?? null;
206
207
    if (empty($key)) {
208
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
209
    }
210
211
    ksort($payload);
212
213
    $buff = '';
214
215
    foreach ($payload as $k => $v) {
216
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
217
    }
218
219
    $sign = md5($buff.'key='.$key);
220
221
    return $upper ? strtoupper($sign) : $sign;
222
}
223
224
/**
225
 * @throws ContainerException
226
 * @throws DecryptException
227
 * @throws InvalidConfigException
228
 * @throws InvalidParamsException
229
 * @throws InvalidSignException
230
 * @throws ServiceNotFoundException
231
 */
232
function verify_wechat_sign(ResponseInterface|ServerRequestInterface $message, array $params): void
233
{
234
    if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
235
        return;
236
    }
237
238
    $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
239
    $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
240
    $random = $message->getHeaderLine('Wechatpay-Nonce');
241
    $sign = $message->getHeaderLine('Wechatpay-Signature');
242
    $body = (string) $message->getBody();
243
244
    $content = $timestamp."\n".$random."\n".$body."\n";
245
    $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
246
247
    if (empty($sign)) {
248
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', ['headers' => $message->getHeaders(), 'body' => $body]);
249
    }
250
251
    $public = get_public_cert(
252
        empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
253
    );
254
255
    $result = 1 === openssl_verify(
256
        $content,
257
        base64_decode($sign),
258
        $public,
259
        'sha256WithRSAEncryption'
260
    );
261
262
    if (!$result) {
263
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', ['headers' => $message->getHeaders(), 'body' => $body]);
264
    }
265
}
266
267
function encrypt_wechat_contents(string $contents, string $publicKey): ?string
268
{
269
    if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
270
        return base64_encode($encrypted);
271
    }
272
273
    return null;
274
}
275
276
function decrypt_wechat_contents(string $encrypted, array $config): ?string
277
{
278
    if (openssl_private_decrypt(base64_decode($encrypted), $decrypted, get_private_cert($config['mch_secret_cert'] ?? ''), OPENSSL_PKCS1_OAEP_PADDING)) {
279
        return $decrypted;
280
    }
281
282
    return null;
283
}
284
285
/**
286
 * @throws ContainerException
287
 * @throws DecryptException
288
 * @throws InvalidConfigException
289
 * @throws InvalidParamsException
290
 * @throws ServiceNotFoundException
291
 */
292
function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
293
{
294
    $data = Pay::wechat()->pay(
295
        [StartPlugin::class, WechatPublicCertsPlugin::class, AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, ResponsePlugin::class, ParserPlugin::class],
296
        $params
297
    )->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

297
    )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
298
299
    $wechatConfig = get_wechat_config($params);
300
301
    foreach ($data as $item) {
302
        $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $wechatConfig)['ciphertext'] ?? '';
303
    }
304
305
    Pay::get(ConfigInterface::class)->set(
306
        'wechat.'.get_tenant($params).'.wechat_public_cert_path',
307
        ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
308
    );
309
310
    if (!is_null($serialNo) && empty($certs[$serialNo])) {
311
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 获取微信 wechat_public_cert_path 配置失败');
312
    }
313
314
    return $certs[$serialNo] ?? '';
315
}
316
317
/**
318
 * @throws ContainerException
319
 * @throws DecryptException
320
 * @throws InvalidConfigException
321
 * @throws InvalidParamsException
322
 * @throws ServiceNotFoundException
323
 */
324
function get_wechat_public_certs(array $params = [], ?string $path = null): void
325
{
326
    reload_wechat_public_certs($params);
327
328
    $config = get_wechat_config($params);
329
330
    if (empty($path)) {
331
        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...
332
333
        return;
334
    }
335
336
    foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
337
        file_put_contents($path.'/'.$serialNo.'.crt', $cert);
338
    }
339
}
340
341
/**
342
 * @throws InvalidConfigException
343
 * @throws DecryptException
344
 */
345
function decrypt_wechat_resource(array $resource, array $config): array
346
{
347
    $ciphertext = base64_decode($resource['ciphertext'] ?? '');
348
    $secret = $config['mch_secret_key'] ?? null;
349
350
    if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
351
        throw new DecryptException(Exception::DECRYPT_WECHAT_CIPHERTEXT_PARAMS_INVALID, '加解密异常: ciphertext 位数过短');
352
    }
353
354
    if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
355
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key]');
356
    }
357
358
    $resource['ciphertext'] = match ($resource['algorithm'] ?? '') {
359
        'AEAD_AES_256_GCM' => decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? ''),
360
        default => throw new DecryptException(Exception::DECRYPT_WECHAT_DECRYPTED_METHOD_INVALID, '加解密异常: algorithm 不支持'),
361
    };
362
363
    return $resource;
364
}
365
366
/**
367
 * @throws DecryptException
368
 */
369
function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData): array|string
370
{
371
    $decrypted = openssl_decrypt(
372
        substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
373
        'aes-256-gcm',
374
        $secret,
375
        OPENSSL_RAW_DATA,
376
        $nonce,
377
        substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
378
        $associatedData
379
    );
380
381
    if (false === $decrypted) {
382
        throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 解密失败,请检查微信 mch_secret_key 是否正确');
383
    }
384
385
    if ('certificate' !== $associatedData) {
386
        $decrypted = json_decode($decrypted, true);
387
388
        if (JSON_ERROR_NONE !== json_last_error()) {
389
            throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 待解密数据非正常数据');
390
        }
391
    }
392
393
    return $decrypted;
394
}
395
396
/**
397
 * @throws ContainerException
398
 * @throws DecryptException
399
 * @throws InvalidConfigException
400
 * @throws InvalidParamsException
401
 * @throws ServiceNotFoundException
402
 */
403
function get_wechat_serial_no(array $params): string
404
{
405
    if (!empty($params['_serial_no'])) {
406
        return $params['_serial_no'];
407
    }
408
409
    $config = get_wechat_config($params);
410
411
    if (empty($config['wechat_public_cert_path'])) {
412
        reload_wechat_public_certs($params);
413
414
        $config = get_wechat_config($params);
415
    }
416
417
    mt_srand();
418
419
    return strval(array_rand($config['wechat_public_cert_path']));
420
}
421
422
/**
423
 * @throws InvalidParamsException
424
 */
425
function get_wechat_public_key(array $config, string $serialNo): string
426
{
427
    $publicKey = $config['wechat_public_cert_path'][$serialNo] ?? null;
428
429
    if (empty($publicKey)) {
430
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_SERIAL_NOT_FOUND, '参数异常: 微信公钥序列号为找到 -'.$serialNo);
431
    }
432
433
    return $publicKey;
434
}
435
436
/**
437
 * @throws ContainerException
438
 * @throws ServiceNotFoundException
439
 */
440
function get_unipay_config(array $params): array
441
{
442
    $unipay = Pay::get(ConfigInterface::class)->get('unipay');
443
444
    return $unipay[get_tenant($params)] ?? [];
445
}
446
447
/**
448
 * @throws ContainerException
449
 * @throws InvalidConfigException
450
 * @throws ServiceNotFoundException
451
 * @throws InvalidSignException
452
 */
453
function verify_unipay_sign(array $params, string $contents, string $sign): void
454
{
455
    if (empty($params['signPubKeyCert'])
456
        && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
457
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [unipay_public_cert_path]');
458
    }
459
460
    $result = 1 === openssl_verify(
461
        hash('sha256', $contents),
462
        base64_decode($sign),
463
        get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
464
        'sha256'
465
    );
466
467
    if (!$result) {
468
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', func_get_args());
469
    }
470
}
471