news 2026/10/26 13:02:10

DES五种分组加密模式MFC源码:工控配置加密与CBC/CTR实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DES五种分组加密模式MFC源码:工控配置加密与CBC/CTR实践

简介:这份源码面向学习密码学与MFC桌面开发的初学者及需要快速验证DES算法的开发者,基于MFC实现一重DES的ECB、CBC、CFB、OFB、CTR五种工作模式,并提供较友好的图形界面与代码注释,便于对照理解各模式的加解密流程,也可按现有结构扩展为三重DES。压缩包共18个文件、约57KB,主要由h头文件、cpp源文件、rc资源脚本、exe可执行文件与txt说明等组成,头文件和源文件承载算法与界面逻辑,资源脚本与可执行文件便于直接查看运行效果,说明文档帮助快速熟悉工程。目前已有1091人浏览学习。读者可获得一套可编译运行的DES多模式示例,借助注释理解分组加密模式差异,并参考工程组织完成界面调用、文件加解密测试与向3DES扩展的改造;程序退出时的警告不影响文件加解密,可作为学习排错时的小提示。

1. 从一台工控上位机的配置加密说起:这版 DES 源码到底能干什么

现场调试时改过一次设备参数,回头发现配置文件是明文的,任何一台机器用记事本就能改。要堵这个口子,又不想为了八字节的密钥把几百 KB 的加密库拖进工程,这套 DES 五种加密模式(MFC实现) 源码就是干这个的:一个能直接编译运行的 MFC 对话框工程,把 ECB、CBC、CFB、OFB、CTR 五种分组模式封装在同一套接口后面,轮函数、密钥编排、S 盒全部手写,不依赖任何第三方库。

它适合两类人:一类要给老旧 Win32 上位机补一块加解密功能,要求单 exe 里搞定、不引 dll;另一类被 DES 的 IP 置换、S 盒压缩、16 轮 Feistel 绕晕过,需要一个能单步跟进去、看着每轮 L/R 寄存器变化的可调试实现。接下来按“能跑通—能读懂—能改”的顺序拆。

2. 五种模式的底层差异:一条异或位置决定了串行、并行和错误扩散

2.1 DES 本体:64 位密钥砍掉校验位,滚出 16 组 48 位子密钥

DES 的输入永远是 8 字节一块,密钥写着是 64 位,其实每字节最低位是奇偶校验,真正参与运算的只有 56 位。加解密主流程固定:初始置换 IP 打散比特顺序 → 16 轮 Feistel → 左右半块交换 → 末尾逆置换 FP 还原字节顺序。真正需要手写的只有三张核心表:PC-1/PC-2(密钥编排)、E(32 位扩展成 48 位)、S 盒(48 位压回 32 位)以及 P 置换。

子密钥生成是这个工程里第一个容易写错的地方。PC-1 先把 64 位密钥按表抽成 56 位,丢掉校验位;然后对半切成 C、D 各 28 位,按 SHIFT 表逐轮循环左移,拼回 56 位再过 PC-2 得到 48 位。SHIFT 表在 16 轮里只有 1 和 2 两种取值,第 1、2、9、16 轮移 1 位,其余移 2 位,写错一位后面全歪。

// DesCore.cpp —— 密钥编排:64 位主密钥展开成 16 组 48 位子密钥 void CDesCore::SetKey(const BYTE key[8]) { // PC1 是 56 项的表,把 64 位里非校验位挑出来 uint64_t k = Permute(BytesToU64(key), PC1, 64, 56); uint32_t c = (uint32_t)(k >> 28); // 高 28 位 uint32_t d = (uint32_t)(k & 0x0FFFFFFF); // 低 28 位 for (int i = 0; i < 16; ++i) { c = Rotate28(c, SHIFT[i]); // SHIFT[i] 只取 1 或 2 d = Rotate28(d, SHIFT[i]); uint64_t cd = ((uint64_t)c << 28) | d; // 拼回 56 位 m_subKeys[i] = Permute(cd, PC2, 56, 48); // 压缩成 48 位子密钥 } }

