Skip to content

Commit

Permalink
feat(x): 新增 WechatMessageCrypto 微信公众号消息加解密
Browse files Browse the repository at this point in the history
  • Loading branch information
fjc0k committed Dec 24, 2020
1 parent 492c695 commit c106915
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/x/WechatMessageCrypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { WechatMessageCrypto } from './WechatMessageCrypto'

jest.mock('crypto', () => ({
...(jest.requireActual('crypto') as any),
randomBytes: () => Buffer.from('123456'),
}))

Object.defineProperty(Date, 'now', {
value: () => 1608776976262,
})

describe('WechatCrypto', () => {
test('第三方平台', () => {
Object.defineProperty(Date, 'now', {
value: () => 1608776976262,
})

const wmc = new WechatMessageCrypto({
appId: 'wxd4970d77949d0829',
token: 'Bc-!9n4xnku!_Dx!@BHVk',
encodingAESKey: 'oMa2AFkWcPLPCsNsHjx9TdrZEPDTLevhXejefjPoQxP',
})

const messageEncrypted =
'BfyISyHfXZ1Y7r4ukwsUMHLxhji9zXHQo/n71zzz6kpJ+wBgzLU5rmtjnyjuRoLfkb1RMXATwyoMPH0WQlZx2//7MWk/JTbU59h1DtabvPRZ16Ue69EpkEJ/OZGwrjoZFdwI8O6Z8IrFLWKdcEOdDVBp8u4eNv9fxQIssoJG2MIaKsV7bQQ+zH0s/2BL09U/C2u+P+D/51qtaHIVip57SDgKtVUa0voscQS4SI6TECpwXZQSBiWjO8awLmLuYiYLjiqgd7jg6lg66da3CbStR85XHrPnoN0QKhRoz8fLBfBmz/HkwMxP6gm7FTp80kQJoXm7byAisqplajpEpqeWinJIcSNacEQ3FxUacAYjzh3TnrcnSLqoMtL5EXKKTPlDpWzPWd0TvXg1I+bvSqfI72RWHq/MjpJzJioiVUHg7WrejSmTPuF0dm263RDCeMDMPvIoZNa8Fpk6Ad6pQg3AssPPmEKgb8F8GpsezgifKAgd5wzFCiu/3IBVPQrRlIznhEB8NPpHCIGLXfnYCiEchAWkbcev/Q1IinG2sBGSruM='
expect(wmc.decryptEncryptedMsg(messageEncrypted)).toMatchSnapshot()
expect(wmc.decryptEncryptedMsgAsXml(messageEncrypted)).toMatchSnapshot()

const notifyEncrypted =
'JeK+KAvspbZhljTCpWcKFBVOnrZN9ubcB1MFBRJoz4OW5ufJmkGzmSFdrhKYV+sg/tD77/zO/11ZW8o+LXu9LxL3LNKbXybM/nP4s1JAzygyU/J5rVaq3THhHwWQUzrRUMR1FCNInPERdK90rZeVEcjl8Mk96ARtrF1c16hn2XoI/M/QvGAXn6VWy6ERWXxWFW3AhgtfW1EruLz9KreI2gnqCblFVsUl3ea/UsmivlQTWrSMTRuV2Y0F+i63ezdiCivOXKIeQfbSgYovxpmVcuvRgJB4xb9h+9LpZfv/1FYnTCF41XG2ve//cnJvkxAdet280vw7HIm+r+w9IHAsODEfsamnlwq9lyEgxtboTfGH1y/pliRgBjg5nPg5ySod4fJRERs89xgezwbTHbkIhvnOiFGFfwSYhRfkHbuGguBkJgRqStGCOMN0x0AYN8Rncz8AjuDoKSex0QNPUVzMxDa7Foef+3FiCgPjNjfe5Z0/3e2p0VUq+PL511PbXQpOYK35pQPlDZIzL618DJReZlbGxUlqktUjEkqNm0OnJwfUjIf3tPLO3yeV6TUocdisKRwCWT6++n+Xw+4FSC+gepYP8cQKxwteX+0Cj+9hZyXZWumkMBXY02joifRkF6deEbmCPpOUjXUbqp2v9/UCYUvvoiuV0wBOnHHFNmxSZQ6+VHNVetcsy3Ze5ZPRKIRgP8FedVyiXnlGNVt+C4r6tUKW5br36qYPBRXJQ5fOc+sLNvRzBC5Zt5W2PkpOwAwiEJ8HD/IcdpSBWRSLoeaveMEOy6xvp6+vEAy7aeWsIwXbCFJF6dSbgTwnXgcDfTM7lnL0+iDw7CPzcdRYInJYsA=='
expect(wmc.decryptEncryptedMsg(notifyEncrypted)).toMatchSnapshot()
expect(wmc.decryptEncryptedMsgAsXml(notifyEncrypted)).toMatchSnapshot()

const msg = '<xml><ID>2020</ID></xml>'
expect(wmc.decryptEncryptedMsg(wmc.encryptMsg(msg))).toMatchSnapshot()
expect(wmc.signEncryptedMsg(wmc.encryptMsg(msg))).toMatchSnapshot()
expect(wmc.signEncryptedMsgAsXml(wmc.encryptMsg(msg))).toMatchSnapshot()

const encryptedMsg = wmc.encryptMsg(msg)
const signedEncryptedMsg = wmc.signEncryptedMsg(encryptedMsg)
expect(
wmc.checkSignature(signedEncryptedMsg.signature, {
encryptedMsg: encryptedMsg,
nonceStr: signedEncryptedMsg.nonceStr,
timestamp: signedEncryptedMsg.timestamp,
}),
).toBeTrue()
})
})
197 changes: 197 additions & 0 deletions src/x/WechatMessageCrypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import crypto from 'crypto'
import { parseXml } from './parseXml'
import { sha1 } from './sha1'

