1. 项目概述:从“黑盒”调用到亲手“造轮子”
如果你在网上搜过“Python AES加密”,大概率会看到这样的代码片段:from Crypto.Cipher import AES,然后调用一个new()函数,传入密钥和模式,接着调用encrypt()或decrypt()。几分钟就能跑通,看起来很简单,对吧?但问题来了,当程序报错“输入数据长度不是16的倍数”,或者你需要和另一个系统对接,对方要求“使用PKCS7填充”时,你是不是就懵了?你只知道调库,却不知道库背后发生了什么。AES对你而言,依然是个神秘的黑盒。
这正是“死记硬背”式学习的典型困境。我们记住了API的调用方式,却对核心原理一无所知,一旦遇到变数就束手无策。今天,我们就来彻底打破这个黑盒。我们不依赖任何高级加密库(比如pycryptodome),仅使用Python标准库,从零开始动手实现AES算法最核心的ECB和CBC两种工作模式。目的不是让你去写一个生产级的加密库(那需要极其严谨的实现和大量优化),而是通过“造轮子”这个过程,让你真正搞懂:
- 分组密码到底是如何“分组”的?
- ECB模式为什么被称为“电子密码本”,它有什么致命缺陷?
- CBC模式如何通过一个“初始化向量”解决ECB的问题,实现更强的安全性?
- 那些令人头疼的填充(Padding),比如PKCS7,到底是怎么工作的?
我敢保证,跟着本文走完一遍,你对AES的理解会远超绝大多数只会调库的程序员。当你能亲手用代码描绘出AES的加密流程图时,那些曾经需要死记硬背的概念,会变得无比清晰和牢固。
2. 核心原理拆解:AES、分组与模式
在动手写代码之前,我们必须把地基打牢。这部分内容看似理论,但它是我们后续每一行代码的指导思想。理解不透,代码就会写成“玄学”。
2.1 AES算法简述:不是魔法,是精密的数学变换
AES(Advanced Encryption Standard,高级加密标准)是一种对称分组密码算法。我们聚焦几个关键点:
- 对称:加密和解密使用同一把密钥。这就像你用同一把钥匙锁门和开门。
- 分组:它不像流密码那样逐位处理,而是把明文切分成固定长度的“块”来处理。AES的标准块大小是128位(16字节)。无论你的明文是“Hello”还是整本《战争与和平》,在AES眼里,都会被切成一个个16字节的小块。
- 密钥长度:支持128位、192位、256位。密钥越长,理论上越安全,但加解密计算量也越大。为了简化,我们后续实现将使用128位密钥(16字节)。
- 核心轮函数:AES加密过程可以看作对数据块进行多轮(10/12/14轮,取决于密钥长度)复杂的数学变换。每一轮都包含四个步骤:
- 字节替换(SubBytes):用一个固定的S盒(Substitution-box)进行非线性替换,提供混淆。
- 行移位(ShiftRows):将数据块矩阵的每一行进行循环左移,提供扩散。
- 列混合(MixColumns):将数据块矩阵的每一列进行线性变换,进一步增强扩散。
- 轮密钥加(AddRoundKey):将当前轮的子密钥与数据块进行异或(XOR)操作。
解密过程就是这些步骤的逆序逆操作。听起来复杂,但当我们用代码一步步实现时,你会发现它们都是非常确定的计算。
2.2 工作模式:如何加密“多个块”?
AES本身只定义了一个数据块(16字节)的加密。对于长明文,就需要“工作模式”来定义如何链接这些块。这是理解ECB和CBC差异的关键。
ECB模式(电子密码本模式):
- 逻辑:最简单的模式。将明文分成独立的16字节块,每个块用相同的密钥独立加密。就像一本密码本,每个明文块对应一个密文块。
- 图示:
明文块1 -> AES加密 -> 密文块1,明文块2 -> AES加密 -> 密文块2, ... 块与块之间毫无关联。 - 问题:由于相同明文块必然产生相同密文块,对于有重复模式的数据(如图像、结构化文本),密文中会保留明文的模式。一张纯色图片用ECB加密后,可能依然能看出轮廓,安全性很差。
CBC模式(密码分组链接模式):
- 逻辑:引入了一个初始化向量(IV, Initialization Vector)。加密时,第一个明文块先与IV进行XOR,然后再进行AES加密,得到第一个密文块;接下来,第二个明文块会与前一个密文块进行XOR,再加密,如此链式进行。
- 图示:
明文块1 XOR IV -> AES加密 -> 密文块1,明文块2 XOR 密文块1 -> AES加密 -> 密文块2, ... - 优势:即使明文相同,只要IV不同,产生的密文就完全不同。并且,一个块的错误会影响后续所有块,模式被隐藏,安全性远高于ECB。IV不需要保密,但必须是随机的且不可预测,通常随密文一起传输或存储。
2.3 填充(Padding):解决“最后一个块”问题
明文长度不总是16字节的整数倍。对于最后一个不足16字节的块,需要进行填充。PKCS7是最常用的方案之一。
- PKCS7填充规则:
- 假设最后一个块还差
N个字节到16字节。那么就用数值N填充这N个字节。 - 例如,最后一个块数据为
[0x48, 0x65, 0x6c, 0x6c, 0x6f](“Hello”),还差11个字节。则填充后为:[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b]。 - 如果明文长度恰好是16的倍数,则需要额外填充一个完整的16字节块,每个字节都是
0x10(16)。这是为了解密时能无歧义地移除填充。
- 假设最后一个块还差
- 解密后,读取最后一个字节的值
N,然后移除末尾的N个字节,即可得到原始明文。
注意:我们这里实现的是PKCS7,但填充方案有多种(如ZeroPadding, ANSIX923等)。在实际跨系统对接时,必须确认双方使用完全相同的填充方案,否则解密必然失败。这是初期调试的一个常见坑点。
3. 动手实现:构建我们自己的AES工具类
理论铺垫完成,现在进入实战环节。我们将创建一个SimpleAES类,它内部实现AES-128的核心轮函数,并对外提供ECB和CBC模式的加解密接口。我们依赖的只有Python标准库。
3.1 基础结构与辅助函数
首先,定义一些常量和辅助函数。AES运算大量在字节和4x4的字节矩阵(状态State)之间转换。
class SimpleAES: # AES-128 常量 BLOCK_SIZE = 16 # 字节 ROUNDS = 10 RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36] # 轮常数 # AES S盒(Substitution Box) - 这里只列出一部分,完整版很长 # 这是一个16x16的矩阵,用于字节替换。实际代码中你需要完整的256个值。 S_BOX = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, # ... 此处应包含完整的256个值 ] # 逆S盒,用于解密 INV_S_BOX = [ ... ] # 同样需要完整值 def __init__(self, key: bytes): """初始化,接收一个16字节的密钥""" if len(key) != self.BLOCK_SIZE: raise ValueError(f"密钥长度必须为{self.BLOCK_SIZE}字节") self.key = key self._key_schedule = self._key_expansion(key) @staticmethod def _bytes_to_matrix(block: bytes): """将16字节转换为4x4状态矩阵(按列填充)""" # 这是AES标准定义的状态矩阵排列方式,不是直观的行优先。 # 假设 block = [b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15] # 状态矩阵 state[行][列] 的填充顺序是:先填第一列,再填第二列... # state[0][0] = b0, state[1][0] = b1, state[2][0] = b2, state[3][0] = b3 # state[0][1] = b4, state[1][1] = b5, ... state[3][3] = b15 state = [[0] * 4 for _ in range(4)] for i in range(4): for j in range(4): state[j][i] = block[i * 4 + j] # 注意下标:j是行,i是列 return state @staticmethod def _matrix_to_bytes(state): """将4x4状态矩阵转换回16字节""" block = bytearray(16) for i in range(4): for j in range(4): block[i * 4 + j] = state[j][i] # 反向操作 return bytes(block) def _sub_bytes(self, state, inverse=False): """字节替换。使用S盒或逆S盒替换状态矩阵中的每个字节。""" sbox = self.INV_S_BOX if inverse else self.S_BOX for i in range(4): for j in range(4): byte_val = state[i][j] # S盒是一个16x16表,用高4位做行索引,低4位做列索引 row = (byte_val >> 4) & 0x0F col = byte_val & 0x0F state[i][j] = sbox[row * 16 + col] return state def _shift_rows(self, state, inverse=False): """行移位。正向:第0行不移,第1行左移1位,第2行左移2位,第3行左移3位。逆向则右移。""" n = 4 new_state = [row[:] for row in state] # 深拷贝一份 for i in range(1, n): # 第0行不动 shift = i if not inverse else n - i for j in range(n): if not inverse: new_state[i][j] = state[i][(j + shift) % n] else: new_state[i][j] = state[i][(j - shift) % n] return new_state # _mix_columns 和 _key_expansion 函数涉及有限域GF(2^8)上的乘法和更复杂的变换, # 为了文章篇幅和清晰度,这里暂不展开完整代码,但会说明其作用。 # 完整实现需要定义 _galois_mul 等辅助函数。实操心得1:状态矩阵的填充顺序:这是新手实现AES时最容易出错的地方之一。AES标准文档定义的状态矩阵是按列填充的,这与我们通常处理二维数组的“行优先”直觉相反。上面的
_bytes_to_matrix和_matrix_to_bytes函数清晰地体现了这一点。如果你发现加密结果与标准库对不上,首先检查这里。
3.2 实现PKCS7填充
在实现核心加密前,我们先搞定填充这个“后勤”问题。
@staticmethod def pkcs7_pad(data: bytes) -> bytes: """PKCS7填充""" pad_len = SimpleAES.BLOCK_SIZE - (len(data) % SimpleAES.BLOCK_SIZE) # 如果pad_len是0,意味着长度是16的倍数,需要填充一整个块(16个0x10) if pad_len == 0: pad_len = SimpleAES.BLOCK_SIZE padding = bytes([pad_len] * pad_len) return data + padding @staticmethod def pkcs7_unpad(padded_data: bytes) -> bytes: """PKCS7去填充""" if not padded_data: raise ValueError("输入数据为空") pad_len = padded_data[-1] # 简单的有效性检查:pad_len必须在合理范围,且末尾pad_len个字节的值都等于pad_len if pad_len < 1 or pad_len > SimpleAES.BLOCK_SIZE: raise ValueError("无效的PKCS7填充") for i in range(1, pad_len + 1): if padded_data[-i] != pad_len: raise ValueError("无效的PKCS7填充") return padded_data[:-pad_len]注意:
pkcs7_unpad中的有效性检查非常重要。如果不检查,攻击者可能通过篡改密文末尾的字节,诱使你的程序解出错误的明文(Padding Oracle攻击的一种基础形态)。生产级代码的检查会更严格。
3.3 实现ECB模式
有了填充和AES单块加密的基础(假设我们已实现_encrypt_block和_decrypt_block方法),ECB模式就非常简单了。
def encrypt_ecb(self, plaintext: bytes) -> bytes: """ECB模式加密""" padded_data = self.pkcs7_pad(plaintext) ciphertext = bytearray() # 分块处理 for i in range(0, len(padded_data), self.BLOCK_SIZE): block = padded_data[i:i + self.BLOCK_SIZE] encrypted_block = self._encrypt_block(block) # 调用单块加密 ciphertext.extend(encrypted_block) return bytes(ciphertext) def decrypt_ecb(self, ciphertext: bytes) -> bytes: """ECB模式解密""" if len(ciphertext) % self.BLOCK_SIZE != 0: raise ValueError("密文长度必须是块大小的整数倍") plaintext_padded = bytearray() for i in range(0, len(ciphertext), self.BLOCK_SIZE): block = ciphertext[i:i + self.BLOCK_SIZE] decrypted_block = self._decrypt_block(block) # 调用单块解密 plaintext_padded.extend(decrypted_block) # 解密后去除填充 return self.pkcs7_unpad(bytes(plaintext_padded))ECB模式的代码直观地反映了其原理:分块、独立处理、合并。它不涉及任何块之间的依赖。
3.4 实现CBC模式
CBC模式需要管理IV和块之间的XOR链。
def encrypt_cbc(self, plaintext: bytes, iv: bytes) -> bytes: """CBC模式加密。IV必须是16字节。""" if len(iv) != self.BLOCK_SIZE: raise ValueError(f"IV长度必须为{self.BLOCK_SIZE}字节") padded_data = self.pkcs7_pad(plaintext) ciphertext = bytearray() previous_block = iv # 第一个块的前一个“密文块”就是IV for i in range(0, len(padded_data), self.BLOCK_SIZE): block = padded_data[i:i + self.BLOCK_SIZE] # 核心步骤:明文块与前一个密文块(或IV)XOR block_to_encrypt = bytes(a ^ b for a, b in zip(block, previous_block)) encrypted_block = self._encrypt_block(block_to_encrypt) ciphertext.extend(encrypted_block) previous_block = encrypted_block # 更新链 return bytes(ciphertext) def decrypt_cbc(self, ciphertext: bytes, iv: bytes) -> bytes: """CBC模式解密。IV必须是16字节。""" if len(iv) != self.BLOCK_SIZE: raise ValueError(f"IV长度必须为{self.BLOCK_SIZE}字节") if len(ciphertext) % self.BLOCK_SIZE != 0: raise ValueError("密文长度必须是块大小的整数倍") plaintext_padded = bytearray() previous_block = iv for i in range(0, len(ciphertext), self.BLOCK_SIZE): block = ciphertext[i:i + self.BLOCK_SIZE] decrypted_block = self._decrypt_block(block) # 核心步骤:解密后的块与前一个密文块XOR,得到原始明文块 plain_block = bytes(a ^ b for a, b in zip(decrypted_block, previous_block)) plaintext_padded.extend(plain_block) previous_block = block # 更新链(注意这里是密文块) return self.pkcs7_unpad(bytes(plaintext_padded))实操心得2:CBC解密的链式依赖:注意看
decrypt_cbc函数,在解密第N块时,与之XOR的是第N-1块的密文(previous_block = block),而不是第N-1块解密后的明文。这是一个关键点。因为加密时是明文 XOR 前密文 -> 加密,所以解密时是解密 -> XOR 前密文。很多人在自己推导时容易在这里搞反。
3.5 补全AES单块加密/解密
由于完整的AES轮函数实现(特别是_mix_columns和密钥扩展_key_expansion)代码量较大且涉及较多数学细节,这里给出核心流程的伪代码和说明。你可以根据AES标准文档(FIPS-197)补全它们。
def _encrypt_block(self, block: bytes) -> bytes: """加密一个16字节块""" state = self._bytes_to_matrix(block) # 初始轮密钥加 state = self._add_round_key(state, self._key_schedule[0:4]) # 进行9轮标准轮函数 for round_num in range(1, self.ROUNDS): state = self._sub_bytes(state) state = self._shift_rows(state) state = self._mix_columns(state) # 需要实现 state = self._add_round_key(state, self._key_schedule[round_num*4:(round_num+1)*4]) # 最后一轮(不进行列混合) state = self._sub_bytes(state) state = self._shift_rows(state) state = self._add_round_key(state, self._key_schedule[self.ROUNDS*4:(self.ROUNDS+1)*4]) return self._matrix_to_bytes(state) def _decrypt_block(self, block: bytes) -> bytes: """解密一个16字节块(逆过程)""" state = self._bytes_to_matrix(block) # 注意:解密时轮密钥的使用顺序是反的 # 初始轮(对应加密的最后一轮) state = self._add_round_key(state, self._key_schedule[self.ROUNDS*4:(self.ROUNDS+1)*4]) state = self._inv_shift_rows(state) state = self._inv_sub_bytes(state) # 中间9轮 for round_num in range(self.ROUNDS-1, 0, -1): state = self._add_round_key(state, self._key_schedule[round_num*4:(round_num+1)*4]) state = self._inv_mix_columns(state) # 需要实现 state = self._inv_shift_rows(state) state = self._inv_sub_bytes(state) # 最后轮(对应加密的初始轮) state = self._add_round_key(state, self._key_schedule[0:4]) return self._matrix_to_bytes(state) def _add_round_key(self, state, round_key): """轮密钥加:状态矩阵与轮密钥矩阵对应字节XOR""" for i in range(4): for j in range(4): state[i][j] ^= round_key[i][j] return state4. 测试与验证:眼见为实
实现完成后,我们必须进行测试。最可靠的方法是使用已知的测试向量(Test Vectors),或者用标准库(如pycryptodome)的结果来验证我们自己的实现。
# test_aes.py import os # 假设我们已经有了完整的SimpleAES类 def test_ecb(): print("测试 ECB 模式...") key = b'ThisIsASecretKey' # 16字节 plaintext = b'Hello, AES! This is a test message for ECB mode.' aes = SimpleAES(key) # 使用我们的实现加密 my_cipher = aes.encrypt_ecb(plaintext) # 使用标准库加密(需安装 pycryptodome) from Crypto.Cipher import AES from Crypto.Util.Padding import pad cipher_lib = AES.new(key, AES.MODE_ECB) lib_cipher = cipher_lib.encrypt(pad(plaintext, AES.block_size)) print(f"明文: {plaintext[:20]}...") print(f"我们的密文 (hex): {my_cipher.hex()[:32]}...") print(f"标准库密文(hex): {lib_cipher.hex()[:32]}...") print(f"加密结果一致吗? {my_cipher == lib_cipher}") # 解密测试 my_decrypted = aes.decrypt_ecb(my_cipher) print(f"解密结果一致吗? {my_decrypted == plaintext}") print("-" * 50) def test_cbc(): print("测试 CBC 模式...") key = b'ThisIsASecretKey' # 16字节 iv = os.urandom(16) # 随机生成IV plaintext = b'Hello, AES! This is a test message for CBC mode. It is better than ECB!' aes = SimpleAES(key) # 我们的实现 my_cipher = aes.encrypt_cbc(plaintext, iv) # 标准库 from Crypto.Cipher import AES from Crypto.Util.Padding import pad cipher_lib = AES.new(key, AES.MODE_CBC, iv=iv) lib_cipher = cipher_lib.encrypt(pad(plaintext, AES.block_size)) print(f"IV: {iv.hex()}") print(f"明文: {plaintext[:20]}...") print(f"我们的密文 (hex): {my_cipher.hex()[:32]}...") print(f"标准库密文(hex): {lib_cipher.hex()[:32]}...") print(f"加密结果一致吗? {my_cipher == lib_cipher}") # 解密测试 my_decrypted = aes.decrypt_cbc(my_cipher, iv) cipher_lib2 = AES.new(key, AES.MODE_CBC, iv=iv) lib_decrypted = cipher_lib2.decrypt(lib_cipher) # 标准库解密不会自动unpad,需要手动操作 from Crypto.Util.Padding import unpad lib_decrypted = unpad(lib_decrypted, AES.block_size) print(f"解密结果一致吗? {my_decrypted == lib_decrypted == plaintext}") print("-" * 50) def visualize_ecb_weakness(): """可视化ECB模式的弱点:重复明文产生重复密文""" print("可视化ECB模式弱点...") key = b'Y' * 16 # 简单密钥 # 构造一个有明显重复模式的“图像”数据(用文本模拟) # 假设每16字节代表一行像素,黑色用‘B’,白色用‘W’ pattern_block = b'B' * 8 + b'W' * 8 # 一个黑白各半的块 plain_image = pattern_block * 8 # 重复8行,模拟一个条纹图像 aes = SimpleAES(key) cipher_ecb = aes.encrypt_ecb(plain_image) # 简单分析密文:将密文每16字节块计算一个简短的“指纹”(如前4字节的hex) print("ECB密文块‘指纹’(前4字节hex):") for i in range(0, len(cipher_ecb), 16): block = cipher_ecb[i:i+16] print(block[:4].hex(), end=' ') print("\n注意:由于相同明文块,这些‘指纹’很可能是重复的,揭示了原始图像的结构。") # 对比CBC iv = b'\x00' * 16 # 为了演示,用全零IV cipher_cbc = aes.encrypt_cbc(plain_image, iv) print("CBC密文块‘指纹’(前4字节hex):") for i in range(0, len(cipher_cbc), 16): block = cipher_cbc[i:i+16] print(block[:4].hex(), end=' ') print("\n注意:即使明文相同,CBC模式下每个密文块都不同,模式被隐藏。") if __name__ == '__main__': # 运行前请确保SimpleAES类已完整实现 # test_ecb() # test_cbc() visualize_ecb_weakness()运行这个测试脚本,你会直观地看到:
- 我们实现的
SimpleAES与标准库的输出是否一致(这是验证正确性的关键)。 - ECB模式下,重复的明文块导致重复的密文块“指纹”。
- CBC模式下,即使明文重复,密文也看起来是随机的。
5. 常见问题与排查技巧实录
在自己实现和调试的过程中,我踩过不少坑。这里把典型问题和解决方法记录下来,希望能帮你节省时间。
5.1 密文与标准库对不上?
这是最常遇到的问题。请按以下清单逐步排查:
- 密钥和明文处理:确认你的密钥和明文都是
bytes类型,且长度正确(AES-128密钥为16字节)。字符串记得用.encode()转换。 - 填充方案:这是头号嫌犯!确认你和对比的标准库使用了完全相同的填充方案。
pycryptodome默认使用PKCS7,但它的pad函数在数据长度恰好为块大小整数倍时,会额外填充一个完整块。你的pkcs7_pad函数也必须这么做。用只有1个字节的明文和刚好16字节的明文分别测试。 - IV的处理(CBC模式):确认IV是16字节,且在加密和解密时使用的是同一个IV。IV通常是随机的,需要和密文一起存储或传输。
- 状态矩阵的填充顺序:再次检查
_bytes_to_matrix和_matrix_to_bytes函数。这是最容易写错的地方之一。可以用一个简单的已知序列(如0x00到0x0F)测试转换是否正确。 - S盒和逆S盒:确保你使用的S盒和逆S盒数据完全正确,且索引计算无误(
row * 16 + col)。 - 列混合(MixColumns):这是实现中最复杂的部分,涉及有限域GF(2^8)上的乘法。务必仔细核对乘法查找表或计算函数。一个错误就会导致所有后续结果全错。
- 密钥扩展(Key Expansion):确保你的
_key_expansion函数生成的轮密钥是正确的。可以搜索“AES test vectors”,找到标准的密钥和对应的扩展密钥进行比对。 - 解密流程:确认解密是加密的精确逆过程。特别是轮密钥的使用顺序、逆列混合和逆行移位的方向。
排查技巧:采用“分而治之”策略。不要一次性写完所有代码再测试。先写一个
_encrypt_block函数,然后用一个最简单的测试向量(例如全零数据和全零密钥)验证单块加密。网上可以找到这些标准测试向量。单块加密正确后,再测试ECB模式,最后测试CBC模式。
5.2 解密后得到乱码或报错“Invalid padding”
- 密钥错误:这是最可能的原因。加解密必须使用相同的密钥。
- IV错误(CBC模式):解密时使用的IV必须和加密时相同。
- 密文被篡改:哪怕只改动密文中的一个比特,在CBC模式下会导致对应块及之后所有块解密失败(比特错误传播),并且很大概率导致最后的填充字节无效,从而触发
pkcs7_unpad中的有效性检查报错。 - 填充逻辑错误:检查你的
pkcs7_unpad函数。解密后,最后一个字节的值pad_len是否在1到16之间?末尾的pad_len个字节是否都等于pad_len?如果明文本身末尾就有像0x01这样的字节,可能会被误认为是填充。PKCS7的这种设计就是为了避免歧义,但你的检查逻辑必须严谨。 - 工作模式不匹配:用ECB加密的密文,不能用CBC模式解密,反之亦然。
5.3 性能问题与优化方向
我们为了教学实现的SimpleAES是纯Python的,且未做任何优化,性能会比C语言实现的标准库(如pycryptodome,底层是C或汇编)慢几个数量级。这很正常。
- 为什么慢?:Python是解释型语言,循环和位运算开销大。AES的S盒替换、列混合等操作在C中可以用查表法极快完成。
- 生产环境怎么办?:绝对不要在需要性能和安全性的生产环境中使用自己实现的密码学算法。务必使用经过严格审计、广泛测试的成熟库,如
pycryptodome、cryptography。 - 学习的意义:我们实现的目的在于理解,而非应用。通过这个慢速的“显微镜”,你看清了AES内部的每一个齿轮是如何转动的。今后当你使用
cipher = AES.new(key, AES.MODE_GCM)时,你心里清楚GCM模式大概是在CTR模式基础上加了GMAC认证,而不是在死记硬背一个API。
5.4 安全性注意事项(非常重要!)
- 本实现仅供学习:
SimpleAES没有经过任何安全审计,存在时序攻击等侧信道漏洞的风险,绝不能用于保护真实数据。 - ECB模式不安全:如演示所示,ECB会泄露明文模式。在任何需要保密性的场合,都应避免使用ECB。
- CBC模式需要正确的IV:IV必须是密码学安全的随机数(如
os.urandom(16)),且不可重复使用。使用固定IV或序列IV会严重削弱安全性。 - 缺乏认证:ECB和CBC只提供保密性,不提供完整性。攻击者可以篡改密文而不被发现(可能导致解密出有意义的错误明文)。现代应用更推荐使用认证加密模式,如GCM(Galois/Counter Mode),它同时提供保密性和完整性。
- 密钥管理:密钥的安全存储和分发是一个比算法实现更复杂的问题。本文不涉及。
通过这个从零实现AES-ECB/CBC的过程,我希望你收获的不仅仅是几行能跑的Python代码。更重要的是,你亲手拆解了分组密码这个“黑盒”,看到了ECB和CBC模式最本质的差异,理解了填充的必要性。下次当你再遇到加密相关的问题时,你脑海中的画面将不再是模糊的“调用某个库”,而是一幅清晰的、由XOR、S盒、行移位构成的加密流水线图。这种深度的理解,才是对抗“死记硬背”最有效的武器。