Encryptor.php 7.2 KB

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