export interface WechatMessageCryptoOptions {
/** 公众号/第三方平台的 APPID */
appId: string

/** 消息校验口令 */
token: string

/** 消息加解密密钥 */
encodingAESKey: string
}

export interface WechatMessageCryptoSignOptions {
/** Unix 时间戳 */
timestamp: number

/** 随机字符串 */
nonceStr: string

/** 加密后的消息 */
encryptedMsg: string
}

export interface WechatMessageCryptoSignEncryptedMsgResult {
/** Unix 时间戳 */
timestamp: number

/** 随机字符串 */
nonceStr: string

/** 签名 */
signature: string
}

/**
* 微信公众号消息加解密。
*/
export class WechatMessageCrypto {
private aesKey!: Buffer

private iv!: Buffer

constructor(private options: WechatMessageCryptoOptions) {
this.aesKey = Buffer.from(`${options.encodingAESKey}=`, 'base64')
this.iv = this.aesKey.slice(0, 16)
}

/**
* 加密原始消息。
*
* @param msg 原始消息
*/
encryptMsg(msg: string): string {
const randomBytes = crypto.randomBytes(16)

const msgLenBuf = Buffer.alloc(4)
const offset = 0
msgLenBuf.writeUInt32BE(Buffer.byteLength(msg), offset)

const msgBuf = Buffer.from(msg)
const appIdBuf = Buffer.from(this.options.appId)

let totalBuf = Buffer.concat([randomBytes, msgLenBuf, msgBuf, appIdBuf])

const cipher = crypto.createCipheriv('aes-256-cbc', this.aesKey, this.iv)
cipher.setAutoPadding(false)
totalBuf = this.PKCS7Encode(totalBuf)
const encryptedBuf = Buffer.concat([
cipher.update(totalBuf),
cipher.final(),
])

return encryptedBuf.toString('base64')
}

/**
* 签名。
*
* @param options 选项
*/
sign(options: WechatMessageCryptoSignOptions): string {
return sha1(
[
this.options.token,
options.timestamp,
options.nonceStr,
options.encryptedMsg,
]
.sort((a, b) => {
a = a.toString()
b = b.toString()
return a > b ? 1 : a < b ? -1 : 0
})
.join(''),
)
}

/**
* 签名加密后的消息。
*
* @param encryptedMsg 加密后的消息
*/
signEncryptedMsg(
encryptedMsg: string,
): WechatMessageCryptoSignEncryptedMsgResult {
const timestamp = Math.round(Date.now() / 1000)
const nonceStr = timestamp.toString(36)
const signature = this.sign({ timestamp, nonceStr, encryptedMsg })
return { timestamp, nonceStr, signature }
}

/**
* 签名加密后的消息并返回封装好的 XML。
*
* @param encryptedMsg 加密后的消息
*/
signEncryptedMsgAsXml(encryptedMsg: string): string {
const { timestamp, nonceStr, signature } = this.signEncryptedMsg(
encryptedMsg,
)
return (
`<xml>` +
`<Encrypt><![CDATA[${encryptedMsg}]]></Encrypt>` +
`<MsgSignature><![CDATA[${signature}]]></MsgSignature>` +
`<TimeStamp>${timestamp}</TimeStamp>` +
`<Nonce><![CDATA[${nonceStr}]]></Nonce>` +
`</xml>`
)
}

/**
* 检查签名是否正确。
*
* @param signature 要验证的签名
* @param payload 载荷
*/
checkSignature(
signature: string,
payload: WechatMessageCryptoSignOptions,
): boolean {
return this.sign(payload) === signature
}

/**
* 解密加密后的消息。
*
* @param encryptedMsg 加密后的消息
*/
decryptEncryptedMsg(encryptedMsg: string): string {
const encryptedMsgBuf = Buffer.from(encryptedMsg, 'base64')

const decipher = crypto.createDecipheriv(
'aes-256-cbc',
this.aesKey,
this.iv,
)
decipher.setAutoPadding(false)
let decryptedBuf = Buffer.concat([
decipher.update(encryptedMsgBuf),
decipher.final(),
])

decryptedBuf = this.PKCS7Decode(decryptedBuf)

const msgSize = decryptedBuf.readUInt32BE(16)
const msgBufStartPos = 16 + 4
const msgBufEndPos = msgBufStartPos + msgSize

const msgBuf = decryptedBuf.slice(msgBufStartPos, msgBufEndPos)

return msgBuf.toString()
}

/**
* 解密加密后的消息并作为 XML 解码返回。
*
* @param encryptedMsg 加密后的消息
*/
decryptEncryptedMsgAsXml<T>(encryptedMsg: string): T {
return parseXml<{ xml: T }>(this.decryptEncryptedMsg(encryptedMsg)).xml
}

private PKCS7Decode(buf: Buffer) {
const padSize = buf[buf.length - 1]
return buf.slice(0, buf.length - padSize)
}

private PKCS7Encode(buf: Buffer) {
const padSize = 32 - (buf.length % 32)
const fillByte = padSize
const padBuf = Buffer.alloc(padSize, fillByte)
return Buffer.concat([buf, padBuf])
}
}
104 changes: 104 additions & 0 deletions src/x/__snapshots__/WechatMessageCrypto.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`WechatCrypto 第三方平台 1`] = `
<xml>
<ToUserName>
<!--[CDATA[gh_2a866851d1f9]]-->
</ToUserName>
<FromUserName>
<!--[CDATA[oZpm301xkURBEwu2mrDfEf41FgaI]]-->
</FromUserName>
<CreateTime>
1608776418
</CreateTime>
<MsgType>
<!--[CDATA[event]]-->
</MsgType>
<Event>
<!--[CDATA[VIEW]]-->
</Event>
<EventKey>
<!--[CDATA[http://dcq.qingzhou.io/wall/index.html?media_id=gh_4e7837ae5767]]-->
</EventKey>
<MenuId>
456330783
</MenuId>
</xml>
`;

