Passed
Pull Request — master (#662)
by Songda
02:21
created

verify_unipay_sign()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

215
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
216
217
        foreach ($data as $item) {
218
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
219
        }
220
221
        $wechatConfig = get_wechat_config($params);
222
223
        Pay::get(ConfigInterface::class)->set(
224
            'wechat.'.get_tenant($params).'.wechat_public_cert_path',
225
            ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
226
        );
227
228
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
229
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
230
        }
231
232
        return $certs[$serialNo] ?? '';
233
    }
234
}
235
236
if (!function_exists('decrypt_wechat_resource')) {
237
    /**
238
     * @throws \Yansongda\Pay\Exception\ContainerException
239
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
240
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
241
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
242
     */
243
    function decrypt_wechat_resource(array $resource, array $params): array
244
    {
245
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
246
        $secret = get_wechat_config($params)['mch_secret_key'] ?? null;
247
248
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
249
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
250
        }
251
252
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
253
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
254
        }
255
256
        switch ($resource['algorithm'] ?? '') {
257
            case 'AEAD_AES_256_GCM':
258
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
259
                break;
260
            default:
261
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
262
        }
263
264
        return $resource;
265
    }
266
}
267
268
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
269
    /**
270
     * @return array|string
271
     *
272
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
273
     */
274
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
275
    {
276
        $decrypted = openssl_decrypt(
277
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
278
            'aes-256-gcm',
279
            $secret,
280
            OPENSSL_RAW_DATA,
281
            $nonce,
282
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
283
            $associatedData
284
        );
285
286
        if ('certificate' !== $associatedData) {
287
            $decrypted = json_decode($decrypted, true);
288
289
            if (JSON_ERROR_NONE !== json_last_error()) {
290
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
291
            }
292
        }
293
294
        return $decrypted;
295
    }
296
}
297
298
if (!function_exists('get_unipay_config')) {
299
    /**
300
     * @throws \Yansongda\Pay\Exception\ContainerException
301
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
302
     */
303
    function get_unipay_config(array $params): array
304
    {
305
        $unipay = Pay::get(ConfigInterface::class)->get('unipay');
306
307
        return $unipay[get_tenant($params)] ?? [];
308
    }
309
}
310
311
if (!function_exists('verify_unipay_sign')) {
312
    /**
313
     * @throws \Yansongda\Pay\Exception\ContainerException
314
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
315
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
316
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
317
     */
318
    function verify_unipay_sign(array $params, string $contents, string $sign): void
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...
319
    {
320
        if (empty($params['signPubKeyCert'])
321
            && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
322
            throw new InvalidConfigException(Exception::UNIPAY_CONFIG_ERROR, 'Missing Unipay Config -- [unipay_public_cert_path]');
323
        }
324
325
        $result = 1 === openssl_verify(
326
            hash('sha256', $contents),
327
            base64_decode($sign),
328
            get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
329
            'sha256');
330
331
        if (!$result) {
332
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Unipay Response Sign Failed', func_get_args());
333
        }
334
    }
335
}
336