| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 | <?phpnamespace App\Service;class DingTalkCrypto{    protected string $token;    protected string $encodingAesKey; // URL safe base64 字符串,长度 43    public function __construct(string $token, string $encodingAesKey)    {        $this->token = $token;        $this->encodingAesKey = $encodingAesKey;    }    /**     * 解密钉钉事件回调消息     *     * @param string $msgSignature URL参数 msg_signature     * @param string $timestamp URL参数 timestamp     * @param string $nonce URL参数 nonce     * @param string $encrypt 消息体 encrypt     * @return array|false     */    public function decryptMsg(string $msgSignature, string $timestamp, string $nonce, string $encrypt)    {        // 1. 验签 (官方要求 token + timestamp + nonce + encrypt 顺序拼接)        $hash = sha1($this->token . $timestamp . $nonce . $encrypt);        if ($hash !== $msgSignature) {            return false;        }        // 2. AES 解密        $aesKey = base64_decode($this->encodingAesKey);        $iv = substr($aesKey, 0, 16);        $cipherText = base64_decode($encrypt);        $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);        if ($decrypted === false) return false;        // 3. 去掉 PKCS#7 padding        $decrypted = $this->pkcs7Unpad($decrypted);        // 4. 去掉前16位随机字符串        $content = substr($decrypted, 16);        // 5. 读取消息长度(4字节 network order)        $lenList = unpack("N", substr($content, 0, 4));        $jsonLen = $lenList[1];        // 6. 获取 JSON        $json = substr($content, 4, $jsonLen);        return json_decode($json, true);    }    /**     * 加密返回给钉钉的消息     *     * @param string $replyMsg 明文消息     * @param string $timestamp     * @param string $nonce     * @return array     */    public function encryptMsg(string $replyMsg, string $timestamp, string $nonce): array    {        $random16 = $this->getRandomStr(16);        $msgLenBin = pack("N", strlen($replyMsg));        $corpId = ''; // 不需要 corpId        $plain = $random16 . $msgLenBin . $replyMsg . $corpId;        // PKCS#7 padding        $blockSize = 32;        $pad = $blockSize - (strlen($plain) % $blockSize);        $pad = $pad === 0 ? $blockSize : $pad;        $plainPadded = $plain . str_repeat(chr($pad), $pad);        // AES 加密        $aesKey = base64_decode($this->encodingAesKey);        $iv = substr($aesKey, 0, 16);        $encrypted = openssl_encrypt($plainPadded, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);        $encryptBase64 = base64_encode($encrypted);        // 计算 msg_signature        $msgSignature = sha1($this->token . $timestamp . $nonce . $encryptBase64);        return [            'msg_signature' => $msgSignature,            'encrypt'       => $encryptBase64        ];    }    private function getRandomStr(int $length = 16): string    {        $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";        $str = '';        for ($i = 0; $i < $length; $i++) {            $str .= $chars[random_int(0, strlen($chars) - 1)];        }        return $str;    }    private function pkcs7Unpad(string $text)    {        $pad = ord(substr($text, -1));        if ($pad < 1 || $pad > 32) return false;        return substr($text, 0, -$pad);    }}
 |