3. AES 加密算法详解:密钥长度、模式、填充方式及Python实现

在接口测试中,数据加密是家常便饭。AES 这词你肯定不陌生,但真正落地时,很多人容易栽跟头。我刚开始做加密测试时也踩过不少坑,今天就跟你聊聊 AES 的核心要点和实战代码。

3.1 AES 是什么?

AES,全称 Advanced Encryption Standard,是目前最主流的对称加密算法。所谓对称,就是加密和解密用同一个密钥。

它的优势很明显:速度快、安全性高、支持多种硬件加速。我们做接口测试时,后端接口常要求用 AES 对请求体或响应体进行加密。说白了,不懂 AES,这活就没法干。

3.2 密钥长度:128、192、256

AES 支持三种密钥长度:128位、192位、256位。长度越长,加密轮数越多,安全性也越高。

我这里简单对比下:

密钥长度 加密轮数 应用场景
128位 10轮 大部分互联网应用,性能优先
192位 12轮 企业级系统,安全与性能平衡
256位 14轮 金融、政府等高安全场景

我在项目中遇到过个案例:客户要求用 256 位密钥,但服务端用的是 Java 默认的密钥长度限制。跑起来后一直报错,最后发现是因为未安装 JCE 无限制权限策略文件。嗯,这个坑你得留意。

个人建议:接口测试场景下,128 位通常够用了。256 位虽然更安全,但性能损耗也更大。除非有明确要求,否则别盲目追高。

3.3 工作模式:ECB、CBC、CFB、OFB、CTR

密钥长度定了,还得选工作模式。这部分很多人容易混淆,我按自己的理解给你捋一遍。

ECB(电子密码本模式)

最简单的模式,每个数据块独立加密。缺点也很明显:相同明文块会得到相同密文块,容易暴露数据规律。说白了,不安全。

我不建议在生产环境中用 ECB,除非你只是做简单的数据脱敏。

CBC(密码分组链接模式)

这是最常用的模式。每个明文块先与前一个密文块 XOR,再加密。这样相同明文块会得到不同密文。

CBC 模式需要一个初始向量 IV。IV 每次加密都应随机生成,且长度等于块大小(16字节)。如果你固定 IV,那安全性会大打折扣。

CFB(密码反馈模式)

将 AES 当作一个密钥流生成器,逐段加密。适合流式数据传输,比如网络协议加密。

OFB(输出反馈模式)

生成的密钥流与前一块无关,只与 AES 输出有关。优点是可以预先计算密钥流,提高吞吐。

但要注意,密钥流不能重用。我曾经见过有人复用 OFB 的密钥流,结果被攻击者轻松破解。

CTR(计数器模式)

用递增的计数器值作为输入,生成密钥流。CTR 模式同样支持并行计算,性能很好。我个人习惯用 CTR,因为它无需填充,且非常适合多线程环境。

下面这个图可以帮你理解几种模式的核心逻辑:

AES 工作模式对比流程图 明文数据 选择工作模式 ECB 独立加密每个块 CBC 需要 IV,链式加密 CTR 计数器驱动,无需填充 密文输出 ▸ 常见选择 CBC + PKCS7 填充 CTR 无需填充,适合流数据 ▸ 避坑提示 ECB 不安全,不要用 OFB/CFB 注意密钥流复用

3.4 填充方式:为什么要用 PKCS7?

AES 是一个块加密算法,每次加密固定 16 字节。如果明文长度不是 16 的倍数,就得填充。

最常见的填充方式是 PKCS7。它根据缺少的字节数填充相同数值。比如缺 5 个字节,就填 5 个 0x05。这样解密时可以根据最后一个字节的值判断填充了多少。

如果明文正好是 16 的倍数呢?有些模式会额外填充一整个 16 字节块。这就是为啥你有时会发现解密后的数据多了 16 个字节 —— 记得去掉填充。

我曾经踩过的坑:有一次测试时发现解密报错,排查半天,原来是服务端用了 ZeroPadding(补零)。但 ZeroPadding 有个问题:如果明文末尾本身就是 0,那解密后就分不清哪些是数据、哪些是填充。最后只能统一改成 PKCS7,问题才解决。

3.5 Python 实现:手把手写代码

理论说完了,来点实际的。下面是我写的一个 AES 工具类,支持 ECB、CBC 和 CTR 模式。

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