Permute是这个工程里唯一一个需要吃透的通用函数,签名是“输入值 + 置换表 + 输入位宽 + 输出位宽”。它的语义是:输出第 i 位取自输入的第 table[i] 位。所以 IP、FP、PC-1、PC-2、E、P 六张表可以共用一套代码,只是参数不同。一个血泪教训是移位时务必写1ULL << n,MSVC 下1 << 32属于未定义行为,Debug 能跑、Release 直接给你一个错的结果,这种玄学 bug 排查起来能耗掉半天。

真正的轮函数长这样:

// DesCore.cpp —— 单轮 Feistel:扩展、异或子密钥、S 盒压缩、P 置换 uint32_t CDesCore::Feistel(uint32_t r, uint64_t subKey) { // E 表把 32 位扩展成 48 位,再和子密钥异或 uint64_t e = Permute(r, E, 32, 48) ^ subKey; uint32_t s = 0; for (int i = 0; i < 8; ++i) { // 每 6 位一组,从高位往低位取 BYTE six = (BYTE)((e >> (42 - 6 * i)) & 0x3F); BYTE row = (BYTE)(((six & 0x20) >> 4) | (six & 0x01)); // 首尾两位定行 BYTE col = (BYTE)((six >> 1) & 0x0F); // 中间四位定列 s = (s << 4) | SBOX[i][row][col]; // 8 个 4 位拼回 32 位 } return Permute(s, P, 32, 32); // P 置换做雪崩 }

S 盒的行列定位是新手最容易翻车的一步:六位输入里第 1 位和第 6 位拼成行号,中间四位是列号,不是简单的前三位后三位。写完拿一组向量对一下就知道对不对,后面第 4 章会给。

单块加解密的骨架则要注意最后那个“左右交换”:

// DesCore.cpp —— 单块加密,不含模式、不含填充 void CDesCore::EncryptBlock(const BYTE in[8], BYTE out[8]) { uint64_t block = Permute(BytesToU64(in), IP, 64, 64); // 初始置换 uint32_t l = (uint32_t)(block >> 32); uint32_t r = (uint32_t)(block & 0xFFFFFFFF); for (int round = 0; round < 16; ++round) { uint32_t prevL = l; l = r; r = prevL ^ Feistel(r, m_subKeys[round]); // L' = R, R' = L ^ f(R, K) } block = ((uint64_t)r << 32) | l; // 16 轮后交换左右再拼 block = Permute(block, FP, 64, 64); // 逆初始置换 U64ToBytes(block, out); }

解密不需要另写一遍轮函数,把m_subKeys的顺序倒过来用就行——这是 Feistel 结构的天然优势,DecryptBlock只需要把 16 轮的子密钥从第 15 组倒着取。很多人第一反应是写两套代码,最后两份实现各自有 bug,对不上的时候会把这次排查变成一堆很难看懂的调试记录。

2.2 分组模式的本质:异或放在加密盒的哪一侧

单块 DES 只能吃 8 字节,而现实里的明文可能是一段配置文本、一整个文件。把块串起来的方式就是模式,五种模式的差别,拆开看就一句话:异或位于加密盒之前还是之后、反馈的是明文还是密文。

ECB 最简单,每块独立加密,C_i = E(P_i)。好处是能并行、能随机访问、错一块不影响别块;坏处是同样的明文块永远得到同样的密文块,一张位图加密完还能看出轮廓。用它加密多行结构重复的配置文件,等于把结构直接暴露出去。

CBC 把上一块的密文异或进当前明文:C_i = E(P_i ^ C_{i-1}),C_0 = IV。加密必须串行,解密却可以并行,因为解密时C_{i-1}已经有了。它还会把错误放大——密文某一块坏掉,解密时当前块全乱、下一块对应比特位错,再往后就恢复正常了。这就是所谓的错误扩散:一个比特的密文错误影响两个明文块。

