├── index.js ├── .gitignore ├── .travis.yml ├── .npmignore ├── test ├── config.js └── crypto.test.js ├── .jshintrc ├── Makefile ├── package.json ├── README.md ├── MIT-License └── lib └── msg_crypto.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/msg_crypto'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | example 5 | .DS_Store 6 | coverage 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | script: make test-coveralls 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | doc 3 | figures 4 | test 5 | node_modules 6 | .gitignore 7 | .jshintrc 8 | .npmignore 9 | .travis.yml 10 | Makefile 11 | MIT-License 12 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | corpid: 'wx1570bcc0b3cf8bd0', 3 | corpsecret: 'yy3S_IBbELpalg9HJbbWj3wnWtc-u8vwz0lT7x2fI80UKAHKdxZtS8IyYc-7jdeA', 4 | token: 'RMNlACHlV5ThzfRlVS4D4', 5 | encodingAESKey: 'bAZC8S8nz5fcQ0Ui2HxQVGbC6FzyrzWKSuDbmzpBo3G' 6 | }; 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "module", 5 | "require", 6 | "__dirname", 7 | "process", 8 | "console", 9 | "it", 10 | "xit", 11 | "describe", 12 | "xdescribe", 13 | "before", 14 | "beforeEach", 15 | "after", 16 | "afterEach" 17 | ], 18 | 19 | "node": true, 20 | "bitwise": true, 21 | "curly": true, 22 | "eqeqeq": true, 23 | "forin": false, 24 | "immed": true, 25 | "latedef": true, 26 | "noarg": true, 27 | "noempty": true, 28 | "nonew": true, 29 | "plusplus": false, 30 | "undef": true, 31 | "strict": false, 32 | "trailing": false, 33 | "globalstrict": true, 34 | "nonstandard": true, 35 | "white": false, 36 | "indent": 2, 37 | "expr": true, 38 | "multistr": true, 39 | "onevar": false, 40 | "unused": "vars" 41 | } 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = spec 3 | TIMEOUT = 2000 4 | ISTANBUL = ./node_modules/.bin/istanbul 5 | MOCHA = ./node_modules/mocha/bin/_mocha 6 | COVERALLS = ./node_modules/coveralls/bin/coveralls.js 7 | 8 | debug: 9 | @NODE_ENV=test ./node_modules/.bin/node-debug $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 10 | $(MOCHA_OPTS) \ 11 | $(TESTS) 12 | 13 | test: 14 | @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \ 15 | $(MOCHA_OPTS) \ 16 | $(TESTS) 17 | 18 | test-cov: 19 | @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 20 | 21 | test-coveralls: 22 | @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS) 23 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 24 | @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage 25 | 26 | test-all: test test-coveralls 27 | 28 | .PHONY: test 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-crypto", 3 | "version": "0.0.2", 4 | "description": "微信公共平台加解密库", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-all" 8 | }, 9 | "config":{ 10 | "travis-cov": { 11 | "threshold": 98 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/node-webot/wechat-crypto.git" 17 | }, 18 | "keywords": [ 19 | "weixin", 20 | "wechat", 21 | "enterprise" 22 | ], 23 | "devDependencies": { 24 | "mocha": "*", 25 | "expect.js": "*", 26 | "travis-cov": "*", 27 | "coveralls": "*", 28 | "mocha-lcov-reporter": "*", 29 | "rewire": "*", 30 | "istanbul": "*" 31 | }, 32 | "author": "Jackson Tian", 33 | "license": "MIT", 34 | "readmeFilename": "README.md", 35 | "directories": { 36 | "test": "test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wechat crypto 2 | ===================== 3 | 4 | 微信公共平台加解密库 5 | 6 | ## 模块状态 7 | - [![NPM version](https://badge.fury.io/js/wechat-crypto.png)](http://badge.fury.io/js/wechat-crypto) 8 | - [![Build Status](https://travis-ci.org/node-webot/wechat-crypto.png?branch=master)](https://travis-ci.org/node-webot/wechat-crypto) 9 | - [![Dependencies Status](https://david-dm.org/node-webot/wechat-crypto.png)](https://david-dm.org/node-webot/wechat-crypto) 10 | - [![Coverage Status](https://coveralls.io/repos/node-webot/wechat-crypto/badge.png)](https://coveralls.io/r/node-webot/wechat-crypto) 11 | 12 | ## 详细文档 13 | - [文档主页](http://node-webot.github.io/wechat-crypto/index.html) 14 | - [API文档](http://node-webot.github.io/wechat-crypto/api.html) 15 | - 代码[测试覆盖率](http://node-webot.github.io/wechat-crypto/coverage/index.html) 16 | 17 | ## License 18 | The MIT license. 19 | 20 | ## 交流群 21 | QQ群:157964097,使用疑问,开发,贡献代码请加群。 22 | 23 | ## 捐赠 24 | 如果您觉得本模块对您有帮助,欢迎请作者一杯咖啡 25 | 26 | ![捐赠wechat](https://cloud.githubusercontent.com/assets/327019/2941591/2b9e5e58-d9a7-11e3-9e80-c25aba0a48a1.png) 27 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jackson Tian 2 | http://weibo.com/shyvo 3 | 4 | The MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /test/crypto.test.js: -------------------------------------------------------------------------------- 1 | var rewire = require('rewire'); 2 | var expect = require('expect.js'); 3 | var WXBizMsgCrypt = require('../lib/msg_crypto'); 4 | var config = require('./config'); 5 | 6 | describe('lib/msg_crypto.js', function () { 7 | it('should not ok', function () { 8 | expect(function () { 9 | new WXBizMsgCrypt(); 10 | }).to.throwException(/please check arguments/); 11 | expect(function () { 12 | new WXBizMsgCrypt('token'); 13 | }).to.throwException(/please check arguments/); 14 | expect(function () { 15 | new WXBizMsgCrypt('token', 'encodingAESKey'); 16 | }).to.throwException(/please check arguments/); 17 | expect(function () { 18 | new WXBizMsgCrypt('token', 'encodingAESKey', 'corpid'); 19 | }).to.throwException(/encodingAESKey invalid/); 20 | }); 21 | 22 | it('getSignature should ok', function () { 23 | var cryptor = new WXBizMsgCrypt(config.token, config.encodingAESKey, config.corpid); 24 | var signature = cryptor.getSignature('timestamp', 'nonce', 'encrypt'); 25 | expect(signature).to.be('8edac0945683c119ee71c9f6256d76c48aa08a96'); 26 | }); 27 | 28 | it('encrypt/decrypt should ok', function () { 29 | var cryptor = new WXBizMsgCrypt(config.token, config.encodingAESKey, config.corpid); 30 | var text = 'hehe'; 31 | var encrypted = cryptor.encrypt(text); 32 | var decrypted = cryptor.decrypt(encrypted); 33 | expect(decrypted.message).to.be(text); 34 | // expect(decrypted.corpIdFromXml).to.be(config.corpid); 35 | }); 36 | 37 | describe('PKCS7Encoder', function () { 38 | var PKCS7Encoder = rewire('../lib/msg_crypto').__get__('PKCS7Encoder'); 39 | 40 | it('encode should ok', function () { 41 | var buf = new Buffer('text'); 42 | var encoded = PKCS7Encoder.encode(buf); 43 | expect((encoded.length) % 32).to.be(0); 44 | expect((encoded[encoded.length - 1] + 4) % 32).to.be(0); 45 | }); 46 | 47 | it('decode should ok', function () { 48 | var buf = new Buffer('text'); 49 | var encoded = PKCS7Encoder.encode(buf); 50 | expect(PKCS7Encoder.decode(encoded).toString()).to.be('text'); 51 | }); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /lib/msg_crypto.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | /** 4 | * 提供基于PKCS7算法的加解密接口 5 | * 6 | */ 7 | var PKCS7Encoder = {}; 8 | 9 | /** 10 | * 删除解密后明文的补位字符 11 | * 12 | * @param {String} text 解密后的明文 13 | */ 14 | PKCS7Encoder.decode = function (text) { 15 | var pad = text[text.length - 1]; 16 | 17 | if (pad < 1 || pad > 32) { 18 | pad = 0; 19 | } 20 | 21 | return text.slice(0, text.length - pad); 22 | }; 23 | 24 | /** 25 | * 对需要加密的明文进行填充补位 26 | * 27 | * @param {String} text 需要进行填充补位操作的明文 28 | */ 29 | PKCS7Encoder.encode = function (text) { 30 | var blockSize = 32; 31 | var textLength = text.length; 32 | //计算需要填充的位数 33 | var amountToPad = blockSize - (textLength % blockSize); 34 | 35 | var result = new Buffer(amountToPad); 36 | result.fill(amountToPad); 37 | 38 | return Buffer.concat([text, result]); 39 | }; 40 | 41 | /** 42 | * 微信企业平台加解密信息构造函数 43 | * 44 | * @param {String} token 公众平台上,开发者设置的Token 45 | * @param {String} encodingAESKey 公众平台上,开发者设置的EncodingAESKey 46 | * @param {String} id 企业号的CorpId或者AppId 47 | */ 48 | var WXBizMsgCrypt = function (token, encodingAESKey, id) { 49 | if (!token || !encodingAESKey || !id) { 50 | throw new Error('please check arguments'); 51 | } 52 | this.token = token; 53 | this.id = id; 54 | var AESKey = new Buffer(encodingAESKey + '=', 'base64'); 55 | if (AESKey.length !== 32) { 56 | throw new Error('encodingAESKey invalid'); 57 | } 58 | this.key = AESKey; 59 | this.iv = AESKey.slice(0, 16); 60 | }; 61 | 62 | /** 63 | * 获取签名 64 | * 65 | * @param {String} timestamp 时间戳 66 | * @param {String} nonce 随机数 67 | * @param {String} encrypt 加密后的文本 68 | */ 69 | WXBizMsgCrypt.prototype.getSignature = function(timestamp, nonce, encrypt) { 70 | var shasum = crypto.createHash('sha1'); 71 | var arr = [this.token, timestamp, nonce, encrypt].sort(); 72 | shasum.update(arr.join('')); 73 | 74 | return shasum.digest('hex'); 75 | }; 76 | 77 | /** 78 | * 对密文进行解密 79 | * 80 | * @param {String} text 待解密的密文 81 | */ 82 | WXBizMsgCrypt.prototype.decrypt = function(text) { 83 | // 创建解密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节 84 | var decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv); 85 | decipher.setAutoPadding(false); 86 | var deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()]); 87 | 88 | deciphered = PKCS7Encoder.decode(deciphered); 89 | // 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID] 90 | // 去除16位随机数 91 | var content = deciphered.slice(16); 92 | var length = content.slice(0, 4).readUInt32BE(0); 93 | 94 | return { 95 | message: content.slice(4, length + 4).toString(), 96 | id: content.slice(length + 4).toString() 97 | }; 98 | }; 99 | 100 | /** 101 | * 对明文进行加密 102 | * 103 | * @param {String} text 待加密的明文 104 | */ 105 | WXBizMsgCrypt.prototype.encrypt = function (text) { 106 | // 算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID] 107 | // 获取16B的随机字符串 108 | var randomString = crypto.pseudoRandomBytes(16); 109 | 110 | var msg = new Buffer(text); 111 | 112 | // 获取4B的内容长度的网络字节序 113 | var msgLength = new Buffer(4); 114 | msgLength.writeUInt32BE(msg.length, 0); 115 | 116 | var id = new Buffer(this.id); 117 | 118 | var bufMsg = Buffer.concat([randomString, msgLength, msg, id]); 119 | 120 | // 对明文进行补位操作 121 | var encoded = PKCS7Encoder.encode(bufMsg); 122 | 123 | // 创建加密对象,AES采用CBC模式,数据采用PKCS#7填充;IV初始向量大小为16字节,取AESKey前16字节 124 | var cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv); 125 | cipher.setAutoPadding(false); 126 | 127 | var cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()]); 128 | 129 | // 返回加密数据的base64编码 130 | return cipheredMsg.toString('base64'); 131 | }; 132 | 133 | module.exports = WXBizMsgCrypt; 134 | --------------------------------------------------------------------------------