Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
# WXBizMsgCrypt

模仿[微信公众平台加密解密技术方案](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318482&lang=zh_CN)
写了一个openresty 版的加密解密。
[the WeChat Message Cryptography](https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419318482&lang=zh_CN) in the openresty lua verison.

注意点:
aes.lua不是openresty 主版本的,而是用了这个[no padding](https://github.com/openresty/lua-resty-string/pull/35)
## aes_pad.lua

extend the [aes.lua](https://github.com/openresty/lua-resty-string) with a `set_padding` method, [discussing here](https://github.com/openresty/lua-resty-string/pull/35)

## crypt.lua

`encrypt(text, timestamp, nonce)`

`decrypt(text_encrypted)`

`get_sha1({token, timestamp, nonce, text_encrypted})`

## Synopsis

```lua
# nginx.conf:

server {
location = /test {
content_by_lua_block {
local token = "pamtest"
local aesKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"
local timestamp = "1409304348"
local nonce = "xxxxxx"
local appId = "wxb11529c136998cb6"
local sample = "<xml><ToUserName><![CDATA[oia2Tj我是中文jewbmiOUlr6X-1crbLOvLw]]></ToUserName><FromUserName><![CDATA[gh_7f083739789a]]></FromUserName><CreateTime>1407743423</CreateTime><MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[eYJ1MbwPRJtOvIEabaxHs7TX2D-HV71s79GUxqdUkjm6Gs2Ed1KF3ulAOA9H1xG0]]></MediaId><Title><![CDATA[testCallBackReplyVideo]]></Title><Description><![CDATA[testCallBackReplyVideo]]></Description></Video></xml>"

local crypt = require "resty.crypt"
local wxmc = crypt:new(token, aesKey, appId)
local encrypted = wxmc:encrypt(sample, timestamp, nonce)
ngx.say("the sample encrypted: ", encrypted)

local gsub = ngx.re.gsub
sample = gsub(encrypted, [=[.*<Encrypt>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</Encrypt>.*]=], "$1")
ngx.say("the sample decrypted: ", wxmc:decrypt(sample))
}
}
}
```

30 changes: 30 additions & 0 deletions lib/resty/aes_pad.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- patched
-- @see https://github.com/openresty/lua-resty-string/pull/35

local aes = require "resty.aes"
local ffi = require "ffi"
local C = ffi.C
ffi.cdef[[
typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX;

int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *ctx, int pad);
]]

-- @link https://github.com/openssl/openssl/blob/master/crypto/evp/evp_enc.c#L569-L576
function aes.set_padding(self, pad)
local encrypt_ctx, decrypt_ctx = self._encrypt_ctx, self._decrypt_ctx

if encrypt_ctx == nil or decrypt_ctx == nil then
return nil, "the aes instance doesn't existed"
end

-- @link https://github.com/openssl/openssl/blob/master/crypto/evp/evp_enc.c#L402-L410
C.EVP_CIPHER_CTX_set_padding(encrypt_ctx, pad)

-- @link https://github.com/openssl/openssl/blob/master/crypto/evp/evp_enc.c#L515-L523
C.EVP_CIPHER_CTX_set_padding(decrypt_ctx, pad)

return 1
end

return aes
83 changes: 41 additions & 42 deletions lib/resty/crypt.lua
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
local random = require "resty.random"
local str = require "resty.string"
local aes = require "resty.aes"
local aes = require "resty.aes_pad"
local bit = require "bit"
local resty_sha1 = require "resty.sha1"
local setmetatable, assert = setmetatable, assert
local str_char, str_sub, str_byte, str_format = string.char, string.sub, string.byte, string.format
local decode_base64, encode_base64 = ngx.decode_base64, ngx.encode_base64

local _M = {}
local _M = { _VERSION = '0.2' }

local mt = { __index = _M }

local function pack_text_len(text_len)
return string.char(
bit.band(bit.rshift(text_len, 24), 0xff),
bit.band(bit.rshift(text_len, 16), 0xff),
bit.band(bit.rshift(text_len, 8), 0xff),
bit.band(text_len, 0xff)
local band, rshift, mask = bit.band, bit.rshift, 0xff
return str_char(
band(rshift(text_len, 24), mask),
band(rshift(text_len, 16), mask),
band(rshift(text_len, 8), mask),
band(text_len, mask)
)
end

local function unpack_text_len(text_len)
local a, b, c, d = string.byte(text_len, 1, 4)
return bit.bor(bit.lshift(a, 24), bit.lshift(b, 16), bit.lshift(c, 8), d), 1 + 4
local bor, lshift = bit.bor, bit.lshift
local a, b, c, d = str_byte(text_len, 1, 4)
return bor(lshift(a, 24), lshift(b, 16), lshift(c, 8), d), 1 + 4
end

local function pkcs7_encode(text)
local amount_to_pad = 32 - (#text % 32)
local PAD_BLOCK_LENGTH = 32
local amount_to_pad = PAD_BLOCK_LENGTH - (#text % PAD_BLOCK_LENGTH)

if amount_to_pad == 0 then
amount_to_pad = 32
amount_to_pad = PAD_BLOCK_LENGTH
end
local str_pad = str_char(amount_to_pad):rep(amount_to_pad)

local padding = ""

local pad = string.char(amount_to_pad)

for i = 1, amount_to_pad do
padding = padding .. pad
end

return text .. padding
return text .. str_pad
end

local function pkcs7_decode(text)
local pad = string.byte(text, #text - 1)
local pad = str_byte(text, #text - 1)

if (pad < 1 or pad > 32) then
pad = 0
end

return string.sub(text, 1, #text - pad);
return str_sub(text, 1, #text - pad);
end

function _M.new (self, token, aes_key, app_id)
local aes_key = ngx.decode_base64(aes_key .. "=")
return setmetatable({token = token, aes_key = aes_key, app_id = app_id}, mt)
local aes_key = decode_base64(aes_key .. "=")
local cipher = aes.cipher(256, "cbc")
local iv = str_sub(aes_key, 0, 16)

return setmetatable({token = token, aes_key = aes_key, app_id = app_id, cipher = cipher, iv = iv}, mt)
end

function _M.get_sha1 (self, sha1_table)
table.sort(sha1_table)
local tb_sort, tb_join = table.sort, table.concat
tb_sort(sha1_table)

local to_sha1 = ""
for k,v in pairs(sha1_table) do
to_sha1 = to_sha1 .. v
end
local to_sha1 = tb_join(sha1_table)

local sha1 = resty_sha1:new()
sha1:update(to_sha1)
Expand All @@ -70,42 +70,41 @@ function _M.get_sha1 (self, sha1_table)
end

function _M.decrypt (self, encrypted)
local ciphertext_dec = ngx.decode_base64(encrypted)
local ciphertext_dec = decode_base64(encrypted)

if ciphertext_dec == nil then
return nil
end

local iv = string.sub(self.aes_key, 0, 16)
local aes_crypt = assert(
aes:new(self.aes_key, nil, aes.cipher(256,"cbc"), {iv=iv}, nil, 0)
aes:new(self.aes_key, nil, self.cipher, {iv = self.iv})
)
aes_crypt:set_padding(0)

local text = aes_crypt:decrypt(ciphertext_dec)
text = pkcs7_decode(string.sub(text, 17, #text))
text = pkcs7_decode(str_sub(text, 17, #text))

local xml_len = unpack_text_len(string.sub(text, 1, 4))
local xml_len = unpack_text_len(str_sub(text, 1, 4))

return string.sub(text, 4 + 1, xml_len + 4)
return str_sub(text, 4 + 1, xml_len + 4)
end


function _M.encrypt (self, text, timestamp, nonce)
local random = str.to_hex(random.bytes(8, true))
local iv = string.sub(self.aes_key, 0, 16)
local prefix = str.to_hex(random.bytes(8, true))

text = random .. pack_text_len(#text) .. text .. self.app_id
text = prefix .. pack_text_len(#text) .. text .. self.app_id
text = pkcs7_encode(text)

local aes_crypt = assert(
aes:new(self.aes_key, nil, aes.cipher(256,"cbc"), {iv=iv}, nil, 0)
aes:new(self.aes_key, nil, self.cipher, {iv = self.iv})
)
aes_crypt:set_padding(0)

local encrypted = ngx.encode_base64(aes_crypt:encrypt(text))
local encrypted = encode_base64(aes_crypt:encrypt(text))
local signature = self:get_sha1({self.token, timestamp, nonce, encrypted})
local xml_content = "<xml><Encrypt><![CDATA[%s]]></Encrypt><MsgSignature><![CDATA[%s]]></MsgSignature><TimeStamp>%s</TimeStamp><Nonce><![CDATA[%s]]></Nonce></xml>";

return string.format(xml_content, encrypted, signature, timestamp, nonce)
return str_format(xml_content, encrypted, signature, timestamp, nonce)
end

return _M