背景/现象:
在使用openresty(1.13.6.2)中使用lua对业务方的token进行加解密的时候,发现AES加密出来的结果和java/python有一定的出入,openresty lua 通过AES加密得到的结果比java/python的多出一串字符串。反之,正常加密串无法解密。
python版的AES加密:
#cat aes.py
from Crypto.Cipher import AES
import base64
import binascii
# -*- coding: UTF-8 -*-
class prpcrypt():
def __init__(self, key):
self.key = "aaaaaaaaaaaaaaaa"
self.mode = AES.MODE_CBC
def encrypt(self, text):
cryptor = AES.new(self.key, self.mode, self.key)
length = 16
count = len(text)
add = length - (count % length)
text = text + ('\0' * add)
print(text)
self.ciphertext = cryptor.encrypt(text)
return binascii.b2a_hex(self.ciphertext)
#return base64.b64encode(self.ciphertext)
crpt=prpcrypt("aaaaaaaaaaaaaaaa")
str1=str(crpt.encrypt("7614463 1574189821 175.168.224.225"))
print str(str1)
#python aes.py
7614463 1574189821 175.168.224.225
0056cc0b278e6123afe0784a940b45525fd95c10585809f5197ac3aed2f9ec60635e59f5008f3c6ecbca932811a85cc0
openresty lua aes加密:
location /t {
rewrite_by_lua_block {
local aes = require "resty.aes"
local str = require "resty.string"
local key = "aaaaaaaaaaaaaaaa"
local text = "7614463 1574189821 175.168.224.225"
local length = 16
local count = string.len(text)
local add = length - (count % length)
local tail_str='\0'
for i=1,add-1 do
tail_str=tail_str .. '\0'
end
text = text .. tail_str
ngx.say(text)
local cript = aes:new(key,nil, aes.cipher(128,"cbc"), {iv=key})
local encrypted = cript:encrypt(text)
ngx.say(str.to_hex(encrypted))
}
}
[root@dev_bao_shops vhosts]# curl http://localhost/t
7614463 1574189821 175.168.224.225
0056cc0b278e6123afe0784a940b45525fd95c10585809f5197ac3aed2f9ec60635e59f5008f3c6ecbca932811a85cc0a0fe3cea8ef9cde779e3a927f7c2b121
前后对比,lua加密后 多出了a0fe3cea8ef9cde779e3a927f7c2b121。
原因分析:
单独通过系统自带的lua库,编写测试程序,发现没有问题,然后翻看了一下openresty lua库 中aes的实现,发现aes底层仍然使用的是openssl底层库【通过 LuaJIT的FFI库 ,FFI是LUA调用外部C函数的库】。前后通过python,java测试程序,都无此问题。问题出现在CBC的pading模式上, AES -CBC-128加密算法下,数据必须以16字节为固定长度进行对齐(参见:https://github.com/openresty/lua-resty-string/issues/59)。Python和JAVA版都是使用ZeroPadding, 而openresty Lua ase默认使用的是 PKCS7Padding模式,
导致以下以下问题: 即使程序中通过对齐方式补齐了\0,进行填充,但PKCS7Padding仍然要填充一个长度为块大小的数据[16字节), 最终导致解密后多出了一串。
解决方法:
方法1: 直接在openresty content_by_lua_block中调用外部c函数,显示使用ZeroPadding模式
location = /t {
content_by_lua_block {
local b64 = require("ngx.base64")
local aes = require "resty.aes"
local function set_padding(aes, pad)
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);
]]
local encrypt_ctx, decrypt_ctx = aes._encrypt_ctx, aes._decrypt_ctx
if encrypt_ctx == nil or decrypt_ctx == nil then
return nil, "the aes instance doesn't existed"
end
方法1, 每次请求都会通过luajit调用外部openssl 的库函数,在高并发情况下,容易崩溃,所以方法1适合访问量很小的场景
方法2: 直接修改openresty 自带的aes.lua库文件,禁止使用PKCS7Padding
ffi.cdef[[
typedef struct engine_st ENGINE;
typedef struct evp_cipher_st EVP_CIPHER;
typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX;
typedef struct env_md_ctx_st EVP_MD_CTX;
typedef struct env_md_st EVP_MD;
const EVP_MD *EVP_md5(void);
const EVP_MD *EVP_sha(void);
const EVP_MD *EVP_sha1(void);
const EVP_MD *EVP_sha224(void);
const EVP_MD *EVP_sha256(void);
const EVP_MD *EVP_sha384(void);
const EVP_MD *EVP_sha512(void);
const EVP_CIPHER *EVP_aes_128_ecb(void);
const EVP_CIPHER *EVP_aes_128_cbc(void);
const EVP_CIPHER *EVP_aes_128_cfb1(void);
const EVP_CIPHER *EVP_aes_128_cfb8(void);
const EVP_CIPHER *EVP_aes_128_cfb128(void);
const EVP_CIPHER *EVP_aes_128_ofb(void);
const EVP_CIPHER *EVP_aes_128_ctr(void);
const EVP_CIPHER *EVP_aes_192_ecb(void);
const EVP_CIPHER *EVP_aes_192_cbc(void);
const EVP_CIPHER *EVP_aes_192_cfb1(void);
const EVP_CIPHER *EVP_aes_192_cfb8(void);
const EVP_CIPHER *EVP_aes_192_cfb128(void);
const EVP_CIPHER *EVP_aes_192_ofb(void);
const EVP_CIPHER *EVP_aes_192_ctr(void);
const EVP_CIPHER *EVP_aes_256_ecb(void);
const EVP_CIPHER *EVP_aes_256_cbc(void);
const EVP_CIPHER *EVP_aes_256_cfb1(void);
const EVP_CIPHER *EVP_aes_256_cfb8(void);
const EVP_CIPHER *EVP_aes_256_cfb128(void);
const EVP_CIPHER *EVP_aes_256_ofb(void);
EVP_CIPHER_CTX *EVP_CIPHER_CTX_new();
void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *a);
int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher,
ENGINE *impl, unsigned char *key, const unsigned char *iv);
int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *ctx, int pad);
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
const unsigned char *in, int inl);
int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);
int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher,
ENGINE *impl, unsigned char *key, const unsigned char *iv);
int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
const unsigned char *in, int inl);
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);
int EVP_BytesToKey(const EVP_CIPHER *type,const EVP_MD *md,
const unsigned char *salt, const unsigned char *data, int datal,
int count, unsigned char *key,unsigned char *iv);
]]
function _M.encrypt(self, s)
local s_len = #s
local max_len = s_len + 16
local buf = ffi_new("unsigned char[?]", max_len)
local out_len = ffi_new("int[1]")
local tmp_len = ffi_new("int[1]")
local ctx = self._encrypt_ctx
C.EVP_CIPHER_CTX_set_padding(ctx, 0)
if C.EVP_EncryptInit_ex(ctx, nil, nil, nil, nil) == 0 then
return nil
end
if C.EVP_EncryptUpdate(ctx, buf, out_len, s, s_len) == 0 then
return nil
end
if C.EVP_EncryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then
return nil
end
return ffi_str(buf, out_len[0] + tmp_len[0])
end
function _M.decrypt(self, s)
local s_len = #s
local buf = ffi_new("unsigned char[?]", s_len)
local out_len = ffi_new("int[1]")
local tmp_len = ffi_new("int[1]")
local ctx = self._decrypt_ctx
C.EVP_CIPHER_CTX_set_padding(ctx, 0)
if C.EVP_DecryptInit_ex(ctx, nil, nil, nil, nil) == 0 then
return nil
end
if C.EVP_DecryptUpdate(ctx, buf, out_len, s, s_len) == 0 then
return nil
end
if C.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then
return nil
end
return ffi_str(buf, out_len[0] + tmp_len[0])
end
方法2的好处是效率高,缺点是硬核修改了openresty lua底层库, 对其他pading类型的AES非CBC加密算法计算有一定的风险。