Encryptor::encode()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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