Passed
Push — master ( a1bcd2...a1b58e )
by Songda
02:08
created

encrypt_wechat_contents()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 15
rs 10
cc 3
nc 3
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
use Psr\Http\Message\MessageInterface;
6
use Psr\Http\Message\ServerRequestInterface;
7
use Yansongda\Pay\Contract\ConfigInterface;
8
use Yansongda\Pay\Exception\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
9
use Yansongda\Pay\Exception\InvalidConfigException;
10
use Yansongda\Pay\Exception\InvalidParamsException;
11
use Yansongda\Pay\Exception\InvalidResponseException;
12
use Yansongda\Pay\Parser\NoHttpRequestParser;
13
use Yansongda\Pay\Pay;
14
use Yansongda\Pay\Plugin\ParserPlugin;
15
use Yansongda\Pay\Plugin\Wechat\PreparePlugin;
16
use Yansongda\Pay\Plugin\Wechat\SignPlugin;
17
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
18
use Yansongda\Pay\Provider\Wechat;
19
use Yansongda\Pay\Rocket;
20
use Yansongda\Supports\Config;
21
use Yansongda\Supports\Str;
22
23
if (!function_exists('should_do_http_request')) {
24
    function should_do_http_request(Rocket $rocket): bool
25
    {
26
        $direction = $rocket->getDirection();
27
28
        return is_null($direction) ||
29
            (NoHttpRequestParser::class !== $direction &&
30
            !in_array(NoHttpRequestParser::class, class_parents($direction)));
31
    }
32
}
33
34
if (!function_exists('get_alipay_config')) {
35
    /**
36
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
37
     * @throws \Yansongda\Pay\Exception\ContainerException
38
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
39
     */
40
    function get_alipay_config(array $params = []): Config
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...
41
    {
42
        $alipay = Pay::get(ConfigInterface::class)->get('alipay');
43
44
        $config = $params['_config'] ?? 'default';
45
46
        return new Config($alipay[$config] ?? []);
47
    }
48
}
49
50
if (!function_exists('get_public_or_private_cert')) {
51
    /**
52
     * @param bool $publicKey 是否公钥
53
     *
54
     * @return resource|string
55
     */
56
    function get_public_or_private_cert(string $key, bool $publicKey = false)
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...
57
    {
58
        if ($publicKey) {
59
            return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
60
        }
61
62
        if (Str::endsWith($key, ['.crt', '.pem'])) {
63
            return openssl_pkey_get_private(
0 ignored issues
show
Bug Best Practice introduced by
The expression return openssl_pkey_get_...key : 'file://' . $key) also could return the type OpenSSLAsymmetricKey which is incompatible with the documented return type resource|string.
Loading history...
64
                Str::startsWith($key, 'file://') ? $key : 'file://'.$key
65
            );
66
        }
67
68
        return "-----BEGIN RSA PRIVATE KEY-----\n".
69
            wordwrap($key, 64, "\n", true).
70
            "\n-----END RSA PRIVATE KEY-----";
71
    }
72
}
73
74
if (!function_exists('verify_alipay_sign')) {
75
    /**
76
     * @param string $sign base64decode 之后的
77
     *
78
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
79
     * @throws \Yansongda\Pay\Exception\ContainerException
80
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
81
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
82
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
83
     */
84
    function verify_alipay_sign(array $params, string $contents, string $sign): void
85
    {
86
        $public = get_alipay_config($params)->get('alipay_public_cert_path');
87
88
        if (empty($public)) {
89
            throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
90
        }
91
92
        $result = 1 === openssl_verify(
93
            $contents,
94
            $sign,
95
            get_public_or_private_cert($public, true),
96
            OPENSSL_ALGO_SHA256);
97
98
        if (!$result) {
99
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', func_get_args());
100
        }
101
    }
102
}
103
104
if (!function_exists('get_wechat_config')) {
105
    /**
106
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
107
     * @throws \Yansongda\Pay\Exception\ContainerException
108
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
109
     */
110
    function get_wechat_config(array $params): Config
111
    {
112
        $wechat = Pay::get(ConfigInterface::class)->get('wechat');
113
114
        $config = $params['_config'] ?? 'default';
115
116
        return new Config($wechat[$config] ?? []);
117
    }
118
}
119
120
if (!function_exists('get_wechat_base_uri')) {
121
    /**
122
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
123
     * @throws \Yansongda\Pay\Exception\ContainerException
124
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
125
     */
126
    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...
127
    {
128
        $config = get_wechat_config($params);
129
130
        return Wechat::URL[$config->get('mode', Pay::MODE_NORMAL)];
131
    }
132
}
133
134
if (!function_exists('get_wechat_authorization')) {
135
    /**
136
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
137
     * @throws \Yansongda\Pay\Exception\ContainerException
138
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
139
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
140
     */
141
    function get_wechat_authorization(array $params, int $timestamp, string $random, string $contents): string
142
    {
143
        $config = get_wechat_config($params);
144
        $mchPublicCertPath = $config->get('mch_public_cert_path');
145
146
        if (empty($mchPublicCertPath)) {
147
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_public_cert_path]');
148
        }
149
150
        $ssl = openssl_x509_parse(get_public_or_private_cert($mchPublicCertPath, true));
151
152
        if (empty($ssl['serialNumberHex'])) {
153
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Parse [mch_public_cert_path] Serial Number Error');
154
        }
155
156
        $auth = sprintf(
157
            'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
158
            $config->get('mch_id', ''),
159
            $random,
160
            $timestamp,
161
            $ssl['serialNumberHex'],
162
            get_wechat_sign($params, $contents),
163
        );
164
165
        return 'WECHATPAY2-SHA256-RSA2048 '.$auth;
166
    }
