Passed
Pull Request — master (#803)
by Songda
01:52
created

get_direction()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

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