简介:一份基于MFC实现的DES五种加密模式源码,面向需要理解分组密码算法和Windows界面编程的开发者,完整实现ECB、CBC、CFB、OFB、CTR五种工作模式,可用于对比不同模式在文件加密中的表现。压缩包共18个文件,以C++头文件、源文件与MFC资源脚本为主,包含对话框控件定义、算法逻辑、工程配置及可执行文件,整体仅57KB,结构清晰。源码界面友好,注释完善,已删除Debug目录;程序退出时的警告仅作为提示,不影响任何加解密操作。开发者可以借助现有框架快速运行体验,并利用注释方便地扩展为三重DES;阅读代码还能了解MFC对话框程序的基本组织方式,便于在此基础上增加输入密钥、选择文件等交互功能,适合算法学习、课程设计或安全编码参考。目前已有1090人学习/下载,是一份教学与实践中可用的样本。
1. DES五种加密模式(MFC实现)源码在讲什么:一个桌面工具的实际价值
“DES五种加密模式(MFC实现)源码”这个标题,最常被拿来问的不是 DES 算法本身,而是怎么把它塞进一个还在维护的 MFC 对话框程序里。实际场景往往是:要给本地配置文件加密、保护数据库里的敏感字段、把导出的临时文件换一种存储格式,很多团队的第一反应仍然是 DES。理由很现实——老代码在这里、不用引第三方库、开发环境里现成的工具链能直接编译。这套源码的价值,是把五种分组加密模式的数据流讲清楚,给出能落到 MFC 工程里的类接口,同时把填充、IV、字符集这类坑提前标出来。适合两类读者:接手老 MFC 项目想给数据加一层保护的开发者,以及想通过一个完整可编译的 DES Demo 理解 ECB、CBC、CFB、OFB、CTR 模式差异的初学者。前者可以照着工程结构改代码,后者可以用标准测试向量验证每一步。
2. DES核心实现:置换表、S盒与MFC工程里的模块边界
拿到“源码”后,第一件事不是去拖界面控件,而是先把核心算法从窗口逻辑里剥离出来。MFC 工程里如果界面代码和算法代码写在一起,调试阶段会非常痛苦。一个加密按钮背后要同时处理输入框取值、字节转换、填充、模式选择、密文显示,任何一步出错都很难快速定位。所以常见做法是先把 DES 核心做成一个独立的类,再封装出五种模式,最后才是界面调用。
2.1 先看清密钥调度:16轮子密钥怎么生成
DES 加密主流程由 16 轮迭代组成,每一轮用到一个 48 位的子密钥。这些子密钥不是直接取自原始密钥,而是经过一次 PC-1 置换去掉奇偶校验位,得到 56 位有效密钥,再拆成左右各 28 位,按轮数循环左移后做 PC-2 压缩置换。整个过程就是密钥调度。
// 密钥调度:64位原始密钥 -> 16个48位子密钥 void DesCore::GenerateSubKeys(const BYTE key[8], BYTE subKeys[16][6]) { BYTE kp[7] = {0}; // PC-1 之后是 56 位,用 7 字节存储 Permute(key, PC1, 56, kp); // 去掉奇偶校验位,按 PC-1 表打乱位序 BYTE c[4] = {0}, d[4] = {0}; // 左半 28 位、右半 28 位 SplitHalf(c, d, kp); // 注意这里是按位拆,不是按字节拆 for (int round = 0; round < 16; round++) { RotateLeft(c, SHIFT_SCHEDULE[round]); // 每轮左移 1 位或 2 位 RotateLeft(d, SHIFT_SCHEDULE[round]); JoinHalf(c, d, kp); // 合并回 56 位 Permute(kp, PC2, 48, subKeys[round]); // 压缩成 48 位子密钥 } }这份代码里 SHIFT_SCHEDULE 就是那张左移位数表,前几轮是 1 位,中间大部分是 2 位,最后一轮又是 1 位,标准表为{1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}。PC1 和 PC2 是两张固定的索引映射表,PC1 把 64 位密钥映射到 56 位,PC2 把 56 位映射到 48 位。Permute 函数的通用写法是遍历目标长度的每一位,从源数据的指定索引处取值并放到目标位置。实现时最容易出错的是 SplitHalf 和 JoinHalf,因为 28 位不是整字节边界,涉及跨字节取位。如果不想在这里花时间,可以按字节展开写成 7 个字节逐位操作,可读性差一点,但是不容易绕晕。
密钥调度正确性是整个 DES 实现的根基。第一轮和第十六轮的子密钥即使只错一位,最终密文也会完全不一样。验证密钥调度最简单的方式是先用标准测试向量过一遍,而不是急着接界面。
2.2 数据块处理的三个边界问题:位序、填充与大小端
DES 的单块加密固定处理 8 字节明文,输出 8 字节密文。三个边界问题几乎每个初学者都会踩:位序定义、填充规则、多字节数据在内存里的存放顺序。
位序方面,DES 标准里说的“第 1 位”是最有效位。实现时如果按字节数组存储,索引 0 的最高位是第 1 位,索引 0 的最低位是第 8 位。很多实现翻车的根源不是置换表写错,而是位序取反了,导致换表后结果和标准向量对不上。我的习惯是固定用“MSB 优先”的位序,所有置换表都按这个语义写,不做任何运行时转换。这样虽然代码里位移多一点,但至少能保证可复现。
填充规则取决于模式。ECB 和 CBC 是分组模式,数据长度不是 8 的倍数时要填充。常见做法是 PKCS#7:缺 n 字节就补 n 个值为 n 的字节,如果数据正好是 8 的倍数也要额外补 8 个字节。CFB、OFB、CTR 是流式模式,不需要填充,但要注意它们按 8 字节块为单位生成密钥流,待加密数据不要求补齐。
大小端的问题主要出现在把整数转成字节数组时。MFC 程序跑在 x86 平台上,内存是小端布局,一个 32 位整数的低字节在低地址。做 CTR 模式时要维护一个 64 位计数器,直接把 long long 转成字节数组需要确认低地址放的是低字节还是高字节。统一按“小端字节序”写入计数器,可以省掉很多联调时的麻烦。
// 单块加解密接口:输入 8 字节,输出 8 字节 void DesCore::EncryptBlock(const BYTE in[8], BYTE out[8]) { BYTE ip[8] = {0}; Permute(in, IP, 64, ip); // 初始置换 BYTE l[4] = {0}, r[4] = {0}; SplitHalf(l, r, ip); for (int round = 0; round < 16; round++) { BYTE feistelOut[4] = {0}; Feistel(r, subKeys[round], feistelOut); // 扩展、异或、S盒、P置换 XorBytes(l, feistelOut, r, feistelOut); // 左右交换 // 这一轮结束后,l、r 的角色互换 SwapCopy(l, r); } BYTE preOutput[8] = {0}; JoinHalf(r, l, preOutput); // 最后一次交换后输出顺序是 R16L16 BYTE tmp[8] = {0}; Permute(preOutput, IP_INV, 64, tmp); memcpy(out, tmp, 8); }Feistel 函数内部要做 E 扩展、与子密钥异或、S 盒代换、P 置换四步。S 盒是最容易写错的地方,标准 S 盒是 8 个 4 行 16 列的矩阵,输入 6 位,首尾两位决定行号,中间 4 位决定列号,输出 4 位。实现时可以把 8 个 S 盒直接展开成一张二维查询表,也可以用三维数组一次性定义。要注意 S 盒的行列索引到底从 0 还是从 1 开始,标准文档里行号写的是 0 到 3,列号是 0 到 15,直接用位运算算出行列再查表就好。
2.3 把核心封装成不依赖窗口的 DesCore 类
为了让后面接 MFC 界面时不返工,DesCore 类要满足三个约束:不引用任何窗口句柄、不依赖 CString、不持有全局状态。这样它既可以被 MFC 对话框调用,也可以被一个控制台测试工程直接编译。
#pragma once #include <windows.h> class DesCore { public: DesCore(); ~DesCore(); // 设置 8 字节密钥,内部生成 16 轮子密钥 void SetKey(const BYTE key[8]); // 单块加解密,输入输出各 8 字节 void EncryptBlock(const BYTE in[8], BYTE out[8]); void DecryptBlock(const BYTE in[8], BYTE out[8]); private: BYTE subKeys[16][6]; // 工具函数:置换、循环左移、异或、S盒代换 void Permute(const BYTE* input, const BYTE* table, int outBitLen, BYTE* output); void Feistel(const BYTE r[4], const BYTE key[6], BYTE out[4]); void GenerateSubKeys(const BYTE key[8]); };这里把 subKeys 作为成员变量,好处是密钥设置一次后可以反复加密多块数据,不用每次加密都重新生成子密钥。GenerateSubKeys 的调用放在 SetKey 内部,避免外部重复调用。Feistel 接收右侧 4 字节和 6 字节子密钥,输出 4 字节。整体接口设计保持对称,后续封装 ECB 模式只需要循环调用 EncryptBlock,封装 CBC 模式只需要在循环里加一个 XOR。
我一般还会在 DesCore 旁边加一个简单的校验函数,用来打印每一轮的子密钥和中间状态,调试时非常有帮助。这个函数只做输出,不影响类接口。等单块加密通过标准向量验证后,再去写五种模式,基本不会碰到“某个模式不通”的问题。
3. 五种模式的分工:ECB、CBC、CFB、OFB、CTR怎么选
DES 核心只解决一个问题:把 8 字节变 8 字节。真正的应用场景里数据往往超过 8 字节,于是模式决定了相邻数据块之间有无关联、同样的明文块在密文中是否可见、是否引入 IV 和填充。选模式本质上是在选数据块之间的耦合方式。
3.1 一次只加密8字节:ECB与CBC的显式区别
ECB 模式最简单,每个 8 字节块独立加密,块与块之间没有任何关联。问题在于相同明文块会产生相同密文块,如果加密的是位图、结构化数据库页,这种模式会直接泄露信息。CBC 模式先把当前明文块与上一个密文块异或,再送入 DES 加密,第一块用 IV 参与计算,形成链路。
// CBC 加密:每块先与前一密文块异或,再调用 DES void DesMode::EncryptCBC(const BYTE* plain, int plainLen, BYTE* cipher, const BYTE* iv) { BYTE feedback[8] = {0}; memcpy(feedback, iv, 8); // 初始向量 BYTE block[8] = {0}; for (int i = 0; i < plainLen; i += 8) { memcpy(block, plain + i, 8); for (int j = 0; j < 8; j++) block[j] ^= feedback[j]; des.EncryptBlock(block, cipher + i); memcpy(feedback, cipher + i, 8); // 下一轮关联 } }CBC 解密是逆过程:先解密得到中间结果,再与上一个密文块异或。所以解密循环里存的 feedback 也必须是密文块,而不是解密后的明文块。这个顺序被我反复写错过,代码里最容易犯的错误是解密时把 feedback 更新成了上一块解密后的明文,一旦写错只有第一块能解对,后面全乱。
3.2 流式模式:CFB、OFB与CTR的位移与并行
CFB、OFB、CTR 三种模式本质上都是把 DES 当作一个密钥流生成器,再把密钥流与明文异或得到密文。
CFB 模式中,每次加密的是上一个密文块,产生密钥流块后与当前明文块异或。解密时同样用加密操作生成密钥流,与密文异或得到明文,所以解密路径完全复用 EncryptBlock。OFB 模式不同之处在于反馈值是上一次生成的密钥流块,一旦密钥和 IV 确定,密钥流序列可以预先算好。CTR 模式则维护一个计数器,每块用不同的计数值加密生成密钥流,块与块之间没有链路,天然支持并行。
// CTR 模式:计数器按小端增长,每块用不同计数值做密钥流 void DesMode::EncryptCTR(const BYTE* plain, int plainLen, BYTE* cipher, const BYTE* nonce, unsigned long long startCount) { BYTE counter[8] = {0}; memcpy(counter, nonce, 4); // 把起始计数值写入后 4 字节,小端序 counter[4] = (BYTE)(startCount & 0xFF); counter[5] = (BYTE)((startCount >> 8) & 0xFF); counter[6] = (BYTE)((startCount >> 16) & 0xFF); counter[7] = (BYTE)((startCount >> 24) & 0xFF); BYTE keystream[8] = {0}; BYTE block[8] = {0}; for (int i = 0; i < plainLen; i += 8) { des.EncryptBlock(counter, keystream); int remain = min(8, plainLen - i); memset(block, 0, 8); memcpy(block, plain + i, remain); for (int j = 0; j < remain; j++) cipher[i + j] = block[j] ^ keystream[j]; // 计数器自增 for (int j = 7; j >= 0; j--) if (++counter[j] != 0) break; } }CTR 的计数范围要留意:如果使用 8 字节计数器,加密数据超过 64GB 才会有计数回绕,普通桌面应用完全不用考虑。但如果把前 4 字节留给随机 nonce,后 4 字节做计数器,那么单次会话最多加密 16GB 数据。MFC 桌面应用加密的通常是几十 KB 级别的文本或配置,怎么拆都够用。并行化是 CTR 最大的优势,每个块的密钥流只依赖 counter 的取值,可以开多线程一次预生成所有密钥流。
3.3 模式选型速查:四类场景怎么定
| 模式 | 填充要求 | 解密并行 | 错误传播 | 典型场景 |
|---|---|---|---|---|
| ECB | 需要 | 支持 | 单块错误只影响本块 | 密钥长度短、随机数加密 |
| CBC | 需要 | 不支持 | 密文块错误影响本块和下一块 | 本地文件加密、数据库字段保护 |
| CFB | 不需要 | 不支持 | 密文块错误影响本块和后续若干块 | 字符流实时加密 |
| OFB | 不需要 | 不支持 | 密文块错误只影响本块对应位 | 需要预生成密钥流的场合 |
| CTR | 不需要 | 支持 | 无传播,块间独立 | 多线程场景、大文件分块加密 |
本地文件加密我一般选 CBC,既有 IV 的随机性,实现也简单。需要多线程处理大文件时选 CTR。CFB 和 OFB 在 MFC 里很少单独用,除非对接的旧协议指定了模式。ECB 不是不能用,但明文里只要存在重复块,密文就会暴露该信息,谨慎对待。
4. MFC落地:对话框工程、模式切换与中国化界面的三个细节
核心算法和五种模式封装好之后,剩下的问题就是怎么放进 MFC 对话框工程里。这部分不需要太多技巧,但要按固定顺序操作,否则编译阶段就会一堆报错。
4.1 在 MFC 工程里挂上 DES 模块:工程组织与字符集设置
常见做法是新建一个 MFC 对话框应用,然后把 DesCore.cpp、DesModule.cpp 两个文件直接拖进工程。头文件包含路径不要用相对路径到处飞,我习惯在工程根目录建一个 crypto 文件夹,把 DES 相关文件全部放进去,然后在项目设置里加上它的包含路径。这样不管对话框类放在哪个目录,都能用#include "crypto/DesModule.h"引用。
字符集设置是 MFC 工程里第一个坑。新版开发环境默认使用 Unicode 字符集,意味着 CString 底层是宽字符,而 DES 处理的是 BYTE 字节数组。在对话框里接收输入后,要先把 CString 转成 UTF-8 字节数组再加密,解密后再把字节数组转回 CString。如果工程里还开着“使用多字节字符集”,转换方式完全不同。我建议直接把工程设置成 Unicode,后面所有转换按宽字符处理。
MFC 的字符串类有一个容易迷惑人的地方:CString 在不同字符集设置下可能是 CStringA 或 CStringW。代码里如果写了GetBuffer()返回值直接传给memcpy,在 Unicode 设置下会复制出两倍长度的数据,而且内容也不是预期字节。统一处理方式是把所有从界面取到的文本先转成std::string的 UTF-8 编码,再交给 DES 模块。
4.2 模式切换与IV输入:ComboBox与Edit控件的联动
界面上至少要放模式下拉框、密钥输入框、IV 输入框、明文编辑框、密文编辑框、加密按钮、解密按钮。模式下拉框的条目顺序和枚举值要保持一致,我是这样初始化的:
void CDesDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 模式下拉框:条目顺序与枚举值一一对应 m_cmbMode.ResetContent(); m_cmbMode.AddString(_T("ECB")); m_cmbMode.AddString(_T("CBC")); m_cmbMode.AddString(_T("CFB")); m_cmbMode.AddString(_T("OFB")); m_cmbMode.AddString(_T("CTR")); m_cmbMode.SetCurSel(1); // 默认 CBC }加密按钮的核心逻辑是取出文本、转字节、调用模式类、转成十六进制显示。注意 IV 只在 ECB 模式下不需要,其他模式都要从输入框读取。如果用户没填 IV,我习惯用定值0000000000000000,但解密时必须有与加密时完全相同的 IV,这一点在界面上要写清楚提示文字。
void CDesDemoDlg::OnBnClickedBtnEncrypt() { CString strInput, strKey, strIv; GetDlgItemText(IDC_EDT_INPUT, strInput); GetDlgItemText(IDC_EDT_KEY, strKey); GetDlgItemText(IDC_EDT_IV, strIv); int modeIndex = m_cmbMode.GetCurSel(); // CString -> UTF-8 字节数组 int utf8Len = WideCharToMultiByte(CP_UTF8, 0, strInput, -1, NULL, 0, NULL, NULL); std::vector<BYTE> plainBytes(utf8Len - 1); WideCharToMultiByte(CP_UTF8, 0, strInput, -1, (char*)plainBytes.data(), utf8Len, NULL, NULL); // 密钥统一 8 字节,不足补零,超过截断 BYTE key[8] = {0}; CStringA keyA(strKey); int keyLen = min(8, (int)keyA.GetLength()); memcpy(key, keyA.GetBuffer(), keyLen); keyA.ReleaseBuffer(); // 模式类统一入口 BYTE iv[8] = {0}; if (modeIndex != 0) { CStringA ivA(strIv); int ivLen = min(8, (int)ivA.GetLength()); memcpy(iv, ivA.GetBuffer(), ivLen); ivA.ReleaseBuffer(); } desModule.SetKey(key); desModule.SetIv(iv); std::vector<BYTE> cipherBytes; if (modeIndex == 0) desModule.EncryptECB(plainBytes.data(), (int)plainBytes.size(), cipherBytes); else if (modeIndex == 1) desModule.EncryptCBC(plainBytes.data(), (int)plainBytes.size(), cipherBytes); // 其他模式同理 // 密文转十六进制 CString strCipher = BytesToHex(cipherBytes.data(), (int)cipherBytes.size()); SetDlgItemText(IDC_EDT_CIPHER, strCipher); }逻辑上要注意几个点:UTF-8 转换时WideCharToMultiByte返回的长度包含结尾的空字符,所以plainBytes要减掉 1。密钥和 IV 都直接从输入框读取,不足 8 字节补零,超过 8 字节截断,这种做法在 Demo 里没问题,生产环境里应当提示用户密钥长度必须为 8 字节。BytesToHex 是一个工具函数,把每个字节拆成两位十六进制字符,输出到界面。
4.3 中文乱码与文件加密的编码处理
MFC 的 Edit 控件显示中文时默认按系统语言处理。如果加密前的文本是中文,直接转成 BYTE 数组时如果用了本地代码页,换一台机器再解密可能变成乱码。这就是为什么加密前要规范化编码。项目内部统一用 UTF-8,加密时把 CString 转 UTF-8 字节序列,解密后把 UTF-8 字节序列转回 CString,这样无论系统区域设置如何都能保证一致。
文件加密在 MFC 里要额外处理文件头。直接把整个文件按 CBC 模式加密,解密后可能需要判断原始长度来去掉填充。常见做法是在文件头写一个自定义结构,保存原始文件长度和填充字节数,再用加密后的数据覆盖文件内容。这样解密时先读取文件头,再按长度精确还原文件。
// CString 转 UTF-8 字节序列 std::vector<BYTE> Utf8FromCString(const CString& text) { int len = WideCharToMultiByte(CP_UTF8, 0, text, -1, NULL, 0, NULL, NULL); std::vector<BYTE> bytes(len - 1); WideCharToMultiByte(CP_UTF8, 0, text, -1, (char*)bytes.data(), len, NULL, NULL); return bytes; } // BYTE 序列转回 CString,原字节必须是 UTF-8 CString CStringFromUtf8(const BYTE* utf8Data, int len) { int utf16Len = MultiByteToWideChar(CP_UTF8, 0, (const char*)utf8Data, len, NULL, 0); CString result; MultiByteToWideChar(CP_UTF8, 0, (const char*)utf8Data, len, result.GetBuffer(utf16Len), utf16Len); result.ReleaseBuffer(); return result; }这两个函数放在一个EncodingUtil.cpp里,MFC 对话框和文件加密模块共用。特别注意MultiByteToWideChar的第四个参数要传实际字节长度,不要传 -1,否则会把输入当作空字符结尾的字符串,导致长度判断出问题。文件里如果正好有0x00字节,也会被错误截断。
5. 避坑与排查:DES在MFC里最常见5个翻车现场
5.1 前8字节能解,后面全乱:填充统一问题
现象:加密后用同一套代码解密,前 8 字节正常,后面的内容要么变成乱码,要么直接解不出。原因:加密时用了 PKCS#7 填充,解密时没有移除填充,或者填充方式不一致。还有一种常见情况是加密时把输入长度按strlen计算,中文字符在 UTF-8 下会占 3 字节,但界面输入的字符数不是字节数。解决:加密前记录原始字节长度,加密后保存填充信息,解密时先解出全部数据,再根据最后一字节的填充值去掉对应长度。如果用块大小 8 的倍数做整块处理,不要简单的new BYTE[plainLen],要分配((plainLen + 8) / 8) * 8的空间。
5.2 Unicode 字符集下崩溃:TCHAR 与 BYTE 混用
现象:程序一开始用多字节字符集编译正常,改成 Unicode 后程序启动或点击加密按钮直接崩溃。原因:CString 底层从char*变成了wchar_t*,GetBuffer()返回的指针宽度变了,代码里如果有memcpy(key, strKey.GetBuffer(8), 8)这种写法,复制出来的根本不是明文密钥。解决:所有从 CString 取字节的操作都先转成CStringA,或者调用CW2A宏,保证拿到的是单字节字符串,再memcpy。同样,所有从char*构造 CString 的地方要用CA2W。
5.3 DEBUG 正常 RELEASE 失败:越界缓冲区的“路过”
现象:DEBUG 配置下单块加密、CBC 加密都正常,切换到 RELEASE 后密文偶尔不对,有时解密直接报错。原因:缓冲区分配比实际需要小了几个字节,DEBUG 下堆管理器会填充哨兵字节,掩盖了越界写;RELEASE 下的堆布局不同,越界写覆盖了相邻数据,导致 DES 内部子密钥或 IV 被破坏。解决:仔细检查所有new和malloc的长度计算,尤其是 CBC、CFB 这种需要多读一个 IV 字节的循环。C 风格的数组参数传递时,最好在所有跨模块接口处用std::vector<BYTE>替换裸指针,同时加上边界断言的 Debug 检查。
5.4 CBC 解密用错 IV:模式接口设计问题
现象:加密用了 CBC 模式并指定了随机 IV,解密时只传入了密钥,结果第一块解出来是乱码。原因:CBC 解密第一块需要与加密时完全一致的 IV,否则第一块错误,并且错误会传播到第二块。很多接口设计把密钥和 IV 分开,但调用者容易忘记设置 IV。解决:在模式类里把 IV 作为 Encrypt 和 Decrypt 的必传参数,不要放到SetIv这种独立接口里。或者在初始化时判断 IV 指针是否为空,为空则使用全零 IV,并在日志里输出警告。界面上的提示文字也要写清楚,同一份密文,加密和解密的 IV 不一致时得到的结果是完全无效的。
5.5 网页工具能解,MFC 解不了:HEX 与 Base64 显示不一致
现象:本地程序加密后把密文复制到网页工具里解密失败,或者网页工具加密的密文本地解不了。原因:大部分网页工具默认用 Base64 输出密文,而 MFC 程序里常常用十六进制字符串显示。另外,有些网页工具对密钥输入按 ASCII 字符处理,有些按 HEX 解析,两边密钥的实际字节值根本不一致。解决:在 MFC 程序里同时提供 HEX 和 Base64 两种密文输出,并把密钥输入框做成两种模式。更稳妥的做法是在界面上增加“从 HEX 导入密钥”和“从 ASCII 导入密钥”两个选项,避免用户直接把字符串当作字节数组输入。
6. 用标准向量喂一遍,再谈并行优化
DES 实现完成后的第一件事不是接界面,而是用一组公开的参考向量验证核心函数的正确性。参考向量可以用下面这组,密钥和明文、密文都是十六进制表示,如果EncryptBlock的输出和预期一致,说明置换表、S 盒、密钥调度基本没错。
void TestDesCore() { DesCore des; BYTE key[8] = {0x13,0x34,0x57,0x79,0x9B,0xBC,0xDF,0xF1}; BYTE plain[8] = {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF}; BYTE expect[8]= {0x85,0xE8,0x13,0x54,0x0F,0xAB,0x40,0x50}; BYTE cipher[8] = {0}; des.SetKey(key); des.EncryptBlock(plain, cipher); bool ok = true; for (int i = 0; i < 8; i++) { if (cipher[i] != expect[i]) { ok = false; char msg[256]; sprintf_s(msg, "第 %d 字节不匹配:预期 %02X,实际 %02X", i, expect[i], cipher[i]); OutputDebugStringA(msg); } } if (ok) OutputDebugStringA("DES 核心测试通过"); }单块正确之后,再验证解密函数,确保DecryptBlock(EncryptBlock(p)) == p。这一步通过后,五种模式基本只需要关注填充和 IV 的传输逻辑,算法本身的信赖度就高了。
接下来可以考虑并行化。CTR 模式天然适合多线程,预生成密钥流时每个线程处理连续一段计数器即可。CBC 模式无法并行加密,但解密时可以按块并行,前提是先拿到完整的密文块。在 MFC 里开std::thread处理大文件时要警惕 UI 线程阻塞,常见做法是把加解密放到工作线程,通过PostMessage把进度和结果回传给对话框界面。
最后说一个我的习惯:源代码里所有跨模块的接口统一使用std::vector<BYTE>而不是裸指针,填充逻辑只放在模式封装层,界面层永远不做字节级别的操作。早期我把填充写进按钮消息处理函数里,后来维护另一个功能时改动了一次填充逻辑,界面和加密模块各改了一半,出现了整整一天才排查出来的兼容性问题。这套结构保持到现在,同样的功能再也没翻过车。希望帮到你。
本文还有配套的精品资源,点击获取