167
}
168
169
if (!function_exists('get_wechat_sign')) {
170
    /**
171
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
172
     * @throws \Yansongda\Pay\Exception\ContainerException
173
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
174
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
175
     */
176
    function get_wechat_sign(array $params, string $contents): string
177
    {
178
        $privateKey = get_wechat_config($params)->get('mch_secret_cert');
179
180
        if (empty($privateKey)) {
181
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
182
        }
183
184
        $privateKey = get_public_or_private_cert($privateKey);
185
186
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
187
188
        $sign = base64_encode($sign);
189
190
        !is_resource($privateKey) ?: openssl_free_key($privateKey);
191
192
        return $sign;
193
    }
194
}
195
196
if (!function_exists('verify_wechat_sign')) {
197
    /**
198
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
199
     *
200
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
201
     * @throws \Yansongda\Pay\Exception\ContainerException
202
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
203
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
204
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
205
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
206
     */
207
    function verify_wechat_sign(MessageInterface $message, array $params): void
208
    {
209
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
210
            return;
211
        }
212
213
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
214
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
215
        $random = $message->getHeaderLine('Wechatpay-Nonce');
216
        $sign = $message->getHeaderLine('Wechatpay-Signature');
217
        $body = $message->getBody()->getContents();
218
219
        $content = $timestamp."\n".$random."\n".$body."\n";
220
        $public = get_wechat_config($params)->get('wechat_public_cert_path.'.$wechatSerial);
221
222
        if (empty($sign)) {
223
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
224
        }
225
226
        $public = get_public_or_private_cert(
227
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public,
228
            true
229
        );
230
231
        $result = 1 === openssl_verify(
232
            $content,
233
            base64_decode($sign),
234
            get_public_or_private_cert($public, true),
235
            'sha256WithRSAEncryption'
236
        );
237
238
        if (!$result) {
239
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
240
        }
241
    }
242
}
243
244
if (!function_exists('encrypt_wechat_contents')) {
245
    /**
246
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
247
     * @throws \Yansongda\Pay\Exception\ContainerException
248
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
249
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
250
     */
251
    function encrypt_wechat_contents(array $params, string $contents, string $serialNo): ?string
252
    {
253
        $config = get_wechat_config($params);
254
255
        $publicKey = $config->get('wechat_public_cert_path.'.$serialNo);
256
257
        if (empty($publicKey)) {
258
            throw new InvalidParamsException(Exception::WECHAT_SERIAL_NO_NOT_FOUND, 'Wechat serial no not found: '.$serialNo);
259
        }
260
261
        if (openssl_public_encrypt($contents, $encrypted, get_public_or_private_cert($publicKey, true), OPENSSL_PKCS1_OAEP_PADDING)) {
262
            return base64_encode($encrypted);
263
        }
264
265
        return null;
266
    }
267
}
268
269
if (!function_exists('reload_wechat_public_certs')) {
270
    /**
271
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
272
     * @throws \Yansongda\Pay\Exception\ContainerException
273
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
274
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
275
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
276
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
277
     */
278
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
279
    {
280
        $data = Pay::wechat()->pay(
281
            [PreparePlugin::class, WechatPublicCertsPlugin::class, SignPlugin::class, ParserPlugin::class],
282
            $params
283
        )->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

283
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
284
285
        foreach ($data as $item) {
286
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
287
        }
288
289
        $wechatConfig = get_wechat_config($params);
290
        $wechatConfig['wechat_public_cert_path'] = ((array) $wechatConfig['wechat_public_cert_path']) + ($certs ?? []);
291
292
        Pay::set(ConfigInterface::class, Pay::get(ConfigInterface::class)->merge([
293
            'wechat' => [$params['_config'] ?? 'default' => $wechatConfig->all()],
294
        ]));
295
296
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
297
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
298
        }
299
300
        return $certs[$serialNo] ?? '';
301
    }
302
}
303
304
if (!function_exists('decrypt_wechat_resource')) {
305
    /**
306
     * @throws \Yansongda\Pay\Exception\ContainerDependencyException
307
     * @throws \Yansongda\Pay\Exception\ContainerException
308
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
309
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
310
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
311
     */
312
    function decrypt_wechat_resource(array $resource, array $params): array
313
    {
314
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
315
        $secret = get_wechat_config($params)->get('mch_secret_key');
316
317
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
318
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
319
        }
320
321
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
322
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
323
        }
324
325
        switch ($resource['algorithm'] ?? '') {
326
            case 'AEAD_AES_256_GCM':
327
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
328
                break;
329
            default:
330
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
331
        }
332
333
        return $resource;
334
    }
335
}
336
337
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
338
    /**
339
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
340
     *
341
     * @return array|string
342
     */
343
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
344
    {
345
        $decrypted = openssl_decrypt(
346
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
347
            'aes-256-gcm',
348
            $secret,
349
            OPENSSL_RAW_DATA,
350
            $nonce,
351
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
352
            $associatedData
353
        );
354
355
        if ('certificate' !== $associatedData) {
356
            $decrypted = json_decode($decrypted, true);
357
358
            if (JSON_ERROR_NONE !== json_last_error()) {
359
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
360
            }
361
        }
362
363
        return $decrypted;
364
    }
365
}
366