CFB 把加密盒挪到反馈路径上:C_i = P_i ^ E(C_{i-1})。它不需要填充,天然适合逐字节到达的流式数据,比如串口收到的定长报文。代价是加密同样串行,因为下一块的输入依赖这一块刚算出来的密文。

OFB 更进一步,把加密盒完全从明文上摘出去:O_i = E(O_{i-1}),C_i = P_i ^ O_i。密钥流只跟 IV 和密钥有关,跟明文一点关系没有,所以发送端可以先离线把密钥流算好,接收端丢了一块也只是对应的那些字节错,不会污染后续。缺点是 IV 绝不能在同一密钥下重复,重复一次整段密钥流就废了。

CTR 是把 OFB 里的反馈结构换成计数器:C_i = P_i ^ E(counter_i)。计数器可以随便跳,所以 CTR 是五种模式里唯一能做到“随机读取第 N 块”的,也是并行度最好的。它的死穴同样是计数器重用——同一密钥下两段数据用了重叠的计数器区间,把两段密文一异或,明文就出来了。

2.3 五种模式的取舍表格

模式是否需要 IV是否需要填充加密能否并行解密能否并行错误扩散典型用途
ECB否是是是单块内加密随机密钥、短小的定长字段
CBC是是否是1 块 + 1 比特配置文件、整段文本、文件落盘
CFB是否否是1 块 + 1 比特串口/网络分片到达的数据
OFB是否可预计算可预计算仅错误位噪声信道、需要预生成密钥流
CTR是(计数器)否是是仅错误位大文件、需要随机访问的存储

工程里这五种模式共用同一个IDesMode接口,区别只在Process内部的循环体,下面第 3 章会把这层结构摊开。选型我一般这么定:落盘存文件用 CBC,传输分片用 CFB 或 CTR,只有在加密随机生成的会话密钥这种场景才会碰 ECB——因为它一次只处理一小段,且不重复。

3. 工程结构拆解:MFC 对话框外壳与三层加密内核怎么接

3.1 目录与文件职责

这套工程的分层很清楚:最上面是 MFC 对话框,中间是模式封装,最底下是单块 DES。三层之间只靠头文件里的函数签名耦合,把crypto/整个目录拷到另一个 MFC 工程里也能编过。

路径职责改动频率
DesMfc.slnVS 解决方案,调试/发布两套配置低
DesMfc/DesMfc.h/.cppCWinApp派生类,程序入口极低
DesMfc/DesMfcDlg.h/.cpp主对话框,按钮响应、控件取值、结果显示高
DesMfc/DesMfc.rc+resource.h对话框布局、控件 ID中
crypto/DesCore.h/.cpp置换表、S 盒、密钥编排、单块加解密极低
crypto/DesModes.h/.cpp五种模式的封装与统一接口中
crypto/DesPadding.h/.cppPKCS#7 填充与去填充低
crypto/HexCodec.h/.cpp十六进制/Base64 编解码,用于界面显示低

这里的边界值得说一下:DesCore完全不认识CString,参数全是BYTE[8]和uint64_t;DesModes也不认识 MFC,用的是std::vector<BYTE>。所有 MFC 类型转换都挤在对话框那一层。这个约束看着多余,但等到你哪天想把内核抽出去做成控制台工具或者单元测试,就知道值了——直接在测试工程里 include 头文件就能跑,不用拖一个窗口起来。

3.2 模式接口设计:一个虚基类把五种模式收口

五种模式对外只暴露一个抽象基类,调用方拿到的永远是IDesMode*。这样界面上那个“加密模式”下拉框切换时,只是换了一个 new 出来的对象,OnBnClickedBtnEncrypt里的代码一行都不用动。

// DesModes.h —— 五种模式的统一入口 enum class DesModeType { ECB, CBC, CFB, OFB, CTR }; class IDesMode { public: virtual ~IDesMode() {} // in/len 是已经补齐到 8 的倍数的输入;out 由调用方保证容量足够 virtual bool Process(const BYTE* in, size_t len, BYTE* out, size_t& outLen) = 0; virtual void SetKey(const BYTE key[8]) = 0; virtual void SetIV(const BYTE iv[8]) = 0; }; // 工厂:界面上选了什么就造什么 IDesMode* CreateDesMode(DesModeType type, bool encrypt);