exports[`WechatCrypto 第三方平台 2`] = `
Object {
"CreateTime": 1608776418,
"Event": "VIEW",
"EventKey": "http://dcq.qingzhou.io/wall/index.html?media_id=gh_4e7837ae5767",
"FromUserName": "oZpm301xkURBEwu2mrDfEf41FgaI",
"MenuId": 456330783,
"MsgType": "event",
"ToUserName": "gh_2a866851d1f9",
}
`;

exports[`WechatCrypto 第三方平台 3`] = `
<xml>
<AppId>
<!--[CDATA[wxd4970d77949d0829]]-->
</AppId>
<CreateTime>
1608776502
</CreateTime>
<InfoType>
<!--[CDATA[authorized]]-->
</InfoType>
<AuthorizerAppid>
<!--[CDATA[wx570bc396a51b8ff8]]-->
</AuthorizerAppid>
<AuthorizationCode>
<!--[CDATA[queryauthcode@@@VIu5qeezG3hye4wWHDLe5FvYzSW4gQm7WRcB82r_LIgeYD-2esY0ew1nMdNh1b51ZEEMoE1iSQ2AzlWPQB9F5A]]-->
</AuthorizationCode>
<AuthorizationCodeExpiredTime>
<!--[CDATA[1608780102]]-->
</AuthorizationCodeExpiredTime>
<PreAuthCode>
<!--[CDATA[preauthcode@@@xAFBuq7Pvcumi9KooVZzZtx8fXJ2TFOMjvFR4hYrPNwQZbHngscDhZZRkGZqWToC4Gt1jkI8uEbZbn-t4SuGNw]]-->
</PreAuthCode>
</xml>
`;

