Completed
Push — master ( 3058e4...73535e )
by Carlos
10:49 queued 07:54
created

Encryptor::encrypt()   A

Complexity

Conditions 2
Paths 7

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
rs 9.4285
cc 2
eloc 10
nc 7
nop 2
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
 * @link      https://github.com/overtrue
19
 * @link      http://overtrue.me
20
 */
21
namespace EasyWeChat\Encryption;
22
23
use EasyWeChat\Core\Exceptions\InvalidConfigException;
24
use EasyWeChat\Core\Exceptions\RuntimeException;
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
     * @throws RuntimeException
69
     */
70
    public function __construct($appId, $token, $AESKey)
71
    {
72
        if (!extension_loaded('openssl')) {
73
            throw new RuntimeException("The ext 'openssl' is required.");
74
        }
75
76
        $this->appId = $appId;
77
        $this->token = $token;
78
        $this->AESKey = $AESKey;
79
        $this->blockSize = 32;
80
    }
81
82
    /**
83
     * Encrypt the message and return XML.
84
     *
85
     * @param string $xml
86
     * @param string $nonce
87
     * @param int    $timestamp
88
     *
89
     * @return string
90
     */
91
    public function encryptMsg($xml, $nonce = null, $timestamp = null)
92
    {
93
        $encrypt = $this->encrypt($xml, $this->appId);
94
95
        !is_null($nonce) || $nonce = substr($this->appId, 0, 10);
96
        !is_null($timestamp) || $timestamp = time();
97
98
        //生成安全签名
99
        $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypt);
100
101
        $response = [
102
            'Encrypt' => $encrypt,
103
            'MsgSignature' => $signature,
104
            'TimeStamp' => $timestamp,
105
            'Nonce' => $nonce,
106
        ];
107
108
        //生成响应xml
109
        return XML::build($response);
110
    }
111
112
    /**
113
     * Decrypt message.
114
     *
115
     * @param string $msgSignature
116
     * @param string $nonce
117
     * @param string $timestamp
118
     * @param string $postXML
119
     *
120
     * @return array
121
     *
122
     * @throws EncryptionException
123
     */
124
    public function decryptMsg($msgSignature, $nonce, $timestamp, $postXML)
125
    {
126
        try {
127
            $array = XML::parse($postXML);
128
        } catch (BaseException $e) {
129
            throw new EncryptionException('Invalid xml.', EncryptionException::ERROR_PARSE_XML);
130
        }
131
132
        $encrypted = $array['Encrypt'];
133
134
        $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypted);
135
136
        if ($signature !== $msgSignature) {
137
            throw new EncryptionException('Invalid Signature.', EncryptionException::ERROR_INVALID_SIGNATURE);
138
        }
139
140
        return XML::parse($this->decrypt($encrypted, $this->appId));
141
    }
142
143
    /**
144
     * Get SHA1.
145
     *
146
     * @return string
147
     *
148
     * @throws EncryptionException
149
     */
150
    public function getSHA1()
151
    {
152
        try {
153
            $array = func_get_args();
154
            sort($array, SORT_STRING);
155
156
            return sha1(implode($array));
157
        } catch (BaseException $e) {
158
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_CALC_SIGNATURE);
159
        }
160
    }
161
162
    /**
163
     * Encode string.
164
     *
165
     * @param string $text
166
     *
167
     * @return string
168
     */
169
    public function encode($text)
170
    {
171
        $padAmount = $this->blockSize - (strlen($text) % $this->blockSize);
172
173
        $padAmount = $padAmount !== 0 ? $padAmount : $this->blockSize;
174
175
        $padChr = chr($padAmount);
176
177
        $tmp = '';
178
179
        for ($index = 0; $index < $padAmount; ++$index) {
180
            $tmp .= $padChr;
181
        }
182
183
        return $text.$tmp;
184
    }
185
186
    /**
187
     * Decode string.
188
     *
189
     * @param string $decrypted
190
     *
191
     * @return string
192
     */
193
    public function decode($decrypted)
194
    {
195
        $pad = ord(substr($decrypted, -1));
196
197
        if ($pad < 1 || $pad > $this->blockSize) {
198
            $pad = 0;
199
        }
200
201
        return substr($decrypted, 0, (strlen($decrypted) - $pad));
202
    }
203
204
    /**
205
     * Return AESKey.
206
     *
207
     * @return string
208
     *
209
     * @throws InvalidConfigException
210
     */
211
    protected function getAESKey()
212
    {
213
        if (empty($this->AESKey) || strlen($this->AESKey) !== 43) {
214
            throw new InvalidConfigException("Configuration mission, 'aes_key' is required.");
215
        }
216
217
        return base64_decode($this->AESKey.'=', true);
218
    }
219
220
    /**
221
     * Encrypt string.
222
     *
223
     * @param string $text
224
     * @param string $appId
225
     *
226
     * @return string
227
     *
228
     * @throws EncryptionException
229
     */
230
    private function encrypt($text, $appId)
231
    {
232
        try {
233
            $key = $this->getAESKey();
234
            $random = $this->getRandomStr();
235
            $text = $this->encode($random.pack('N', strlen($text)).$text.$appId);
236
237
            $iv = substr($key, 0, 16);
238
239
            $encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
240
241
            return base64_encode($encrypted);
242
        } catch (BaseException $e) {
243
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_ENCRYPT_AES);
244
        }
245
    }
246
247
    /**
248
     * Decrypt message.
249
     *
250
     * @param string $encrypted
251
     * @param string $appId
252
     *
253
     * @return string
254
     *
255
     * @throws EncryptionException
256
     */
257
    private function decrypt($encrypted, $appId)
258
    {
259
        try {
260
            $key = $this->getAESKey();
261
            $ciphertext = base64_decode($encrypted, true);
262
            $iv = substr($key, 0, 16);
263
264
            $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
265
        } catch (BaseException $e) {
266
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_DECRYPT_AES);
267
        }
268
269
        try {
270
            $result = $this->decode($decrypted);
271
272
            if (strlen($result) < 16) {
273
                return '';
274
            }
275
276
            $content = substr($result, 16, strlen($result));
277
            $listLen = unpack('N', substr($content, 0, 4));
278
            $xmlLen = $listLen[1];
279
            $xml = substr($content, 4, $xmlLen);
280
            $fromAppId = trim(substr($content, $xmlLen + 4));
281
        } catch (BaseException $e) {
282
            throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_INVALID_XML);
283
        }
284
285
        if ($fromAppId !== $appId) {
286
            throw new EncryptionException('Invalid appId.', EncryptionException::ERROR_INVALID_APPID);
287
        }
288
289
        return $xml;
290
    }
291
292
    /**
293
     * Generate random string.
294
     *
295
     * @return string
296
     */
297
    private function getRandomStr()
298
    {
299
        return substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'), 0, 16);
300
    }
301
}
302