Python 中 Crypto 对 JS 中 CryptoJS AES 加密解密的实现及问题处理

  • A+
所属分类:Python

Python 中 Crypto 对 JS 中 CryptoJS AES 加密解密的实现及问题处理

发布于 2022-08-12 Python  脚本 

起因是需要写一个爬虫去请求某个网站的数据,网站的鉴权方式比较简单,主要判断依据是 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) {
    
    cfg = this.cfg.extend(cfg);

    
    var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher);

    
    cfg.iv = derivedParams.iv;

    
    var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);

    
    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)

运行通过。

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin