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

get_direction()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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