CBC 的实现把链式反馈写得最直白:

// DesModes.cpp —— CBC 加密:明文先和上一块密文异或,再进 DES bool CCbcMode::Process(const BYTE* in, size_t len, BYTE* out, size_t& outLen) { if (len % 8 != 0) return false; // 入口已由 Padding 层补齐 BYTE prev[8]; memcpy(prev, m_iv, 8); // C_0 = IV for (size_t off = 0; off < len; off += 8) { BYTE tmp[8]; for (int i = 0; i < 8; ++i) tmp[i] = in[off + i] ^ prev[i]; // P_i XOR C_{i-1} m_core.EncryptBlock(tmp, out + off); // C_i = E(...) memcpy(prev, out + off, 8); // 链下去,注意取的是密文 } outLen = len; return true; }

解密版本的差异只有两处:异或的位置挪到了DecryptBlock之后,反馈的prev取的是输入密文而不是输出。这两行改动如果你写反了,加密出来的东西自己能解回来,但跟别的实现(比如另一端的 Java)永远对不上,属于最难查的一类问题。

CTR 的实现要多一步计数器自增,而且必须约定成大端,否则同一份数据在 x86 和 ARM 上跑出来的密文不一样:

// DesModes.cpp —— CTR:加密计数器得到密钥流,再和明文异或 bool CCtrMode::Process(const BYTE* in, size_t len, BYTE* out, size_t& outLen) { BYTE counter[8]; memcpy(counter, m_iv, 8); // IV 就是计数器初值 for (size_t off = 0; off < len; off += 8) { BYTE ks[8]; m_core.EncryptBlock(counter, ks); // 对计数器加密得到密钥流 size_t n = (len - off < 8) ? (len - off) : 8; for (size_t i = 0; i < n; ++i) out[off + i] = in[off + i] ^ ks[i]; IncrementU64Be(counter); // 大端 +1,跨平台必须一致 } outLen = len; return true; }

IncrementU64Be的实现就是从第 7 个字节往前加,遇到不进位就停。看着朴素,但用uint64_t直接++再按主机字节序写出去,在小端机器上就变成了低字节在前,两台机器之间的密文就对不上。

3.3 对话框怎么调用内核:控件取值、编码转换、结果显示

对话框里真正要处理的琐事,全都集中在“把CString变成一段字节”这一步。工程默认是 Unicode 字符集,CString底层是wchar_t,一个中文占两个字节,直接拿GetLength()当字节数用必错。

// DesMfcDlg.cpp —— 加密按钮的完整响应 void CDesMfcDlg::OnBnClickedBtnEncrypt() { UpdateData(TRUE); // 控件内容刷到成员变量 // 统一转成 UTF-8 字节流,避免 wchar_t 与 char 混用 CStringA keyStr = CT2A(m_strKey, CP_UTF8); CStringA plainStr = CT2A(m_strPlain, CP_UTF8); if (keyStr.GetLength() != 8) { CString msg; msg.Format(_T("DES 密钥必须正好 8 字节,当前 %d 字节"), keyStr.GetLength()); AfxMessageBox(msg); return; } // 填充到 8 的整数倍 std::vector<BYTE> padded; DesPadding::Pad((const BYTE*)(LPCSTR)plainStr, plainStr.GetLength(), padded); // 按下拉框选项造对应的模式对象 std::unique_ptr<IDesMode> mode( CreateDesMode((DesModeType)m_cbMode.GetCurSel(), true)); mode->SetKey((const BYTE*)(LPCSTR)keyStr); mode->SetIV(m_iv); // m_iv 由“生成随机 IV”按钮写入 std::vector<BYTE> cipher(padded.size()); size_t outLen = 0; if (!mode->Process(padded.data(), padded.size(), cipher.data(), outLen)) { AfxMessageBox(_T("加密失败:输入长度不是 8 的倍数")); return; } cipher.resize(outLen); // 密文里可能有 0x00,绝不能塞回 CString 显示,走 Hex m_strCipher = HexCodec::Encode(cipher); UpdateData(FALSE); // 回写到结果编辑框 }

