AES
AES(Advanced Encryption Standard)是目前最广泛使用的对称加密算法。在 JS 逆向中,绝大多数对称加密都是 AES。 依赖库:pip install pycryptodome 还原:直接复制,确认 key / iv 的来源即可。 还原:找到 someStr 的生成逻辑(通常是固定字符串或从接口返回)。 Python 还原: 搜索关键字:AES.encrypt、AES.decrypt、createCipheriv、aes-128、aes-256、MODE_CBC 特征:密文长度是 16 的倍数(未编码时);Base64 密文长
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/encrypt | https://pycryptodome.readthedocs.io/en/latest/src/cipher/AES.html
适用场景:JS 逆向 AES 加密识别与 Python 还原(2026-05-07 整理)
AES(Advanced Encryption Standard)是目前最广泛使用的对称加密算法。在 JS 逆向中,绝大多数对称加密都是 AES。
关键参数速查
| 参数 | 常见值 | 说明 |
|---|---|---|
| Key 长度 | 16 / 24 / 32 字节 | 对应 AES-128 / 192 / 256 |
| IV 长度 | 16 字节 | 必须与块大小一致 |
| 块大小 | 16 字节(固定) | AES 块大小始终为 128 位 |
| 常用模式 | CBC、ECB、CTR、GCM | 见概念基础 |
| 常用填充 | PKCS#7、ZeroPadding | CryptoJS 默认 PKCS#7 |
Python 实现
依赖库:pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
# ── CBC 模式(最常见)──────────────────────────────────────────
def aes_cbc_encrypt(plaintext: str, key: str, iv: str) -> str:
"""AES-CBC 加密,返回 Base64 字符串"""
key_b = key.encode('utf-8') # 必须是 16/24/32 字节
iv_b = iv.encode('utf-8') # 必须是 16 字节
data = plaintext.encode('utf-8')
cipher = AES.new(key_b, AES.MODE_CBC, iv_b)
encrypted = cipher.encrypt(pad(data, AES.block_size)) # PKCS#7 填充
return base64.b64encode(encrypted).decode()
def aes_cbc_decrypt(ciphertext_b64: str, key: str, iv: str) -> str:
"""AES-CBC 解密,输入 Base64 字符串"""
key_b = key.encode('utf-8')
iv_b = iv.encode('utf-8')
data = base64.b64decode(ciphertext_b64)
cipher = AES.new(key_b, AES.MODE_CBC, iv_b)
return unpad(cipher.decrypt(data), AES.block_size).decode('utf-8')
# ── ECB 模式(不推荐,但逆向中常见)──────────────────────────
def aes_ecb_encrypt(plaintext: str, key: str) -> str:
"""AES-ECB 加密,无需 IV"""
key_b = key.encode('utf-8')
data = plaintext.encode('utf-8')
cipher = AES.new(key_b, AES.MODE_ECB)
return base64.b64encode(cipher.encrypt(pad(data, AES.block_size))).decode()
def aes_ecb_decrypt(ciphertext_b64: str, key: str) -> str:
key_b = key.encode('utf-8')
data = base64.b64decode(ciphertext_b64)
cipher = AES.new(key_b, AES.MODE_ECB)
return unpad(cipher.decrypt(data), AES.block_size).decode('utf-8')
# ── CTR 模式(流密码,无需填充)──────────────────────────────
from Crypto.Cipher import AES
from Crypto.Util import Counter
def aes_ctr_encrypt(plaintext: str, key: str, nonce: int = 0) -> str:
key_b = key.encode('utf-8')
ctr = Counter.new(128, initial_value=nonce)
cipher = AES.new(key_b, AES.MODE_CTR, counter=ctr)
return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()
def aes_ctr_decrypt(ciphertext_b64: str, key: str, nonce: int = 0) -> str:
key_b = key.encode('utf-8')
ctr = Counter.new(128, initial_value=nonce)
cipher = AES.new(key_b, AES.MODE_CTR, counter=ctr)
return cipher.decrypt(base64.b64decode(ciphertext_b64)).decode()
# ── GCM 模式(带认证标签)────────────────────────────────────
def aes_gcm_encrypt(plaintext: str, key: str) -> dict:
"""返回 {'ciphertext': ..., 'nonce': ..., 'tag': ...},均为 hex"""
key_b = key.encode('utf-8')
cipher = AES.new(key_b, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode())
return {
'ciphertext': ciphertext.hex(),
'nonce': cipher.nonce.hex(),
'tag': tag.hex()
}
def aes_gcm_decrypt(ciphertext_hex: str, key: str, nonce_hex: str, tag_hex: str) -> str:
key_b = key.encode('utf-8')
cipher = AES.new(key_b, AES.MODE_GCM, nonce=bytes.fromhex(nonce_hex))
plaintext = cipher.decrypt_and_verify(
bytes.fromhex(ciphertext_hex),
bytes.fromhex(tag_hex)
)
return plaintext.decode()
# ── 输出为 Hex(而非 Base64)────────────────────────────────
def aes_cbc_encrypt_hex(plaintext: str, key: str, iv: str) -> str:
key_b = key.encode('utf-8')
iv_b = iv.encode('utf-8')
cipher = AES.new(key_b, AES.MODE_CBC, iv_b)
return cipher.encrypt(pad(plaintext.encode(), AES.block_size)).hex()
# ── ZeroPadding(手动实现)──────────────────────────────────
def zero_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - len(data) % block_size
return data + b'\x00' * (pad_len if pad_len != block_size else 0)
def zero_unpad(data: bytes) -> bytes:
return data.rstrip(b'\x00')
JavaScript 实现
使用 CryptoJS
const CryptoJS = require('crypto-js');
// ── CBC 模式 ─────────────────────────────────────────────────
function aesCbcEncrypt(plaintext, key, iv) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
const ivWA = CryptoJS.enc.Utf8.parse(iv);
const encrypted = CryptoJS.AES.encrypt(plaintext, keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString(); // 默认 Base64
// return encrypted.ciphertext.toString(CryptoJS.enc.Hex); // 输出 Hex
}
function aesCbcDecrypt(ciphertext, key, iv) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
const ivWA = CryptoJS.enc.Utf8.parse(iv);
const decrypted = CryptoJS.AES.decrypt(ciphertext, keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
// ── ECB 模式 ─────────────────────────────────────────────────
function aesEcbEncrypt(plaintext, key) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
return CryptoJS.AES.encrypt(plaintext, keyWA, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
function aesEcbDecrypt(ciphertext, key) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
return CryptoJS.AES.decrypt(ciphertext, keyWA, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
}
// ── CTR 模式 ─────────────────────────────────────────────────
function aesCtrEncrypt(plaintext, key, iv) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
const ivWA = CryptoJS.enc.Utf8.parse(iv);
return CryptoJS.AES.encrypt(plaintext, keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CTR,
padding: CryptoJS.pad.NoPadding // CTR 不需要填充
}).toString();
}
// ── ZeroPadding ──────────────────────────────────────────────
function aesZeroPadEncrypt(plaintext, key, iv) {
const keyWA = CryptoJS.enc.Utf8.parse(key);
const ivWA = CryptoJS.enc.Utf8.parse(iv);
return CryptoJS.AES.encrypt(plaintext, keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
}).toString();
}
// ── 输入输出为 Hex ───────────────────────────────────────────
function aesCbcEncryptHex(plaintext, hexKey, hexIv) {
const keyWA = CryptoJS.enc.Hex.parse(hexKey);
const ivWA = CryptoJS.enc.Hex.parse(hexIv);
return CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(plaintext), keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
).ciphertext.toString(CryptoJS.enc.Hex);
}
// 使用示例
const key = '1234567890abcdef'; // 16字节 → AES-128
const iv = 'abcdef1234567890'; // 16字节
console.log(aesCbcEncrypt('hello world', key, iv));
console.log(aesCbcDecrypt(aesCbcEncrypt('hello world', key, iv), key, iv));
使用 Node.js 内置 crypto
const crypto = require('crypto');
// ── CBC 加解密 ───────────────────────────────────────────────
function aesCbcEncrypt(plaintext, key, iv) {
const cipher = crypto.createCipheriv('aes-128-cbc',
Buffer.from(key, 'utf8'),
Buffer.from(iv, 'utf8')
);
// 根据 key 长度选择: aes-128-cbc / aes-192-cbc / aes-256-cbc
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
function aesCbcDecrypt(ciphertext, key, iv) {
const decipher = crypto.createDecipheriv('aes-128-cbc',
Buffer.from(key, 'utf8'),
Buffer.from(iv, 'utf8')
);
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// ── ECB ─────────────────────────────────────────────────────
function aesEcbEncrypt(plaintext, key) {
const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(key), null);
return cipher.update(plaintext, 'utf8', 'base64') + cipher.final('base64');
}
// ── GCM ─────────────────────────────────────────────────────
function aesGcmEncrypt(plaintext, key) {
const iv = crypto.randomBytes(12); // GCM 推荐 12 字节
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key), iv);
let enc = cipher.update(plaintext, 'utf8', 'hex');
enc += cipher.final('hex');
const tag = cipher.getAuthTag();
return { ciphertext: enc, iv: iv.toString('hex'), tag: tag.toString('hex') };
}
function aesGcmDecrypt(ciphertext, key, ivHex, tagHex) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm', Buffer.from(key), Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
let dec = decipher.update(ciphertext, 'hex', 'utf8');
dec += decipher.final('utf8');
return dec;
}
逆向中的典型情形
情形一:CryptoJS 标准调用
// 源码中搜到类似代码
var encrypted = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(key), {
iv: CryptoJS.enc.Utf8.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
还原:直接复制,确认 key / iv 的来源即可。
情形二:key 和 iv 经过处理
// key 是从某字符串截取的
var key = CryptoJS.enc.Utf8.parse(someStr.substring(0, 16));
var iv = CryptoJS.enc.Utf8.parse(someStr.substring(16, 32));
还原:找到 someStr 的生成逻辑(通常是固定字符串或从接口返回)。
情形三:key 是 Hex 字符串
var key = CryptoJS.enc.Hex.parse('3132333435363738393061626364656f');
Python 还原:
key = bytes.fromhex('3132333435363738393061626364656f')
填充方式对照
from Crypto.Util.Padding import pad, unpad
# PKCS#7(pycryptodome 中 PKCS#5 == PKCS#7)
padded = pad(data, 16, style='pkcs7')
# ZeroPadding
padded = pad(data, 16, style='iso7816') # 不完全等同,需手动实现
# 手动 ZeroPadding
def zero_pad(data, bs=16):
rem = len(data) % bs
return data + b'\x00' * (bs - rem if rem else 0)
// CryptoJS 填充选项
CryptoJS.pad.Pkcs7 // PKCS#7(默认)
CryptoJS.pad.ZeroPadding // 零填充
CryptoJS.pad.NoPadding // 无填充(CTR/GCM 等流模式)
CryptoJS.pad.Iso10126 // ISO 10126
CryptoJS.pad.Iso97971 // ISO 7816-4
快速识别
搜索关键字:AES.encrypt、AES.decrypt、createCipheriv、aes-128、aes-256、MODE_CBC
特征:密文长度是 16 的倍数(未编码时);Base64 密文长度通常较长。
最佳实践
先确认模式和填充,再写 Python 还原代码:AES-CBC 与 AES-ECB、AES-GCM 的 Python 实现差异很大;填充错误(Pkcs7 vs ZeroPadding)会导致解密乱码或报错。在断点处打印 mode/padding 参数值,不要依赖猜测。
key 和 iv 的编码要与 JS 保持一致:CryptoJS 默认使用 WordArray,若 JS 中 key 是字符串(如 "1234567890abcdef"),Python 中应用 key.encode();若 JS 中 key 是 CryptoJS.enc.Hex.parse("...") 则应用 bytes.fromhex(...),编码不一致是还原失败的最常见原因。
# JS: key = "1234567890abcdef" (UTF-8 字符串)
key = b"1234567890abcdef" # Python: encode
# JS: key = CryptoJS.enc.Hex.parse("31323334...") (16 进制)
key = bytes.fromhex("31323334...") # Python: fromhex
用已知请求包验证还原结果,再接入爬虫:从 DevTools Network 面板复制真实密文,用自己实现的函数重新加密/解密后对比,100% 匹配才说明还原正确。避免"感觉应该对"就上线采集。
ECB 模式无 iv,不要传 iv 参数:ECB 模式(MODE_ECB)不使用初始化向量,若强行传入 iv 会导致库报错或行为异常。
GCM/CCM 模式需要处理 tag 验证:AES-GCM 的密文末尾附带 tag(通常 16 字节),解密时需要分离 ciphertext 和 tag,并传入 nonce(即 iv)。
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
常见陷阱
陷阱:填充方式不匹配导致解密结果末尾有乱码或抛出异常
现象: 解密后明文末尾出现 \x0b\x0b\x0b... 等不可读字节,或抛出 ValueError: PKCS#7 padding is incorrect。
原因: JS 中使用了 ZeroPadding 或 NoPadding,但 Python 用了默认的 PKCS7;或者反过来。填充数据不符合预期格式时,解填充会失败。
解决: 在 JS 断点处确认 CryptoJS.pad.xxx 的具体值,Python 端对应选择 pad.pkcs7 / 手动处理 Zero padding。
from Crypto.Util.Padding import unpad
# PKCS7
plaintext = unpad(raw, 16, style='pkcs7')
# ZeroPadding(手动去除末尾 \x00)
plaintext = raw.rstrip(b'\x00')
陷阱:CryptoJS 输出的密文格式是 OpenSSL-compatible,直接取 toString() 是 Base64
现象: 直接用 encrypt(...).toString() 得到的 Base64 解码后前 8 字节是 Salted__ 开头,与预期的纯密文不符。
原因: CryptoJS 在用字符串 key(非 WordArray)时,内部用 MD5-based KDF 生成 key 和 iv,并在密文前附加 Salted__ + salt(OpenSSL 兼容格式),Python 直接解密会失败。
解决: 确认 JS 中 key 是 WordArray(CryptoJS.enc.Hex.parse 或 CryptoJS.enc.Utf8.parse),还是原始字符串。若是原始字符串,Python 端也必须用相同的 KDF 生成 key/iv,或者在 JS 断点处直接取生成后的 key/iv WordArray 值。
陷阱:AES-CBC 还原时 iv 写死为全零导致第一块解密正确,后续块正确但第一块明文错误
现象: 解密结果除第一个 16 字节块是乱码外,其余内容正确。
原因: CBC 模式中,第一块明文 = decrypt(第一块密文) XOR iv,iv 必须正确才能还原第一块。iv 全零与实际 iv 不同时,只有第一块受影响(后续块用前一块密文做 iv)。
解决: 在 JS 断点处打印真实 iv 值,确保 Python 端使用完全相同的 iv。