| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 | <?phpnamespace App\Service;class DingTalkCrypto{    protected $token;    protected $encodingAesKey;  // URL safe base64 字符串,长度 43    // protected $corpid; // 如果需要    public function __construct(string $token, string $encodingAesKey)    {        $this->token = $token;        $this->encodingAesKey = $encodingAesKey;    }    /**     * 验签 + 解密消息体     *     * @param string $signature 从请求 query 参数 signature     * @param string $timestamp 从请求参数 timestamp     * @param string $nonce 从请求参数 nonce     * @param string $encrypt 消息体中的 encrypt 字段     * @return array|false 解密后的数组,失败返回 false     */    public function decryptMsg(string $signature, string $timestamp, string $nonce, string $encrypt)    {        // 1. 验签        $tmpArr = [$this->token, $timestamp, $nonce, $encrypt];        sort($tmpArr, SORT_STRING);        $tmpStr = implode($tmpArr);        $hash = sha1($tmpStr);        if ($hash !== $signature) {            return false;        }        // 2. 解密        $aesKey = base64_decode($this->encodingAesKey . '=');  // 注意要加一个 '=' 补足 padding        $iv = substr($aesKey, 0, 16);        // 解密,使用 AES-256-CBC        $cipherText = base64_decode($encrypt);        $decrypted = openssl_decrypt($cipherText, 'AES-256-CBC', $aesKey, OPENSSL_ZERO_PADDING, $iv);        if ($decrypted === false) {            return false;        }        // 去掉 padding, PKCS#7        $decrypted = $this->pkcs7Unpad($decrypted);        // 解出: 16 位随机字符串 + 4 字节内容长度 network-order + 内容 + corpId(如果有的话)        // 跳过前 16 位        $content = substr($decrypted, 16);        // 读长度        $lenList = unpack("N", substr($content, 0, 4));        $xmlLen = $lenList[1];        $xmlContent = substr($content, 4, $xmlLen);        // 如果有 corpId 在 xmlContent 后还有        // $fromCorpId = substr($content, 4 + $xmlLen);        $json = $xmlContent;  // 钉钉回调 body 是 JSON 字符串        // 解析        $arr = json_decode($json, true);        return $arr;    }    /**     * 加密回复消息,比如返回 “success”     *     * @param string $replyMsg 明文消息,比如 "success"     * @param string $timestamp     * @param string $nonce     * @return array 包含加密后json结构     */    public function encryptMsg(string $replyMsg, string $timestamp, string $nonce)    {        // 1. 构造消息内容        $random16 = $this->getRandomStr(16);        $replyMsgXml = $replyMsg;  // 钉钉回调用 JSON 格式内容        $msgLen = strlen($replyMsgXml);        $msgLenBin = pack("N", $msgLen);        $corpId = '';  // 如果不需要 corpId,可以空        $plain = $random16 . $msgLenBin . $replyMsgXml . $corpId;        // 2. PKCS#7 padding        $blockSize = 32;        $pad = $blockSize - (strlen($plain) % $blockSize);        if ($pad == 0) {            $pad = $blockSize;        }        $plainPadded = $plain . str_repeat(chr($pad), $pad);        // 3. 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);        // 4. 签名        $signatureArr = [$this->token, $timestamp, $nonce, $encryptBase64];        sort($signatureArr, SORT_STRING);        $signature = sha1(implode($signatureArr));        // 5. 返回结构        return [            'msg_signature' => $signature,            'encrypt'       => $encryptBase64        ];    }    private function getRandomStr($length = 16)    {        $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";        $str = '';        for ($i = 0; $i < $length; $i++) {            $str .= $chars[ord(bin2hex(random_bytes(1))) % strlen($chars)];        }        return $str;    }    private function pkcs7Unpad($text)    {        // 去掉结尾的 padding        $pad = ord(substr($text, -1));        if ($pad < 1 || $pad > 32) {            // padding 有问题            return false;        }        return substr($text, 0, -$pad);    }}
 |