Passed
Push — master ( 0bdc6d...063142 )
by mingyoung
02:44
created

Encryptor::decrypt()   C

Complexity

Conditions 7
Paths 15

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 15
nop 2
dl 0
loc 41
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the overtrue/wechat.
5
 *
6
 * (c) overtrue <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
/**
13
 * Encryptor.php.
14
 *
15
 * @author    overtrue <[email protected]>
16
 * @copyright 2015 overtrue <[email protected]>
17
 *
18
 * @see      https://github.com/overtrue
19
 * @see      http://overtrue.me
20
 */
21
22
namespace EasyWeChat\Encryption;
23
24
use EasyWeChat\Core\Exceptions\InvalidConfigException;
25
use EasyWeChat\Support\XML;
26
use Exception as BaseException;
27
28
/**
29
 * Class Encryptor.
30
 */
31
class Encryptor
32
{
33
    /**
34
     * App id.
35
     *
36
     * @var string
37
     */
38
    protected $appId;
39
40
    /**
41
     * App token.
42
     *
43
     * @var string
44
     */
45
    protected $token;
46
47
    /**
48
     * AES key.
49
     *
50
     * @var string
51
     */
52
    protected $AESKey;
53
54
    /**
55
     * Block size.
56
     *
57
     * @var int
58
     */
59
    protected $blockSize;
60
61
    /**
62
     * Constructor.
63
     *
64
     * @param string $appId
65
     * @param string $token
66
     * @param string $AESKey
67
     */
68
    public function __construct($appId, $token, $AESKey)
69
    {
70
        $this->appId = $appId;
71
        $this->token = $token;
72
        $this->AESKey = $AESKey;
73
        $this->blockSize = 32;
74
    }
75
76
    /**
77
     * Encrypt the message and return XML.
78
     *
79
     * @param string $xml
80
     * @param string $nonce
81
     * @param int    $timestamp
82
     *
83
     * @return string
84
     */
85
    public function encryptMsg($xml, $nonce = null, $timestamp = null)
86
    {
87
        $encrypt = $this->encrypt($xml, $this->appId);
88
89
        !is_null($nonce) || $nonce = substr($this->appId, 0, 10);
90
        !is_null($timestamp) || $timestamp = time();
91
92
        //生成安全签名
93
        $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypt);
94
95
        $response = [
96
            'Encrypt' => $encrypt,
97
            'MsgSignature' => $signature,
98
            'TimeStamp' => $timestamp,
99
            'Nonce' => $nonce,
100
        ];
101
102
        //生成响应xml
103
        return XML::build($response);
104
    }
105
106
    /**
107
     * Decrypt message.
108
     *
109
     * @param string $msgSignature
110
     * @param string $nonce
111
     * @param string $timestamp
112
     * @param string $postXML
113
     *
114
     * @return array
115
     *
116
     * @throws EncryptionException
117
     */
118
    public function decryptMsg($msgSignature, $nonce, $timestamp, $postXML)
119
    {
120
        try {
121
            $array = XML::parse($postXML);
122
        } catch (BaseException $e) {
123
            throw new EncryptionException('Invalid xml.', EncryptionException::ERROR_PARSE_XML);
124
        }
125
126
        $encrypted = $array['Encrypt'];
127
128
        $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypted);
129
130
        if ($signature !== $msgSignature) {
131
            throw new EncryptionException('Invalid Signature.', EncryptionException::ERROR_INVALID_SIGNATURE);
132
        }
133
134
        return XML::parse($this->decrypt($encrypted, $this->appId));
135
    }
136
137
    /**
138
     * Get SHA1.
139
     *
140
     * @return string
141
     *
142
     * @throws EncryptionException
143
     */
144
    public function getSHA1()
145
    {
146
        try {
147
            $array = func_get_args();
148
            sort($array, SORT_STRING);
149
150
            return sha1(implode($array));
151
        } catch (BaseException $e) {
152
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_CALC_SIGNATURE);
153
        }
154
    }
