├── util ├── o2x.d.ts ├── x2o.d.ts ├── sign.d.ts ├── x2o.ts ├── index.ts ├── index.d.ts ├── sign.ts ├── try-throw.d.ts ├── x2o-builder.d.ts ├── x2o.js ├── index.js ├── try-throw.ts ├── sign.js ├── try-throw.js ├── o2x.ts ├── o2x.js ├── x2o-builder.ts └── x2o-builder.js ├── tsconfig.json ├── .editorconfig ├── LICENSE ├── package.json ├── .gitignore ├── test ├── o2x.js ├── index.js └── x2o.js ├── index.d.ts ├── index.ts ├── index.js └── README.md /util/o2x.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将对象转化为xml字符串 3 | */ 4 | export default function o2x(obj: any): string; 5 | -------------------------------------------------------------------------------- /util/x2o.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将xml字符串转化为对象 3 | */ 4 | export default function x2o(xml: string): any; 5 | -------------------------------------------------------------------------------- /util/sign.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: (...args: string[]) => string; 2 | /** 3 | * 生成签名 4 | */ 5 | export default _default; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es5", 5 | "lib": [ 6 | "es2015" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /util/x2o.ts: -------------------------------------------------------------------------------- 1 | import Builder from './x2o-builder'; 2 | 3 | /** 4 | * 将xml字符串转化为对象 5 | */ 6 | export default function x2o(xml: string): any { 7 | return new Builder(xml).build(); 8 | } 9 | -------------------------------------------------------------------------------- /util/index.ts: -------------------------------------------------------------------------------- 1 | export { default as sign } from './sign'; 2 | export { default as x2o } from './x2o'; 3 | export { default as o2x } from './o2x'; 4 | export { default as tryThrow } from './try-throw'; 5 | -------------------------------------------------------------------------------- /util/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as sign } from './sign'; 2 | export { default as x2o } from './x2o'; 3 | export { default as o2x } from './o2x'; 4 | export { default as tryThrow } from './try-throw'; 5 | -------------------------------------------------------------------------------- /util/sign.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | /** 4 | * 生成签名 5 | */ 6 | export default (...args: string[]) => createHash('sha1') 7 | .update(args.sort().join('')) 8 | .digest('hex') 9 | -------------------------------------------------------------------------------- /util/try-throw.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 尝试执行函数,并返回执行结果 3 | * @param errcode 失败时的错误码 4 | * @param errmsg 失败时的错误描述 5 | * @param fn 尝试执行的函数 6 | */ 7 | export default function (errcode: number, errmsg: string, fn?: () => T): T; 8 | -------------------------------------------------------------------------------- /util/x2o-builder.d.ts: -------------------------------------------------------------------------------- 1 | export default class Builder { 2 | private names; 3 | private values; 4 | private value; 5 | constructor(xml?: string); 6 | start(name: string): void; 7 | text(content: string): void; 8 | end(name: string): void; 9 | build(): any; 10 | } 11 | -------------------------------------------------------------------------------- /util/x2o.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var x2o_builder_1 = require("./x2o-builder"); 4 | /** 5 | * 将xml字符串转化为对象 6 | */ 7 | function x2o(xml) { 8 | return new x2o_builder_1.default(xml).build(); 9 | } 10 | exports.default = x2o; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var sign_1 = require("./sign"); 4 | exports.sign = sign_1.default; 5 | var x2o_1 = require("./x2o"); 6 | exports.x2o = x2o_1.default; 7 | var o2x_1 = require("./o2x"); 8 | exports.o2x = o2x_1.default; 9 | var try_throw_1 = require("./try-throw"); 10 | exports.tryThrow = try_throw_1.default; 11 | -------------------------------------------------------------------------------- /util/try-throw.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 尝试执行函数,并返回执行结果 3 | * @param errcode 失败时的错误码 4 | * @param errmsg 失败时的错误描述 5 | * @param fn 尝试执行的函数 6 | */ 7 | export default function ( 8 | errcode: number, errmsg: string, 9 | fn: () => T = () => { throw new Error(`${errcode}: ${errmsg}`); } 10 | ): T { 11 | try { 12 | return fn(); 13 | } catch (err) { 14 | throw Object.assign(err, { errcode, errmsg }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /util/sign.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var crypto_1 = require("crypto"); 4 | /** 5 | * 生成签名 6 | */ 7 | exports.default = (function () { 8 | var args = []; 9 | for (var _i = 0; _i < arguments.length; _i++) { 10 | args[_i] = arguments[_i]; 11 | } 12 | return crypto_1.createHash('sha1') 13 | .update(args.sort().join('')) 14 | .digest('hex'); 15 | }); 16 | -------------------------------------------------------------------------------- /util/try-throw.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * 尝试执行函数,并返回执行结果 5 | * @param errcode 失败时的错误码 6 | * @param errmsg 失败时的错误描述 7 | * @param fn 尝试执行的函数 8 | */ 9 | function default_1(errcode, errmsg, fn) { 10 | if (fn === void 0) { fn = function () { throw new Error(errcode + ": " + errmsg); }; } 11 | try { 12 | return fn(); 13 | } 14 | catch (err) { 15 | throw Object.assign(err, { errcode: errcode, errmsg: errmsg }); 16 | } 17 | } 18 | exports.default = default_1; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, cxy930123 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /util/o2x.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将对象转化为xml字符串 3 | */ 4 | export default function o2x(obj: any): string { 5 | if (typeof obj === 'string') { 6 | let cdata = /[<>&'"]/.test(obj); 7 | if (obj.includes(']]>')) { 8 | cdata = false; 9 | } 10 | return cdata ? `` : obj 11 | .replace(/&/g, '&') 12 | .replace(//g, '>') 14 | .replace(/'/g, ''') 15 | .replace(/"/g, '"'); 16 | } 17 | if (typeof obj === 'number') { 18 | return Number.isFinite(obj) ? String(obj) : ''; 19 | } 20 | if (typeof obj !== 'object' || obj === null) { 21 | return ''; 22 | } 23 | if (Array.isArray(obj)) { 24 | return obj.map(item => `${o2x(item)}`).join(''); 25 | } 26 | return Object.keys(obj).map(key => `<${key}>${o2x(obj[key])}`).join(''); 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wxcrypt", 3 | "version": "1.4.3", 4 | "description": "WXBizMsgCrypt for NodeJS", 5 | "files": [ 6 | "**/*.d.ts", 7 | "**/*.js" 8 | ], 9 | "scripts": { 10 | "start": "tsc", 11 | "test": "mocha" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/cxy930123/wxcrypt.git" 16 | }, 17 | "keywords": [ 18 | "wechat", 19 | "crypt", 20 | "crypto", 21 | "wxbizmsgcrypt" 22 | ], 23 | "author": { 24 | "name": "cxy930123", 25 | "email": "mail@xingyu1993.cn" 26 | }, 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/cxy930123/wxcrypt/issues" 30 | }, 31 | "homepage": "https://github.com/cxy930123/wxcrypt#readme", 32 | "dependencies": { 33 | "@types/node": "*" 34 | }, 35 | "devDependencies": { 36 | "mocha": "^6.2.0", 37 | "randomstring": "^1.1.5", 38 | "should": "^13.2.3", 39 | "typescript": "^3.5.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /util/o2x.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * 将对象转化为xml字符串 5 | */ 6 | function o2x(obj) { 7 | if (typeof obj === 'string') { 8 | var cdata = /[<>&'"]/.test(obj); 9 | if (obj.includes(']]>')) { 10 | cdata = false; 11 | } 12 | return cdata ? "" : obj 13 | .replace(/&/g, '&') 14 | .replace(//g, '>') 16 | .replace(/'/g, ''') 17 | .replace(/"/g, '"'); 18 | } 19 | if (typeof obj === 'number') { 20 | return Number.isFinite(obj) ? String(obj) : ''; 21 | } 22 | if (typeof obj !== 'object' || obj === null) { 23 | return ''; 24 | } 25 | if (Array.isArray(obj)) { 26 | return obj.map(function (item) { return "" + o2x(item) + ""; }).join(''); 27 | } 28 | return Object.keys(obj).map(function (key) { return "<" + key + ">" + o2x(obj[key]) + ""; }).join(''); 29 | } 30 | exports.default = o2x; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | -------------------------------------------------------------------------------- /test/o2x.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const { o2x, x2o } = require('../util'); 3 | 4 | describe('#obj2xml', () => { 5 | describe('#parse string', () => { 6 | it('#basic string', () => { 7 | const text = 'This is a basic string.'; 8 | should(o2x(text)).eql(text); 9 | }) 10 | 11 | it('#null should return empty string', () => { 12 | should(o2x(null)).eql(''); 13 | }) 14 | 15 | it(`#entities (&<>'")`, () => { 16 | should(o2x(`&<]]>'"`)).eql('&<]]>'"'); 17 | }) 18 | 19 | it('cdata', () => { 20 | should(o2x('')).eql(']]>'); 21 | }) 22 | }) 23 | 24 | describe('#parse array', () => { 25 | it('#empty dimensional array', () => { 26 | should(o2x([])).eql(''); 27 | }) 28 | 29 | it('#one dimensional array', () => { 30 | should(o2x(['a', 'b'])).eql('ab'); 31 | }) 32 | 33 | it('#nested array', () => { 34 | should(o2x([['1', null], ' '])).eql('1 ') 35 | }) 36 | 37 | it('#complex array', () => { 38 | should(o2x({ xml: [{ node: [[null]] }] })).eql(''); 39 | }) 40 | }) 41 | 42 | describe('#parse normal object', () => { 43 | it('#xml with normal ', () => { 44 | const obj = { xml: { a: 'a', b: 'c', c: 'b' } }; 45 | should(x2o(o2x(obj))).eql(obj); 46 | }) 47 | 48 | it('#more than one root tag', () => { 49 | const obj = { a: null, b: null }; 50 | should(x2o(o2x(obj))).eql(obj); 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const randomstring = require('randomstring'); 3 | const WXBizMsgCrypt = require('..'); 4 | const { x2o } = require('../util'); 5 | 6 | describe('#main', () => { 7 | const token = randomstring.generate(); 8 | const encodingAESKey = randomstring.generate(43); 9 | const appid = randomstring.generate(18); 10 | const crypto = new WXBizMsgCrypt(token, encodingAESKey, appid); 11 | 12 | let Encrypt, MsgSignature, TimeStamp, Nonce; 13 | let str, timestamp, nonce, echostr; 14 | 15 | it('#encrypt', () => { 16 | str = randomstring.generate(); 17 | timestamp = `${Date.now()}`; 18 | nonce = randomstring.generate(); 19 | ({ 20 | xml: { 21 | Encrypt, MsgSignature, TimeStamp, Nonce 22 | } 23 | } = x2o(crypto.encryptMsg(str, timestamp, nonce))); 24 | }) 25 | 26 | it('#encrypt should not be undefined', () => { 27 | should(Encrypt).not.be.undefined(); 28 | }) 29 | 30 | it('#signature should not be undefined', () => { 31 | should(MsgSignature).not.be.undefined(); 32 | }) 33 | 34 | it('#timestamp should not be undefined', () => { 35 | should(TimeStamp).not.be.undefined(); 36 | }) 37 | 38 | it('#nonce should not be undefined', () => { 39 | should(Nonce).not.be.undefined(); 40 | }) 41 | 42 | it('#decrypt', () => { 43 | echostr = crypto.decrypt(MsgSignature, TimeStamp, Nonce, Encrypt); 44 | }) 45 | 46 | it('#echostr should equals str', () => { 47 | should(echostr).equals(str); 48 | }) 49 | 50 | it('#timestamp should not be changed', () => { 51 | should(TimeStamp).equals(timestamp); 52 | }) 53 | 54 | it('#nonce should not be changed', () => { 55 | should(Nonce).equals(nonce); 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { x2o, o2x } from './util'; 2 | declare class WXBizMsgCrypt { 3 | private token; 4 | private appid; 5 | static readonly sign: (...args: string[]) => string; 6 | static readonly x2o: typeof x2o; 7 | static readonly o2x: typeof o2x; 8 | private aesKey; 9 | private iv; 10 | /** 11 | * 构造函数 12 | * @param token 公众号或企业微信Token 13 | * @param encodingAESKey 用于消息体的加密 14 | * @param appid 公众号的AppID或企业微信的CropID 15 | */ 16 | constructor(token: string, encodingAESKey: string, appid: string); 17 | /** 18 | * 将密文翻译成明文 19 | * @param msgSignature 消息体签名 20 | * @param timestamp 时间戳 21 | * @param nonce 用于签名的随机字符串 22 | * @param msgEncrypt 消息体(Base64编码的密文) 23 | */ 24 | private decrypt; 25 | /** 26 | * 验证URL函数(仅用于企业微信) 27 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 28 | * @param timestamp 从接收消息的URL中获取的timestamp参数 29 | * @param nonce 从接收消息的URL中获取的nonce参数 30 | * @param echostr 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值 31 | * @return 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理 32 | */ 33 | verifyURL(msgSignature: string, timestamp: string, nonce: string, echostr: string): string; 34 | /** 35 | * 解密函数 36 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 37 | * @param timestamp 从接收消息的URL中获取的timestamp参数 38 | * @param nonce 从接收消息的URL中获取的nonce参数 39 | * @param postData 从接收消息的URL中获取的整个post数据 40 | * @return 解密后的msg,以xml组织 41 | */ 42 | decryptMsg(msgSignature: string, timestamp: string, nonce: string, postData: string): string; 43 | /** 44 | * 加密函数 45 | * @param replyMsg 返回的消息体原文 46 | * @param timestamp 时间戳,调用方生成 47 | * @param nonce 随机字符串,调用方生成 48 | * @return 用于返回的密文,以xml组织 49 | */ 50 | encryptMsg(replyMsg: string, timestamp: string, nonce: string): string; 51 | } 52 | export = WXBizMsgCrypt; 53 | -------------------------------------------------------------------------------- /util/x2o-builder.ts: -------------------------------------------------------------------------------- 1 | export default class Builder { 2 | private names: string[] = []; 3 | private values = []; 4 | private value = null; 5 | 6 | constructor(xml: string = '') { 7 | // 非转义普通文本|开始标签|结束标签|CDATA内容|缺少CDATA结束标记|非法字符(<)|转义字符|非法字符(&)|非法字符串(]]>)|剩余普通文本 8 | const regex = /([^]*?)(?:<(?:(\w+)>|\/(\w+)>|!\[CDATA\[(?:([^]*?)\]\]>|())|())|&(?:(\w+);|())|(\]\]\>))|([^]+)$/g; 9 | let match: RegExpMatchArray; 10 | while (match = regex.exec(xml)) { 11 | // 非转义普通文本 12 | if (match[1]) this.text(match[1]); 13 | // 开始标签 14 | if (match[2]) this.start(match[2]); 15 | // 结束标签 16 | if (match[3]) this.end(match[3]); 17 | // CDATA内容 18 | if (typeof match[4] !== 'undefined') this.text(match[4]); 19 | // 缺少CDATA结束标记 20 | if (typeof match[5] !== 'undefined') throw new Error('缺少CDATA结束标记'); 21 | // 非法字符(<) 22 | if (typeof match[6] !== 'undefined') throw new Error('非法字符:<'); 23 | // 转义字符 24 | if (match[7]) { 25 | switch (match[7]) { 26 | case 'amp': 27 | this.text('&'); 28 | break; 29 | case 'lt': 30 | this.text('<'); 31 | break; 32 | case 'gt': 33 | this.text('>'); 34 | break; 35 | case 'apos': 36 | this.text("'"); 37 | break; 38 | case 'quot': 39 | this.text('"'); 40 | break; 41 | default: 42 | throw new Error(`未知的实体名称:${match[7]}`); 43 | } 44 | } 45 | // 非法字符(&) 46 | if (typeof match[8] !== 'undefined') throw new Error('字符“&”只能用于构成转义字符'); 47 | // 非法字符串(]]>) 48 | if (typeof match[9] !== 'undefined') throw new Error('字符序列“]]>”不能出现在内容中'); 49 | // 剩余普通文本 50 | if (match[10]) this.text(match[10]); 51 | } 52 | } 53 | 54 | start(name: string) { 55 | if (typeof this.value === 'string') { 56 | if (this.value.trim()) { 57 | throw new Error('兄弟节点中不能同时含有文本节点和标签节点'); 58 | } 59 | this.value = null; 60 | } 61 | if (name.toLowerCase() === 'item') { 62 | this.value = this.value || []; 63 | if (!Array.isArray(this.value)) { 64 | throw new Error('标签用于表示数组项,因此不能和其他普通标签成为兄弟节点'); 65 | } 66 | } else { 67 | this.value = this.value || {}; 68 | if (Array.isArray(this.value)) { 69 | throw new Error('标签用于表示数组项,因此不能和其他普通标签成为兄弟节点'); 70 | } 71 | if (this.value.hasOwnProperty(name)) { 72 | throw new Error(`标签<${name}>在兄弟节点中出现了多次`); 73 | } 74 | } 75 | this.names.push(name); 76 | this.values.push(this.value); 77 | this.value = null; 78 | } 79 | 80 | text(content: string) { 81 | this.value = this.value || ''; 82 | if (typeof this.value !== 'string') { 83 | if (content.trim()) { 84 | throw new Error('兄弟节点中不能同时含有标签节点和文本节点'); 85 | } 86 | } else { 87 | this.value += content; 88 | } 89 | } 90 | 91 | end(name: string) { 92 | if (name !== this.names.pop()) { 93 | throw new Error('开始标签和结束标签不匹配'); 94 | } 95 | const value = this.values.pop(); 96 | if (name.toLowerCase() === 'item') { 97 | value.push(this.value); 98 | } else { 99 | value[name] = this.value; 100 | } 101 | this.value = value; 102 | } 103 | 104 | build() { 105 | if (this.names.length > 0) { 106 | throw new Error('部分标签没有闭合'); 107 | } 108 | return this.value; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/x2o.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const { x2o } = require('../util'); 3 | 4 | describe('#xml2obj', () => { 5 | describe('#parse string', () => { 6 | it('#basic string', () => { 7 | const text = 'This is a basic string.'; 8 | should(x2o(text)).eql(text); 9 | }) 10 | 11 | it('#string with "&" should throw error', () => { 12 | should(() => x2o('string with "&"')).throwError(); 13 | }) 14 | 15 | it('#string with "<" should throw error', () => { 16 | should(() => x2o('string with "<"')).throwError(); 17 | }) 18 | 19 | it('#string with "]]>" should throw error', () => { 20 | should(() => x2o('string with "]]>"')).throwError(); 21 | }) 22 | 23 | it(`#entities (&<>'")`, () => { 24 | should(x2o('&<>'"')).eql(`&<>'"`); 25 | }) 26 | 27 | it('cdata', () => { 28 | should(x2o('before]]>end')).eql('beforeend'); 29 | }) 30 | 31 | it('cdata without ending should throw error', () => { 32 | should(() => x2o(' { 36 | should(() => x2o('&entity;')).throwError(); 37 | }) 38 | }) 39 | 40 | describe('#parse empty node', () => { 41 | it('#empty string should become null', () => { 42 | should(x2o('')).eql(null); 43 | }) 44 | 45 | it('#the value of empty node should be null', () => { 46 | should(x2o('')).eql({ xml: null }); 47 | }) 48 | 49 | it('#the value of node noly contains whitespace should be text', () => { 50 | should(x2o(' ')).eql({ xml: ' ' }); 51 | should(x2o(' ')).eql({ xml: ' ' }); 52 | should(x2o('\n')).eql({ xml: '\n' }); 53 | should(x2o('\r')).eql({ xml: '\r' }); 54 | should(x2o('\t')).eql({ xml: '\t' }); 55 | should(x2o('')).eql({ xml: ' ' }); 56 | }) 57 | 58 | it('#the value of empty cdata node should become empty string', () => { 59 | should(x2o('')).eql({ xml: '' }); 60 | }) 61 | }) 62 | 63 | describe('#parse array', () => { 64 | it('#empty array', () => { 65 | should(x2o('')).eql([null]); 66 | }) 67 | 68 | it('#one dimensional array', () => { 69 | should(x2o('ab')).eql(['a', 'b']); 70 | }) 71 | 72 | it('#nested array', () => { 73 | should(x2o('1 ')).eql([['1', null], ' ']) 74 | }) 75 | 76 | it('#complex array', () => { 77 | should(x2o('')).eql({ xml: [{ node: [[null]] }] }); 78 | }) 79 | 80 | it('#mix item with common tags should throw error', () => { 81 | should(() => x2o('')).throwError(); 82 | }) 83 | 84 | it('#mix item with text should throw error', () => { 85 | should(() => x2o('text')).throwError(); 86 | should(() => x2o('text')).throwError(); 87 | }) 88 | }) 89 | 90 | describe('#parse normal xml', () => { 91 | it('#xml with normal whitespace', () => { 92 | const xml = ` 93 | 94 | a 95 | c 96 | b 97 | 98 | `; 99 | should(x2o(xml)).eql({ xml: { a: 'a', b: 'c', c: 'b' } }); 100 | }) 101 | 102 | it('#xml with more than one root tag', () => { 103 | should(x2o('')).eql({ a: null, b: null }); 104 | }) 105 | 106 | it('#tags with same name in siblings should cause error', () => { 107 | should(() => x2o('')).throwError(); 108 | should(() => x2o('')).throwError(); 109 | should(() => x2o('')).throwError(); 110 | }) 111 | 112 | it('#tags not matching should throw error', () => { 113 | should(() => x2o('')).throwError(); 114 | should(() => x2o('')).throwError(); 115 | should(() => x2o('')).throwError(); 116 | should(() => x2o(' x2o('')).throwError(); 118 | should(() => x2o('')).throwError(); 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /util/x2o-builder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var Builder = /** @class */ (function () { 4 | function Builder(xml) { 5 | if (xml === void 0) { xml = ''; } 6 | this.names = []; 7 | this.values = []; 8 | this.value = null; 9 | // 非转义普通文本|开始标签|结束标签|CDATA内容|缺少CDATA结束标记|非法字符(<)|转义字符|非法字符(&)|非法字符串(]]>)|剩余普通文本 10 | var regex = /([^]*?)(?:<(?:(\w+)>|\/(\w+)>|!\[CDATA\[(?:([^]*?)\]\]>|())|())|&(?:(\w+);|())|(\]\]\>))|([^]+)$/g; 11 | var match; 12 | while (match = regex.exec(xml)) { 13 | // 非转义普通文本 14 | if (match[1]) 15 | this.text(match[1]); 16 | // 开始标签 17 | if (match[2]) 18 | this.start(match[2]); 19 | // 结束标签 20 | if (match[3]) 21 | this.end(match[3]); 22 | // CDATA内容 23 | if (typeof match[4] !== 'undefined') 24 | this.text(match[4]); 25 | // 缺少CDATA结束标记 26 | if (typeof match[5] !== 'undefined') 27 | throw new Error('缺少CDATA结束标记'); 28 | // 非法字符(<) 29 | if (typeof match[6] !== 'undefined') 30 | throw new Error('非法字符:<'); 31 | // 转义字符 32 | if (match[7]) { 33 | switch (match[7]) { 34 | case 'amp': 35 | this.text('&'); 36 | break; 37 | case 'lt': 38 | this.text('<'); 39 | break; 40 | case 'gt': 41 | this.text('>'); 42 | break; 43 | case 'apos': 44 | this.text("'"); 45 | break; 46 | case 'quot': 47 | this.text('"'); 48 | break; 49 | default: 50 | throw new Error("\u672A\u77E5\u7684\u5B9E\u4F53\u540D\u79F0\uFF1A" + match[7]); 51 | } 52 | } 53 | // 非法字符(&) 54 | if (typeof match[8] !== 'undefined') 55 | throw new Error('字符“&”只能用于构成转义字符'); 56 | // 非法字符串(]]>) 57 | if (typeof match[9] !== 'undefined') 58 | throw new Error('字符序列“]]>”不能出现在内容中'); 59 | // 剩余普通文本 60 | if (match[10]) 61 | this.text(match[10]); 62 | } 63 | } 64 | Builder.prototype.start = function (name) { 65 | if (typeof this.value === 'string') { 66 | if (this.value.trim()) { 67 | throw new Error('兄弟节点中不能同时含有文本节点和标签节点'); 68 | } 69 | this.value = null; 70 | } 71 | if (name.toLowerCase() === 'item') { 72 | this.value = this.value || []; 73 | if (!Array.isArray(this.value)) { 74 | throw new Error('标签用于表示数组项,因此不能和其他普通标签成为兄弟节点'); 75 | } 76 | } 77 | else { 78 | this.value = this.value || {}; 79 | if (Array.isArray(this.value)) { 80 | throw new Error('标签用于表示数组项,因此不能和其他普通标签成为兄弟节点'); 81 | } 82 | if (this.value.hasOwnProperty(name)) { 83 | throw new Error("\u6807\u7B7E<" + name + ">\u5728\u5144\u5F1F\u8282\u70B9\u4E2D\u51FA\u73B0\u4E86\u591A\u6B21"); 84 | } 85 | } 86 | this.names.push(name); 87 | this.values.push(this.value); 88 | this.value = null; 89 | }; 90 | Builder.prototype.text = function (content) { 91 | this.value = this.value || ''; 92 | if (typeof this.value !== 'string') { 93 | if (content.trim()) { 94 | throw new Error('兄弟节点中不能同时含有标签节点和文本节点'); 95 | } 96 | } 97 | else { 98 | this.value += content; 99 | } 100 | }; 101 | Builder.prototype.end = function (name) { 102 | if (name !== this.names.pop()) { 103 | throw new Error('开始标签和结束标签不匹配'); 104 | } 105 | var value = this.values.pop(); 106 | if (name.toLowerCase() === 'item') { 107 | value.push(this.value); 108 | } 109 | else { 110 | value[name] = this.value; 111 | } 112 | this.value = value; 113 | }; 114 | Builder.prototype.build = function () { 115 | if (this.names.length > 0) { 116 | throw new Error('部分标签没有闭合'); 117 | } 118 | return this.value; 119 | }; 120 | return Builder; 121 | }()); 122 | exports.default = Builder; 123 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { sign, x2o, o2x, tryThrow } from './util'; 2 | import { createDecipheriv, pseudoRandomBytes, createCipheriv } from 'crypto'; 3 | 4 | class WXBizMsgCrypt { 5 | static readonly sign = sign; 6 | static readonly x2o = x2o; 7 | static readonly o2x = o2x; 8 | 9 | private aesKey: Buffer; 10 | private iv: Buffer; 11 | 12 | /** 13 | * 构造函数 14 | * @param token 公众号或企业微信Token 15 | * @param encodingAESKey 用于消息体的加密 16 | * @param appid 公众号的AppID或企业微信的CropID 17 | */ 18 | constructor( 19 | private token: string, 20 | encodingAESKey: string, 21 | private appid: string 22 | ) { 23 | this.aesKey = Buffer.from(encodingAESKey, 'base64'); 24 | this.iv = this.aesKey.slice(0, 16); 25 | } 26 | 27 | /** 28 | * 将密文翻译成明文 29 | * @param msgSignature 消息体签名 30 | * @param timestamp 时间戳 31 | * @param nonce 用于签名的随机字符串 32 | * @param msgEncrypt 消息体(Base64编码的密文) 33 | */ 34 | private decrypt( 35 | msgSignature: string, 36 | timestamp: string, 37 | nonce: string, 38 | msgEncrypt: string 39 | ) { 40 | // 校验消息体签名 41 | if (msgSignature !== sign(this.token, timestamp, nonce, msgEncrypt)) { 42 | tryThrow(-40001, '签名验证错误'); 43 | } 44 | 45 | // AES解密 46 | const decipher = tryThrow(-40004, 'AESKey 非法', () => createDecipheriv('aes-256-cbc', this.aesKey, this.iv).setAutoPadding(false)); 47 | let buffer = tryThrow( 48 | -40007, 'AES 解密失败', 49 | () => Buffer.concat([ 50 | decipher.update(msgEncrypt, 'base64'), 51 | decipher.final() 52 | ]) 53 | ); 54 | 55 | // 去除开头的随机字符串[16字节] 56 | buffer = buffer.slice(16); 57 | 58 | // 获取消息明文长度[4字节] 59 | const msgLen = buffer.readInt32BE(0); 60 | buffer = buffer.slice(4); 61 | 62 | // 获取消息明文[msgLen字节] 63 | const msgDecrypt = buffer.slice(0, msgLen).toString(); 64 | buffer = buffer.slice(msgLen); 65 | 66 | // 获取尾部填充部分的长度 67 | const padLen = buffer.slice(-1)[0]; 68 | 69 | // 去除尾部填充部分[padLen字节] 70 | buffer = buffer.slice(0, -padLen); 71 | 72 | // 校验AppID(CropID) 73 | const appid = buffer.toString(); 74 | if (appid !== this.appid) { 75 | tryThrow(-40005, 'appid/corpid 校验错误'); 76 | } 77 | 78 | return msgDecrypt; 79 | } 80 | 81 | /** 82 | * 验证URL函数(仅用于企业微信) 83 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 84 | * @param timestamp 从接收消息的URL中获取的timestamp参数 85 | * @param nonce 从接收消息的URL中获取的nonce参数 86 | * @param echostr 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值 87 | * @return 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理 88 | */ 89 | verifyURL( 90 | msgSignature: string, 91 | timestamp: string, 92 | nonce: string, 93 | echostr: string 94 | ): string { 95 | return this.decrypt.apply(this, arguments); 96 | } 97 | 98 | /** 99 | * 解密函数 100 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 101 | * @param timestamp 从接收消息的URL中获取的timestamp参数 102 | * @param nonce 从接收消息的URL中获取的nonce参数 103 | * @param postData 从接收消息的URL中获取的整个post数据 104 | * @return 解密后的msg,以xml组织 105 | */ 106 | decryptMsg( 107 | msgSignature: string, 108 | timestamp: string, 109 | nonce: string, 110 | postData: string 111 | ) { 112 | return this.decrypt( 113 | msgSignature, 114 | timestamp, 115 | nonce, 116 | tryThrow(-40002, 'xml解析失败', () => x2o(postData).xml.Encrypt) 117 | ); 118 | } 119 | 120 | /** 121 | * 加密函数 122 | * @param replyMsg 返回的消息体原文 123 | * @param timestamp 时间戳,调用方生成 124 | * @param nonce 随机字符串,调用方生成 125 | * @return 用于返回的密文,以xml组织 126 | */ 127 | encryptMsg( 128 | replyMsg: string, 129 | timestamp: string, 130 | nonce: string 131 | ) { 132 | // 生成随机字符串[16字节] 133 | const random16 = pseudoRandomBytes(16); 134 | 135 | // 消息明文 136 | const msgDecrypt = Buffer.from(replyMsg); 137 | 138 | // 消息明文长度 139 | const msgLen = Buffer.alloc(4); 140 | msgLen.writeInt32BE(msgDecrypt.length, 0); 141 | 142 | // AppID(或CropID) 143 | const appid = Buffer.from(this.appid); 144 | 145 | // 计算填充长度 146 | const rawLen = [ 147 | random16, 148 | msgLen, 149 | msgDecrypt, 150 | appid 151 | ].map( 152 | buffer => buffer.length 153 | ).reduce((prev, next) => prev + next); 154 | const padLen = 32 - rawLen % 32; 155 | 156 | // 尾部填充部分 157 | const padding = Buffer.alloc(padLen, padLen); 158 | 159 | // AES加密 160 | const cipher = tryThrow(-40004, 'AESKey 非法', () => createCipheriv('aes-256-cbc', this.aesKey, this.iv).setAutoPadding(false)); 161 | const msgEncrypt = tryThrow( 162 | -40006, 'AES 加密失败', 163 | () => Buffer.concat([ 164 | cipher.update(random16), 165 | cipher.update(msgLen), 166 | cipher.update(msgDecrypt), 167 | cipher.update(appid), 168 | cipher.update(padding), 169 | cipher.final() 170 | ]).toString('base64') 171 | ); 172 | 173 | // 生成消息密文 174 | const msgSignature = tryThrow(-40003, 'sha加密生成签名失败', () => sign(this.token, timestamp, nonce, msgEncrypt)); 175 | return tryThrow( 176 | -40011, '生成xml失败', 177 | () => o2x({ 178 | xml: { 179 | Encrypt: msgEncrypt, 180 | MsgSignature: msgSignature, 181 | TimeStamp: timestamp, 182 | Nonce: nonce 183 | } 184 | }) 185 | ); 186 | } 187 | } 188 | 189 | export = WXBizMsgCrypt; 190 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var util_1 = require("./util"); 3 | var crypto_1 = require("crypto"); 4 | var WXBizMsgCrypt = /** @class */ (function () { 5 | /** 6 | * 构造函数 7 | * @param token 公众号或企业微信Token 8 | * @param encodingAESKey 用于消息体的加密 9 | * @param appid 公众号的AppID或企业微信的CropID 10 | */ 11 | function WXBizMsgCrypt(token, encodingAESKey, appid) { 12 | this.token = token; 13 | this.appid = appid; 14 | this.aesKey = Buffer.from(encodingAESKey, 'base64'); 15 | this.iv = this.aesKey.slice(0, 16); 16 | } 17 | /** 18 | * 将密文翻译成明文 19 | * @param msgSignature 消息体签名 20 | * @param timestamp 时间戳 21 | * @param nonce 用于签名的随机字符串 22 | * @param msgEncrypt 消息体(Base64编码的密文) 23 | */ 24 | WXBizMsgCrypt.prototype.decrypt = function (msgSignature, timestamp, nonce, msgEncrypt) { 25 | var _this = this; 26 | // 校验消息体签名 27 | if (msgSignature !== util_1.sign(this.token, timestamp, nonce, msgEncrypt)) { 28 | util_1.tryThrow(-40001, '签名验证错误'); 29 | } 30 | // AES解密 31 | var decipher = util_1.tryThrow(-40004, 'AESKey 非法', function () { return crypto_1.createDecipheriv('aes-256-cbc', _this.aesKey, _this.iv).setAutoPadding(false); }); 32 | var buffer = util_1.tryThrow(-40007, 'AES 解密失败', function () { return Buffer.concat([ 33 | decipher.update(msgEncrypt, 'base64'), 34 | decipher.final() 35 | ]); }); 36 | // 去除开头的随机字符串[16字节] 37 | buffer = buffer.slice(16); 38 | // 获取消息明文长度[4字节] 39 | var msgLen = buffer.readInt32BE(0); 40 | buffer = buffer.slice(4); 41 | // 获取消息明文[msgLen字节] 42 | var msgDecrypt = buffer.slice(0, msgLen).toString(); 43 | buffer = buffer.slice(msgLen); 44 | // 获取尾部填充部分的长度 45 | var padLen = buffer.slice(-1)[0]; 46 | // 去除尾部填充部分[padLen字节] 47 | buffer = buffer.slice(0, -padLen); 48 | // 校验AppID(CropID) 49 | var appid = buffer.toString(); 50 | if (appid !== this.appid) { 51 | util_1.tryThrow(-40005, 'appid/corpid 校验错误'); 52 | } 53 | return msgDecrypt; 54 | }; 55 | /** 56 | * 验证URL函数(仅用于企业微信) 57 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 58 | * @param timestamp 从接收消息的URL中获取的timestamp参数 59 | * @param nonce 从接收消息的URL中获取的nonce参数 60 | * @param echostr 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值 61 | * @return 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理 62 | */ 63 | WXBizMsgCrypt.prototype.verifyURL = function (msgSignature, timestamp, nonce, echostr) { 64 | return this.decrypt.apply(this, arguments); 65 | }; 66 | /** 67 | * 解密函数 68 | * @param msgSignature 从接收消息的URL中获取的msg_signature参数 69 | * @param timestamp 从接收消息的URL中获取的timestamp参数 70 | * @param nonce 从接收消息的URL中获取的nonce参数 71 | * @param postData 从接收消息的URL中获取的整个post数据 72 | * @return 解密后的msg,以xml组织 73 | */ 74 | WXBizMsgCrypt.prototype.decryptMsg = function (msgSignature, timestamp, nonce, postData) { 75 | return this.decrypt(msgSignature, timestamp, nonce, util_1.tryThrow(-40002, 'xml解析失败', function () { return util_1.x2o(postData).xml.Encrypt; })); 76 | }; 77 | /** 78 | * 加密函数 79 | * @param replyMsg 返回的消息体原文 80 | * @param timestamp 时间戳,调用方生成 81 | * @param nonce 随机字符串,调用方生成 82 | * @return 用于返回的密文,以xml组织 83 | */ 84 | WXBizMsgCrypt.prototype.encryptMsg = function (replyMsg, timestamp, nonce) { 85 | var _this = this; 86 | // 生成随机字符串[16字节] 87 | var random16 = crypto_1.pseudoRandomBytes(16); 88 | // 消息明文 89 | var msgDecrypt = Buffer.from(replyMsg); 90 | // 消息明文长度 91 | var msgLen = Buffer.alloc(4); 92 | msgLen.writeInt32BE(msgDecrypt.length, 0); 93 | // AppID(或CropID) 94 | var appid = Buffer.from(this.appid); 95 | // 计算填充长度 96 | var rawLen = [ 97 | random16, 98 | msgLen, 99 | msgDecrypt, 100 | appid 101 | ].map(function (buffer) { return buffer.length; }).reduce(function (prev, next) { return prev + next; }); 102 | var padLen = 32 - rawLen % 32; 103 | // 尾部填充部分 104 | var padding = Buffer.alloc(padLen, padLen); 105 | // AES加密 106 | var cipher = util_1.tryThrow(-40004, 'AESKey 非法', function () { return crypto_1.createCipheriv('aes-256-cbc', _this.aesKey, _this.iv).setAutoPadding(false); }); 107 | var msgEncrypt = util_1.tryThrow(-40006, 'AES 加密失败', function () { return Buffer.concat([ 108 | cipher.update(random16), 109 | cipher.update(msgLen), 110 | cipher.update(msgDecrypt), 111 | cipher.update(appid), 112 | cipher.update(padding), 113 | cipher.final() 114 | ]).toString('base64'); }); 115 | // 生成消息密文 116 | var msgSignature = util_1.tryThrow(-40003, 'sha加密生成签名失败', function () { return util_1.sign(_this.token, timestamp, nonce, msgEncrypt); }); 117 | return util_1.tryThrow(-40011, '生成xml失败', function () { return util_1.o2x({ 118 | xml: { 119 | Encrypt: msgEncrypt, 120 | MsgSignature: msgSignature, 121 | TimeStamp: timestamp, 122 | Nonce: nonce 123 | } 124 | }); }); 125 | }; 126 | WXBizMsgCrypt.sign = util_1.sign; 127 | WXBizMsgCrypt.x2o = util_1.x2o; 128 | WXBizMsgCrypt.o2x = util_1.o2x; 129 | return WXBizMsgCrypt; 130 | }()); 131 | module.exports = WXBizMsgCrypt; 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxcrypt 2 | 3 | 微信公众号和企业号接收消息和事件时用于加解密的类,即官方的`WXBizMsgCrypt`类。(NodeJS版本) 4 | 5 | ## 目录 6 | 7 | - [安装](#安装) 8 | - [引入](#引入) 9 | - [ES6](#es6) 10 | - [Typescript](#typescript) 11 | - [NodeJS](#nodejs) 12 | - [使用](#使用) 13 | - [初始化加解密类](#初始化加解密类) 14 | - [验证URL函数](#验证url函数) 15 | - [解密函数](#解密函数) 16 | - [加密函数](#加密函数) 17 | - [错误处理](#错误处理) 18 | - [辅助函数](#辅助函数) 19 | - [签名函数-`sign`](#签名函数-sign) 20 | - [对象转XML字符串-`o2x`](#对象转xml字符串-o2x) 21 | - [XML字符串转对象-`x2o`](#xml字符串转对象-x2o) 22 | 23 | ## 安装 24 | 25 | ```bash 26 | $ npm install wxcrypt 27 | ``` 28 | 29 | ## 引入 30 | 31 | ### ES6: 32 | 33 | ```js 34 | import * as WXBizMsgCrypt from 'wxcrypt'; 35 | ``` 36 | 37 | ### Typescript: 38 | 39 | ```ts 40 | import WXBizMsgCrypt = require('wxcrypt'); 41 | ``` 42 | 43 | ### NodeJS: 44 | 45 | ```js 46 | const WXBizMsgCrypt = require('wxcrypt'); 47 | ``` 48 | 49 | ## 使用 50 | 51 | ### 初始化加解密类 52 | 53 | ```js 54 | /** 55 | * @param {string} token 公众号或企业微信Token 56 | * @param {string} encodingAESKey 用于消息体的加密 57 | * @param {string} appid 公众号的AppID或企业微信的CropID 58 | */ 59 | new WXBizMsgCrypt(token, encodingAESKey, appid); 60 | ``` 61 | 62 | ### 验证URL函数 63 | 64 | > __注意:本方法仅企业微信可用__ 65 | 66 | 本函数实现: 67 | 68 | 1. 签名校验 69 | 2. 解密数据包,得到明文消息内容 70 | 71 | ```js 72 | /** 73 | * 验证URL函数(仅用于企业微信) 74 | * @param {string} msgSignature 从接收消息的URL中获取的msg_signature参数 75 | * @param {string} timestamp 从接收消息的URL中获取的timestamp参数 76 | * @param {string} nonce 从接收消息的URL中获取的nonce参数 77 | * @param {string} echostr 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值 78 | * @return {string} 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理 79 | */ 80 | verifyURL(msgSignature, timestamp, nonce, echostr) 81 | ``` 82 | 83 | ### 解密函数 84 | 85 | 本函数实现: 86 | 87 | 1. 签名校验 88 | 2. 解密数据包,得到明文消息结构体 89 | 90 | ```js 91 | /** 92 | * @param {string} msgSignature 从接收消息的URL中获取的msg_signature参数 93 | * @param {string} timestamp 从接收消息的URL中获取的timestamp参数 94 | * @param {string} nonce 从接收消息的URL中获取的nonce参数 95 | * @param {string} postData 从接收消息的URL中获取的整个post数据 96 | * @return {string} 解密后的msg,以xml组织 97 | */ 98 | decryptMsg(msgSignature, timestamp, nonce, postData) 99 | ``` 100 | 101 | ### 加密函数 102 | 103 | 本函数实现: 104 | 105 | 1. 加密明文消息结构体 106 | 2. 生成签名 107 | 3. 构造被动响应包 108 | 109 | ```js 110 | /** 111 | * 加密函数 112 | * @param {string} replyMsg 返回的消息体原文 113 | * @param {string} timestamp 时间戳,调用方生成 114 | * @param {string} nonce 随机字符串,调用方生成 115 | * @return {string} 用于返回的密文,以xml组织 116 | */ 117 | encryptMsg(replyMsg, timestamp, nonce) 118 | ``` 119 | 120 | ### 错误处理 121 | 122 | 调用方法时,如有错误,则会在错误对象上加上两个属性: 123 | 124 | - `errcode` 数字类型的错误码 125 | - `errmsg` 错误描述 126 | 127 | 目前支持的错误码如下: 128 | 129 | | 错误码 | 错误描述 | 130 | | ----- | ------- | 131 | | -40001 | 签名验证错误 | 132 | | -40002 | xml解析失败 | 133 | | -40003 | sha加密生成签名失败 | 134 | | -40004 | AESKey 非法 | 135 | | -40005 | appid/corpid 校验错误 | 136 | | -40006 | AES 加密失败 | 137 | | -40007 | AES 解密失败 | 138 | | -400011 | 生成xml失败 | 139 | 140 | --- 141 | 142 | > __相关链接__: 143 | > 144 | > 1. 微信公众平台技术文档[《消息加解密接入指引》](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318479&token=&lang=zh_CN) 145 | > 2. 企业微信开发文档[《加解密方案说明》](https://work.weixin.qq.com/api/doc#12976) 146 | 147 | ## 辅助函数 148 | 149 | 除了对WXBizMsgCrypt的实现,本项目还提供几个辅助函数。 150 | 151 | ### 签名函数-`sign` 152 | 153 | 传入若干个字符串,用于生成签名。可用于公众号url签名校验。具体算法为: 154 | 155 | ```js 156 | sha1(sort(str1、str2、...)) 157 | ``` 158 | 159 | #### 引入 160 | 161 | ```js 162 | import { sign } from 'wxcrypt'; // ES6 163 | const { sign } = require('wxcrypt'); // CommonJS 164 | ``` 165 | 166 | #### 使用 167 | 168 | ```ts 169 | sign(...args: string[]): string; 170 | ``` 171 | 172 | ### 对象转XML字符串-`o2x` 173 | 174 | 传入任意对象,生成xml字符串。 175 | 176 | > __注意:__ 177 | > 178 | > 1. 支持的基本类型有`string`和`number`,非法类型和`null`会被转成空字符串 179 | > 2. 最外层可以是对象、数组或基本类型 180 | > 3. 数组项会用``标签包起来 181 | 182 | #### 引入 183 | 184 | ```js 185 | import { o2x } from 'wxcrypt'; // ES6 186 | const { o2x } = require('wxcrypt'); // CommonJS 187 | ``` 188 | 189 | #### 使用 190 | 191 | ```ts 192 | o2x(obj: any): string 193 | ``` 194 | 195 | #### 示例1:数组的处理 196 | 197 | 数组项将用``标签包裹,并成为兄弟节点 198 | 199 | ```js 200 | o2x({ 201 | xml: { 202 | timestamp: 1536123965810, 203 | articles: [ 204 | { 205 | title: 'Article1', 206 | desc: 'Description1' 207 | }, 208 | { 209 | title: 'Article2', 210 | desc: 'Description2' 211 | } 212 | ] 213 | } 214 | }) 215 | ``` 216 | 217 | 将返回如下字符串(格式化之后): 218 | 219 | ```xml 220 | 221 | 1536123965810 222 | 223 | 224 | Article1 225 | Description1 226 | 227 | 228 | Article2 229 | Description2 230 | 231 | 232 | 233 | ``` 234 | 235 | #### 示例2:特殊字符的处理 236 | 237 | 函数自动判断特殊字符的处理方式(使用CDATA或转义),具体如下: 238 | 239 | 1. 含有特殊字符(`<>&'"`)时,使用CDATA处理 240 | 2. 含有`]]>`时,由于会和CDATA冲突,使用转义处理 241 | 242 | ```js 243 | o2x({ 244 | xml: { 245 | // 没有特殊字符,不处理 246 | type: 'video', 247 | // 引号是特殊字符,使用CDATA处理 248 | title: '"愤怒"的小鸟', 249 | // 含有"]]>",需要转义 250 | description: ']]><[[' 251 | } 252 | }) 253 | ``` 254 | 255 | 将返回如下字符串(格式化之后): 256 | 257 | ```xml 258 | 259 | video 260 | <![CDATA["愤怒"的小鸟]]> 261 | ]]><[[ 262 | 263 | ``` 264 | 265 | ### XML字符串转对象-`x2o` 266 | 267 | 传入xml字符串,生成js对象 268 | 269 | > __注意:__ 270 | > 271 | > 1. 虽然xml最外层应该只有一个根节点,但这不是强制的 272 | > 2. 除了``标签,兄弟节点的标签名不可以相同 273 | > 3. ``标签代表数组项,不可以与其他标签成为兄弟节点 274 | > 4. 所有的文本节点都会转化为字符串(而不是数字或布尔类型) 275 | 276 | #### 引入 277 | 278 | ```js 279 | import { x2o } from 'wxcrypt'; // ES6 280 | const { x2o } = require('wxcrypt'); // CommonJS 281 | ``` 282 | 283 | #### 使用 284 | 285 | ```ts 286 | x2o(xml: string): any 287 | ``` 288 | 289 | #### 示例:``标签节点的处理 290 | 291 | 每个``标签节点将转成一个数组项,且支持嵌套 292 | 293 | ```js 294 | x2o(` 295 | 1536123965810 296 | 297 | 298 | Article1 299 | Description1 300 | 301 | 302 | Article2 303 | Description2 304 | 305 | 306 | `) 307 | ``` 308 | 309 | 将返回如下对象: 310 | 311 | ```js 312 | { 313 | xml: { 314 | timestamp: '1536123965810', 315 | articles: [{ 316 | title: 'Article1', 317 | desc: 'Description1' 318 | }, { 319 | title: 'Article2', 320 | desc: 'Description2' 321 | }] 322 | } 323 | } 324 | ``` 325 | --------------------------------------------------------------------------------