class AESUtil:
    def __init__(self, key, mode='CBC', iv=None):
        self.key = key.encode('utf-8')   # 密钥,需为16/24/32字节
        self.bs = 16                     # 块大小
        self.mode = mode.upper()
        
        # 初始化向量
        if iv:
            self.iv = iv.encode('utf-8')
        else:
            self.iv = None
            
    def encrypt(self, data):
        data = data.encode('utf-8')
        
        if self.mode == 'ECB':
            cipher = AES.new(self.key, AES.MODE_ECB)
        elif self.mode == 'CBC':
            # 如果没有提供IV,使用密钥前16字节作为IV(仅做演示,不安全)
            iv = self.iv if self.iv else self.key[:self.bs]
            cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
        elif self.mode == 'CTR':
            # CTR模式需用counter
            from Crypto.Util import Counter
            ctr = Counter.new(128)
            cipher = AES.new(self.key, AES.MODE_CTR, counter=ctr)
        else:
            raise ValueError(f"不支持的AES模式: {self.mode}")
        
        # ECB/CBC 需要填充,CTR不需要
        if self.mode in ('ECB', 'CBC'):
            padded_data = pad(data, self.bs)
        else:
            padded_data = data
            
        encrypted = cipher.encrypt(padded_data)
        return base64.b64encode(encrypted).decode()
    
    def decrypt(self, ciphertext):
        raw = base64.b64decode(ciphertext.encode())
        
        if self.mode == 'ECB':
            cipher = AES.new(self.key, AES.MODE_ECB)
        elif self.mode == 'CBC':
            iv = self.iv if self.iv else self.key[:self.bs]
            cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
        elif self.mode == 'CTR':
            from Crypto.Util import Counter
            ctr = Counter.new(128)
            cipher = AES.new(self.key, AES.MODE_CTR, counter=ctr)
        else:
            raise ValueError(f"不支持的AES模式: {self.mode}")
        
        decrypted = cipher.decrypt(raw)
        
        if self.mode in ('ECB', 'CBC'):
            try:
                decrypted = unpad(decrypted, self.bs)
            except ValueError:
                # 如果填充不正确,说明密钥或密文有问题
                raise ValueError("解密失败:填充数据无效")
        
        return decrypted.decode('utf-8')

# 使用示例
if __name__ == '__main__':
    key = 'thisisademokey!'   # 16字节密钥
    data = 'Hello, AES!'
    
    # ECB模式
    aes_ecb = AESUtil(key, 'ECB')
    encrypted = aes_ecb.encrypt(data)
    decrypted = aes_ecb.decrypt(encrypted)
    print(f'ECB: {encrypted} -> {decrypted}')
    
    # CBC模式
    aes_cbc = AESUtil(key, 'CBC', iv='initvector123456')
    encrypted = aes_cbc.encrypt(data)
    decrypted = aes_cbc.decrypt(encrypted)
    print(f'CBC: {encrypted} -> {decrypted}')
    
    # CTR模式
    aes_ctr = AESUtil(key, 'CTR')
    encrypted = aes_ctr.encrypt(data)
    decrypted = aes_ctr.decrypt(encrypted)
    print(f'CTR: {encrypted} -> {decrypted}')

3.6 常见问题与避坑指南

  • 密钥长度不对:Python 的 pycryptodome 要求密钥必须是 16、24 或 32 字节。如果你传入的密钥长度不对,会直接报错。我习惯在创建对象时就校验长度。
  • IV 不固定:CBC 模式每次加密都应生成随机 IV,不要硬编码。固定 IV 会让攻击者发现数据中的规律模式。
  • 编码问题:加密后的结果是字节串,不要直接打印,最好用 base64 编码后再传输。解密时记得先解 base64。
  • CTR 模式不用填充:如果你的模式是 CTR,别调用 pad 和 unpad,否则会多出字符。
小总结:
  • 密钥长度:128 位够用,256 位更安全
  • 工作模式:首选 CBC(需 IV)或 CTR(无需填充)
  • 填充方式:PKCS7,CTR/OFB/CFB 无需填充
  • Python 实现:用 pycryptodome,注意编码与填充

你想想看,AES 其实并不复杂,关键是搞清楚每个参数的含义。我当初花了整整一个下午才跑通第一个加密测试,现在把这些经验整理出来,就是为了让你少走弯路。如果后续在集成测试中遇到加密问题,相信你能轻松应对了。