exports[`WechatCrypto 第三方平台 4`] = `
Object {
"AppId": "wxd4970d77949d0829",
"AuthorizationCode": "queryauthcode@@@VIu5qeezG3hye4wWHDLe5FvYzSW4gQm7WRcB82r_LIgeYD-2esY0ew1nMdNh1b51ZEEMoE1iSQ2AzlWPQB9F5A",
"AuthorizationCodeExpiredTime": "1608780102",
"AuthorizerAppid": "wx570bc396a51b8ff8",
"CreateTime": 1608776502,
"InfoType": "authorized",
"PreAuthCode": "preauthcode@@@xAFBuq7Pvcumi9KooVZzZtx8fXJ2TFOMjvFR4hYrPNwQZbHngscDhZZRkGZqWToC4Gt1jkI8uEbZbn-t4SuGNw",
}
`;

exports[`WechatCrypto 第三方平台 5`] = `"020</ID></xml>wxd4970d77949d0829"`;

exports[`WechatCrypto 第三方平台 6`] = `
Object {
"nonceStr": "qlto9c",
"signature": "5d314725828b0ac96591b33ac048160ce302481c",
"timestamp": 1608776976,
}
`;

exports[`WechatCrypto 第三方平台 7`] = `
<xml>
<Encrypt>
<!--[CDATA[+nvf4v2JqvaIoY8htvZxvPygcD8VpelK9jQZKu4ZvPV20Nns2lOuq4xyrxFuCid8M9tEMjNCehDG3bx5BBhOPw==]]-->
</Encrypt>
<MsgSignature>
<!--[CDATA[5d314725828b0ac96591b33ac048160ce302481c]]-->
</MsgSignature>
<TimeStamp>
1608776976
</TimeStamp>
<Nonce>
<!--[CDATA[qlto9c]]-->
</Nonce>
</xml>
`;
1 change: 1 addition & 0 deletions src/x/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from './parseXml'
export * from './RedisCookieJar'
export * from './sha1'
export * from './uuid'
export * from './WechatMessageCrypto'
// @endindex

0 comments on commit c106915

Please sign in to comment.