Python手搓DES加密算法:从Feistel网络到CBC模式完整实现
1. 项目概述:从零手搓一个经典的对称加密算法
最近在整理一些老项目的代码,发现不少地方还在用DES(Data Encryption Standard)做简单的数据混淆。虽然现在AES是主流,但DES作为密码学史上的一个里程碑,其精巧的Feistel网络结构和完整的加解密流程,依然是理解现代对称加密算法的绝佳入门案例。用Python从头实现一遍DES,不仅能让你彻底搞懂分组加密、轮函数、密钥编排这些核心概念,更能让你在面试或技术讨论时,对“为什么DES被淘汰”、“AES做了哪些改进”这类问题有底气十足的回答。这篇文章,我就带你用纯Python,不依赖pycryptodome这样的重型库,一步步把DES算法“造”出来。我们会深入每一行代码背后的数学原理和设计意图,并分享我在实现过程中踩过的坑和调试技巧。无论你是刚学完Python语法想找个有挑战性的练手项目,还是对密码学感兴趣但被各种数学符号劝退的开发者,这篇详解都能让你有所收获。
2. DES算法核心原理与设计思路拆解
在动手写代码之前,我们必须先吃透DES算法的“设计图纸”。DES是一种分组密码,每次处理64位(8字节)的明文数据块,输出64位的密文。它的密钥长度是64位,但实际有效的只有56位,另外8位是奇偶校验位。整个加密过程就像一条精密的流水线,包含初始置换、16轮Feistel迭代、最终置换三大阶段。而其中最核心、最精妙的部分,就是这16轮迭代所采用的Feistel网络结构。
2.1 为什么是Feistel网络?一个天才的设计
Feistel结构是DES乃至许多经典密码(如Blowfish)的基石。它的核心思想在于:将输入块分成左右两半(L0, R0),在每一轮中,右半部分(Ri-1)经过一个轮函数F处理后,与左半部分(Li-1)进行异或操作,得到新的右半部分(Ri);而旧的右半部分则直接成为新的左半部分(Li)。用公式表示就是: Li = Ri-1 Ri = Li-1 ⊕ F(Ri-1, Ki)
这里Ki是每一轮的子密钥。这个结构的绝妙之处在于,无论轮函数F本身设计得多么简单甚至不可逆,整个加解密过程都是可逆的。解密时,只需要使用相同的子密钥,但按相反的顺序应用即可。这极大地降低了对轮函数设计的要求,让我们可以把精力集中在让F函数足够“混乱”和“扩散”上。在DES中,这个F函数包含了扩展置换、与子密钥异或、S盒替换和P盒置换四个步骤,是算法安全性的核心。
注意:理解Feistel网络的可逆性是关键。你可以自己用纸笔推导一下,假设已知Li和Ri,以及Ki,如何还原出Li-1和Ri-1。这个练习能让你真正理解加解密为何对称。
2.2 密钥编排算法:从一把主钥匙生成16把子钥匙
DES的加密强度很大程度上依赖于每一轮使用的子密钥Ki都不同。密钥编排算法(Key Schedule)就是负责从初始的64位密钥(实际56位)生成这16个48位子密钥的流程。这个过程同样充满了置换和移位操作。
首先,一个称为“置换选择1”(PC-1)的固定表格会丢弃8个校验位,并将剩下的56位打乱重排,分成两个28位的半密钥C0和D0。接下来,在每一轮i,C(i-1)和D(i-1)会根据一个预定义的左循环移位表(对于第1, 2, 9, 16轮移1位,其他轮移2位)进行移位,得到Ci和Di。最后,将Ci和Di合并后的56位,通过另一个“置换选择2”(PC-2)的表格,压缩并重排成48位的子密钥Ki。
这个设计确保了子密钥之间具有高度的非线性关系,即使攻击者破解了某一轮的子密钥,也很难反推出主密钥或其他轮的子密钥。在代码实现时,我们需要预先定义好PC-1、PC-2以及循环移位表这三个核心表格。
3. 核心模块的Python实现与细节剖析
理论清晰后,我们就可以开始搭建DES的各个功能模块了。我将按照数据处理的流程,逐一实现置换、S盒、密钥生成等核心函数。这里会用到大量的位操作,Python的整数类型和位运算符(&,|,^,<<,>>,~)将是我们的主要工具。
3.1 位操作工具函数:一切的基础
由于DES算法处处都在处理特定位的提取、设置和置换,我们先封装几个底层工具函数,会让后续代码清晰很多。
def bit_string_to_int(bit_string): """将二进制字符串(如'1101')转换为整数""" return int(bit_string, 2) def int_to_bit_string(n, length=64): """将整数转换为指定位长度的二进制字符串,左侧补零""" return format(n, f'0{length}b') def get_bit(block, pos, block_size=64): """ 从数据块(整数表示)中提取特定位。 DES中位序通常从1开始(最左位为第1位),且是Big-Endian。 pos: 位的位置(1-indexed) """ # 将目标位移到最低位,然后与1进行与操作 if pos < 1 or pos > block_size: raise ValueError(f"Position {pos} out of range for block size {block_size}") # 注意:我们假设block的二进制形式,最高位(MSB)对应位置1 return (block >> (block_size - pos)) & 1 def set_bit(block, pos, value, block_size=64): """将数据块(整数表示)的特定位设置为0或1""" if value not in (0, 1): raise ValueError("Bit value must be 0 or 1") current_bit = get_bit(block, pos, block_size) if current_bit == value: return block # 通过异或翻转特定位 mask = 1 << (block_size - pos) return block ^ mask def permute(block, permutation_table, input_size): """ 通用的置换函数。 block: 输入数据块(整数表示) permutation_table: 置换表(列表,元素为位置序号,通常1-indexed) input_size: 输入块的位数 返回置换后的新整数。 """ result = 0 for i, pos in enumerate(permutation_table): bit = get_bit(block, pos, input_size) if bit: # 将提取到的位,设置到结果的新位置上(新位置是i+1) result = set_bit(result, i+1, 1, len(permutation_table)) return resultpermute函数是DES的瑞士军刀,初始置换、扩展置换、P盒置换、各种密钥置换都是靠它完成的。理解它的关键在于:置换表permutation_table中的每个数字,指定了输出结果的第i位,应该从输入数据的第几位取。比如[58, 50, 42, ...]这个初始置换表,意思是结果的第一位是输入的第58位,第二位是输入的第50位,以此类推。
3.2 实现S盒替换:非线性混淆的核心
S盒(Substitution-box)是DES算法中唯一的非线性部件,也是其安全性的关键。它将6位输入映射为4位输出。DES共有8个不同的S盒(S1到S8),每个都是一个4行16列的查找表。
S盒的查表规则有点特别:6位输入中,首尾两位(bits 1 & 6)组成一个2位数,决定行号(0-3);中间四位(bits 2-5)组成一个4位数,决定列号(0-15)。然后从表中对应位置取出一个0-15的数字,转换为4位二进制输出。
# DES的8个S盒定义 (来自FIPS PUB 46-3标准) S_BOXES = [ # S1 [ [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], [0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], [4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13] ], # S2 ... S8 此处省略,实际代码需完整列出 ] def s_box_substitution(input_48bit): """对48位输入进行S盒替换,输出32位""" output_32bit = 0 # 将48位分成8组,每组6位 for i in range(8): # 提取6位 start_bit = 48 - i * 6 # 注意我们的位序是从左到右(MSB到LSB) six_bits = 0 for j in range(6): six_bits = (six_bits << 1) | get_bit(input_48bit, start_bit - j, 48) # 计算行号和列号 row = ((six_bits >> 5) & 0b10) | ((six_bits >> 0) & 0b01) # 取首尾位 col = (six_bits >> 1) & 0b1111 # 取中间4位 # 查表 s_box_value = S_BOXES[i][row][col] # 将4位输出拼接到结果中 output_32bit = (output_32bit << 4) | s_box_value return output_32bit实操心得:S盒的实现最容易出错的地方就是位提取的顺序。一定要对照标准文档,确认你的位编号方式(是MSB为1还是LSB为1)和分组顺序(是从左到右还是从右到左)。我建议在函数开头用一组固定的测试数据(如全零输入)验证输出是否符合标准测试向量。
3.3 密钥生成器:按轮次生产子密钥
密钥生成器是一个有状态的类,它接收初始密钥,然后能按需生成每一轮需要的子密钥。
class KeyScheduler: # PC-1置换表(56位输出,忽略校验位) PC1 = [57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, ... # 省略完整表格 ] # PC-2置换表(48位输出) PC2 = [14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, ... # 省略完整表格 ] # 每轮左循环移位位数 SHIFT_SCHEDULE = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] def __init__(self, initial_key): # 初始密钥是64位整数(包含8位校验位) self.initial_key = initial_key # 应用PC-1置换,得到56位有效密钥,并分成C0和D0 key_56 = permute(initial_key, self.PC1, 64) self.C = key_56 >> 28 # 高28位 self.D = key_56 & ((1 << 28) - 1) # 低28位 self.round = 0 def get_next_key(self): """生成下一轮的子密钥(48位)""" if self.round >= 16: raise StopIteration("All 16 round keys have been generated.") # 根据轮次移位 shift = self.SHIFT_SCHEDULE[self.round] self.C = ((self.C << shift) | (self.C >> (28 - shift))) & ((1 << 28) - 1) self.D = ((self.D << shift) | (self.D >> (28 - shift))) & ((1 << 28) - 1) # 合并C和D,然后应用PC-2置换 combined_56 = (self.C << 28) | self.D round_key = permute(combined_56, self.PC2, 56) self.round += 1 return round_key def reset(self): """重置调度器状态,用于解密或重新加密""" self.__init__(self.initial_key)密钥生成器的逻辑是线性的,但要注意循环移位的实现。((self.C << shift) | (self.C >> (28 - shift))) & ((1 << 28) - 1)这行代码实现了28位整数内的循环左移,& ((1 << 28) - 1)是为了确保结果仍然在28位以内,丢弃可能溢出的高位。
4. 完整的DES加解密流程实现
有了所有零部件,现在我们可以组装完整的DES引擎了。我们将实现一个DES类,它提供encrypt_block和decrypt_block方法,用于处理单个64位数据块。
4.1 定义所有置换表
首先,我们需要把所有标准置换表定义成常量。这是最枯燥但必须精确无误的一步。这里以初始置换IP和其逆置换IP^-1为例:
# 初始置换IP表 IP = [58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7] # 最终置换(IP逆置换) IP_INV = [40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25] # 扩展置换E表(将32位扩展到48位) E = [32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, ... # 省略完整表格 ] # P盒置换表 P = [16, 7, 20, 21, 29, 12, 28, 17, ... # 省略完整表格 ]4.2 轮函数F的实现
轮函数F是每一轮加密的核心,它接收32位的右半部分R和48位的子密钥K,输出32位。
def f_function(r_block_32bit, round_key_48bit): """DES的轮函数F""" # 1. 扩展置换:32位 -> 48位 expanded = permute(r_block_32bit, E, 32) # 2. 与轮密钥异或 xored = expanded ^ round_key_48bit # 3. S盒替换:48位 -> 32位 substituted = s_box_substitution(xored) # 4. P盒置换 output = permute(substituted, P, 32) return output4.3 加密单块的核心逻辑
现在,我们可以实现加密一个64位数据块的主函数了。
class DES: def __init__(self, key): # 密钥可以是64位整数,也可以是8字节的字节串 if isinstance(key, bytes): if len(key) != 8: raise ValueError("DES key must be 8 bytes (64 bits)") self.key = int.from_bytes(key, 'big') # 大端序 else: self.key = key # 假设是整数 self.key_scheduler = KeyScheduler(self.key) def encrypt_block(self, plaintext_block): """加密一个64位的明文块,返回64位密文块(整数)""" if isinstance(plaintext_block, bytes): if len(plaintext_block) != 8: raise ValueError("Plaintext block must be 8 bytes") block = int.from_bytes(plaintext_block, 'big') else: block = plaintext_block # 1. 初始置换IP block = permute(block, IP, 64) # 2. 拆分成左右两部分,各32位 L = block >> 32 R = block & 0xFFFFFFFF # 3. 16轮Feistel迭代 self.key_scheduler.reset() # 确保从第一轮密钥开始 for i in range(16): round_key = self.key_scheduler.get_next_key() new_R = L ^ f_function(R, round_key) L = R R = new_R # 4. 最后一轮结束后,交换左右(DES标准要求) # 注意:第16轮后不交换,但我们的循环结束时已经交换了,所以需要再换回来? # 实际上,在循环中,第16次迭代后,L保存的是R15,R保存的是L15^F(R15,K16) # 根据标准,最终组合前需要交换。所以: final_block = (R << 32) | L # 5. 最终置换IP^-1 ciphertext = permute(final_block, IP_INV, 64) return ciphertext def decrypt_block(self, ciphertext_block): """解密一个64位的密文块,返回64位明文块(整数)""" # 解密过程与加密几乎相同,唯一区别是子密钥的使用顺序相反 if isinstance(ciphertext_block, bytes): if len(ciphertext_block) != 8: raise ValueError("Ciphertext block must be 8 bytes") block = int.from_bytes(ciphertext_block, 'big') else: block = ciphertext_block # 初始置换 block = permute(block, IP, 64) L = block >> 32 R = block & 0xFFFFFFFF # 关键:预先生成所有轮密钥,并逆序使用 self.key_scheduler.reset() round_keys = [self.key_scheduler.get_next_key() for _ in range(16)] for i in range(15, -1, -1): # 从第16轮密钥反向到第1轮 round_key = round_keys[i] new_R = L ^ f_function(R, round_key) L = R R = new_R final_block = (R << 32) | L plaintext = permute(final_block, IP_INV, 64) return plaintext注意事项:加解密的对称性体现在子密钥的使用顺序上。加密时按K1, K2, ..., K16顺序使用;解密时则按K16, K15, ..., K1顺序使用。Feistel网络的结构保证了其他部分完全一致。很多初学者在这里搞错,导致解密失败。一个简单的记忆方法是:加密和解密函数可以共用绝大部分代码,只需在迭代循环中反转
round_keys列表的顺序即可。
4.4 工作模式与填充:让DES处理任意长度数据
上面的encrypt_block和decrypt_block只能处理恰好64位(8字节)的数据。现实中,我们需要加密任意长度的消息。这就需要引入工作模式和填充方案。最经典的模式是ECB(电子密码本)和CBC(密码块链接)。
ECB模式最简单,就是将消息分割成多个8字节块,分别加密,然后拼接。但其安全性较差,因为相同的明文块会产生相同的密文块,容易暴露模式。
CBC模式则更安全。它在加密前,先将当前明文块与前一个密文块(或一个初始向量IV)进行异或,然后再加密。这样,即使明文相同,加密结果也会因上下文不同而不同。
我们以CBC模式为例,实现一个完整的加密/解密函数,并处理PKCS#7填充。
def des_cbc_encrypt(key, iv, plaintext_bytes): """ 使用DES-CBC模式加密任意长度的字节数据。 key: 8字节的密钥 iv: 8字节的初始化向量 plaintext_bytes: 明文字节串 返回密文字节串。 """ des = DES(key) block_size = 8 # 1. PKCS#7填充 padding_len = block_size - (len(plaintext_bytes) % block_size) if padding_len == 0: padding_len = block_size # 如果长度正好是块大小的倍数,则填充一个完整的块 padded_plaintext = plaintext_bytes + bytes([padding_len] * padding_len) # 2. 分割成块 cipher_blocks = [] previous_block = iv # 第一个块的前一个“密文块”是IV for i in range(0, len(padded_plaintext), block_size): block = padded_plaintext[i:i+block_size] # 与前一密文块(或IV)异或 block_int = int.from_bytes(block, 'big') xor_int = block_int ^ int.from_bytes(previous_block, 'big') # 加密 encrypted_int = des.encrypt_block(xor_int) encrypted_block = encrypted_int.to_bytes(block_size, 'big') cipher_blocks.append(encrypted_block) previous_block = encrypted_block return b''.join(cipher_blocks) def des_cbc_decrypt(key, iv, ciphertext_bytes): """使用DES-CBC模式解密""" des = DES(key) block_size = 8 if len(ciphertext_bytes) % block_size != 0: raise ValueError("Ciphertext length must be a multiple of block size") plaintext_blocks = [] previous_cipher_block = iv for i in range(0, len(ciphertext_bytes), block_size): cipher_block = ciphertext_bytes[i:i+block_size] cipher_int = int.from_bytes(cipher_block, 'big') # 解密 decrypted_int = des.decrypt_block(cipher_int) # 与前一密文块异或(注意是前一个密文块,不是解密后的块) xor_int = decrypted_int ^ int.from_bytes(previous_cipher_block, 'big') plaintext_block = xor_int.to_bytes(block_size, 'big') plaintext_blocks.append(plaintext_block) previous_cipher_block = cipher_block # 更新前一个密文块 # 去除PKCS#7填充 padded_data = b''.join(plaintext_blocks) padding_len = padded_data[-1] # 简单的填充验证 if padding_len < 1 or padding_len > block_size: raise ValueError("Invalid padding") if padded_data[-padding_len:] != bytes([padding_len]) * padding_len: raise ValueError("Invalid padding") return padded_data[:-padding_len]实操心得:CBC模式的安全性依赖于IV(初始化向量)。IV必须每次加密都随机生成,并且不需要保密,但绝不能重复使用同一个IV和密钥对不同的消息进行加密。你可以将IV和密文一起存储或传输。解密时,使用相同的IV即可。另外,填充错误是常见的攻击向量,在解密后一定要严格验证填充的合法性,但要注意避免“填充预言攻击”,即不要因为填充错误就立即返回错误信息,最好统一处理。
5. 测试、验证与性能优化
实现完成后,我们必须进行严格的测试,确保算法正确无误。同时,纯Python实现的DES性能肯定无法与C语言库相比,但我们可以探讨一些优化思路。
5.1 使用标准测试向量验证
NIST(美国国家标准与技术研究院)等机构提供了标准的DES测试向量,用于验证实现的正确性。最经典的测试是使用全零的密钥和全零的明文。
def test_des_known_answers(): """使用已知答案测试DES实现""" # 测试1: 全零密钥和全零明文 key = bytes([0x00] * 8) plaintext = bytes([0x00] * 8) des = DES(key) ciphertext_int = des.encrypt_block(plaintext) ciphertext_hex = format(ciphertext_int, '016x') print(f"全零测试 - 密文: {ciphertext_hex}") # 已知标准密文应该是: 0x8CA64DE9C1B123A5 (具体值需查标准文档) # assert ciphertext_hex == '8ca64de9c1b123a5' # 解密验证 decrypted_int = des.decrypt_block(ciphertext_int) decrypted_bytes = decrypted_int.to_bytes(8, 'big') assert decrypted_bytes == plaintext, "解密结果与原始明文不符!" print("加解密自洽测试通过。") # 测试2: 使用一个随机密钥和明文 import os test_key = os.urandom(8) test_plain = os.urandom(8) des2 = DES(test_key) ct = des2.encrypt_block(test_plain) pt = des2.decrypt_block(ct) assert pt.to_bytes(8, 'big') == test_plain print("随机数据测试通过。") # 测试3: CBC模式测试 iv = os.urandom(8) message = b"This is a test message for DES-CBC!" ciphertext = des_cbc_encrypt(test_key, iv, message) decrypted = des_cbc_decrypt(test_key, iv, ciphertext) assert decrypted == message print("CBC模式测试通过。") print("所有基础测试通过!")如果测试通过,说明你的DES核心实现基本正确。你还可以在网上找到更全面的测试向量集,进行更彻底的验证。
5.2 常见问题与调试技巧实录
在实现DES的过程中,我遇到了不少坑,这里总结一下,帮你快速排雷:
位序问题:这是最大的坑。标准文档中的位编号通常是从左到右(MSB为1),而我们在代码中用整数表示时,二进制字符串的最左边是最高位。我的
get_bit和set_bit函数就是按照这个约定实现的。务必确保你所有的置换表、S盒查表逻辑都遵循同一套位序约定。一个有效的调试方法是,单独测试初始置换IP:输入一个只有第1位是1的明文(即0x8000000000000000),看输出是否等于置换表第一个元素(58)所指示的位置为1。S盒输出错误:S盒的输入是6位,输出是4位。确保你正确地从6位中提取了行和列。行号由第1位和第6位组成(注意是1-indexed),列号由中间4位组成。可以写一个单元测试,对每个S盒,遍历所有64种输入,将输出与标准值对比。
密钥生成错误:子密钥不对会导致整个加解密失败。检查PC-1和PC-2置换表是否准确,特别是循环移位表
SHIFT_SCHEDULE。可以打印出每一轮生成的子密钥(十六进制),与已知正确的密钥调度结果对比。Feistel网络最后一轮交换:DES标准规定,在16轮迭代后,左右两部分需要交换一次,然后再进行最终置换。但在具体实现时,由于循环结构,你可能在循环结束时已经得到了交换后的结果。仔细跟踪你代码中L和R的赋值逻辑,确保最终组合成64位块时,顺序是正确的。一个简单的检查方法是:加密一个块后立即解密,看是否能还原。
性能瓶颈:纯Python的位操作和循环比较慢。对于需要高性能的场景,这个实现仅适用于学习和验证。如果必须用Python且需要一定性能,可以考虑以下优化:
- 预计算:对于固定的置换操作,可以预先计算好查找表(Look-up Table)。例如,可以将8位输入的256种可能经过P盒置换后的结果全部算好存起来,这样运行时只需要一次查表,而不是32次位操作。这对S盒同样有效(但S盒本身已经是查表了)。
- 使用位切片技术:将64位数据用Python的
int表示,利用整数运算的并行性。我们现在的permute函数是逐位操作的,可以优化为同时处理多个位。但这会大大增加代码复杂度。 - 使用
numpy或ctypes调用C库:对于生产环境,最好的选择是使用pycryptodome或cryptography这些封装了C语言实现的库。我们的手搓实现,核心价值在于理解原理。
5.3 DES的安全性讨论与当代应用
最后,我们必须清醒地认识到,DES在今天已经不再安全。其56位的密钥长度,在现代计算能力(特别是GPU和专用硬件)面前,可以在合理时间内被暴力破解。1999年,电子前沿基金会(EFF)制造的“Deep Crack”机器,就在不到3天的时间里破解了一个DES密钥。
因此,绝对不要在任何需要真正安全性的新系统中使用DES。那为什么我们还要学习它呢?原因有三:
- 教学价值:DES结构清晰,是理解分组密码、Feistel网络、混淆与扩散等概念的完美教材。
- 历史兼容:一些遗留系统可能还在使用DES或它的变体3DES(Triple DES)。3DES通过使用两个或三个密钥进行三次加密,将有效密钥长度提升到112或168位,安全性有所提高,但速度慢了三倍,且仍有理论上的攻击可能。
- 理解演进:学习DES的弱点(密钥短、S盒设计有争议),能更好地理解其继任者AES(Advanced Encryption Standard)在设计上的改进,如更大的分组(128位)、更长的密钥(128/192/256位)、以及基于置换-置换网络(SPN)的更简洁结构。
如果你需要在Python项目中使用加密,请使用现代算法,如AES(通过pycryptodome库)。但通过亲手实现DES,你获得的底层洞察力,是单纯调用Cipher_AES.new(key, AES.MODE_CBC)所无法比拟的。下次当你配置TLS或者讨论加密算法时,你就能清晰地知道,数据在底层经历了怎样一场精心设计的“混乱之旅”。