这条链路上有两个必须记住的点。第一,CT2A这种转换宏在字符串短于 128 字节时用栈上缓冲区,超了会走_alloca,如果在循环里反复用,栈会一路涨上去。第二,密文是任意字节序列,里面出现0x00是家常便饭,一旦用CString承载,GetLength()到第一个 0 就截断,你会看到“解密出来少了一截”。所以界面上一律显示 Hex 或者 Base64,回填CString的永远是编码后的可见字符。

填充层也顺手贴一下,PKCS#7 的规则是缺几个字节就补几个该数值,缺 8 个就补 8 个 0x08,注意整除时也要补满一整块:

// DesPadding.cpp —— PKCS#7 填充,块长固定 8 void DesPadding::Pad(const BYTE* in, size_t len, std::vector<BYTE>& out) { size_t padLen = 8 - (len % 8); // len 是 8 的倍数时结果为 8 out.assign(in, in + len); out.insert(out.end(), padLen, (BYTE)padLen); } bool DesPadding::Unpad(std::vector<BYTE>& data) { if (data.empty() || data.size() % 8 != 0) return false; BYTE padLen = data.back(); if (padLen < 1 || padLen > 8) return false; // 值非法,多半是密钥或 IV 错了 for (size_t i = data.size() - padLen; i < data.size(); ++i) if (data[i] != padLen) return false; // 逐字节校验,防止误判 data.resize(data.size() - padLen); return true; }

Unpad里那个逐字节校验不是多余的。密钥错的时候,解密出来的最后一块是随机字节,如果只取最后一个字节当长度,有大约三十分之一的概率取到 1~8 之间的合法值,于是程序静默地把最后几个字节吃掉,你以为解密成功了,其实数据已经缺了一角。加上校验,密钥错的时候就能明确报出来。

4. 编译与验证:从 VS 工程配置到一组已知向量跑通

4.1 编译前的三件事:MFC 组件、平台工具集、字符集

这套工程是纯 MFC 对话框程序,没有用任何第三方库,所以编译不过基本只有三个原因。第一,Visual Studio 安装时没勾“使用 C++ 的桌面开发”里的 MFC 组件,一编译就是MSB8041: 此项目需要 MFC 库。第二,平台工具集对不上,老工程默认是 v100 或 v120,新装的 VS 只有 v143,需要在“项目属性 → 常规 → 平台工具集”里改一下。第三,字符集被人手动切成多字节,导致CT2A、CString的类型全变,一堆C2664 无法将参数 1 从 const wchar_t* 转换为 LPCSTR。

配置项位置推荐值说明
MFC 使用方式项目属性 → 高级 → MFC 的使用在共享 DLL 中使用 MFC体积小;要单文件分发就改“在静态库中使用”
字符集项目属性 → 高级 → 字符集使用 Unicode 字符集与源码里的_T()、CT2A保持一致
平台工具集项目属性 → 常规v143(或本机已装版本)老工程升级时必改
C++ 语言标准项目属性 → C/C++ → 语言ISO C++17代码里用了std::unique_ptr、enum class
运行库项目属性 → C/C++ → 代码生成多线程 DLL (/MD)与 MFC 共享 DLL 配套,混用会链接报错

如果懒得开 IDE,用开发者命令行也能编:

:: 在“x64 Native Tools Command Prompt”里执行,路径按自己的改 cd /d D:\work\DesMfc msbuild DesMfc.sln /p:Configuration=Release /p:Platform=x64 /m

/m是并行编译,对这种小工程其实没多大意义,但换成大解决方案时能省不少时间。

