An overview of the

Wechat public platform development, in the message receiving, event push, etc., can choose plain text, can choose ciphertext. For details, see the access guide. After we choose safe mode, we need to encrypt and decrypt the message. First take a look at the official document: message encryption and decryption instructions

Wechat public platform provides c++, PHP, Java, python, c# 5 language sample code.

With so many node applications available today, there is no Node version.

implementation

  1. Extract an encryption moduleWXMsgCrypto.js:
var crypto = require('crypto')

class PKCS7 {
  @param {String} text Decrypted text */
  decode(text) {
    let pad = text[text.length - 1]
    if (pad < 1 || pad > 32) {
      pad = 0
    }
    return text.slice(0, text.length - pad)
  }
  /** * fill the complement * @param {String} text The plaintext to fill the complement */
  encode(text) {
    const blockSize = 32
    const textLength = text.length
    // Calculate the number of bits to fill
    const amountToPad = blockSize - (textLength % blockSize)
    const result = Buffer.alloc(amountToPad)
    result.fill(amountToPad)
    return Buffer.concat([text, result])
  }
}

/** * wechat official account message encryption and decryption * official document (written very badly) : https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Message_Encryption/Technical_Plan.html */
class WXMsgCrypto {
  * @param {String} Token (token) * @param {String} encodingAESKey Message encryption key * @param {String} AppId appId */ of the public id
  constructor(token, encodingAESKey, appId) {
    if(! token || ! encodingAESKey || ! appId) {throw new Error('please check arguments')}this.token = token
    this.appId = appId

    let AESKey = Buffer.from(encodingAESKey + '='.'base64')
    if(AESKey.length ! = =32) {
      throw new Error('encodingAESKey invalid')}this.key = AESKey
    this.iv = AESKey.slice(0.16)
    this.pkcs7 = new PKCS7()
  }
  /** * obtain signature * @param {String} timestamp timestamp * @param {String} nonce random number * @param {String} encrypted text */
  getSignature(timestamp, nonce, encrypt) {
    const sha = crypto.createHash('sha1')
    const arr = [this.token, timestamp, nonce, encrypt].sort()
    sha.update(arr.join(' '))
    return sha.digest('hex')}* @param {String} text Indicates the ciphertext to be decrypted */
  decrypt(text) {
    // create decryption object, AES using CBC mode, data using PKCS#7 padding; The initial vector size of IV is 16 bytes. Take the first 16 bytes of the AESKey
    const decipher = crypto.createDecipheriv('aes-256-cbc'.this.key, this.iv)
    decipher.setAutoPadding(false)

    let deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()])

    deciphered = this.pkcs7.decode(deciphered)
    // algorithm: AES_Encrypt[random(16B) + msg_len(4B) + MSG + $CorpID]
    // Remove the 16-bit random number
    const content = deciphered.slice(16)
    const length = content.slice(0.4).readUInt32BE(0)

    return {
      message: content.slice(4, length + 4).toString(),
      appId: content.slice(length + 4).toString()
    }
  }
  /** * Encrypt plaintext * algorithm: Base64_Encode(AES_Encrypt[Random (16B) + MSg_len (4B) + MSG + $appId]) * @param {String} text Plain text to be encrypted */
  encrypt(text) {
    // 16B Random character string
    const randomString = crypto.pseudoRandomBytes(16)

    const msg = Buffer.from(text)
    // Get the network byte order of 4B content length
    const msgLength = Buffer.alloc(4)
    msgLength.writeUInt32BE(msg.length, 0)

    const id = Buffer.from(this.appId)

    const bufMsg = Buffer.concat([randomString, msgLength, msg, id])

    // Complement the plaintext
    const encoded = this.pkcs7.encode(bufMsg)

    // create encrypted object, AES using CBC mode, data using PKCS#7 padding; The initial vector size of IV is 16 bytes. Take the first 16 bytes of the AESKey
    const cipher = crypto.createCipheriv('aes-256-cbc'.this.key, this.iv)
    cipher.setAutoPadding(false)

    const cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()])

    return cipheredMsg.toString('base64')}}module.exports = WXMsgCrypto

Copy the code
  1. koa2Server supportxml.server/index.jsPseudocode:
const xmlParser = require('koa-xml-body')
/ / support XML
app.use(xmlParser({
  key: 'xmlBody'.// Parse XML data to ctx.request.xmlBody
  xmlOptions: {
    explicitArray: false,}}))Copy the code