155
156
    /**
157
     * Encode string.
158
     *
159
     * @param string $text
160
     *
161
     * @return string
162
     */
163
    public function encode($text)
164
    {
165
        $padAmount = $this->blockSize - (strlen($text) % $this->blockSize);
166
167
        $padAmount = $padAmount !== 0 ? $padAmount : $this->blockSize;
168
169
        $padChr = chr($padAmount);
170
171
        $tmp = '';
172
173
        for ($index = 0; $index < $padAmount; ++$index) {
174
            $tmp .= $padChr;
175
        }
176
177
        return $text.$tmp;
178
    }
179
180
    /**
181
     * Decode string.
182
     *
183
     * @param string $decrypted
184
     *
185
     * @return string
186
     */
187
    public function decode($decrypted)
188
    {
189
        $pad = ord(substr($decrypted, -1));
190
191
        if ($pad < 1 || $pad > $this->blockSize) {
192
            $pad = 0;
193
        }
194
195
        return substr($decrypted, 0, (strlen($decrypted) - $pad));
196
    }
197
198
    /**
199
     * Return AESKey.
200
     *
201
     * @return string
202
     *
203
     * @throws InvalidConfigException
204
     */
205
    protected function getAESKey()
206
    {
207
        if (empty($this->AESKey)) {
208
            throw new InvalidConfigException("Configuration mission, 'aes_key' is required.");
209
        }
210
211
        if (strlen($this->AESKey) !== 43) {
212
            throw new InvalidConfigException("The length of 'aes_key' must be 43.");
213
        }
214
215
        return base64_decode($this->AESKey.'=', true);
216
    }
217
218
    /**
219
     * Encrypt string.
220
     *
221
     * @param string $text
222
     * @param string $appId
223
     *
224
     * @return string
225
     *
226
     * @throws EncryptionException
227
     */
228
    private function encrypt($text, $appId)
229
    {
230
        try {
231
            $key = $this->getAESKey();
232
            $random = $this->getRandomStr();
233
            $text = $this->encode($random.pack('N', strlen($text)).$text.$appId);
234
235
            $iv = substr($key, 0, 16);
236
237
            $encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
238
239
            return base64_encode($encrypted);
240
        } catch (BaseException $e) {
241
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_ENCRYPT_AES);
242
        }
243
    }
244
245
    /**
246
     * Decrypt message.
247
     *
248
     * @param string $encrypted
249
     * @param string $appId
250
     *
251
     * @return string
252
     *
253
     * @throws EncryptionException
254
     */
255
    private function decrypt($encrypted, $appId)
256
    {
257
        try {
258
            $key = $this->getAESKey();
259
            $ciphertext = base64_decode($encrypted, true);
260
            $iv = substr($key, 0, 16);
261
262
            $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
263
        } catch (BaseException $e) {
264
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_DECRYPT_AES);
265
        }
266
267
        try {
268
            $result = $this->decode($decrypted);
269
270
            if (strlen($result) < 16) {
271
                return '';
272
            }
273
274
            $content = substr($result, 16, strlen($result));
275
            $listLen = unpack('N', substr($content, 0, 4));
276
            $xmlLen = $listLen[1];
277
            $xml = substr($content, 4, $xmlLen);
278
            $fromAppId = trim(substr($content, $xmlLen + 4));
279
        } catch (BaseException $e) {
280
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_INVALID_XML);
281
        }
282
283
        if ($fromAppId !== $appId) {
284
            throw new EncryptionException('Invalid appId.', EncryptionException::ERROR_INVALID_APPID);
285
        }
286
287
        $dataSet = json_decode($xml, true);
288
        if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
289
            // For mini-program JSON formats.
290
            // Convert to XML if the given string can be decode into a data array.
291
            $xml = XML::build($dataSet);
292
        }
293
294
        return $xml;
295
    }
296
297
    /**
298
     * Generate random string.
299
     *
300
     * @return string
301
     */
302
    private function getRandomStr()
303
    {
304
        return substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'), 0, 16);
305
    }
306
}
307