C语言实现3DES加密算法:从DES原理到CBC模式完整指南

📅 2026/7/4 2:33:39
C语言实现3DES加密算法:从DES原理到CBC模式完整指南
1. 项目概述为什么在C语言里折腾3DES最近在整理一些老项目的代码又看到了不少还在用3DES做数据加密的模块。说实话现在AES高级加密标准已经是绝对的主流无论是性能还是安全性都远超3DES。但现实情况是很多遗留系统、金融行业的特定接口或者一些对兼容性有苛刻要求的嵌入式设备里3DES依然“活”得很好。你可能觉得它过时了但作为一个开发者尤其是用C语言做底层开发的理解并能在C语言里实现3DES依然是一项很实用的技能。这不仅能帮你维护老代码更能让你透彻理解分组加密算法那些核心的玩意儿密钥编排、Feistel网络、工作模式……这些东西一通百通以后再学AES或者其他对称加密会轻松很多。这个项目就是带你从零开始在纯C语言的环境下完整地走一遍3DES加密和解密的实现过程。我们不依赖OpenSSL那样的大型库虽然生产环境强烈推荐用库而是自己动手把DES的核心——S盒、P置换、密钥生成——都实现一遍然后用它们搭出3DES。我会把重点放在“为什么”要这么做以及实际编码中那些容易踩坑的细节上。比如为什么3DES的密钥长度是168位但实际常用112位ECB模式和CBC模式在代码实现上到底差在哪怎么处理最后一块不足8字节的数据这些才是从“知道”到“会做”的关键。2. 3DES算法核心原理与自实现DES基础在动手写3DES之前我们必须先把它底层的DES数据加密标准搞清楚。3DES本质上就是DES的“三重奏”它的安全性建立在DES的基础之上。自己实现一个DES虽然性能上无法和硬件优化或成熟库相比但对于理解算法精髓至关重要。2.1 DES算法的核心流程与C语言结构设计DES是一种分组密码一次处理64位8字节的明文数据块输出64位的密文。它的核心是一个叫做Feistel的网络结构这种结构有个很棒的特性加密和解密过程几乎相同只是子密钥的使用顺序相反这大大简化了我们的实现。整个DES流程可以分解为几个清晰的步骤我们在C语言里用函数来对应初始置换IP对输入的64位明文进行一个固定的比特位重排。这个置换表是公开的。在C里我们可以用一个64位的unsigned long long类型变量来表示数据块然后通过位操作来完成置换。但更清晰的做法是定义一个uint8_t data[8]的数组然后按位查表搬运。这里第一个坑就来了DES标准文档里的位序是从左到右为1到64但我们的字节序大端/小端和位操作习惯会影响实现。一个稳妥的方法是始终以“比特数组”的视角来思考先实现一个通用的permute函数接受数据块和置换表一个指定新位置的表作为参数。生成子密钥输入的64位密钥实际只有56位有效每字节的第8位是奇偶校验位经过一系列置换和循环左移生成16个48位的子密钥K1~K16。这是DES的关键步骤之一。我们需要实现key_schedule根据主密钥生成16个子密钥的数组。这里涉及PC-1置换去掉校验位56位、循环左移根据轮数不同移1或2位、PC-2置换压缩成48位。循环左移需要特别注意56位密钥分成的左右28位部分要分别移位并且是循环的。16轮Feistel迭代这是DES的“心脏”。每一轮的操作都一样 a. 将64位数据分成左右各32位的L和R。 b. 本轮输出的L_new R_old。 c. 本轮输出的R_new L_old XOR f(R_old, subkey)。 这个f函数是核心中的核心它先将32位的R通过“扩展置换E”膨胀成48位然后与48位的子密钥进行异或接着经过8个“S盒”将48位压缩回32位最后经过一个“P置换”得到输出。S盒是DES唯一非线性的部分也是其安全性的关键它通过查表实现8个S盒每个都将6位输入映射为4位输出。末置换IP^-116轮结束后将左右半部分交换这是Feistel结构的要求然后进行一次末置换它是初始置换的逆过程得到最终的64位密文。注意所有置换表IP, IP^-1, E, P, PC-1, PC-2以及8个S盒都是公开的、固定的常数。在代码中我们应该将它们定义为全局的const数组。网上能找到的C实现差异往往就出在这些表的数值是否正确以及位序的理解是否一致上。2.2 从DES到3DES密钥与模式解析理解了DES3DES就很简单了。3DES顾名思义就是用DES算法对同一个数据块处理三次。它有三种常见的密钥方案EEE3模式三个独立密钥C E(K3, D(K2, E(K1, P)))。加密是E-D-E解密是D-E-D。这里E代表DES加密D代表DES解密。使用三个独立的56位有效密钥通常表示为168位含校验位则为192位。这是理论上最安全的形式。EDE3模式三个独立密钥C E(K3, D(K2, E(K1, P)))。注意它和EEE3的加密过程公式一样但通常我们说EDE3时强调的是中间是解密步骤。实际上因为DES加解密结构对称D(K, X) E(K’, X)K’是密钥的某种逆序所以EEE3和EDE3在安全性上没有本质区别但EDE3有一个巨大优势当K1K2K3时它就退化成了标准的DES提供了向后的兼容性。因此EDE3是实际应用中最常见、最推荐的模式。EDE2模式两个独立密钥C E(K3, D(K2, E(K1, P)))其中K3 K1。即密钥为K1, K2, K1。有效密钥长度112位。这是NIST标准中批准使用的在安全性和性能之间取得了较好的平衡足以抵御当前已知的暴力攻击。在我们的C语言实现中我们会采用EDE3模式作为标准。这意味着我们需要准备三个DES密钥结构体包含16个子密钥。实现一个tdes_encrypt函数内部依次调用des_encrypt_block(key1, plaintext, temp1)-des_decrypt_block(key2, temp1, temp2)-des_encrypt_block(key3, temp2, ciphertext)。解密函数tdes_decrypt则相反des_decrypt_block(key3, ...)-des_encrypt_block(key2, ...)-des_decrypt_block(key1, ...)。这里的关键是我们的des_encrypt_block和des_decrypt_block函数必须是纯粹的、可重入的只依赖于输入的数据块和密钥结构。3. C语言实现DES核心模块理论说够了我们开始写代码。我会先搭建DES的骨架再填充血肉。3.1 数据结构与常量定义首先定义我们需要的类型和常量。为了避免平台差异使用标准整数类型。#include stdint.h // 为了使用uint8_t, uint32_t等 #include string.h // 为了memcpy等 // 假设我们以8字节数组表示一个64位数据块 typedef uint8_t des_block[8]; // DES密钥结构体包含16轮的子密钥 typedef struct { uint8_t subkeys[16][6]; // 每个子密钥48位用6个字节存储 } des_key_schedule; // 3DES密钥结构体包含三个DES密钥 typedef struct { des_key_schedule k1, k2, k3; } tdes_key_schedule; // 声明全局的置换表和S盒具体数值很长此处省略需从标准文档中获取 extern const uint8_t ip_table[64]; // 初始置换表 extern const uint8_t fp_table[64]; // 末置换表逆初始置换 extern const uint8_t e_table[48]; // 扩展置换表 extern const uint8_t p_table[32]; // P置换表 extern const uint8_t pc1_table[56]; // 密钥置换选择1 extern const uint8_t pc2_table[48]; // 密钥置换选择2 extern const uint8_t shift_schedule[16]; // 每轮循环左移的位数1或2 extern const uint8_t s_boxes[8][4][16]; // 8个S盒每个是4行16列的查找表3.2 位操作工具函数与置换实现由于DES是对比特位进行操作我们需要一些辅助函数来处理比特。// 从数据块中获取第pos位位序从1开始从左到右 static int get_bit(const uint8_t *data, int pos) { pos--; // 转换为从0开始 int byte_idx pos / 8; int bit_idx 7 - (pos % 8); // DES标准是最高位最左为第1位我们按大端处理位序 return (data[byte_idx] bit_idx) 0x01; } // 设置数据块的第pos位为value0或1 static void set_bit(uint8_t *data, int pos, int value) { pos--; int byte_idx pos / 8; int bit_idx 7 - (pos % 8); if (value) { data[byte_idx] | (1 bit_idx); } else { data[byte_idx] ~(1 bit_idx); } } // 通用置换函数用table表对src进行置换结果存入dst。table定义了输出位对应的输入位位置。 static void permute(const uint8_t *src, uint8_t *dst, const uint8_t *table, int len) { memset(dst, 0, (len 7) / 8); // 按字节数初始化dst为0 for (int i 0; i len; i) { int src_pos table[i]; // table[i]的值表示输出的第i1位来自输入的哪一位 int bit_value get_bit(src, src_pos); set_bit(dst, i 1, bit_value); } }实操心得get_bit和set_bit中关于位序7 - (pos % 8)的处理是关键。DES标准是“大端位序”Most Significant Bit first而我们的CPU是小端字节序。这里我们约定data[0]的最高位第7位是整个64位数据块的第1位。保持这个约定贯穿始终就能避免混乱。很多自己实现的DES跑不对问题都出在位序和表的数据对不上。3.3 密钥生成与S盒变换的实现接下来实现密钥调度和Feistel函数中的S盒处理。// 生成DES的16轮子密钥 void des_key_setup(const uint8_t key[8], des_key_schedule *ks) { uint8_t permuted_key[7] {0}; // 56位7字节 uint8_t c[4], d[4]; // C0和D0各28位用4字节存储实际只用低28位 // 1. 经过PC-1置换去掉校验位得到56位密钥 permute(key, permuted_key, pc1_table, 56); // 2. 分割成C0和D0 (各28位) // 从permuted_key[0]-[6]这7字节中提取。需要小心处理比特。 // 这里简化我们可以将permuted_key看作一个56位的比特流。 // 实际代码需要按比特提取到c和d数组。为了清晰此处略去详细的位提取代码。 // ... for (int round 0; round 16; round) { // 3. 对C和D进行循环左移根据shift_schedule[round] // 4. 将移位后的C和D合并成56位然后经过PC-2置换生成48位子密钥 // permute(combined_cd, ks-subkeys[round], pc2_table, 48); // 同样这里需要详细的位合并与置换操作。 } } // Feistel函数中的f函数 static void f_function(const uint8_t r[4], const uint8_t subkey[6], uint8_t output[4]) { uint8_t expanded_r[6] {0}; uint8_t xor_result[6] {0}; uint8_t sbox_output[4] {0}; // 1. 扩展置换32位R - 48位 permute(r, expanded_r, e_table, 48); // 2. 与子密钥异或 for (int i 0; i 6; i) { xor_result[i] expanded_r[i] ^ subkey[i]; } // 3. 经过8个S盒48位 - 32位 for (int i 0; i 8; i) { // 取xor_result中对应的6位 // 第1位和第6位组成行号中间4位组成列号 int row ((xor_result[i*6/8] (7 - (i*6)%8)) 0x01) 1; row | ((xor_result[(i*65)/8] (7 - ((i*65)%8))) 0x01); // 注意上面的位提取非常繁琐是容易出错的点。通常我们会将xor_result看作一个连续的48位比特流来操作。 // 更清晰的实现是先将6字节的xor_result转换成一个48位的比特数组再分组处理。 int col ... // 提取中间4位 uint8_t sbox_val s_boxes[i][row][col]; // 将4位输出放到sbox_output的对应位置 // ... } // 4. P置换 permute(sbox_output, output, p_table, 32); }注意事项S盒处理是DES实现中最容易写错的部分。48位输入被分成8组6位每组通过一个S盒变成4位。这涉及到从字节数组中精确地提取连续的比特位。我强烈建议单独写一个函数bits_to_int和int_to_bits来处理这种跨字节的比特流操作或者直接使用一个48位的比特数组比如uint8_t bits[48]作为中间表示这样逻辑会清晰很多。直接在字节上做移位和掩码代码可读性极差且极易出错。4. 完整DES加密与3DES组装有了上面的基础模块我们就可以组装完整的DES加密/解密单块函数并最终构建3DES。4.1 DES单块加密与解密函数// DES加密一个64位数据块 void des_encrypt_block(const des_key_schedule *ks, const uint8_t plaintext[8], uint8_t ciphertext[8]) { uint8_t ip_block[8] {0}; uint8_t l[4], r[4], new_l[4], new_r[4]; // 1. 初始置换IP permute(plaintext, ip_block, ip_table, 64); // 2. 分割成L0和R0 memcpy(l, ip_block, 4); memcpy(r, ip_block 4, 4); // 3. 16轮Feistel迭代 for (int i 0; i 16; i) { memcpy(new_l, r, 4); // L[i] R[i-1] // R[i] L[i-1] XOR f(R[i-1], K[i]) uint8_t f_out[4] {0}; f_function(r, ks-subkeys[i], f_out); for (int j 0; j 4; j) { new_r[j] l[j] ^ f_out[j]; } // 为下一轮准备 memcpy(l, new_l, 4); memcpy(r, new_r, 4); } // 4. 最后交换L16和R16Feistel网络特性 uint8_t final_block[8] {0}; memcpy(final_block, r, 4); // 注意先放R16 memcpy(final_block 4, l, 4); // 再放L16 // 5. 末置换FP permute(final_block, ciphertext, fp_table, 64); } // DES解密一个64位数据块过程与加密完全相同只是子密钥使用顺序相反K16到K1 void des_decrypt_block(const des_key_schedule *ks, const uint8_t ciphertext[8], uint8_t plaintext[8]) { // 只需复制des_encrypt_block的代码然后将for循环改为 // for (int i 15; i 0; i--) { // 使用 ks-subkeys[i] // } // 其余部分完全一致 }4.2 3DES的ECB模式实现最简单的模式是ECB电子密码本模式每个数据块独立加密。我们先实现它。// 设置3DES密钥EDE3模式三个独立密钥 void tdes_key_setup(const uint8_t key1[8], const uint8_t key2[8], const uint8_t key3[8], tdes_key_schedule *ks) { des_key_setup(key1, ks-k1); des_key_setup(key2, ks-k2); des_key_setup(key3, ks-k3); } // 3DES-EDE3 ECB模式加密 void tdes_ecb_encrypt(const tdes_key_schedule *ks, const uint8_t *plaintext, size_t length, uint8_t *ciphertext) { // 假设plaintext长度是8的倍数填充问题后面讨论 for (size_t i 0; i length; i 8) { uint8_t temp1[8], temp2[8]; des_encrypt_block(ks-k1, plaintext i, temp1); des_decrypt_block(ks-k2, temp1, temp2); des_encrypt_block(ks-k3, temp2, ciphertext i); } } // 3DES-EDE3 ECB模式解密 void tdes_ecb_decrypt(const tdes_key_schedule *ks, const uint8_t *ciphertext, size_t length, uint8_t *plaintext) { for (size_t i 0; i length; i 8) { uint8_t temp1[8], temp2[8]; des_decrypt_block(ks-k3, ciphertext i, temp1); des_encrypt_block(ks-k2, temp1, temp2); des_decrypt_block(ks-k1, temp2, plaintext i); } }重要提示ECB模式有严重的安全缺陷相同的明文块会加密成相同的密文块这会泄露数据模式。它不能用于加密有重复模式的数据如图像、结构化文本。生产环境绝对不要使用ECB模式。这里实现它只是为了演示和测试算法核心的正确性。4.3 更安全的CBC模式实现与填充方案CBC密码块链接模式是实际中最常用的模式之一。它引入了一个初始化向量IV使得每个密文块都依赖于之前所有的明文块。// 3DES-EDE3 CBC模式加密带PKCS#7填充 int tdes_cbc_encrypt(const tdes_key_schedule *ks, const uint8_t iv[8], const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext, size_t *ciphertext_len) { // 1. 计算填充后的长度PKCS#7 size_t block_count (plaintext_len 8) / 8; // 向上取整到8的倍数 size_t padded_len block_count * 8; uint8_t pad_byte padded_len - plaintext_len; // 2. 准备填充后的缓冲区可以原地操作但需确保ciphertext缓冲区足够大 uint8_t *padded_data (uint8_t*)malloc(padded_len); if (!padded_data) return -1; // 内存分配失败 memcpy(padded_data, plaintext, plaintext_len); memset(padded_data plaintext_len, pad_byte, pad_byte); // 填充 // 3. CBC加密 uint8_t previous_block[8]; memcpy(previous_block, iv, 8); // 第一个块的前一个块是IV for (size_t i 0; i padded_len; i 8) { // 明文块与前一个密文块或IV异或 uint8_t xored_block[8]; for (int j 0; j 8; j) { xored_block[j] padded_data[i j] ^ previous_block[j]; } // 对异或结果进行3DES-EDE3加密ECB核心 uint8_t temp1[8], temp2[8]; des_encrypt_block(ks-k1, xored_block, temp1); des_decrypt_block(ks-k2, temp1, temp2); des_encrypt_block(ks-k3, temp2, ciphertext i); // 更新“前一个密文块”为当前密文块 memcpy(previous_block, ciphertext i, 8); } free(padded_data); *ciphertext_len padded_len; return 0; // 成功 } // 3DES-EDE3 CBC模式解密 int tdes_cbc_decrypt(const tdes_key_schedule *ks, const uint8_t iv[8], const uint8_t *ciphertext, size_t ciphertext_len, uint8_t *plaintext, size_t *plaintext_len) { if (ciphertext_len % 8 ! 0) { return -1; // 密文长度必须是8的倍数 } uint8_t previous_block[8]; memcpy(previous_block, iv, 8); for (size_t i 0; i ciphertext_len; i 8) { // 对当前密文块进行3DES-EDE3解密ECB核心 uint8_t temp1[8], temp2[8], decrypted_block[8]; des_decrypt_block(ks-k3, ciphertext i, temp1); des_encrypt_block(ks-k2, temp1, temp2); des_decrypt_block(ks-k1, temp2, decrypted_block); // 解密后的块与前一个密文块或IV异或得到明文块 for (int j 0; j 8; j) { plaintext[i j] decrypted_block[j] ^ previous_block[j]; } // 更新“前一个密文块” memcpy(previous_block, ciphertext i, 8); } // 处理PKCS#7填充检查最后一个字节的值移除相应数量的填充字节 uint8_t pad_byte plaintext[ciphertext_len - 1]; if (pad_byte 1 || pad_byte 8) { return -2; // 填充错误 } // 验证填充字节是否都正确 for (size_t i ciphertext_len - pad_byte; i ciphertext_len; i) { if (plaintext[i] ! pad_byte) { return -2; // 填充错误 } } *plaintext_len ciphertext_len - pad_byte; return 0; }实操心得CBC模式有两个关键点。第一是IV初始化向量它必须是随机的、不可预测的且每次加密都应不同可以不用保密。重复使用IV会严重削弱安全性。第二是填充。因为分组密码只能处理完整的数据块所以必须填充。PKCS#7是最常用的填充方案它用缺少的字节数作为填充值。解密后必须严格验证填充的合法性否则可能成为“填充预言攻击”的漏洞。在我们的实现中解密函数返回了具体的错误码这在实际项目中很重要但要注意不要将具体的错误信息如“填充错误”直接暴露给最终用户以防信息泄露。5. 测试、验证与性能考量代码写完了怎么知道它对不对以及我们自己实现的这个3DES性能到底怎么样5.1 使用已知答案测试KAT密码学实现必须通过已知答案测试。NIST等机构发布了标准的测试向量Test Vectors包含密钥、明文和对应的密文。我们可以用这些数据来验证我们的代码。#include stdio.h #include string.h // 一个简单的测试用例示例非官方完整向量 void test_des_encrypt() { des_key_schedule ks; uint8_t key[8] {0x13, 0x34, 0x57, 0x79, 0x9B, 0xBC, 0xDF, 0xF1}; // 含校验位 uint8_t plaintext[8] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; uint8_t expected_ciphertext[8] {0x85, 0xE8, 0x13, 0x54, 0x0F, 0x0A, 0xB4, 0x05}; uint8_t ciphertext[8]; des_key_setup(key, ks); des_encrypt_block(ks, plaintext, ciphertext); if (memcmp(ciphertext, expected_ciphertext, 8) 0) { printf(DES加密单块测试通过\n); } else { printf(DES加密单块测试失败\n); // 打印十六进制以调试 for(int i0; i8; i) printf(%02X , ciphertext[i]); printf(\n); } } void test_tdes_ecb() { tdes_key_schedule ks; // 使用三个密钥的测试向量示例 uint8_t key1[8] {...}, key2[8] {...}, key3[8] {...}; uint8_t plaintext[8] {...}; uint8_t expected_ciphertext[8] {...}; uint8_t ciphertext[8]; tdes_key_setup(key1, key2, key3, ks); tdes_ecb_encrypt(ks, plaintext, 8, ciphertext); // 比较ciphertext和expected_ciphertext // ... }你需要去找到官方的测试向量文件比如NIST Special Publication 800-20的附录用大量的测试用例来验证你的实现包括各种边界情况。这是保证代码正确的唯一方法。5.2 与标准库如OpenSSL交叉验证另一个好办法是用同样的密钥、IV和明文分别用我们的实现和OpenSSL库进行加密比较结果是否一致。# 使用OpenSSL命令行工具生成一个测试用例 openssl rand -out plaintext.bin 64 # 生成64字节随机明文 openssl rand -out key.bin 24 # 3DES需要24字节密钥168位含校验 openssl rand -out iv.bin 8 # CBC需要8字节IV # 用OpenSSL的3DES-CBC加密 openssl enc -des-ede3-cbc -in plaintext.bin -out ciphertext_openssl.bin -K $(xxd -p key.bin | tr -d \n) -iv $(xxd -p iv.bin | tr -d \n) # 然后我们编写一个C程序读取key.bin, iv.bin, plaintext.bin // 用自己的tdes_cbc_encrypt函数加密将结果输出到ciphertext_my.bin // 最后用diff或cmp比较两个密文文件是否完全相同。如果完全一致那恭喜你核心算法实现基本没问题了。5.3 性能分析与优化方向我们自己写的纯C实现性能肯定比不上经过高度优化甚至使用CPU指令集加速的库。我们可以做一个简单的性能测试#include time.h void benchmark() { tdes_key_schedule ks; uint8_t key1[8], key2[8], key3[8], iv[8]; // ... 初始化密钥和IV size_t data_len 1024 * 1024; // 1MB uint8_t *data (uint8_t*)malloc(data_len); uint8_t *cipher (uint8_t*)malloc(data_len 8); // 考虑填充 // 预热避免冷启动影响 // ... clock_t start clock(); size_t cipher_len; tdes_cbc_encrypt(ks, iv, data, data_len, cipher, cipher_len); clock_t end clock(); double time_used ((double)(end - start)) / CLOCKS_PER_SEC; double speed (data_len / 1024.0 / 1024.0) / time_used; // MB/s printf(加密速度%.2f MB/s\n, speed); free(data); free(cipher); }在我的老旧笔记本上测试一个未优化的纯C实现速度大概在10-30 MB/s左右。而OpenSSL开启硬件加速后可以达到数百MB/s甚至更高。主要的性能瓶颈在于位操作我们大量的get_bit/set_bit和permute函数调用每个都涉及循环和位运算非常慢。S盒查表虽然查表很快但我们的实现中提取6位输入的过程太复杂。优化方向使用预计算的置换表与其在运行时通过permute函数逐位处理不如预先计算好“按字节置换”的查找表。例如可以计算一个uint32_t ip_table[256][8]这样的表直接通过查表完成一个字节的置换映射。这需要仔细设计但能带来数量级的性能提升。将S盒输入输出合并处理将扩展置换E、异或、S盒选择、P置换合并成几个大的查找表比如将6位输入直接映射到32位输出的一部分。这是专业实现常用的技巧但会显著增加代码体积表很大。使用更大的数据类型用uint32_t或uint64_t来存储中间结果利用CPU的位并行操作能力。循环展开手动展开Feistel循环。使用编译器优化开启-O2或-O3优化选项。个人建议除非是在极度受限的、无法使用外部库的环境如某些Bootloader或特定嵌入式固件否则在生产环境中请务必使用成熟的、经过审计的加密库如OpenSSL、LibreSSL、mbed TLS等。自己实现的加密算法即使功能正确也极易因侧信道攻击计时攻击、功耗分析等而泄露密钥。这些库经过了多年的优化和安全审查远比自己写的要可靠和安全得多。这个C语言实现项目最大的价值在于学习和理解而不是用于实际产品。6. 常见问题与排查技巧实录在实现和调试过程中我踩过不少坑。这里总结几个最常见的问题和解决方法。6.1 密文输出全是0或者完全不对可能原因1密钥或明文未正确初始化。C语言中局部变量不会自动初始化为0。如果你声明了uint8_t key[8];但没有赋值那么里面就是栈上的随机值。务必确保密钥和IV被正确赋值。可能原因2位序错误。这是最最常见的问题。DES的置换表和S盒都是基于特定的位序定义的第1位是最高位。如果你的get_bit和set_bit函数中的位序7 - (pos % 8)搞反了或者你的测试向量数据是十六进制字节但你对字节内位的理解有误就会导致全部错误。调试方法找一个非常简单的测试向量比如全0密钥加密全0明文手动计算第一轮后的中间结果与你的程序输出对比。或者单步调试查看经过初始置换IP后的数据是否与预期一致。可能原因3置换表数据错误。从网上复制粘贴置换表和S盒时很容易抄错一个数字。务必使用权威来源如NIST官方文档的表格并仔细核对。建议将表数据以十六进制数组的形式写在头文件里并加上详细的注释说明来源。6.2 加密和解密结果不一致无法还原明文可能原因1加密和解密使用的密钥不一致。检查密钥生成函数des_key_setup是否正确处理了三个密钥。在3DES中加密是K1,K2,K3解密必须是K3,K2,K1EDE3模式。确认密钥传入顺序。可能原因2CBC模式的IV问题。解密时必须使用和加密时完全相同的IV。IV通常需要和密文一起存储或传输。确保你的解密函数接收到了正确的IV。可能原因3填充处理错误。加密时填充了但解密后没有正确移除填充。或者填充方案不匹配比如加密用了PKCS#7解密却用了零填充。仔细检查填充和去填充的代码逻辑特别是对填充字节合法性的验证。可能原因4Feistel网络最后未交换。在DES的16轮结束后需要将左右两部分交换一次再进行末置换。如果忘记交换加密和解密过程就不对称了。检查des_encrypt_block函数中在调用permute进行末置换前是否将L和R数组正确交换了。6.3 多块数据加解密时从第二块开始出错可能原因CBC模式的状态未正确更新。在CBC加密循环中previous_block必须在加密完当前块后立即更新为本次产生的密文块而不是异或前的明文块。同样在解密循环中previous_block必须更新为当前正在解密的密文块。仔细检查循环内的内存拷贝语句。6.4 性能慢得无法忍受如前所述这是预期之中的。我们的参考实现以清晰易懂为首要目标没有做任何优化。所有位操作都是通过函数调用和循环完成的。如果你需要性能参考第5.3节的优化方向。但再次强调追求性能时直接使用OpenSSL等库是更明智的选择。6.5 在特定平台或编译器下结果异常可能原因未定义行为或字节序问题。我们的实现假设了uint8_t是8位无符号整数C标准保证并且没有依赖具体的字节序因为我们以字节数组和位操作来定义数据流。但是如果你在代码中使用了uint32_t或uint64_t的类型双关type punning来进行快速位操作就可能会受到CPU字节序大端/小端的影响。确保所有涉及多字节整数的位操作都是可移植的或者使用编译器提供的字节序转换宏如htobe32。调试密码学代码二分法和对比法是最有效的。先让单块DES加密跑通测试向量。然后测试单块DES解密。接着测试ECB模式的3DES。最后再测试CBC模式。每一步都跟已知的正确结果或OpenSSL的输出对比。一旦某一步出错就集中精力排查那一个函数。