Passed
Branch master (afcd62)
by Songda
02:00
created

get_private_cert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

248
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
249
250
        foreach ($data as $item) {
251
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
252
        }
253
254
        $wechatConfig = get_wechat_config($params);
255
        $wechatConfig['wechat_public_cert_path'] = ((array) $wechatConfig['wechat_public_cert_path']) + ($certs ?? []);
256
257
        Pay::set(ConfigInterface::class, Pay::get(ConfigInterface::class)->merge([
258
            'wechat' => [$params['_config'] ?? 'default' => $wechatConfig->all()],
259
        ]));
260
261
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
262
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
263
        }
264
265
        return $certs[$serialNo] ?? '';
266
    }
267
}
268
269
if (!function_exists('decrypt_wechat_resource')) {
270
    /**
271
     * @throws \Yansongda\Pay\Exception\ContainerException
272
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
273
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
274
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
275
     */
276
    function decrypt_wechat_resource(array $resource, array $params): array
277
    {
278
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
279
        $secret = get_wechat_config($params)->get('mch_secret_key');
280
281
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
282
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
283
        }
284
285
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
286
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
287
        }
288
289
        switch ($resource['algorithm'] ?? '') {
290
            case 'AEAD_AES_256_GCM':
291
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
292
                break;
293
            default:
294
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
295
        }
296
297
        return $resource;
298
    }
299
}
300
301
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
302
    /**
303
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
304
     *
305
     * @return array|string
306
     */
307
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
308
    {
309
        $decrypted = openssl_decrypt(
310
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
311
            'aes-256-gcm',
312
            $secret,
313
            OPENSSL_RAW_DATA,
314
            $nonce,
315
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
316
            $associatedData
317
        );
318
319
        if ('certificate' !== $associatedData) {
320
            $decrypted = json_decode($decrypted, true);
321
322
            if (JSON_ERROR_NONE !== json_last_error()) {
323
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
324
            }
325
        }
326
327
        return $decrypted;
328
    }
329
}
330