
现在写对称加密大多数人第一反应是AESDES在工程里早就退居二线了。但你要是正在学密码学、要写相关课程设计或者面试时被问到Feistel结构手写一遍DES的Python实现绝对是最值得做的练习。DES是教科书级的对称分组加密算法64位分组、56位有效密钥、16轮Feistel网络再加上8个S盒和一堆置换表它把现代分组密码的核心概念完整演示了一遍。这篇文章我不讲太多泛泛的背景直接沿着“从零实现DES”这条主线把整体结构、关键代码、测试向量和踩坑经验一起说清楚。目标是让你照着这篇文章能在一个小时内写出一个通过标准测试向量、还能对真实文本加解密的Python版DES。适合刚接触密码学的初学者、写课程设计的同学以及想搞明白分组密码底层原理的开发者。1. 为什么还要自己手写一遍DES我最早接触DES是用C写的后来工作里又用Python重写了一遍。周围不少朋友问过我同一个问题明明有pycryptodome这种现成库调一个接口就完事为什么还要自己折腾我的回答是调库拿到的是黑盒自己写一遍拿到的是一整套思维模型。DES虽然老但它的设计思想一点不老。手动实现一轮DES你不得不去理解每个细节初始置换IP、轮函数里的E扩展置换、8个S盒、P置换、密钥编排里的PC-1和PC-2、最后一轮的左右交换、逆初始置换IP^-1。这些概念在AES、SM4这些现代算法里虽然细节变了但骨架是相通的。把DES手写一遍再看其它算法的文档速度完全不一样。还有一个很打动我的点DES的加解密是同构的。解密不需要单独写一个F函数的逆而是把同一套Feistel流程跑一遍只是把16个子密钥的顺序倒过来。我当年第一次理解这件事时觉得这种对称之美是DES最优雅的地方。很多新手搞不懂“为什么解密不用求逆函数”这个疑问其实值得亲手写一遍代码去解开。手写DES还能顺带训练一些工程基本功字节和位之间的转换、大表数据的组织、分块时的边界对齐、填充协议的设计这些在平时写业务代码时很少遇到但在写底层协议、做嵌入式移植、分析二进制格式的时候全是基本功。当然也得说清楚边界手写版是绝佳的学习工具并不是生产工具。真实系统里要加密数据请用成熟的密码库并且优先选AES、SM4这类现代算法单DES已经是淘汰的东西不适合再用于实际保密。2. 动手前先把DES整体结构装进脑子2.1 64位分组、56位密钥与Feistel网络核心框架DES每次处理一个64位分组也就是8个字节。通常我们说DES是64位密钥严格讲是64位密钥材料其中第8、16、24、32、40、48、56、64这8个位是奇偶校验位真正参与算法运算的只有56位。整体流程可以拆成三步先做初始置换IP然后跑16轮Feistel网络最后经过左右交换和逆初始置换IP^-1输出密文。每次置换只是重新排列位的位置不改变位的值这一点新手容易忽略。Feistel网络一轮很简单。64位数据经过IP后被切成左半和右半各32位。一轮的更新规则是L_i R_{i-1}R_i L_{i-1} ⊕ F(R_{i-1}, K_i)也就是说这一轮的右半直接变成下一轮的左半左半则要和一轮函数F的输出做异或。这里的F就是整个DES的核心后面单独讲。为什么解密不需要F的逆函数关键在于R_{i-1}本身就是上一轮的左半部分。解密时已知L_i立刻就能拿到R_{i-1}L_i于是L_{i-1}R_i ⊕ F(L_i, K_i)就能算回来。异或是对称操作F在这里只是被异或的对象所以F本身不需要可逆。这个结构的好处是设计者可以随心所欲构造一个足够混乱的非线性函数只要它正向可计算就能用于构造可逆的分组密码。这是Feistel结构经久不衰的根本原因。2.2 密钥编排从64位主密钥到16个48位子密钥每轮Feistel网络都要用到一个48位的子密钥K_i16轮就需要16个。密钥编排干的事情就是把最初的64位密钥材料加工成这16个48位子密钥。过程分成四步。第一步PC-1置换。64位密钥里只有56位会进入下一步被跳过的正是那8个奇偶校验位。PC-1输出的56位对半分成C0和D0两个28位部分。第二步循环左移。C和D两个部分各自做循环左移每一轮移动的位数不是固定的而是查表得到第1轮和第9轮左移1位其余轮数左移2位。这个SHIFT表是DES标准的一部分不能自己乱改改一处结果就全错。第三步PC-2压缩置换。把每轮循环左移后的C_i和D_i拼接成56位再用PC-2表从中选出48位作为这轮的子密钥K_i。整个过程可以用一个简单的表概括步骤输入长度输出长度作用PC-164位56位剔除奇偶校验位循环左移28位28位28位28位让每轮子密钥不同PC-256位48位压缩并打乱生成子密钥我建议初学者把PC-1、PC-2和SHIFT表抄下来的时候多核对几遍这种表一旦抄错一个数字整个密钥编排就废了而且程序不会报错只会输出错误结果排查起来很费劲。2.3 轮函数F扩展、异或、S盒、P置换轮函数F接收两个输入32位的右半部分R和48位的子密钥K_i输出32位结果。F内部要做四件事E扩展置换、与子密钥异或、S盒替换、P置换。E扩展置换把32位的R扩展到48位具体做法是每4位一组每组向外扩展成6位其中首尾位会被复制一次。扩展的意义在于让R的位数能和48位子密钥对齐做异或同时让数据变化更快扩散到下一轮。48位结果和K_i做异或之后进入8个S盒。每个S盒接收6位输入输出4位8个S盒一共把48位压缩回32位。S盒是DES唯一的非线性组件也是DES抵抗线性密码分析、差分密码分析等攻击的关键所在。6位输入中第1位和第6位拼成一个2位的行号0到3中间4位拼成一个4位的列号0到15查表得到0到15之间的数然后转成4位二进制输出。最后一步是P置换。32位S盒输出经过P表的重新排列产生本轮F函数的最终输出。P置换的意义是扩散让一个S盒的输出尽可能影响到下一轮多个S盒的输入这是“雪崩效应”的重要来源。轮函数F的流水线也整理成一个表步骤输入长度输出长度作用E扩展置换32位48位扩展并对齐密钥长度与K_i异或48位48位48位混入密钥S盒替换48位32位非线性变换P置换32位32位重新分布位增强扩散3. Python实现的关键细节数据表示与常见陷阱3.1 数据表示位列表方案为什么更适合学习写代码之前首先要想清楚用什么数据结构表示64位的块。我见过两种主流做法一种是把块存成一个整数通过移位和掩码做运算另一种是把块展开成一个Python列表每个元素是0或1表示一个位。我最终选了位列表方案原因是它和置换表配合起来太直观了。置换本质上就是查表一个permute函数写出来就一行def permute(bits, table): return [bits[x - 1] for x in table]table里写着“取第几位放到当前位置”这一行代码就直接表达了这个语义调试的时候对着标准文档核对表数据非常方便。用整数加位运算也能做而且性能更好但每做一次置换都要写一堆循环和移位代码可读性差很多刚开始学习时很容易把自己绕晕。位列表方案的代价是速度和内存。Python里每个int对象都有额外开销一个64位块用列表存下来比用一个整数多耗几十倍内存运算也慢。但作为教学版本这点代价完全可以接受换来的可读性和可调试性非常值。3.2 标准表的下标陷阱从1开始而Python从0开始这是我在实现过程中踩得最狠的坑没有之一。DES标准文档里的所有置换表比如IP、E、PC-1、PC-2里面的数字全部从1开始编号。比如IP表的第一个数字是58意思是“输出的第1位来自输入的第58位”。但Python列表的下标从0开始。所以permute函数里必须做x-1否则取到的就会是输入的第59位有一个位的位置错位后面所有结果全错。更讨厌的是这种错位不会抛异常程序照样跑但输出对不上标准测试向量。我建议把permute函数单独写好然后反复测试。第一次实现的时候我以为自己记住了下标问题结果写S盒查表时又犯了一次S盒的行号、列号计算也涉及从0还是从1开始的语义。S盒表本身的行就是0到3列就是0到15这个和Python下标天然一致但如果拿着资料里手工标注的1-based表格对照就得多留个心眼。3.3 轮函数与S盒查表的实现要点轮函数F的核心代码其实很短。E置换就是把P置换函数复用一次异或用列表推导式一行搞定S盒查表稍微绕一点。S盒查表的关键是6位输入如何变成行号和列号。假设某组6位输入是chunk那么行号由第1位和第6位拼出列号由中间4位拼出row (chunk[0] 1) | chunk[5] col (chunk[1] 3) | (chunk[2] 2) | (chunk[3] 1) | chunk[4] val S_BOX[i][row][col]想理解这段代码可以这么类比6位输入就像一个二维坐标首尾两位指明“在第几行”中间四位指明“在第几列”到对应S盒的表里查一个数字出来。S盒8个表每个都是4行16列刚好对应行号0到3、列号0到15。查表得到的val是0到15的整数需要转成4位二进制追加到输出列表里。我用列表的extend方法一次补进4个bitout.extend([(val 3) 1, (val 2) 1, (val 1) 1, val 1])这样8组处理完48位输入就变成了32位输出。这个过程不难但S盒的8张表一共512个数字抄错一个都不行。我第一次动手时偷懒跳过核对结果对不上测试向量后来把8张表的每一行都和公开参考资料逐行比对才发现第4个S盒的第三行某个数字被我抄错了。3.4 面对真实文本分组与PKCS7填充DES一次加密一个64位分组但真实文本长度不可能总是8字节的整数倍。这就必须引入填充方案。我采用PKCS7填充这是分组密码里最常见的做法逻辑很自然还缺几个字节就补上几个字节每个填充字节的值等于填充长度。比如数据长度是10字节需要补到16字节就补6个值为0x06的字节。如果数据刚好是8字节的整数倍呢PKCS7的做法是仍然补一个完整的8字节块每个字节都是0x08。也就是说分组是8字节时数据长度模8为0pad_len算出来是8而不是0。这样做的好处是解密时可以通过最后一个字节的值确定要删几个填充字节不会和二义性纠缠。在我的最终封装函数里明文先pad分组加密解密后unpad。这一步看起来简单但特别容易和“子密钥反序”搞混。正确的顺序是加密的时候先pad再切块解密的时候先逐块解出完整数据再unpad去掉填充顺序不要反。4. 完整可运行的DES代码Python4.1 完整代码清单下面这一版我整理了完整的实现包括全部置换表和S盒数据Python 3.8以上版本无需任何第三方依赖复制就能运行。代码里同时包含了ECB模式下的字符串加解密封装以及标准测试向量验证。from typing import List 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_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, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1] P [16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25] PC1 [57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4] PC2 [14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32] SHIFT [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] S_BOX [ [ [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] ], [ [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], [3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], [0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9] ], [ [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], [1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12] ], [ [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], [3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14] ], [ [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], [4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3] ], [ [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], [9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], [4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13] ], [ [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], [1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], [6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12] ], [ [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], [1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], [7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], [2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11] ] ] def bytes2bin(data: bytes) - List[int]: bits [] for b in data: for i in range(7, -1, -1): bits.append((b i) 1) return bits def bin2bytes(bits: List[int]) - bytes: out [] for i in range(0, len(bits), 8): v 0 for bit in bits[i:i 8]: v (v 1) | bit out.append(v) return bytes(out) def bits2hex(bits: List[int]) - str: return bin2bytes(bits).hex().upper() def permute(bits: List[int], table: List[int]) - List[int]: return [bits[x - 1] for x in table] def generate_subkeys(key_bits: List[int]) - List[List[int]]: assert len(key_bits) 64, 密钥必须是64位 key56 permute(key_bits, PC1) left, right key56[:28], key56[28:] subkeys [] for shift in SHIFT: left left[shift:] left[:shift] right right[shift:] right[:shift] subkeys.append(permute(left right, PC2)) return subkeys def sbox_substitute(bits48: List[int]) - List[int]: out [] for i in range(8): chunk bits48[i * 6:(i 1) * 6] row (chunk[0] 1) | chunk[5] col (chunk[1] 3) | (chunk[2] 2) | (chunk[3] 1) | chunk[4] val S_BOX[i][row][col] out.extend([(val 3) 1, (val 2) 1, (val 1) 1, val 1]) return out def round_function(right32: List[int], subkey48: List[int]) - List[int]: expanded permute(right32, E) xored [a ^ b for a, b in zip(expanded, subkey48)] substituted sbox_substitute(xored) return permute(substituted, P) def des_block(bits64: List[int], subkeys: List[List[int]]) - List[int]: ip permute(bits64, IP) left, right ip[:32], ip[32:] for subkey in subkeys: new_left right new_right [a ^ b for a, b in zip(left, round_function(right, subkey))] left, right new_left, new_right pre_output right left return permute(pre_output, IP_INV) def des_encrypt_block(bits64: List[int], key_bits: List[int]) - List[int]: return des_block(bits64, generate_subkeys(key_bits)) def des_decrypt_block(bits64: List[int], key_bits: List[int]) - List[int]: return des_block(bits64, generate_subkeys(key_bits)[::-1]) def pkcs7_pad(data: bytes) - bytes: pad_len 8 - len(data) % 8 return data bytes([pad_len]) * pad_len def pkcs7_unpad(data: bytes) - bytes: pad_len data[-1] if pad_len 1 or pad_len 8: raise ValueError(填充值非法) return data[:-pad_len] def des_encrypt(plaintext: bytes, key: bytes) - bytes: if len(key) ! 8: raise ValueError(DES密钥必须是8字节64位) key_bits bytes2bin(key) subkeys generate_subkeys(key_bits) padded pkcs7_pad(plaintext) result [] for i in range(0, len(padded), 8): block bytes2bin(padded[i:i 8]) result.append(bin2bytes(des_block(block, subkeys))) return b.join(result) def des_decrypt(ciphertext: bytes, key: bytes) - bytes: if len(key) ! 8: raise ValueError(DES密钥必须是8字节64位) key_bits bytes2bin(key) subkeys generate_subkeys(key_bits)[::-1] result [] for i in range(0, len(ciphertext), 8): block bytes2bin(ciphertext[i:i 8]) result.append(bin2bytes(des_block(block, subkeys))) return pkcs7_unpad(b.join(result)) if __name__ __main__: test_key bytes.fromhex(133457799BBCDFF1) test_msg bytes.fromhex(0123456789ABCDEF) print(标准测试向量输出:, bits2hex(des_encrypt_block(bytes2bin(test_msg), bytes2bin(test_key)))) key b12345678 plaintext bHello, DES implementation! cipher des_encrypt(plaintext, key) print(文本加密结果:, cipher.hex().upper()) recovered des_decrypt(cipher, key) print(解密与原文一致:, recovered plaintext)4.2 用标准测试向量验证实现写完代码最怕的就是“看起来对了实际错得离谱”。DES有个非常经典的公开测试向量密钥取十六进制133457799BBCDFF1明文取0123456789ABCDEF期望密文是85E813540F0AB405。上面代码运行时第一行打印的标准测试向量输出就是这一串值。如果你的实现输出不是这个说明至少有一个表或一个逻辑环节有问题。这个向量相当于一把尺子能精准地帮你定位问题。我实际调试时的经验是一开始跑出错误结果不要慌先在代码里加几个print分别打印IP置换后的左右半部分、第一轮F函数的输出、第一轮子密钥然后找一份靠谱的参考实现逐段比对中间值。比对你就会发现问题往往出在很具体的地方比如某个S盒的数据少写了一个0或者PC-2表某行前后顺序颠倒。4.3 真实字符串的加解密测试标准测试向量通过之后再拿真实文本验证一次。我的代码里用b12345678当密钥明文是Hello, DES implementation!这个明文长度不是8的整数倍正好测到了PKCS7填充逻辑。运行后会先打印出一段十六进制密文然后打印True表示解密后的数据和原始明文完全一致。这里有一个容易被忽略的点密钥必须是8字节。如果你习惯用字符串当密钥一定要先encode成bytes并且保证编码后长度正好是8。比如中文密钥经过UTF-8编码后长度很大就不能直接用。我的封装函数里已经做了长度校验少了会直接抛ValueError。5. 常见问题与排查技巧实录5.1 测试向量对不上先按这个顺序排查如果标准测试向量输出不对建议按下面这个顺序排查第一确认S盒数据完全正确。S盒是数据量最大的部分抄错概率最高而且一旦错一个值输出立刻不对。把8张表逐行和公开资料核对不要只看开头几行。第二确认所有置换表的下标都经过减一处理。记住标准表从1开始Python从0开始最容易出问题的就是E表和PC-2表因为这两个表数字范围跨度大抄的时候很容易眼晕。第三确认最后一轮的左右交换。DES在16轮结束之后必须把R16放在左边、L16放在右边然后才做IP逆置换。我的代码里是right left这一步很多人会写反。第四确认子密钥生成时循环左移的位数。SHIFT表是[1,1,2,2,...]第1轮和第9轮都是1位其余是2位别把表记成固定2位。症状可能原因处理方式标准向量输出完全不对S盒数据错、置换表下标错逐表核对并打印中间值前面几轮对后面发散循环左移位数表错核对SHIFT表解密失败子密钥没有反序检查[::-1]5.2 解密出来乱码三个高频原因解密乱码是第二个高频问题通常出在三个地方。子密钥反序。加密用的是generate_subkeys(key_bits)解密必须用generate_subkeys(key_bits)[::-1]。如果解密直接复用了加密的子密钥顺序只有加密的逆过程永远不会成立结果一定是乱码。填充没有处理。DES只能处理完整8字节块解密后必须调用pkcs7_unpad去掉填充字节。如果你只做了逐块解密就急着转字符串多出来的填充字节会变成乱码字符。更隐蔽的是如果你加密前没pad解密后却unpad或者反过来都会出问题。切块边界没对齐。密文长度必须是8的整数倍如果因为填充错误导致密文长度出现意外最后一组不满8字节代码就会在处理最后一块时异常或产生错误结果。碰到这种情况回过头检查加密时是否对明文长度做了pad。5.3 速度慢是正常的学习版与工程版的边界如果你拿这版代码去加密一个几MB的文件会明显感觉到慢。位列表方案每个位都是一个Python对象运算开销非常大加密1MB数据可能需要好几秒这在学习和验证场景下没问题但绝不能拿去搞真实加密。想提速可以做两件事一是把每个块从位列表改成整数通过移位和掩码配合置换表实现运算会快一个数量级二是直接换用成熟密码库。我个人的建议是学习阶段先用位列表把算法逻辑彻底搞明白之后再考虑优化。永远不要觉得自己手写了一个能跑的DES就可以替代工程中的标准库。5.4 弱密钥、分组模式与下一步练习写完之后还有两个值得了解的知识点。第一DES存在弱密钥。全0和全1这类密钥会让16轮子密钥全部相同还有一类半弱密钥会让子密钥只有两种模式轮流出现。真实系统里使用DES时通常需要过滤掉这类密钥。虽然现在不建议用单DES但这个常识在分析别的算法时也很有参考价值。第二我这版代码默认是ECB模式。ECB有个明显的缺陷相同的明文块会得到相同的密文块。测试的时候你会发现用同一把密钥反复加密同一个块输出永远一样。真实场景更常用CBC模式每个明文块先和上一块密文异或再进入DES这样相同明文块在不同位置会得到不同密文。我在写完DES之后顺手给外层加了一个CBC模式改动量很小加密时把上一个密文块缓存下来当前明文块先和它异或再进des_block解密时在上一个块完整解出之前先把密文缓存下来当前块解密后和缓存值异或回去走一遍。这一步做完你对分组密码工作模式的理解也会更具体。如果你也想手动实现一遍DES我的建议是先别看完整代码自己把置换表和S盒敲进去然后对着标准测试向量调试一步步把问题找出来。踩坑的过程恰恰是这个练习价值最大的部分。