从零实现DES加密算法:Feistel网络与C语言实战详解
1. 项目概述为什么我们今天还要聊DES如果你刚接触密码学或者正在寻找一个经典的对称加密算法来练手DESData Encryption Standard绝对是一个绕不开的名字。尽管在今天的应用场景中它早已因为56位的密钥长度被AESAdvanced Encryption Standard等更安全的算法取代但理解DES就像是学习计算机科学时去理解一台老式计算机的架构——它奠定了现代分组密码的许多基础思想和结构。我当年啃下DES的实现后再去看AES、SM4这些现代算法感觉就像打通了任督二脉很多复杂的概念比如Feistel网络、S盒一下子就清晰了。这个项目就是带你从零开始彻底搞懂DES加密算法的原理并用C语言亲手实现它。这不仅仅是为了完成一个“第1关对称加密算法”的作业更是为了让你掌握密码学核心的“积木块”。你会发现网络上很多关于DES的代码要么过于简略要么存在理解偏差。我将结合自己踩过的坑把每个步骤掰开揉碎从密钥生成到最后的加解密流程让你不仅能写出代码更能明白每一行代码背后的数学和逻辑。准备好了吗我们开始这场从原理到实现的深度之旅。2. DES算法核心原理深度拆解DES是一种分组密码算法它采用64位为一组进行加密和解密密钥长度名义上是64位但实际有效的只有56位另外8位用于奇偶校验。它的核心设计思想是混淆Confusion和扩散Diffusion通过重复的替代和置换操作使得密文与明文、密文与密钥之间的关系变得极其复杂。2.1 核心结构Feistel网络DES的整体结构采用了经典的Feistel网络Feistel Cipher。这是理解DES乃至许多其他对称加密算法的钥匙。它的精妙之处在于加密和解密可以使用完全相同的结构只是子密钥的使用顺序相反。这极大地简化了硬件和软件的实现。Feistel网络将输入的64位明文分成左右两半各32位记为L0和R0。然后进行16轮完全相同的操作。在每一轮ii从1到16中右半部分Ri-1直接成为下一轮的左半部分Li。左半部分Li-1与一个轮函数F作用于Ri-1和本轮子密钥Ki的结果进行异或XOR得到下一轮的右半部分Ri。用公式表示就是 Li Ri-1 Ri Li-1 XOR F(Ri-1, Ki)经过16轮后得到L16和R16注意最后还需要进行一次左右交换输出R16, L16作为最终的64位密文。注意这个“最后交换”的步骤非常关键它保证了Feistel网络的对称性使得解密过程只需倒序使用子密钥即可而无需设计一个逆函数F^-1。这是Feistel结构最伟大的贡献之一。2.2 心脏部件轮函数F(R, K)详解轮函数F是DES安全性的核心它接受32位的右半部分输入R和48位的子密钥K输出一个32位的结果。它包含了DES几乎所有的精华操作扩展置换、密钥混合、S盒替代和P盒置换。第一步扩展置换E盒将32位的输入R通过一个固定的扩展置换表E盒扩展为48位。这个操作有两个目的一是让数据长度与48位的子密钥匹配以便进行异或二是实现“扩散”让R中的一位能影响下一轮两个S盒的输入。第二步与子密钥异或Key Mixing将扩展后的48位结果与本轮48位的子密钥Ki进行按位异或XOR操作。这是将密钥引入加密过程的关键步骤。第三步S盒替代Substitution这是DES中唯一的非线性操作是“混淆”的主要来源。将上一步得到的48位数据分成8组每组6位分别送入8个不同的S盒Substitution Box。每个S盒是一个4行16列的查找表它将6位输入映射为4位输出。具体规则是6位输入的头尾两位组成一个2位数决定行号0-3中间4位组成一个4位数决定列号0-15。根据行列坐标在S盒表中查找对应的4位输出值。8个S盒总共输出32位。实操心得S盒的实现是DES代码中最容易出错的地方之一。一定要仔细核对S盒的表格数据标准中有明确定义并确保你的索引计算是正确的。一个常见的错误是将二进制索引直接当作十进制使用或者行、列索引计算反了。第四步P盒置换Permutation将S盒输出的32位结果通过一个固定的置换表P盒进行重新排列。这是进一步的“扩散”使得S盒的输出位被散布到下一轮的不同位置。至此轮函数F执行完毕输出32位结果用于与左半部分进行异或。2.3 密钥调度算法从56位到16个子密钥DES的初始密钥是64位但每8位中的最后1位是奇偶校验位实际参与运算的只有56位。密钥调度算法负责将这56位有效密钥生成16轮所需的16个48位子密钥。流程如下置换选择1PC-1对64位输入密钥进行置换并去掉8个校验位得到56位的密钥。这56位被分成两个28位的半密钥C0和D0。循环左移对于每一轮iC(i-1)和D(i-1)分别进行循环左移。左移的位数根据轮数而定第1、2、9、16轮左移1位其余轮次左移2位。得到Ci和Di。置换选择2PC-2将Ci和Di合并成的56位中间密钥通过PC-2置换压缩并重排输出48位的本轮子密钥Ki。正是由于每轮循环左移的位数不同才产生了16个不同的子密钥。解密时只需将子密钥的使用顺序倒置K16, K15, ..., K1即可。3. DES加密/解密的完整流程实现理解了核心原理我们就可以用代码来构建整个DES引擎了。下面我将以C语言为例分模块实现。为了清晰我们会将各个置换表、S盒定义为常量数组。3.1 数据结构与常量定义首先我们需要定义所有DES标准中规定的固定表格。这里只列出关键部分示例完整表格需参考标准文档。// 示例IP初始置换表64位输入64位输出 const int IP_Table[64] { 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, // ... 省略后续56个元素 }; // 示例第一个S盒 S1 const int S1_Box[4][16] { {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} }; // 其他S盒S2-S8、扩展置换表E、P盒置换表、逆初始置换表IP^-1、 // 密钥置换PC-1、PC-2表、循环左移位数表等都需要完整定义。为了方便操作我们通常将64位数据用两个32位无符号整数uint32_t来表示高低位或者用一个64位无符号整数uint64_t来表示。这里我们使用uint64_t来简化位操作。3.2 核心工具函数实现我们需要一些基础的位操作函数例如置换函数和循环左移函数。#include stdint.h /** * 通用置换函数 * param src 源数据64位或更少 * param table 置换表指针 * param table_size 置换表大小也是输出位数 * return 置换后的结果 */ uint64_t permute(uint64_t src, const int *table, int table_size) { uint64_t result 0; for (int i 0; i table_size; i) { int src_pos table[i] - 1; // 表格中位置是从1开始计数转换为从0开始 uint64_t bit (src (64 - src_pos)) 0x01; // 取出src中对应位 result (result 1) | bit; // 将bit放到result的最低位 } // 注意如果table_size不是64result的高位是0。对于输出小于64位的置换如PC-2需要额外处理。 // 更严谨的做法是让result的类型和移位位数随table_size动态变化这里为简化使用uint64_t。 return result; } /** * 对28位数据进行循环左移 * param data 28位数据存放在uint32_t的低28位 * param shift 左移位数1或2 * return 循环左移后的28位数据 */ uint32_t leftRotate28(uint32_t data, int shift) { // 确保只操作低28位 uint32_t mask 0x0FFFFFFF; data mask; return ((data shift) | (data (28 - shift))) mask; }3.3 密钥生成模块实现这是DES的第一步也是独立于加解密过程可以预先计算的。// 假设已经定义了 PC1_Table[56], PC2_Table[48], shift_schedule[16] void generate_subkeys(uint64_t initial_key, uint64_t subkeys[16]) { uint64_t permuted_key_56; uint32_t C, D; int i; // 1. 通过PC-1置换得到56位密钥实际存储在64位变量的低56位 permuted_key_56 permute(initial_key, PC1_Table, 56); // 2. 分割成C0和D0各28位 C (uint32_t)((permuted_key_56 28) 0x0FFFFFFF); // 取高28位 D (uint32_t)(permuted_key_56 0x0FFFFFFF); // 取低28位 // 3. 生成16轮子密钥 for (i 0; i 16; i) { // 3.1 循环左移 C leftRotate28(C, shift_schedule[i]); D leftRotate28(D, shift_schedule[i]); // 3.2 合并C和D56位 uint64_t combined_CD ((uint64_t)C 28) | (uint64_t)D; // 3.3 通过PC-2置换压缩成48位子密钥 subkeys[i] permute(combined_CD, PC2_Table, 48); } }3.4 轮函数F的实现这是DES算法最核心的函数。// 假设已经定义了 E_Table[48], P_Table[32], 以及8个S盒 S1_Box 到 S8_Box uint32_t F_function(uint32_t R, uint64_t K) { uint64_t expanded_R; uint64_t mixed; uint32_t output 0; int i; // 1. 扩展置换32位 - 48位 expanded_R permute((uint64_t)R, E_Table, 48); // 注意类型转换 // 2. 与子密钥异或 mixed expanded_R ^ K; // 都是48位数据存在于64位变量的低48位 // 3. S盒替代48位 - 32位 // 将48位mixed分成8个6位组 for (i 0; i 8; i) { int shift 42 - i * 6; // 获取第i个6位组从最高位开始 uint8_t six_bits (mixed shift) 0x3F; // 取出6位 // 计算S盒的行和列索引 int row ((six_bits 0x20) 4) | (six_bits 0x01); // 首位和末位组成行 int col (six_bits 1) 0x0F; // 中间4位组成列 uint8_t four_bits; // 根据i选择对应的S盒 switch (i) { case 0: four_bits S1_Box[row][col]; break; case 1: four_bits S2_Box[row][col]; break; // ... 补充 case 2 到 case 7 case 7: four_bits S8_Box[row][col]; break; default: four_bits 0; break; } // 将4位输出合并到最终的32位结果中 output (output 4) | four_bits; } // 4. P盒置换 output (uint32_t)permute((uint64_t)output, P_Table, 32); return output; }3.5 加密主函数实现现在我们可以组装完整的加密流程了。uint64_t des_encrypt(uint64_t plaintext, uint64_t initial_key) { uint64_t subkeys[16]; uint64_t permuted_text; uint32_t L, R, temp; int i; // 0. 生成16轮子密钥 generate_subkeys(initial_key, subkeys); // 1. 初始置换IP permuted_text permute(plaintext, IP_Table, 64); // 2. 分割成L0和R0 L (uint32_t)(permuted_text 32); // 高32位 R (uint32_t)(permuted_text 0xFFFFFFFF); // 低32位 // 3. 16轮Feistel迭代 for (i 0; i 16; i) { temp R; // R L XOR F(R, K) R L ^ F_function(R, subkeys[i]); L temp; // L 旧的R } // 4. 最后交换第16轮后本应是L16,R16交换后为R16,L16 // 注意因为最后一轮结束后我们已经做了 LR_old, RL_old^F(...) // 所以此时 (L, R) 实际是 (R16, L16) 不需要仔细分析。 // 标准的Feistel最后一轮后数据是 (L16, R16)其中 // L16 R15 // R16 L15 XOR F(R15, K16) // 然后需要交换左右两部分得到 (R16, L16) 作为预输出。 // 在我们的循环中循环结束后L保存的是R15R保存的是L15^F(R15,K16)。 // 所以我们需要交换L和R。 temp L; L R; R temp; // 此时(L, R) 就是 (R16, L16) // 5. 合并为64位预输出 uint64_t pre_output ((uint64_t)L 32) | (uint64_t)R; // 6. 逆初始置换IP^-1得到最终密文 uint64_t ciphertext permute(pre_output, IP_Inverse_Table, 64); return ciphertext; }3.6 解密函数实现得益于Feistel网络的完美对称性解密函数与加密函数几乎完全相同唯一的区别是子密钥的使用顺序是反的。uint64_t des_decrypt(uint64_t ciphertext, uint64_t initial_key) { uint64_t subkeys[16]; uint64_t permuted_text; uint32_t L, R, temp; int i; // 生成子密钥和加密一样 generate_subkeys(initial_key, subkeys); // 初始置换IP permuted_text permute(ciphertext, IP_Table, 64); L (uint32_t)(permuted_text 32); R (uint32_t)(permuted_text 0xFFFFFFFF); // 16轮Feistel迭代但子密钥逆序使用K16, K15, ..., K1 for (i 15; i 0; i--) { temp R; R L ^ F_function(R, subkeys[i]); // 使用 subkeys[i] i从15递减到0 L temp; } // 最后交换 temp L; L R; R temp; // 合并并逆初始置换 uint64_t pre_output ((uint64_t)L 32) | (uint64_t)R; uint64_t plaintext permute(pre_output, IP_Inverse_Table, 64); return plaintext; }4. 从DES到现代算法的思考与常见问题实现一个可工作的DES是学习的第一步。但在实际应用和深入理解密码学的道路上你会遇到更多问题。4.1 工作模式如何加密超过64位的数据我们实现的DES是基础的电码本ECB, Electronic Codebook模式。它有一个致命缺点相同的明文块会加密成相同的密文块。这对于有规律的数据如图像来说会留下明显的纹理特征安全性很低。因此实际应用中必须使用更安全的工作模式例如CBCCipher Block Chaining每个明文块先与前一个密文块异或再进行加密。需要一个初始化向量IV。这是过去非常常用的模式。CTRCounter将计数器加密后与明文异或。它可以并行计算非常适合现代处理器。实操心得永远不要在生产环境中使用ECB模式。即使你只是内部测试也养成使用CBC或CTR等模式的好习惯。IV初始化向量必须是随机且不可预测的但不需要保密。4.2 DES的安全性为何不足3DES又是什么DES被淘汰的根本原因是其56位的密钥长度。在算力飞速发展的今天暴力破解56位密钥2^56种可能对于大型组织或国家力量已非难事。为此催生了3DESTriple DES。3DES并非设计一个全新算法而是用DES进行三次加密以增加有效密钥长度。常见的有两种方式EDEEncrypt-Decrypt-EncryptCiphertext E(K3, D(K2, E(K1, Plaintext)))。如果K1K2K3则退化为单DES提供了向后兼容性。EEEEncrypt-Encrypt-Encrypt使用三个不同的密钥。3DES的有效密钥长度可达112位当K1和K3独立K2用于解密时安全性大大增强。但它也有明显缺点速度慢是DES的三倍且分组长度仍是64位在某些场景下可能受到“生日攻击”的影响。因此它最终也被更高效、更安全的AES所取代。4.3 常见实现陷阱与调试技巧在编写和调试DES代码时我踩过不少坑这里分享几个关键点位序问题DES标准文档中数据位通常是从左到右、从1开始编号MSB first。而在我们的代码和计算机内存中字节序和位序可能不同。确保你的置换函数permute正确地处理了这种映射。上面的示例代码采用了一种常见方法假设输入src的最高位第63位对应位置1。这需要与你定义置换表时的预期保持一致。S盒索引计算这是最高频的错误点。务必反复确认行、列索引的计算公式。row ((six_bits 4) 0x02) | (six_bits 0x01)和row ((six_bits 0x20) 4) | (six_bits 0x01)是等价的都是取首尾位。col (six_bits 1) 0x0F是取中间4位。测试向量一定要使用官方或广泛认可的测试向量来验证你的实现。例如可以使用NIST美国国家标准与技术研究院发布的已知答案测试KAT向量。输入特定的明文和密钥看输出的密文是否完全一致。这是验证算法实现正确性的唯一可靠方法。密钥生成验证单独测试你的generate_subkeys函数。找一组测试密钥手动计算或对照可靠实现检查每一轮生成的子密钥是否正确。密钥错了整个加解密过程就全错了。最后交换与合并在16轮迭代结束后一定要进行左右交换再进行逆初始置换。这个步骤很容易被忽略或写错导致解密失败。4.4 DES在当今的定位与学习价值今天DES本身已不再适用于需要安全保护的场合。但它的学习价值丝毫没有减弱密码学入门基石它完整包含了分组密码的所有基本概念置换、替代、混淆、扩散、Feistel结构、密钥调度、工作模式。理解现代算法的基础学习AES时你会对比其SPN结构Substitution-Permutation Network与Feistel网络的区别学习SM4时你会发现其整体结构与DES的相似与不同。有了DES的底子这些理解起来会快得多。实现能力的锻炼手动实现DES是一次对位操作、数据结构和算法逻辑的绝佳训练。当你成功运行自己的DES程序并完成一个完整的加密-解密循环后那种对密码学从抽象到具体的掌控感是只看论文和公式无法获得的。这或许就是这个经典项目历经数十年依然值得每一位对安全感兴趣的程序员去亲手实现的原因。它不仅仅是一个算法更是一把打开密码学大门的钥匙。