> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# AES
- URL: https://blog.vercanti.com/aes/
- Published: 2026-08-28T14:35:02.000Z
- Updated: 2026-08-28T14:57:54.000Z
- Description: 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 密文长
- Author: yellowdog
- Tags: js逆向, 加密算法

> 官方文档：<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`

```python
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

```javascript
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

```javascript
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 标准调用

```javascript
// 源码中搜到类似代码
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 经过处理

```javascript
// key 是从某字符串截取的
var key = CryptoJS.enc.Utf8.parse(someStr.substring(0, 16));
var iv  = CryptoJS.enc.Utf8.parse(someStr.substring(16, 32));

```

**还原**：找到 `someStr` 的生成逻辑（通常是固定字符串或从接口返回）。

### 情形三：key 是 Hex 字符串

```javascript
var key = CryptoJS.enc.Hex.parse('3132333435363738393061626364656f');

```

**Python 还原**：

```python
key = bytes.fromhex('3132333435363738393061626364656f')

```

---

## 填充方式对照

```python
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)

```

```javascript
// 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(...)`，编码不一致是还原失败的最常见原因。

```python
# 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）。

```python
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。

```python
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。

---

## 参见

- [逆向实战模板](https://blog.vercanti.com/ni-xiang-shi-zhan-mo-ban/)
- [MD5](https://blog.vercanti.com/md5/)
- [RSA](https://blog.vercanti.com/rsa/)
- [HMAC](https://blog.vercanti.com/hmac/)