Passed
Push — master ( 78b346...e9f486 )
by Carlos
02:47
created

Encryptor::pkcs7Unpad()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
ccs 4
cts 5
cp 0.8
crap 3.072
rs 10
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
namespace EasyWeChat\Kernel;
13
14
use EasyWeChat\Kernel\Exceptions\RuntimeException;
15
use EasyWeChat\Kernel\Support\AES;
16
use EasyWeChat\Kernel\Support\XML;
17
use Throwable;
18
use function EasyWeChat\Kernel\Support\str_random;
19
20
/**
21
 * Class Encryptor.
22
 *
23
 * @author overtrue <[email protected]>
24
 */
25
class Encryptor
26
{
27
    public const ERROR_INVALID_SIGNATURE = -40001; // Signature verification failed
28
    public const ERROR_PARSE_XML = -40002; // Parse XML failed
29
    public const ERROR_CALC_SIGNATURE = -40003; // Calculating the signature failed
30
    public const ERROR_INVALID_AES_KEY = -40004; // Invalid AESKey
31
    public const ERROR_INVALID_APP_ID = -40005; // Check AppID failed
32
    public const ERROR_ENCRYPT_AES = -40006; // AES EncryptionInterface failed
33
    public const ERROR_DECRYPT_AES = -40007; // AES decryption failed
34
    public const ERROR_INVALID_XML = -40008; // Invalid XML
35
    public const ERROR_BASE64_ENCODE = -40009; // Base64 encoding failed
36
    public const ERROR_BASE64_DECODE = -40010; // Base64 decoding failed
37
    public const ERROR_XML_BUILD = -40011; // XML build failed
38
    public const ILLEGAL_BUFFER = -41003; // Illegal buffer
39
40
    /**
41
     * App id.
42
     *
43
     * @var string
44
     */
45
    protected $appId;
46
47
    /**
48
     * App token.
49
     *
50
     * @var string
51
     */
52
    protected $token;
53
54
    /**
55
     * @var string
56
     */
57
    protected $aesKey;
58
59
    /**
60
     * Block size.
61
     *
62
     * @var int
63
     */
64
    protected $blockSize = 32;
65
66
    /**
67
     * Constructor.
68
     *
69
     * @param string      $appId
70
     * @param string|null $token
71
     * @param string|null $aesKey
72
     */
73 12
    public function __construct(string $appId, string $token = null, string $aesKey = null)
74
    {
75 12
        $this->appId = $appId;
76 12
        $this->token = $token;
77 12
        $this->aesKey = base64_decode($aesKey.'=', true);
78 12
    }
79
80
    /**
81
     * Get the app token.
82
     *
83
     * @return string
84
     */
85 1
    public function getToken(): string
86
    {
87 1
        return $this->token;
88
    }
89
90
    /**
91
     * Encrypt the message and return XML.
92
     *
93
     * @param string $xml
94
     * @param string $nonce
95
     * @param int    $timestamp
96
     *
97
     * @return string
98
     *
99
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
100
     */
101 1
    public function encrypt($xml, $nonce = null, $timestamp = null): string
102
    {
103
        try {
104 1
            $xml = $this->pkcs7Pad(str_random(16).pack('N', strlen($xml)).$xml.$this->appId, $this->blockSize);
105
106 1
            $encrypted = base64_encode(AES::encrypt(
107 1
                $xml,
108 1
                $this->aesKey,
109 1
                substr($this->aesKey, 0, 16),
110 1
                OPENSSL_NO_PADDING
111
            ));
112
            // @codeCoverageIgnoreStart
113
        } catch (Throwable $e) {
114
            throw new RuntimeException($e->getMessage(), self::ERROR_ENCRYPT_AES);
115
        }
116
        // @codeCoverageIgnoreEnd
117
118 1
        !is_null($nonce) || $nonce = substr($this->appId, 0, 10);
119 1
        !is_null($timestamp) || $timestamp = time();
120
121
        $response = [
122 1
            'Encrypt' => $encrypted,
123 1
            'MsgSignature' => $this->signature($this->token, $timestamp, $nonce, $encrypted),
124 1
            'TimeStamp' => $timestamp,
125 1
            'Nonce' => $nonce,
126
        ];
127
128
        //生成响应xml
129 1
        return XML::build($response);
130
    }
131
132
    /**
133
     * Decrypt message.
134
     *
135
     * @param string $content
136
     * @param string $msgSignature
137
     * @param string $nonce
138
     * @param string $timestamp
139
     *
140
     * @return string
141
     *
142
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
143
     */
144 5
    public function decrypt($content, $msgSignature, $nonce, $timestamp): string
145
    {
146 5
        $signature = $this->signature($this->token, $timestamp, $nonce, $content);
147
148 5
        if ($signature !== $msgSignature) {
149 1
            throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE);
150
        }
151
152 4
        $decrypted = AES::decrypt(
153 4
            base64_decode($content, true),
154 4
            $this->aesKey,
155 4
            substr($this->aesKey, 0, 16),
156 4
            OPENSSL_NO_PADDING
157
        );
158 4
        $result = $this->pkcs7Unpad($decrypted);
159 4
        $content = substr($result, 16, strlen($result));
160 4
        $contentLen = unpack('N', substr($content, 0, 4))[1];
161
162 4
        if (trim(substr($content, $contentLen + 4)) !== $this->appId) {
163 1
            throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID);
164
        }
165
166 3
        return substr($content, 4, $contentLen);
167
    }
168
169
    /**
170
     * Get SHA1.
171
     *
172
     * @return string
173
     */
174 5
    public function signature(): string
175
    {
176 5
        $array = func_get_args();
177 5
        sort($array, SORT_STRING);
178
179 5
        return sha1(implode($array));
180
    }
181
182
    /**
183
     * PKCS#7 pad.
184
     *
185
     * @param string $text
186
     * @param int    $blockSize
187
     *
188
     * @return string
189
     *
190
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
191
     */
192 2
    public function pkcs7Pad(string $text, int $blockSize): string
193
    {
194 2
        if ($blockSize > 256) {
195 1
            throw new RuntimeException('$blockSize may not be more than 256');
196
        }
197 1
        $padding = $blockSize - (strlen($text) % $blockSize);
198 1
        $pattern = chr($padding);
199
200 1
        return $text.str_repeat($pattern, $padding);
201
    }
202
203
    /**
204
     * PKCS#7 unpad.
205
     *
206
     * @param string $text
207
     *
208
     * @return string
209
     */
210 4
    public function pkcs7Unpad(string $text): string
211
    {
212 4
        $pad = ord(substr($text, -1));
213 4
        if ($pad < 1 || $pad > $this->blockSize) {
214
            $pad = 0;
215
        }
216
217 4
        return substr($text, 0, (strlen($text) - $pad));
218
    }
219
}
220