C语言实现DES加密算法:从原理到代码的完整实践指南

📅 2026/7/2 4:48:07
C语言实现DES加密算法:从原理到代码的完整实践指南
1. 项目概述为什么要在C语言里手搓DES如果你正在学习密码学或者想深入理解对称加密的底层运作那么自己动手用C语言实现一遍DES算法绝对是一个绕不开的经典项目。DESData Encryption Standard虽然现在已不再是主流的安全标准被AES取代但它作为现代分组密码的基石其精巧的Feistel网络结构和清晰的轮函数设计是理解密码学思想的绝佳教材。网上现成的库很多但“知其然”不如“知其所以然”自己从头实现一遍你会对位操作、置换、S盒这些概念有肌肉记忆般的理解。这个项目适合两类朋友一是正在学习《密码学》或《信息安全》课程的学生需要完成课程设计或加深理解二是对底层技术有好奇心的开发者想摆脱“调包侠”的标签看看这些加密函数到底在后台干了些什么。整个过程就像搭一个精密的乐高模型每一步都需要严丝合缝。接下来我会带你拆解DES的每一个关键步骤并附上可直接编译运行的C代码示例咱们不玩虚的直接上干货。2. DES算法核心原理与结构拆解2.1 DES算法的工作流程总览DES是一种分组密码每次处理64位8字节的明文数据块输出64位的密文。它的核心是一个对称结构加密和解密使用同一套流程仅在子密钥的使用顺序上相反。整个算法可以概括为以下几个大步骤初始置换IP对输入的64位明文进行一个固定的位置重排可以把它想象成洗牌目的是打乱明文的原始顺序。16轮Feistel迭代这是DES的心脏。每一轮都会使用一个不同的48位子密钥对数据的右半部分进行加密变换然后与左半部分进行异或操作最后左右交换。这个过程重复16次。最终置换IP⁻¹经过16轮折腾后再进行一次置换这次是初始置换的逆操作将数据还原成最终的密文输出。整个算法的安全性完全依赖于密钥。DES使用64位密钥但其中8位是奇偶校验位实际起作用的只有56位。这56位密钥会通过一系列置换和移位生成16个不同的48位子密钥供每一轮使用。2.2 Feistel网络结构DES的对称之美DES采用Feistel网络结构这是它设计上的一个妙处。这种结构最大的优点是加密和解密过程几乎完全相同只是子密钥的使用顺序反过来加密用K1到K16解密就用K16到K1。这极大地简化了硬件和软件的实现。每一轮Round的操作都遵循一个固定模式将64位输入分成左32位L和右32位R。本轮的输出右半部分 R’ 上一轮的左半部分 L。本轮输出的左半部分 L’ 上一轮的右半部分 R ⊕ F(上一轮的左半部分L, 本轮子密钥K)。其中F就是轮函数它是每轮加密强度的核心。这个结构保证了即使轮函数F本身不是可逆的整个加密过程也是可逆的这是Feistel网络的精髓。3. 关键步骤的C语言实现与深度解析3.1 数据结构设计如何表示“位”在高级语言里我们习惯以字节char为单位操作。但DES算法全程都在和单个的“位”打交道。因此选择一个高效、清晰的数据结构来表示位数组是关键。这里我推荐两种方式并解释为什么我选择第一种。方案一使用unsigned char数组这是最直观和灵活的方法。我们可以定义一个unsigned char数组数组的每一个元素代表一个位0或1。例如一个64位的块可以用unsigned char bits[64]来表示。typedef unsigned char BYTE; BYTE plaintext_bits[64]; // 存储64位明文 BYTE key_bits[64]; // 存储64位密钥含校验位为什么选它虽然内存效率不是最高一个char占8位我们只用了1位但它操作起来极其简单明了。通过数组下标直接访问和修改特定位在实现各种置换表IP PC-1等时代码可读性非常高非常适合教学和原理实现。我们首要目标是清晰而不是极致的性能。方案二使用unsigned long long(64位整数)这是性能最优的方案。一个unsigned long long类型在大多数现代系统上正好是64位。所有的位操作都可以通过C语言的位运算,|,,,^来完成速度极快。typedef unsigned long long ULL; ULL data_block 0; // 一个变量就存下64位它的缺点当我们需要实现置换比如把第58位放到第1位时使用纯位运算会非常绕代码像天书不利于理解和调试。在学习的初期我不建议使用这种方法。在本项目的代码示例中为了极致清晰我们将统一采用方案一即unsigned char数组来表示位流。在实际产品级代码中可能会采用方案二并进行复杂的位掩码优化。3.2 核心模块一子密钥生成DES的56位有效密钥需要生成16个48位的子密钥。这个过程是固定的可以分为以下几步密钥置换选择1PC-1输入64位密钥丢弃8个奇偶校验位第8, 16, 24, 32, 40, 48, 56, 64位并对剩下的56位进行置换。我们用一张有56个元素的表来定义这个置换。分割与循环左移将PC-1输出的56位分成两个28位的半部分C0和D0。接下来进行16轮处理每一轮中C和D分别根据一个固定的移位表对于第1, 2, 9, 16轮移1位其他轮移2位进行循环左移。得到C1D1, C2D2, ..., C16D16。密钥置换选择2PC-2在每一轮中将Ci和Di合并成的56位再通过PC-2置换表进行压缩置换丢弃其中8位输出一个48位的子密钥Ki。C语言实现要点我们需要预先定义好PC-1表、移位表和PC-2表。这些表都是标准化的可以直接从DES标准中复制过来。实现一个通用的permute函数它接受一个输入位数组、一个置换表、以及输入输出的位数。这个函数将在整个项目中反复使用。循环左移函数需要小心处理数组边界将最高位移到最低位。实操心得在测试子密钥生成时一个非常好的方法是使用一组标准的测试向量Test Vector。你可以找到已知的明文、密钥和密文然后打印出你程序生成的第一轮或第二轮子密钥与标准值对比。这是定位错误最快的方式比盲目调试高效得多。3.3 核心模块二轮函数F轮函数F是DES每一轮加密强度的来源它接受32位的右半部分输入R和48位的子密钥K输出一个32位的结果。其内部又分为四步扩展置换E将32位的R扩展为48位。这不是简单补零而是通过重复某些位来实现的。扩展表E定义了输出48位分别来自输入32位的哪一位。扩展的目的之一是为了与48位的子密钥进行异或操作。与子密钥异或将扩展后的48位结果与本轮子密钥Ki进行按位异或XOR得到48位的中间结果。S盒替代S-Box Substitution这是DES算法中唯一非线性的部分也是其安全性的核心。将上一步的48位结果分成8组每组6位。每一组独立进入一个不同的S盒共8个S盒。每个S盒是一个4行16列的查找表它将6位输入映射为4位输出。具体规则是6位中的首尾两位组成行号0-3中间四位组成列号0-15查找表中对应位置的数字0-15就是4位输出。8个S盒共输出32位。P盒置换P将S盒输出的32位结果通过一个固定的P置换表进行重排得到最终的32位轮函数输出。C语言实现难点与技巧S盒的实现S盒通常用二维数组来定义。例如S1[4][16]。关键是如何从6位输入b1b2b3b4b5b6中正确计算出行号(b11)|b6和列号(b23)|(b32)|(b41)|b5。这里位运算的优先级要特别注意保险起见多使用括号。位序问题在纸上描述算法时通常把最高位Most Significant Bit, MSB称为第1位。但在我们的unsigned char数组实现中bits[0]可能代表的是最低位LSB或最高位这取决于你的设计。你必须从头到尾保持一致的位序约定否则所有置换都会错乱。我建议让bits[0]代表最高位这样在阅读和调试时更直观因为置换表通常也是从1开始计数的。这意味着在从字节如char类型明文加载到位数组时需要做相应的位序转换。3.4 核心模块三主加密流程整合有了子密钥生成和轮函数主加密流程就剩下搭积木了。我们需要实现初始置换IP调用一次permute函数。16轮迭代将IP置换后的64位分成L0和R0各32位。for (i1; i16; i)L[i] R[i-1];R[i] xor(L[i-1], F(R[i-1], K[i]));//F是轮函数最终处理第16轮后先不交换直接得到L16和R16。然后合并成R16L16注意顺序再经过逆初始置换IP⁻¹就得到了最终的64位密文。重要注意事项很多初学者在这里会犯错。在DES标准中16轮迭代结束后需要再进行一次“左右交换”即交换L16和R16然后再进行IP⁻¹置换。但在一些描述和实现中将“交换”这步合并到了最后一轮迭代中即第16轮不进行左右交换。这两种理解是等价的但实现代码会略有不同。我们的代码将采用“16轮每轮都交换最后合并为R16L16”的方式这与经典教材的描述一致逻辑更清晰。4. 完整的C语言代码实现示例下面我将给出一个高度精简但结构完整的DES加密核心代码框架。为了节省篇幅我省略了完整的置换表IP, PC-1, PC-2, E, P, S-Boxes等这些表很长但都是公开的标准值你可以轻松从维基百科或标准文档中找到并复制。#include stdio.h #include string.h #include stdint.h // 类型定义用unsigned char数组表示位流 typedef unsigned char BIT; #define BLOCK_SIZE 64 #define HALF_BLOCK_SIZE 32 #define SUBKEY_SIZE 48 // 函数声明 void char_to_bits(const char* input, BIT* bits, int len); void bits_to_char(const BIT* bits, char* output, int len); void permute(const BIT* input, BIT* output, const int* table, int n); void generate_subkeys(const BIT* key_bits, BIT subkeys[16][SUBKEY_SIZE]); void des_round(BIT* left, BIT* right, const BIT* subkey); void des_encrypt_block(const BIT* plain_bits, BIT* cipher_bits, BIT subkeys[16][SUBKEY_SIZE]); void des_decrypt_block(const BIT* cipher_bits, BIT* plain_bits, BIT subkeys[16][SUBKEY_SIZE]); // 全局置换表示例仅列出结构实际表内容需补全 int IP_TABLE[64] {58, 50, 42, ...}; // 初始置换表 int IP_INV_TABLE[64] {40, 8, 48, ...}; // 逆初始置换表 int PC1_TABLE[56] {57, 49, 41, ...}; // 密钥置换1表 int PC2_TABLE[48] {14, 17, 11, ...}; // 密钥置换2表 int E_TABLE[48] {32, 1, 2, ...}; // 扩展表 int P_TABLE[32] {16, 7, 20, 21, ...}; // P盒置换表 int S_BOX[8][4][16] { // S盒三维数组 // 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 类似 }; // 工具函数将8字节字符串转换为64位数组bits[0]为最高位 void char_to_bits(const char* input, BIT* bits, int len) { for (int i 0; i len; i) { unsigned char byte input[i]; for (int j 0; j 8; j) { bits[i * 8 j] (byte (7 - j)) 0x01; // 注意取最高位MSB先存 } } } // 工具函数将位数组转换回字符串 void bits_to_char(const BIT* bits, char* output, int len) { memset(output, 0, len/8 1); for (int i 0; i len; i 8) { unsigned char byte 0; for (int j 0; j 8; j) { byte (byte 1) | bits[i j]; // 注意bits[i]是最高位 } output[i/8] byte; } } // 核心置换函数 void permute(const BIT* input, BIT* output, const int* table, int n) { for (int i 0; i n; i) { // 置换表table中的值是从1开始计数的所以需要减1 output[i] input[table[i] - 1]; } } // 核心生成16轮子密钥 void generate_subkeys(const BIT* key_bits, BIT subkeys[16][SUBKEY_SIZE]) { BIT pc1_out[56]; BIT C[28], D[28]; BIT CD[56]; // 1. PC-1置换 permute(key_bits, pc1_out, PC1_TABLE, 56); // 2. 分割成C0和D0 memcpy(C, pc1_out, 28 * sizeof(BIT)); memcpy(D, pc1_out 28, 28 * sizeof(BIT)); // 3. 16轮循环左移并生成子密钥 int shift_schedule[16] {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}; // 移位表 for (int round 0; round 16; round) { // 循环左移C和D left_shift(C, shift_schedule[round], 28); left_shift(D, shift_schedule[round], 28); // 合并C和D memcpy(CD, C, 28 * sizeof(BIT)); memcpy(CD 28, D, 28 * sizeof(BIT)); // PC-2置换生成该轮子密钥 permute(CD, subkeys[round], PC2_TABLE, SUBKEY_SIZE); } } // 辅助函数循环左移 void left_shift(BIT* bits, int shifts, int len) { BIT temp[28]; memcpy(temp, bits, len * sizeof(BIT)); for (int i 0; i len; i) { bits[i] temp[(i shifts) % len]; } } // 核心轮函数F void f_function(const BIT* right, const BIT* subkey, BIT* output) { BIT expanded[48]; BIT xored[48]; BIT sbox_out[32]; // 1. 扩展置换 E permute(right, expanded, E_TABLE, 48); // 2. 与子密钥异或 for (int i 0; i 48; i) { xored[i] expanded[i] ^ subkey[i]; } // 3. S盒替代 (这里简化处理实际需按6位分组查8个S盒) // 假设我们有一个函数 sbox_substitution 来处理这个过程 sbox_substitution(xored, sbox_out); // 4. P盒置换 permute(sbox_out, output, P_TABLE, 32); } // 核心加密单个64位块 void des_encrypt_block(const BIT* plain_bits, BIT* cipher_bits, BIT subkeys[16][SUBKEY_SIZE]) { BIT ip_out[BLOCK_SIZE]; BIT L[HALF_BLOCK_SIZE], R[HALF_BLOCK_SIZE]; BIT temp[HALF_BLOCK_SIZE]; // 1. 初始置换 IP permute(plain_bits, ip_out, IP_TABLE, BLOCK_SIZE); // 2. 分割成L0和R0 memcpy(L, ip_out, HALF_BLOCK_SIZE * sizeof(BIT)); memcpy(R, ip_out HALF_BLOCK_SIZE, HALF_BLOCK_SIZE * sizeof(BIT)); // 3. 16轮Feistel迭代 for (int i 0; i 16; i) { BIT f_out[HALF_BLOCK_SIZE]; // 保存当前的R因为它将成为下一轮的L memcpy(temp, R, HALF_BLOCK_SIZE * sizeof(BIT)); // 计算F(R, K) f_function(R, subkeys[i], f_out); // R[i] L[i-1] XOR F(R[i-1], K[i]) for (int j 0; j HALF_BLOCK_SIZE; j) { R[j] L[j] ^ f_out[j]; } // L[i] R[i-1] memcpy(L, temp, HALF_BLOCK_SIZE * sizeof(BIT)); } // 4. 最终合并与逆置换 (注意16轮后不交换合并为R16L16) BIT combined[BLOCK_SIZE]; memcpy(combined, R, HALF_BLOCK_SIZE * sizeof(BIT)); memcpy(combined HALF_BLOCK_SIZE, L, HALF_BLOCK_SIZE * sizeof(BIT)); permute(combined, cipher_bits, IP_INV_TABLE, BLOCK_SIZE); } // 解密函数结构与加密完全相同仅子密钥使用顺序相反 void des_decrypt_block(const BIT* cipher_bits, BIT* plain_bits, BIT subkeys[16][SUBKEY_SIZE]) { // 解密过程使用子密钥的顺序是 K16, K15, ..., K1 BIT ip_out[BLOCK_SIZE]; BIT L[HALF_BLOCK_SIZE], R[HALF_BLOCK_SIZE]; BIT temp[HALF_BLOCK_SIZE]; permute(cipher_bits, ip_out, IP_TABLE, BLOCK_SIZE); memcpy(L, ip_out, HALF_BLOCK_SIZE * sizeof(BIT)); memcpy(R, ip_out HALF_BLOCK_SIZE, HALF_BLOCK_SIZE * sizeof(BIT)); for (int i 15; i 0; i--) { // 注意循环从15到0 memcpy(temp, R, HALF_BLOCK_SIZE * sizeof(BIT)); BIT f_out[HALF_BLOCK_SIZE]; f_function(R, subkeys[i], f_out); // 使用 subkeys[i] for (int j 0; j HALF_BLOCK_SIZE; j) { R[j] L[j] ^ f_out[j]; } memcpy(L, temp, HALF_BLOCK_SIZE * sizeof(BIT)); } BIT combined[BLOCK_SIZE]; memcpy(combined, R, HALF_BLOCK_SIZE * sizeof(BIT)); memcpy(combined HALF_BLOCK_SIZE, L, HALF_BLOCK_SIZE * sizeof(BIT)); permute(combined, plain_bits, IP_INV_TABLE, BLOCK_SIZE); } // 主函数示例 int main() { char plaintext[9] 12345678; // 8字节明文 char key[9] abcdefgh; // 8字节密钥 char ciphertext[9] {0}; char decrypted[9] {0}; BIT plain_bits[BLOCK_SIZE]; BIT key_bits[BLOCK_SIZE]; BIT cipher_bits[BLOCK_SIZE]; BIT decrypted_bits[BLOCK_SIZE]; BIT subkeys[16][SUBKEY_SIZE]; // 1. 将字符串转换为位数组 char_to_bits(plaintext, plain_bits, BLOCK_SIZE); char_to_bits(key, key_bits, BLOCK_SIZE); // 2. 生成16轮子密钥 generate_subkeys(key_bits, subkeys); // 3. 加密 des_encrypt_block(plain_bits, cipher_bits, subkeys); bits_to_char(cipher_bits, ciphertext, BLOCK_SIZE); printf(Ciphertext (hex): ); for(int i0; i8; i) printf(%02x , (unsigned char)ciphertext[i]); printf(\n); // 4. 解密 des_decrypt_block(cipher_bits, decrypted_bits, subkeys); bits_to_char(decrypted_bits, decrypted, BLOCK_SIZE); printf(Decrypted text: %s\n, decrypted); return 0; }5. 调试、验证与常见问题排查自己实现DES99%的时间会花在调试上。下面是我踩过坑后总结的排查清单。5.1 如何验证你的实现是正确的使用标准测试向量Test Vectors这是最权威的方法。NIST等机构发布过标准的DES测试数据包括明文、密钥和密文。你可以用你的程序加密已知的明文和密钥看输出的密文是否完全一致。这是验证算法实现是否正确的黄金标准。加密-解密回环测试用任意数据加密后再解密看是否能还原。这是最基本的测试但只能证明你的算法大体可逆不能保证每一步都正确。分步骤输出比对对于复杂的算法可以打印中间结果。例如生成第一轮的子密钥K1与已知正确的K1进行比对。或者打印第一轮迭代后L1和R1的值。网上可以找到一些详细的中间过程示例这是定位错误轮次或模块的利器。5.2 常见Bug与排查技巧问题现象可能原因排查思路加密后再解密结果与原文不一致1. 子密钥生成错误2. 轮函数F计算错误特别是S盒3. 初始/最终置换表填错4. 加密解密流程中左右部分处理反了1. 先验证子密钥生成。用标准密钥打印出K1-K16与正确值比对。2. 单独测试轮函数F。给定一个简单的R和K手动计算S盒输出与程序输出比对。3. 检查所有置换表IP, IP⁻¹, PC-1, PC-2, E, P的数据是否准确复制特别注意数组下标是从0开始而置换表通常从1开始。4. 检查16轮迭代结束后合并成R16L16还是L16R16确保与IP⁻¹置换的输入要求匹配。加密结果与标准测试向量不符但自己加密解密能还原极有可能是位序问题。这是最隐蔽的Bug。确认你的char_to_bits和bits_to_char函数中位的顺序MSB还是LSB在前与你的置换表期望的顺序是否一致。整个程序必须统一采用一种位序约定。一个技巧是用全零和全一的输入进行测试跟踪第一个位的变化。程序运行崩溃段错误数组越界访问。检查所有数组的访问下标特别是置换函数permute中table[i] - 1不能为负数或超过输入数组长度。确保所有声明的数组大小足够。S盒输出完全不对1. S盒数据错误2. 行号列号计算错误1. 再次核对8个S盒的每一个数字一个数字错全盘皆错。2. 重点检查从6位输入计算4位行号和列号的代码。将6位输入打印成二进制手动计算行列再与程序计算的结果对比。对于某些特定密钥/明文结果不对可能遇到了DES的弱密钥或半弱密钥。DES有少数几个密钥被称为弱密钥如全0、全1等它们会使加密强度降低。但正常随机密钥不会遇到。可以先避开这些已知的弱密钥进行测试。独家避坑技巧在实现置换函数permute时不要一上来就写循环。先写一个最简单的测试创建一个数组input {0,0,0,...,1}即只有最后一位是1其他是0。然后经过置换看输出的1出现在哪个位置。这能帮你快速验证置换表本身和你的位序理解是否正确。同理测试S盒时可以构造一个只有中间四位有效的输入看输出是否与查找表对应。5.3 从教学实现到实用代码的思考我们上面的代码是“教学实现”追求清晰而非效率。在实际应用中你需要考虑更多工作模式DES是分组密码如何加密超过64位的数据这就需要工作模式如ECB不推荐、CBC常用、CFB、OFB等。你需要实现一种模式来处理任意长度的数据。填充Padding当数据不是64位的整数倍时需要进行填充。PKCS#7是常用的填充方案。性能优化将位数组操作替换为对unsigned long long的位运算并使用预计算的查找表如将多个S盒和P盒合并优化性能会有数量级的提升。安全性切记DES因密钥过短56位已不安全切勿用于真实的敏感数据加密。此项目仅供学习和理解密码学原理。实际应用应使用AES等更安全的算法。实现一个完整的DES算法就像完成一次精密的电子焊接需要极大的耐心和细心。当你的程序第一次通过标准测试向量时那种成就感是无与伦比的。它不仅加深了你对对称加密的理解更锻炼了你对底层位操作的掌控力和系统调试能力。这份代码骨架和排查思路希望能帮你少走弯路顺利通关这个经典的密码学实践项目。如果在实现S盒或置换时卡住了回头仔细对照标准表格一步步手动演算一个小例子永远是破解难题的最有效方法。