<?php /* * This file is part of the overtrue/wechat. * * (c) overtrue <i@overtrue.me> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Encryptor.php. * * @author overtrue <i@overtrue.me> * @copyright 2015 overtrue <i@overtrue.me> * * @see https://github.com/overtrue * @see http://overtrue.me */ namespace EasyWeChat\Encryption; use EasyWeChat\Core\Exceptions\InvalidConfigException; use EasyWeChat\Support\XML; use Exception as BaseException; /** * Class Encryptor. */ class Encryptor { /** * App id. * * @var string */ protected $appId; /** * App token. * * @var string */ protected $token; /** * AES key. * * @var string */ protected $AESKey; /** * Block size. * * @var int */ protected $blockSize; /** * Constructor. * * @param string $appId * @param string $token * @param string $AESKey */ public function __construct($appId, $token, $AESKey) { $this->appId = $appId; $this->token = $token; $this->AESKey = $AESKey; $this->blockSize = 32; } /** * Encrypt the message and return XML. * * @param string $xml * @param string $nonce * @param int $timestamp * * @return string */ public function encryptMsg($xml, $nonce = null, $timestamp = null) { $encrypt = $this->encrypt($xml, $this->appId); !is_null($nonce) || $nonce = substr($this->appId, 0, 10); !is_null($timestamp) || $timestamp = time(); //生成安全签名 $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypt); $response = [ 'Encrypt' => $encrypt, 'MsgSignature' => $signature, 'TimeStamp' => $timestamp, 'Nonce' => $nonce, ]; //生成响应xml return XML::build($response); } /** * Decrypt message. * * @param string $msgSignature * @param string $nonce * @param string $timestamp * @param string $postXML * * @return array * * @throws EncryptionException */ public function decryptMsg($msgSignature, $nonce, $timestamp, $postXML) { try { $array = XML::parse($postXML); } catch (BaseException $e) { throw new EncryptionException('Invalid xml.', EncryptionException::ERROR_PARSE_XML); } $encrypted = $array['Encrypt']; $signature = $this->getSHA1($this->token, $timestamp, $nonce, $encrypted); if ($signature !== $msgSignature) { throw new EncryptionException('Invalid Signature.', EncryptionException::ERROR_INVALID_SIGNATURE); } return XML::parse($this->decrypt($encrypted, $this->appId)); } /** * Get SHA1. * * @return string * * @throws EncryptionException */ public function getSHA1() { try { $array = func_get_args(); sort($array, SORT_STRING); return sha1(implode($array)); } catch (BaseException $e) { throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_CALC_SIGNATURE); } } /** * Encode string. * * @param string $text * * @return string */ public function encode($text) { $padAmount = $this->blockSize - (strlen($text) % $this->blockSize); $padAmount = 0 !== $padAmount ? $padAmount : $this->blockSize; $padChr = chr($padAmount); $tmp = ''; for ($index = 0; $index < $padAmount; ++$index) { $tmp .= $padChr; } return $text.$tmp; } /** * Decode string. * * @param string $decrypted * * @return string */ public function decode($decrypted) { $pad = ord(substr($decrypted, -1)); if ($pad < 1 || $pad > $this->blockSize) { $pad = 0; } return substr($decrypted, 0, (strlen($decrypted) - $pad)); } /** * Return AESKey. * * @return string * * @throws InvalidConfigException */ protected function getAESKey() { if (empty($this->AESKey)) { throw new InvalidConfigException("Configuration mission, 'aes_key' is required."); } if (43 !== strlen($this->AESKey)) { throw new InvalidConfigException("The length of 'aes_key' must be 43."); } return base64_decode($this->AESKey.'=', true); } /** * Encrypt string. * * @param string $text * @param string $appId * * @return string * * @throws EncryptionException */ private function encrypt($text, $appId) { try { $key = $this->getAESKey(); $random = $this->getRandomStr(); $text = $this->encode($random.pack('N', strlen($text)).$text.$appId); $iv = substr($key, 0, 16); $encrypted = openssl_encrypt($text, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv); return base64_encode($encrypted); } catch (BaseException $e) { throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_ENCRYPT_AES); } } /** * Decrypt message. * * @param string $encrypted * @param string $appId * * @return string * * @throws EncryptionException */ private function decrypt($encrypted, $appId) { try { $key = $this->getAESKey(); $ciphertext = base64_decode($encrypted, true); $iv = substr($key, 0, 16); $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv); } catch (BaseException $e) { throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_DECRYPT_AES); } try { $result = $this->decode($decrypted); if (strlen($result) < 16) { return ''; } $content = substr($result, 16, strlen($result)); $listLen = unpack('N', substr($content, 0, 4)); $xmlLen = $listLen[1]; $xml = substr($content, 4, $xmlLen); $fromAppId = trim(substr($content, $xmlLen + 4)); } catch (BaseException $e) { throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_INVALID_XML); } if ($fromAppId !== $appId) { throw new EncryptionException('Invalid appId.', EncryptionException::ERROR_INVALID_APPID); } $dataSet = json_decode($xml, true); if ($dataSet && (JSON_ERROR_NONE === json_last_error())) { // For mini-program JSON formats. // Convert to XML if the given string can be decode into a data array. $xml = XML::build($dataSet); } return $xml; } /** * Generate random string. * * @return string */ private function getRandomStr() { return substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'), 0, 16); } }