Passed
Pull Request — master (#668)
by Songda
03:02 queued 38s
created

get_tenant()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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
     * @param string $sign base64decode 之后的
73
     *
74
     * @throws \Yansongda\Pay\Exception\ContainerException
75
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
76
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
77
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
78
     */
79
    function verify_alipay_sign(array $params, string $contents, string $sign): void
80
    {
81
        $public = get_alipay_config($params)['alipay_public_cert_path'] ?? null;
82
83
        if (empty($public)) {
84
            throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
85
        }
86
87
        $result = 1 === openssl_verify(
88
            $contents,
89
            $sign,
90
            get_public_cert($public),
91
            OPENSSL_ALGO_SHA256);
92
93
        if (!$result) {
94
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Alipay Response Sign Failed', func_get_args());
95
        }
96
    }
97
}
98
99
if (!function_exists('get_wechat_config')) {
100
    /**
101
     * @throws \Yansongda\Pay\Exception\ContainerException
102
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
103
     */
104
    function get_wechat_config(array $params): array
105
    {
106
        $wechat = Pay::get(ConfigInterface::class)->get('wechat');
107
108
        return $wechat[get_tenant($params)] ?? [];
109
    }
110
}
111
112
if (!function_exists('get_wechat_base_uri')) {
113
    /**
114
     * @throws \Yansongda\Pay\Exception\ContainerException
115
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
116
     */
117
    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...
118
    {
119
        $config = get_wechat_config($params);
120
121
        return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL];
122
    }
123
}
124
125
if (!function_exists('get_wechat_sign')) {
126
    /**
127
     * @throws \Yansongda\Pay\Exception\ContainerException
128
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
129
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
130
     */
131
    function get_wechat_sign(array $params, string $contents): string
132
    {
133
        $privateKey = get_wechat_config($params)['mch_secret_cert'] ?? null;
134
135
        if (empty($privateKey)) {
136
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
137
        }
138
139
        $privateKey = get_private_cert($privateKey);
140
141
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
142
143
        return base64_encode($sign);
144
    }
145
}
146
147
if (!function_exists('verify_wechat_sign')) {
148
    /**
149
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
150
     *
151
     * @throws \Yansongda\Pay\Exception\ContainerException
152
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
153
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
154
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
155
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
156
     */
157
    function verify_wechat_sign(MessageInterface $message, array $params): void
158
    {
159
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
160
            return;
161
        }
162
163
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
164
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
165
        $random = $message->getHeaderLine('Wechatpay-Nonce');
166
        $sign = $message->getHeaderLine('Wechatpay-Signature');
167
        $body = (string) $message->getBody();
168
169
        $content = $timestamp."\n".$random."\n".$body."\n";
170
        $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
171
172
        if (empty($sign)) {
173
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
174
        }
175
176
        $public = get_public_cert(
177
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
178
        );
179
180
        $result = 1 === openssl_verify(
181
            $content,
182
            base64_decode($sign),
183
            $public,
184
            'sha256WithRSAEncryption'
185
        );
186
187
        if (!$result) {
188
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
189
        }
190
    }
191
}
192
193
if (!function_exists('encrypt_wechat_contents')) {
194
    function encrypt_wechat_contents(string $contents, string $publicKey): ?string
195
    {
196
        if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
197
            return base64_encode($encrypted);
198
        }
199
200
        return null;
201
    }
202
}
203
204
if (!function_exists('reload_wechat_public_certs')) {
205
    /**
206
     * @throws \Yansongda\Pay\Exception\ContainerException
207
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
208
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
209
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
210
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
211
     */
212
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
213
    {
214
        $data = Pay::wechat()->pay(
215
            [PreparePlugin::class, WechatPublicCertsPlugin::class, SignPlugin::class, ParserPlugin::class],
216
            $params
217
        )->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

217
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
218
219
        foreach ($data as $item) {
220
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
221
        }
222
223
        $wechatConfig = get_wechat_config($params);
224
        $wechatConfig['wechat_public_cert_path'] = ((array) $wechatConfig['wechat_public_cert_path']) + ($certs ?? []);
225
226
        Pay::set(ConfigInterface::class, Pay::get(ConfigInterface::class)->merge([
227
            'wechat' => [$params['_config'] ?? 'default' => $wechatConfig],
228
        ]));
229
230
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
231
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
232
        }
233
234
        return $certs[$serialNo] ?? '';
235
    }
236
}
237
238
if (!function_exists('decrypt_wechat_resource')) {
239
    /**
240
     * @throws \Yansongda\Pay\Exception\ContainerException
241
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
242
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
243
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
244
     */
245
    function decrypt_wechat_resource(array $resource, array $params): array
246
    {
247
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
248
        $secret = get_wechat_config($params)['mch_secret_key'] ?? null;
249
250
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
251
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
252
        }
253
254
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
255
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
256
        }
257
258
        switch ($resource['algorithm'] ?? '') {
259
            case 'AEAD_AES_256_GCM':
260
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
261
                break;
262
            default:
263
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
264
        }
265
266
        return $resource;
267
    }
268
}
269
270
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
271
    /**
272
     * @return array|string
273
     *
274
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
275
     */
276
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
277
    {
278
        $decrypted = openssl_decrypt(
279
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
280
            'aes-256-gcm',
281
            $secret,
282
            OPENSSL_RAW_DATA,
283
            $nonce,
284
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
285
            $associatedData
286
        );
287
288
        if ('certificate' !== $associatedData) {
289
            $decrypted = json_decode($decrypted, true);
290
291
            if (JSON_ERROR_NONE !== json_last_error()) {
292
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
293
            }
294
        }
295
296
        return $decrypted;
297
    }
298
}
299