4.2 用已知向量验证实现是否正确

界面能弹出来、按钮能点,不代表加密算对了。第一次编译通过之后,别急着调界面,先建一个空的 Win32 控制台工程,把crypto/目录整个加进去,跑一组公开的经典向量。这组向量是 DES 领域最常用的自检数据:密钥133457799BBCDFF1,明文0123456789ABCDEF,标准实现应该输出85E813540F0AB405。

// TestVector.cpp —— 控制台自检工程,不进最终发布 #include "DesCore.h" #include "DesModes.h" #include <cstdio> #include <cstring> static void HexPrint(const char* tag, const BYTE* p, size_t n) { printf("%s", tag); for (size_t i = 0; i < n; ++i) printf("%02X", p[i]); printf("\n"); } int main() { 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,0x0A,0xB4,0x05}; CDesCore core; core.SetKey(key); BYTE out[8] = {0}; core.EncryptBlock(plain, out); HexPrint("got = ", out, 8); HexPrint("expect = ", expect, 8); printf("ECB vector: %s\n", memcmp(out, expect, 8) == 0 ? "PASS" : "FAIL"); // 再验一次往返:解密结果必须等于原明文 BYTE back[8] = {0}; core.DecryptBlock(out, back); printf("round-trip: %s\n", memcmp(back, plain, 8) == 0 ? "PASS" : "FAIL"); return 0; }

这组向量能过,说明 IP、FP、E、P、S 盒、密钥编排六张表全对。过不了也不用慌,按这个顺序二分:先只验SetKey打印第 0 组子密钥(标准实现应该是1B02EFFC7072),对得上说明 PC-1、SHIFT、PC-2 没问题,问题在轮函数;对不上就往前查表。这种“先定位到哪一层”的排查方式,比在界面里瞎点要快得多。

4.3 一次完整的界面到文件流程

内核验通之后,界面上那条路就好走了。启动程序,在密钥框里敲满 8 个字符(比如DesKey01),点“生成随机 IV”,选一种模式,把要加密的文本贴进明文框,点“加密”,结果框里会出现一串十六进制。把这串十六进制连同 IV 一起记下来,切到解密页填回去,能还原成原文就说明整条链路通了。

顺序上有个细节:IV 要在加密时生成,并且要跟着密文一起保存。很多人在界面上把 IV 做成一个固定初值0000000000000000,测试阶段看着一切正常,等同一密钥加密两份不同文件时,CBC 模式的前 8 个字节一样,这就给了对方一个可对比的突破口。正确做法是加密时随机生成 IV,密文写成“IV + 数据”的形式一起落盘,解密时先切出前 8 字节当 IV。

再往下就是文件加密。MFC 里读文件用CFile比CStdioFile更合适,因为后者按文本行处理,遇到二进制内容会自作主张。整个文件读进std::vector<BYTE>,补齐、加密、写回,对几十 KB 的配置文件完全够用;几个 G 的日志文件就得分块流式处理,第 6 章会讲怎么改。

5. 避坑与排查:五处让加密结果对不上的地方

现象:Unicode 工程下密钥框输入12345678,提示“密钥必须 8 字节,当前 16 字节”。原因:m_strKey是CStringW,GetLength()返回的是字符数没问题,但如果中途用sizeof(m_strKey)或者把CString强转成const char*,拿到的就成宽字符数组了,长度直接翻倍,后面还会跟一串 0x00。解决:转换统一走CT2A(m_strKey, CP_UTF8),用返回的CStringA取长度和指针;整个工程里不允许出现(LPSTR)(LPCTSTR)这种双重强转。

现象:Debug 版本加密结果正常,切 Release 之后密文完全不同。原因:八成是移位写成了1 << n。MSVC 里对 32 位int移 32 位以上是未定义行为,Debug 下可能碰巧得到 0,Release 下编译器优化直接按别的值算。另一个常见源头是Permute里的循环变量用了int做位指针,超过 31 就溢出。解决:所有位操作一律uint64_t,常量后缀写ULL,循环变量用int但在移位表达式里显式转成uint64_t。