Note the argument: explicitArray, which defaults to true, whether to put child node data into the array.

`<xml>
<MsgId>6197906553041859764</MsgId>
</xml>`
// When explicitArray is true, parse:
ctx.request.xmlBody // { xml: { MsgId: [ '6197906553041859764' ] } }
// When false, parse:
ctx.request.xmlBody // { xml: { MsgId: '6197906553041859764' } }
Copy the code
  1. Encryption and decryption using (pseudocode) :
// use the log4js implementation of log, please implement the corresponding method.
const WXMsgCrypto = require('. /.. /util/WXMsgCrypto')
const wxmc = new WXMsgCrypto($Token, $EncodingAESKey, $AppID)


/** * wechat callback interface * 1, add get route, used for wechat authentication * 2, add verification middleware * 3, add other routes, used for wechat callback (message push, event push, etc.) */
router.get('/wxcallback', ctx => {
  log.trace('[get] /wxcallback ctx.request.query:', ctx.request.query)
  let params = ctx.request.query
  ctx.body = params.echostr
})
router.use(async (ctx, next) => {
  if (ctx.request.path === '/api/wechat/wxcallback') {
    // log.trace(`[${ctx.request.method}] ${ctx.request.path}`)
    // log.trace('ctx.request.query', ctx.request.query)
    let query = ctx.request.query
    let xml = ctx.request.xmlBody && ctx.request.xmlBody.xml
    / / check
    let msgSignature = wxmc.getSignature(query.timestamp, query.nonce, xml.Encrypt)
    if(msgSignature ! == query.msg_signature) { log.error(`"${ctx.request.method} ${ctx.request.url}\nctx.request.query: The ${JSON.stringify(ctx.request.query)}\nctx.request.body: The ${JSON.stringify(ctx.request.body)}\nctx.request.xmlBody: The ${JSON.stringify(ctx.request.xmlBody)}\n Calculates msgSignature:${msgSignature}"`)
      ctx.status = 403
      ctx.body = 'Failed: Failed to verify the signature. '
    } else {
      await next()
    }
  } else {
    await next()
  }
})
router.all('/wxcallback', ctx => {
  log.trace(` [${ctx.request.method}] /wxcallback ctx.request.xmlBody:`, ctx.request.xmlBody)
  let xml = ctx.request.xmlBody && ctx.request.xmlBody.xml
  // If the encryption mode is 2, decryption is required
  if (config.wechatMessageEncryptMode === '2' && xml) {
    log.trace('to decrypt... ')
    let xmlSource = wxmc.decrypt(xml.Encrypt)
    log.trace('Decrypt xmlSource:', xmlSource)
    // let parser = new xml2js.Parser()
    xml2js.parseString(xmlSource.message, {
      explicitArray: false,
    }, (err, result) => {
      if (err) {
        log.error(Decryption error: ', err)
      }
      xml = result.xml
    })
  }
  let result = 'success'
  if (xml) {
    log.trace(`xml:`, xml)
    if (xml.MsgType === 'event') { // Event push
      switch (xml.Event) {
        case 'TEMPLATESENDJOBFINISH': // The template message has been sent
          log.info('Push event sent complete:', xml.Status)
          break}}else if (xml.MsgType === 'text') { // Text message
      // Assemble returns data
      let query = ctx.request.query
      let timestamp = new Date().getTime()
      result = `<xml> <ToUserName><! [CDATA[${xml.FromUserName}]]></ToUserName> <FromUserName><! [CDATA[${xml.ToUserName}]]></FromUserName>
          <CreateTime>${timestamp}</CreateTime> <MsgType><! [CDATA[text]]></MsgType> <Content><! [CDATA] [What you said:${xml.Content}]]></Content>
        </xml>`
      // Encrypt the returned data and assemble the encrypted returned data
      if (config.wechatMessageEncryptMode === '2') {
        let encryptData = wxmc.encrypt(result)
        // console.log('encryptData', encryptData)
        let msgSignature = wxmc.getSignature(timestamp, query.nonce, encryptData)
        result = `<xml> <Encrypt><! [CDATA[${encryptData}]]></Encrypt>
          <MsgSignature>${msgSignature}</MsgSignature>
          <TimeStamp>${timestamp}</TimeStamp>
          <Nonce>${query.nonce}</Nonce>
        </xml>`
      }
      log.trace('response xml:', result)
      ctx.res.setHeader('Content-Type'.'application/xml')
    }
  }
  ctx.body = result
})

Copy the code