Encryptor.php 5.6 KB

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