前言
AES(Advanced Encryption Standard,高级加密标准)是目前Web接口加密里使用最多的对称加密算法。对称加密意味着加密和解密使用同一个密钥 Key,前端加密、后端解密(或者反过来),在前后端报文保护、参数加密场景中非常普及。
重要前置提醒:
前端JS里的AES不存在绝对安全。密钥如果写死在前端代码中,只要被逆向拿到key和iv,密文就能直接解开。AES只是用来防抓包直读明文,不能防止专业逆向。高安全场景一般做法:AES加密业务数据,再用RSA把AES密钥加密后传输。
本文会讲清楚AES核心概念、模式、填充规则,同时给出两套实现:经典的 CryptoJS 版本 + 浏览器原生 Web Crypto API(推荐新标准),附带踩坑说明,也是爬虫逆向中识别AES加密的核心参考。
一、AES基础概念
AES是分组加密算法,固定分组长度为 128 bit(16字节),根据密钥长度分为三类:
- AES-128:key 16字节(最常用)
- AES-192:key 24字节
- AES-256:key 32字节
字节换算:1字节 = 8bit,所以 AES128 key = 16个字符(UTF8)。
1. 工作模式(Mode)【逆向重点区分】
不同模式加密流程完全不一样,抓包逆向时首先要判断模式:
- ECB :电子密码本模式。不推荐使用,不安全。没有IV,相同明文+相同key加密结果永远一样,容易被分析。很多老网站遗留代码在用。
- CBC :密码分组链接模式。Web中最常见,需要额外传入 IV(偏移向量,固定16字节);前一组密文参与下一组明文加密,打乱重复特征。
- GCM :伽罗瓦/计数器模式。现代推荐,带认证,除了加密还会产出authTag,能够校验密文是否被篡改;很多新项目、小程序、App配套接口在用。>
还有CFB、OFB、CTR等,网页里相对少见。
2. 填充(Padding)
AES是分组加密,明文长度如果不是分组大小(16字节)的整数倍,就需要填充补齐。
- PKCS7Padding:Web默认主流填充,CryptoJS默认就是这个(很多后端Java/Python也是PKCS7)
- ZeroPadding:补0填充,兼容性差,很少用
- NoPadding:不填充,要求明文长度必须是16字节整数倍,否则直接报错
坑点:前后端填充方式不一致,会直接解密报错、报
bad padding。
3. IV 偏移向量
CBC / GCM 模式必须要有IV,固定16字节。
- IV不需要保密,但不能固定写死不变(安全规范),最好每次加密随机生成,随密文一起传给后端;
- 逆向经常遇到网站写死IV:
1234567890123456,这是典型偷懒写法。
4. 编码格式
加密出来是二进制字节流,网络传输不能直接发二进制,一般会转成:
- Base64:最普遍
- Hex(16进制字符串):部分系统使用
二、CryptoJS 实现(项目最常见,逆向高频)
说明:CryptoJS是第三方库,不是浏览器原生API,老项目大量使用,也是爬虫逆向遇到最多的AES实现。
安装:
npm install crypto-js或者直接引入CDN脚本。
AES-CBC 加密解密示例
import CryptoJS from 'crypto-js'
// 配置
const key = CryptoJS.enc.Utf8.parse('1234567890123456') // 16字节 key AES-128
const iv = CryptoJS.enc.Utf8.parse('1234567890123456') // 16字节 iv
// 加密函数
function aesEncrypt(plainText) {
const encrypted = CryptoJS.AES.encrypt(plainText, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString() // 默认输出 base64
}
// 解密函数
function aesDecrypt(cipherText) {
const decrypted = CryptoJS.AES.decrypt(cipherText, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return decrypted.toString(CryptoJS.enc.Utf8)
}
// 测试
const txt = 'hello aes 测试内容'
const secret = aesEncrypt(txt)
console.log('密文base64:', secret)
console.log('解密结果:', aesDecrypt(secret))
ECB模式示例(仅用于兼容老系统,不推荐生产)
function aesEcbEncrypt(plainText) {
const encrypted = CryptoJS.AES.encrypt(plainText, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}
ECB 不需要 iv 参数。
AES-GCM 示例(带tag校验)
GCM会额外生成一个authTag,后端解密时必须带上tag校验完整性:
function aesGcmEncrypt(plainText) {
const encrypted = CryptoJS.AES.encrypt(plainText, key, {
iv: iv,
mode: CryptoJS.mode.GCM,
padding: CryptoJS.pad.NoPadding
})
return {
ciphertext: encrypted.ciphertext.toString(CryptoJS.enc.Base64),
tag: encrypted.getAuthTag().toString(CryptoJS.enc.Base64)
}
}
三、浏览器原生 Web Crypto API(现代标准,无第三方依赖)
window.crypto.subtle 是浏览器内置密码学API,推荐新项目优先使用,性能更好、安全策略更规范;注意:只能在 HTTPS / localhost 环境下使用,http普通页面会被禁用。
原生API写法比较繁琐,区分字节数组,不再像CryptoJS一样直接传字符串。
/**
* Web Crypto AES-CBC
* @param {string} plainText 明文
* @param {string} keyStr 16字节key
* @param {string} ivStr 16字节iv
* @returns {Promise<string>} base64密文
*/
async function webCryptoAesCbcEncrypt(plainText, keyStr, ivStr) {
const encoder = new TextEncoder()
const keyRaw = encoder.encode(keyStr)
const ivRaw = encoder.encode(ivStr)
const dataRaw = encoder.encode(plainText)
// 导入密钥
const key = await crypto.subtle.importKey(
'raw',
keyRaw,
{ name: 'AES-CBC' },
false,
['encrypt', 'decrypt']
)
// 加密
const buf = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv: ivRaw },
key,
dataRaw
)
// ArrayBuffer 转 base64
return btoa(String.fromCharCode(...new Uint8Array(buf)))
}
async function webCryptoAesCbcDecrypt(base64Cipher, keyStr, ivStr) {
const encoder = new TextEncoder()
const keyRaw = encoder.encode(keyStr)
const ivRaw = encoder.encode(ivStr)
const cipherBuf = Uint8Array.from(atob(base64Cipher), c => c.charCodeAt(0))
const key = await crypto.subtle.importKey(
'raw',
keyRaw,
{ name: 'AES-CBC' },
false,
['encrypt', 'decrypt']
)
const plainBuf = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: ivRaw },
key,
cipherBuf
)
return new TextDecoder().decode(plainBuf)
}
// 调用示例
;(async () => {
const key = '1234567890123456'
const iv = '1234567890123456'
const cipher = await webCryptoAesCbcEncrypt('测试原生AES', key, iv)
console.log('原生加密密文:', cipher)
const plain = await webCryptoAesCbcDecrypt(cipher, key, iv)
console.log('原生解密:', plain)
})()
四、逆向时如何判断是AES?
做爬虫逆向遇到接口密文,可以通过下面特征快速识别:
- 密文是标准Base64字符串,长度经常是24、32、44、64等;
- JS代码里出现关键词:
CryptoJS.AES、mode.CBC、Pkcs7、iv; - 代码里固定取出16位字符串作为iv、16/32位字符串作为key;
- GCM模式会额外返回/传递
tag参数。
常见逆向卡点:
- key、iv不是直接明文字符串,是hex、base64解码之后才拿到;
- key经过md5、sha256处理后截取前16字节当做AES key;
- 后端是Java,默认PKCS7,前端填错填充直接解密失败。
五、生产环境最佳实践 & 避坑清单
- ❌ 禁止前端硬编码固定Key,禁止ECB模式上线;
- ✅ 每次加密随机生成16字节IV,iv随密文一起传给后端;
- ✅ 敏感业务:AES加密报文,RSA公钥加密AES的临时key;
- ✅ GCM优先于CBC,自带防篡改校验;
- ✅ 统一约定:编码UTF8、填充PKCS7、输出Base64;
- ❌ 不要自己手写AES底层算法,极易出漏洞,优先成熟库;
- ⚠️ Web Crypto API 在http页面、文件本地打开(file://)直接不可用。
六、补充:Node.js 内置crypto模块(后端配套)
很多时候前端CryptoJS-AES,后端是Node crypto,配套互通,简单示例:
const crypto = require('crypto')
function nodeAesEncrypt(text, key, iv) {
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
let r = cipher.update(text, 'utf8', 'base64')
r += cipher.final('base64')
return r
}
保证 key、iv、模式、填充完全一致,前后端才能互通解密。
结语
AES本身是安全强度很高的分组加密算法,风险从来不在于AES算法本身,而在于密钥的分发和保管。前端只能作为传输层防护手段,用来防止明文直接被抓包查看,不能用来保护最高等级的机密数据。
在逆向工作中,只要定位到key、iv、mode、padding这四要素,AES密文就可以直接还原;在业务开发中,合理搭配RSA+AES混合加密方案,才是Web接口加密的成熟方案。