现象:同一个文件加密两次,ECB 结果一样,CBC 结果也一样。原因:ECB 结果一样是模式本身的性质,不是 bug;CBC 结果一样则是因为 IV 被写死成了常量。解决:CBC/CFB/OFB/CTR 四种模式的 IV 必须每次随机生成,用CryptGenRandom或者 C++11 的std::random_device填 8 字节,然后随密文一起存储。工程里那个“生成随机 IV”按钮就是干这个的,别图省事跳过。

现象:解密出来的文本末尾多出几个乱码字符,或者少了几个字。原因:填充没处理干净。多出乱码是根本没调Unpad;少了几个字,多半是Unpad只看了最后一个字节就resize,而密钥或 IV 错导致的随机末字节刚好落在 1~8 之间,把正常数据当填充吃掉了。解决:Unpad里逐字节比对,全部等于padLen才裁;再加一层上层校验,比如加密时在明文头部塞 4 字节魔数和 4 字节 CRC,解密后先验魔数,不对就直接报“密钥错误”而不是“解密成功”。

现象:界面上贴一段中文,加密再解密回来,只剩前半截。原因:密文被塞回了CString。DES 输出的密文是任意字节,里面出现0x00的概率不低(8 字节里至少一个 0 的概率大约 3%),CString底层是strlen语义,遇到 0 就截断,显示和后续处理全都少一截。解决:密文永远不直接进CString,先转成 Hex 或 Base64 字符串再显示;内存里传递全程用std::vector<BYTE>加显式长度,任何时刻都不靠\0判断结尾。

6. 二次开发:模式可配置、算法可替换、文件可流式处理

拿到这套代码之后,最常见的改造需求是“别让我每次都在界面上手选模式,配置文件里读”。这时候CreateDesMode那个工厂就是现成的接入点,把字符串"CBC"映射成DesModeType::CBC即可,改动局限在十几行。真正值得花时间的是另外三件事。

第一,把密钥和 IV 的管理从对话框里抽出去。现在m_iv是对话框成员,一旦要做“加密整个目录”,这套就撑不住了。我一般的做法是定义一个CryptoContext结构体,装密钥、IV、模式类型,序列化成 8+8+4 字节的小块,加密结果落盘时写成magic(4) + version(1) + ctx(20) + cipher。这样一份文件自带解密所需全部信息,换台机器也能打开,不用再靠人脑记 IV。

第二,接口留好换算法的位置。IDesMode只暴露Process、SetKey、SetIV三个纯虚函数,这个粒度正合适——想加 AES,写一个CAesCore和对应的CAesCbcMode就行,SetKey接受const BYTE*加长度参数(AES 支持 16/24/32 字节三种密钥)比固定BYTE[8]更通用。老代码里SetKey(const BYTE key[8])这个签名其实是个历史包袱,新写的类我一般改成SetKey(const BYTE* key, size_t len),DES 内部再自己检查len == 8。反过来,如果只是想把 DES 换成 3DES 做过渡,更省事的做法是在DesCore外面套一层,做 EDE 三次调用,密钥扩成 24 字节,模式层一行不动。

第三,大文件要走流式。现在Process一次性吃完整块内存,几 MB 没问题,几百 MB 就开始报警。改法是给IDesMode加一对Init和Update:Init负责重置内部状态(CBC 把prev置成 IV,CTR 置成计数器初值),Update每次处理若干个完整块并保留跨调用的链值,最后调用Final做填充和收尾。这个改动对 CBC、CFB、OFB、CTR 都是加几行状态变量的事,唯独填充要挪到最后一次调用,中途不能补。

// 流式改造后的调用形态 CDesStream stream(DesModeType::CBC, /*encrypt=*/true); stream.SetKey(key, 8); stream.SetIV(iv, 8); std::vector<BYTE> chunk(64 * 1024), out; while (size_t n = ReadChunk(file, chunk)) { // 中途不填充,只在最后一次 Final 里补 PKCS#7 stream.Update(chunk.data(), n, out); WriteChunk(fileOut, out); } stream.Final(out); // 这里才补齐并写最后一块 WriteChunk(fileOut, out);

