DingTalkEncryptor.js 4.8 KB
/* eslint-disable no-bitwise */
'use strict';

const CryptoJS  = require("crypto-js");
const Crypto = require('crypto');
const DingTalkEncryptException = require('./DingTalkEncryptException');

class DingTalkEncryptor {
  constructor(token, encodingAesKey, corpIdOrSuiteKey) {
    this.utf8 = 'utf-8';
    this.base64 = 'base64';
    this.AES_ENCODE_KEY_LENGTH = 43;
    this.RANDOM_LENGTH = 16;

    this.token = token;
    this.encodingAesKey = encodingAesKey;
    this.aesKey =  CryptoJS.enc.Base64.parse(encodingAesKey+'=');
    this.corpId = corpIdOrSuiteKey;
    this.keySpec = this.aesKey;
    this.iv =  CryptoJS.enc.Base64.parse((encodingAesKey+'=').substring(0,22));
  }

  // verify encodingAesKey
  set encodingAesKey(val) {
    if (!val || val.length !== this.AES_ENCODE_KEY_LENGTH) {
      throw new DingTalkEncryptException(900004);
    }
  }

  encrypt(random, plainText) {
    try {
      const randomBuf = Buffer.from(random);
      const plainTextBuf = Buffer.from(plainText);
      const textLen = plainTextBuf.length;
      const textLenBuf = Buffer.from([(textLen >> 24 & 255), (textLen >> 16 & 255), (textLen >> 8 & 255), (textLen & 255)]);
      const cropIdBuf = Buffer.from(this.corpId);
      const padCount = 32 - (randomBuf.length + textLenBuf.length + plainTextBuf.length + cropIdBuf.length) % 32;
      const padBuf = Buffer.from(new Array(padCount).fill(padCount));
      const finalBuf = Buffer.concat([randomBuf, textLenBuf, plainTextBuf, cropIdBuf, padBuf]);
      console.log("randomBuf@@@",randomBuf.toString('utf-8'))
      console.log("plainTextBuf@@@",plainTextBuf.toString('utf-8'))
      console.log("textLenBuf@@@",textLen,textLenBuf.toString('hex'))
      console.log("cropIdBuf@@@",cropIdBuf.toString('utf-8'))
      console.log("padCount@@@",padCount )
      console.log("padBuf@@@",padBuf)
      console.log("finalBuf@@@",finalBuf.toString('hex'))
 
      const encrypted = CryptoJS.AES.encrypt(finalBuf.toString('hex'), this.keySpec, {   
          iv: this.iv,
          mode: CryptoJS.mode.CBC,
          padding:CryptoJS.pad.Pkcs7 
      }); 
      return encrypted.toString(CryptoJS.enc.Base64);
    } catch (e) {
      console.log(e)
      throw new DingTalkEncryptException(900007);
    }
  }

  decrypt(encrypted) {
    let decrypt;
    try {
      // decrypt   
      const ciphertext =  CryptoJS.enc.Base64.parse(encrypted);
      decrypt = CryptoJS.AES.decrypt( {ciphertext}, this.keySpec, {   
          iv: this.iv,
          mode: CryptoJS.mode.CBC,
          padding:CryptoJS.pad.Pkcs7 
      }); 
    } catch (e) {
      throw new DingTalkEncryptException(900008);
    }
    //encrypt = Base64_Encode(AES_Encrypt[random(16B) + msg_len(4B) + msg + $key]) 钉钉加密消息格式
    let cropId;
    let plainText;  
    try { 

      const finalDecrypt =  decrypt.toString(CryptoJS.enc.Hex);
      const textLen = parseInt(finalDecrypt.substring(32,8+32), 16) ;
      plainText = CryptoJS.enc.Hex.parse(finalDecrypt.substring(40, 40 + textLen*2)).toString( CryptoJS.enc.Utf8);
      cropId =   CryptoJS.enc.Hex.parse(finalDecrypt.substring(40 + textLen*2)).toString(CryptoJS.enc.Utf8);

    } catch (e) { 
      throw new DingTalkEncryptException(900009);
    }

    if (cropId != this.corpId) {
      console.log(cropId, this.corpId)
      throw new DingTalkEncryptException(900010);
    } else {
      return plainText;
    }
  }

  getSignature(token, timestamp, nonce, encrypt) {
    timestamp = timestamp + '';
    const strArr = [token, timestamp, nonce, encrypt];
    strArr.sort();
    const sha1 = CryptoJS.SHA1(strArr.join('')); 
    return sha1.toString(CryptoJS.enc.Hex);
  }

  getEncryptedMap(plaintext, timeStamp, nonce) {
    timeStamp = timeStamp + '';
    if (plaintext == null) {
      throw new DingTalkEncryptException(900001);
    } else if (timeStamp == null) {
      throw new DingTalkEncryptException(900002);
    } else if (nonce == null) {
      throw new DingTalkEncryptException(900003);
    } else {
      const encrypt = this.encrypt(this.getRandomStr(this.RANDOM_LENGTH), plaintext);
      const signature = this.getSignature(this.token, timeStamp, nonce, encrypt);
      return {
        msg_signature: signature,
        encrypt: encrypt,
        timeStamp: timeStamp,
        nonce: nonce
      };
    }
  }

  getDecryptMsg(msgSignature, timeStamp, nonce, encryptMsg) {
    const signature = this.getSignature(this.token, timeStamp, nonce, encryptMsg);
    if (signature !== msgSignature) {
      throw new DingTalkEncryptException(900006);
    } else {
      return this.decrypt(encryptMsg);
    }
  }

  getRandomStr(size) {
    const base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let randomStr = '';
    for (let i = size; i > 0; --i) {
      randomStr += base[Math.floor(Math.random() * base.length)];
    }
    return randomStr;
  };
}

module.exports = DingTalkEncryptor;