Encryptor.php 5.3 KB

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