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.encryptAES.decryptcreateCipherivaes-128aes-256MODE_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 字节),解密时需要分离 ciphertexttag,并传入 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.parseCryptoJS.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。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog