Python 中 Crypto 对 JS 中 CryptoJS AES 加密解密的实现及问题处理
起因是需要写一个爬虫去请求某个网站的数据,网站的鉴权方式比较简单,主要判断依据是 request headers 中所需的 token、signature 和 md5 。
token:获取方式比较简单,通过 login 获取即可。
signature:虽然使用了 webpack 混淆,不过通过查看源码和检索关键字,可以发现生成逻辑是对请求参数作 SHA256 with RSA 。
md5:同上,可以发现使用的是 CryptoJS 对某些参数做了 AES 加密。
前端 JS 中实现类似这样:
request.headers.md5 = CryptoJS.AES.encrypt(message, secret_key).toString()
其中 secret_key 为常量。
因为看 JS 实现中的函数调用没有指定 mode 和 iv,我就当作是 ECB mode 了,就有了如下 python 实现。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def add_to_16(value: str) -> bytes:
while len(value) % 16 != 0:
value += '\0'
return str.encode(value)
def encrypt(raw, key):
raw = pad(raw.encode(), 16)
cipher = AES.new(add_to_16(key), AES.MODE_ECB)
return base64.b64encode(cipher.encrypt(raw))
def decrypt(enc, key):
enc = base64.b64decode(enc)
cipher = AES.new(add_to_16(key), AES.MODE_ECB)
return unpad(cipher.decrypt(enc), 16)
经过测试,python 实现中的是可以做到互相加密解密的,但是却无法对网站中生成的 md5 内容实现解密。
而调用 CryptoJS 的 decrypt,却可以正确对加密后的内容解密。
CryptoJS.AES.decrypt(cipher_text, secret_key).toString(CryptoJS.enc.Utf8)
经过查看 CryptoJS 文档,发现默认使用的是 CBC mode,那么 iv 又是怎么获得的?
查看源码:https://github.com/brix/crypto-js/blob/develop/src/cipher-core.js
如果 secret_key 为 string 类型,实际调用的是 CryptoJS.lib.PasswordBasedCipher。
_createHelper: (function () {
function selectCipherStrategy(key) {
if (typeof key == 'string') {
return PasswordBasedCipher;
} else {
return SerializableCipher;
}
}
return function (cipher) {
return {
encrypt: function (message, key, cfg) {
return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
},
decrypt: function (ciphertext, key, cfg) {
return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
}
};
};
}())
此时传入的 secret_key 会被用来生成一个新的 WordArray,这才是实际使用的 password。
而生成的过程具有随机性,所以相同的 message 和 secret_key 加密得到的结果会不同。
encrypt: function (cipher, message, password, cfg) {
// Apply config defaults
cfg = this.cfg.extend(cfg);
// Derive key and other params
var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher);
// Add IV to config
cfg.iv = derivedParams.iv;
// Encrypt
var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);
// Mix in derived params
ciphertext.mixIn(derivedParams);
return ciphertext;
}
看到这里其实还会发现另外一个问题,CryptoJS 是不对 secret_key 做长度校验的。
但是大部分其他语言的类库实现中都要求标准的 key 长度。
AES 标准规范中分组长度为 128,密钥长度可以为 AES-128、AES-192、AES-256,对应的加密轮数为 10、12、14。
相关 issues:AES encryption doesn’t check key size #293
综合考虑后,对用非标准的 key 长度的情况,决定还是使用 pyexecjs 库调用 CryptoJS 来实现解密。
安装 pyexecjs:
pip install pyexecjs
aes.js 放在 py 文件同目录下,并添加两个函数方便调用:
function encrypt(content, psw) {
return CryptoJS.AES.encrypt(content, psw).toString()
}
function decrypt(content, psw) {
return CryptoJS.AES.decrypt(content, psw).toString(CryptoJS.enc.Utf8);
}
简单测试:
import execjs
secret_key = 'secret_key'
def execjs_test():
node = execjs.get()
ctx = node.compile(open('aes.js', 'r', encoding='utf-8').read())
encrypt_rst = ctx.call('encrypt', '123456', secret_key)
print(encrypt_rst)
decrypt_rst = ctx.call('decrypt', encrypt_rst, secret_key)
print(decrypt_rst)
运行通过。