验证改造是否正确的办法很土但有效:拿一个 100 MB 的随机二进制文件,走流式加密再流式解密,最后算一次整文件 CRC 对比原文件。只要有一个块没接上链、或者计数器漏加一次,CRC 立刻就对不上,比肉眼比对快得多。

最后说个习惯。这套代码我第一次跑通之后,在crypto/目录里留了一个TestVector.cpp没删,每次动完DesCore或者调完编译器优化选项,都先跑一遍那组向量再碰界面。DES 这种位运算密集的东西,改动一行移位代码,Debug 下能跑,Release 下出错,靠肉眼检查很难发现——从那以后我每次改完加密内核,都强制走一遍自检用例再开对话框。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 13:01:21

5种本地大模型服务搭建方法:从零基础到高并发生产部署

1. 项目概述&#xff1a;为什么“搭LLM服务”正在从实验室走向工位最近三个月&#xff0c;我陆续帮五类不同背景的朋友落地了本地大模型服务——有做教育产品的UI设计师想给课件加智能批注&#xff0c;有制造业的自动化工程师想让PLC日志自动归因&#xff0c;有高校文科导师想批…

作者头像 李华
网站建设 2026/10/26 13:00:45

AI Agent设计模式解析:提升开发效率的6种核心方法

1. 为什么AI Agent设计模式值得程序员关注&#xff1f;去年我在帮一个初创团队重构他们的AI客服系统时&#xff0c;亲眼目睹了设计模式带来的改变。原本杂乱的if-else逻辑被状态机模式替代后&#xff0c;对话流程的维护成本直接降低了70%。这让我深刻意识到&#xff0c;掌握AI …

作者头像 李华
网站建设 2026/10/26 13:00:42

MiMo-v2.6多模态RL训练看板解读:核心指标与异常排查指南

第一次接触 MiMo-v2.6 的 RL 训练看板&#xff0c;大多数人都会被满屏的曲线劝退。Reward、KL、Clip Fraction、Entropy、TPS、Refusal Rate&#xff0c;几十个缩写堆在一个页面上&#xff0c;看起来什么都在涨&#xff0c;但真要你回答“模型到底变好了没有”&#xff0c;又说…

作者头像 李华
网站建设 2026/10/26 13:00:09

Redisson分布式锁源码剖析:从加锁到红锁的实战避坑指南

分布式锁这个话题&#xff0c;只要你的系统里出现过“多个服务实例同时改同一份数据”的场景&#xff0c;就绕不开它。我最早接触分布式锁是在一个订单状态流转的服务里&#xff0c;当时两个实例同时收到同一笔订单的支付回调&#xff0c;结果状态被改了两次&#xff0c;库存扣…

作者头像 李华
网站建设 2026/10/26 12:59:52

libigl 构建指南:从源码编译到 OpenGL 可视化实战

简介&#xff1a;本资源为经CMake成功构建后的libigl几何处理库完整工程包&#xff0c;面向图形学研究者、计算机辅助设计&#xff08;CAD&#xff09;开发者及三维几何算法学习者&#xff0c;解决C项目中快速集成高质量几何处理能力的痛点。压缩包共2000个文件&#xff0c;总大…

作者头像 李华
网站建设 2026/10/26 12:59:46

AI热词日报:从Agent搭建到模型部署,把热搜变成实践

今天是2026年10月2日。我照例把今天的AI圈热词拉出来看了一遍&#xff0c;一眼扫过去&#xff0c;既有“无限制AI聊天”“无审核生成”这类靠关键词吸流量的词&#xff0c;也有“AI Agent搭建”“AI模型部署”“AI工程实践”这种实打实的内容。今天的日报&#xff0c;我打算换一…

作者头像 李华