Passed
Push — master ( 6c65d2...8466fe )
by Songda
03:29 queued 01:39
created

get_public_or_private_cert()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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