拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MFC中DES五种加密模式的实现:ECB、CBC、CFB、OFB、CTR源码解析

简介面向MFC开发者与密码学初学者的DES加密模式示例源码包基于一重DES实现了ECB、CBC、CFB、OFB、CTR五种常用模式。资源配套友好MFC界面代码中加入了详细注释可清晰对照每种模式的分组链接差异并方便进一步扩展为三重DES加密适合在课程设计或安全项目演示中直接参考使用。压缩包共18个文件包括4个头文件、3个C程序文件、RC资源脚本、工程文件dsw/dsp、可执行程序、ReadMe说明等整体仅57KB结构紧凑便于携带查阅。目前已有1088人学习下载口碑积累可见其教学价值。代码保留了可运行的exe便于实际验证加解密流程同时文件中已去除Debug目录工程配置精简更适合以源码阅读和二次开发为目的的场景。五种DES加密模式的MFC实现源码思路与踩坑实录前段时间在维护一个老项目遇到一个用MFC写的加密模块客户要求把数据加密从ECB模式扩展到支持多种加密模式最后把DES的五种经典模式——ECB、CBC、CFB、OFB、CTR——全部用MFC对话框工程实现了一遍代码也整理成了可以复用的源码。这次实践下来最大的感受是DES本身虽然老了但亲手用C在MFC里把这五种模式逐一实现对理解分组密码的设计思路帮助特别大而且这类源码在不少旧系统维护场景里依然有直接参考价值。如果你正在维护MFC老项目、要给现有系统加上加密功能或者刚学完密码学理论想看看代码长什么样这篇文章应该对你有用。1. 从ECB到CTR五种模式的设计动机与适用场景很多刚接触DES的人以为调一个加解密函数就完事了其实真正的复杂度全在“模式”上。DES本身只处理一个8字节分组但实际数据动辄几百字节甚至几兆字节怎么把这一个分组的能力扩展成对任意长度数据的加密方案就是模式要解决的问题。五种模式分别对应了不同历史阶段对“安全”和“性能”的不同理解。1.1 ECB和CBC分组密码的基础玩法ECB最简单也最直观把明文切成8字节一块每块独立用DES加密块与块之间没有任何关联。坏处是同样的明文块永远得到同样的密文块这在很多场景下会泄漏信息。比如一张BMP位图用ECB加密之后图片轮廓还能隐约看出来因为大面积相同颜色的像素块加密后仍然相同。实际项目里如果要加密结构化数据、日志或者图片ECB基本可以直接排除。CBC就是冲着ECB的缺陷来的。每块明文在加密之前要先和上一块的密文做异或第一个块则和一个初始向量IV做异或。这样即使两块明文完全相同只要上文不同密文也不同。CBC引入了“链”的概念每个密文块依赖前面所有块这带来一个特性解密时如果某个密文块损坏最多影响当前块和下一块的还原之后的块不受影响。这种容错性在存储加密、网络传输加密里很关键。1.2 CFB、OFB、CTR把分组密码变成流密码CFB、OFB、CTR这三种模式做的事情本质相同用DES生成一串密钥流再把密钥流和明文逐字节异或得到密文。区别只在密钥流怎么来。CFB是密文反馈把上一块密文作为输入去加密生成当前块的密钥流。解密时也能从密文反推密钥流所以加解密可以共用同一个DES加密函数不需要实现解密算法。OFB是输出反馈把“加密上一块输出得到的结果”作为当前块的输入。密钥流一旦生成就只和初始状态有关和明文密文都无关所以可以预先算好一堆密钥流等数据到了直接异或吞吐量很高。但代价是如果传输过程出现数据丢失或错位解密端没法自己恢复同步。CTR是计数器模式输入直接是一个从IV开始逐块递增的计数器加密计数器得到密钥流再与明文异或。CTR最大的优势是每一块的密钥流生成互相独立可以并行计算这在现代CPU多核环境下性能优势非常明显所以SSL/TLS等协议里大量使用。1.3 五种模式的特性对比从实践角度我习惯用下面这个表来选型模式密文是否分组相关需要IV错误传播并行性典型场景ECB否否单块出错仅影响本块可并行单块数据加密基本不建议用于多块数据CBC是是本块和下一块解密可并行文件加密、消息加密CFB是是本块和下一块不可并行流式通信场景OFB否是仅本块不可并行卫星通信、遥测数据CTR否是仅本块加解密都可并行高速通信、磁盘加密做MFC项目时我一般推荐CBC兼顾安全和实现复杂度CTR作为性能优化备选。CFB和OFB更多是项目有历史依赖时才需要兼容。2. MFC工程结构与DES核心类的设计MFC环境写加密模块最大的问题是很多人把逻辑全堆在对话框的按钮响应函数里最终代码没法看也没法测。我的做法是先搭一个和界面无关的加密核心层按钮只负责取输入、显示输出验证和调试都在核心层完成。2.1 工程结构怎么搭在VS里创建一个MFC对话框应用然后在解决方案里加三个层次的文件DES核心算法文件DesCore.h / DesCore.cpp负责单块8字节的加解密不涉及任何MFC类型。模式封装文件DesMode.h / DesMode.cpp负责把五种模式实现为对DesCore的调用处理分组、IV、填充逻辑。界面调用层在对话框类里调用DesMode的接口完成CString和字节数组的转换。这样分层的好处是核心逻辑可以脱离MFC单独测试。我甚至在调试时写了一个控制台小工程直接引用DesCore.cpp和DesMode.cpp用标准测试向量验证正确后再回到MFC工程里联调省掉了大量在MessageBox和断点之间来回折腾的时间。2.2 单块DES加解密类的接口设计DES的单块算法是有公开标准实现的S盒、置换表、密钥编排这些内容网上能查到很多版本但接口设计往往被忽略。我的DesCore对外只暴露两个静态函数class DesCore { public: // 单块加密plain 8字节 - cipher 8字节 static void EncryptBlock(const unsigned char plain[8], unsigned char cipher[8], const unsigned char key[8]); // 单块解密cipher 8字节 - plain 8字节 static void DecryptBlock(const unsigned char cipher[8], unsigned char plain[8], const unsigned char key[8]); };内部实现大致包括初始置换IP、16轮Feistel迭代、轮密钥生成、逆初始置换。轮函数里用S盒做非线性替换用P盒做置换扩散。这些过程教科书里都有我不再贴完整代码但有一点要提醒实现时务必保留一份和标准测试向量一致的中间值调试工具比如在每一轮结束时输出右边32位的值和NIST文档核对。没有这个一旦结果不对你根本不知道是S盒写错还是密钥编排算错。2.3 模式封装类的职责划分DesMode类负责四种工作分组、填充、IV管理和模式分发。接口设计如下class DesMode { public: // 加密输入明文数据和密钥按mode模式加密输出密文 static bool Encrypt(const std::vectorunsigned char plain, const std::vectorunsigned char key, const std::vectorunsigned char iv, int mode, std::vectorunsigned char cipher); // 解密输入密文数据和密钥按mode模式解密输出明文 static bool Decrypt(const std::vectorunsigned char cipher, const std::vectorunsigned char key, const std::vectorunsigned char iv, int mode, std::vectorunsigned char plain); enum Mode { ECB 0, CBC 1, CFB 2, OFB 3, CTR 4 }; };为什么用std::vector而不用CString或者char数组因为加密处理的是二进制数据中间可能包含大量0x00CString遇到\0就会截断char数组又容易越界。std::vector能精确表达长度后续转成CString或Base64只是界面层的事。3. 五种模式的MFC实现核心逻辑与关键代码模式分发的核心是一个switch语句每个case里做对应的分组循环。下面我按代码实际运行顺序把五种模式的核心逻辑逐个拆开讲。3.1 ECB模式实现ECB实现最简单注意处理最后不足8字节的分组size_t blocks plain.size() / 8; size_t remain plain.size() % 8; for (size_t i 0; i blocks; i) { DesCore::EncryptBlock(plain.data() i * 8, cipher.data() i * 8, key.data()); } // 剩余不足8字节的部分需要填充 if (remain 0) { unsigned char tmp[8] {0}; memcpy(tmp, plain.data() blocks * 8, remain); // PKCS7填充填充值为缺失字节数 for (size_t j remain; j 8; j) tmp[j] (unsigned char)(8 - remain); DesCore::EncryptBlock(tmp, cipher.data() blocks * 8, key.data()); }这里有个实现细节容易被忽略如果明文长度恰好是8的整数倍是否还要额外填充一个完整分组按PKCS7的规则答案是“要填充”。因为你必须让解密端能区分“原始数据就是8字节整”和“原始数据不足8字节后补了0”。如果不补解密时无法确定最后一块是不是填充出来的。很多人写ECB只处理remain0的情况结果正好8字节的数据解密后尾部多出8个0x08就是这个原因。3.2 CBC模式实现CBC的加密循环是这样的unsigned char prev[8]; memcpy(prev, iv.data(), 8); // 初始用外部传入的IV size_t totalBlocks plain.size() % 8 0 ? plain.size() / 8 : plain.size() / 8 1; for (size_t i 0; i totalBlocks; i) { unsigned char input[8] {0}; if (i totalBlocks - 1 plain.size() % 8 ! 0) { // 最后一块做PKCS7填充 size_t remain plain.size() % 8; memcpy(input, plain.data() i * 8, remain); for (size_t j remain; j 8; j) input[j] (unsigned char)(8 - remain); } else { memcpy(input, plain.data() i * 8, 8); } // 核心明文先和上一块密文或IV异或再加密 for (size_t j 0; j 8; j) { input[j] ^ prev[j]; } DesCore::EncryptBlock(input, cipher.data() i * 8, key.data()); memcpy(prev, cipher.data() i * 8, 8); // 当前密文作为下一轮的反馈 }CBC解密时方向是反的先解密密文块再和前一密文块异或得到明文。这也是很多新手容易搞混的地方——解密时“异或前一块密文”是在解密之后进行不是之前。3.3 CFB、OFB、CTR三种流模式实现对比这三种模式代码结构相似差异就在反馈源上我把它们放在一起说// CFB反馈上一块密文 unsigned char feedback[8]; memcpy(feedback, iv.data(), 8); for (size_t i 0; i totalBlocks; i) { unsigned char keystream[8]; DesCore::EncryptBlock(feedback, keystream, key.data()); for (size_t j 0; j 8; j) { cipher[i * 8 j] plain[i * 8 j] ^ keystream[j]; } memcpy(feedback, cipher.data() i * 8, 8); // 反馈密文 } // OFB反馈加密后的前一轮输出 unsigned char feedback[8]; memcpy(feedback, iv.data(), 8); for (size_t i 0; i totalBlocks; i) { unsigned char keystream[8]; DesCore::EncryptBlock(feedback, keystream, key.data()); for (size_t j 0; j 8; j) { cipher[i * 8 j] plain[i * 8 j] ^ keystream[j]; } memcpy(feedback, keystream, 8); // 注意不同点反馈加密后的输出 } // CTR反馈计数器自增 unsigned char counter[8]; memcpy(counter, iv.data(), 8); for (size_t i 0; i totalBlocks; i) { unsigned char keystream[8]; DesCore::EncryptBlock(counter, keystream, key.data()); for (size_t j 0; j 8; j) { cipher[i * 8 j] plain[i * 8 j] ^ keystream[j]; } // 计数器按大端序递增 for (int j 7; j 0; j--) { if (counter[j] ! 0) break; } }CTR的计数器递增看着简单其实有个坑如果直接对一个字节溢出的情况要考虑。大端序递增的意思是低字节在数组尾部所以从后往前加加到某个字节不为0就停止。还有一点CTR模式不需要填充因为密钥流是按字节异或的最后不足8字节的明文只要生成部分密钥流异或即可不需要补齐。这一点和ECB、CBC完全不同。4. 填充、IV管理与字节序实现中绕不开的细节模式实现能跑通只是第一步真正让代码能在实际项目里可靠运行还得处理好填充、IV、字节序以及MFC字符串转换这些细节。我在调试过程中至少有一半时间耗在这些问题上。4.1 分组不满8字节怎么办上面已经提到PKCS7填充这里补充一个实际使用中的决策点填充动作放哪一层我见过有些实现把填充放在DesMode外部调用方先自己填充再调用。我建议放在DesMode内部处理统一用PKCS7这样调用方不用关心数据长度。但要注意解密时必须在返回明文前先验证最后一块的填充值合法性防止无效填充数据被当成明文返回。网上也有用PKCS5的说法对8字节分组的DES来说PKCS5和PKCS7的效果完全一样不用纠结叫法。4.2 IV的生成与重置规则CBC、CFB、OFB、CTR都需要IV。实际项目中IV的生成方式直接影响安全性如果你对每个会话都复用同一个IV那加密强度会大打折扣攻击者可以通过对比密文推测明文关系。MFC项目里我常用的方案是用系统时间加上一个随机种子生成IV把IV直接拼在密文头部一起存储或传输。这样解密端天然知道用的什么IV不需要额外通道传递。但要注意一点IV本身不要求保密只要求不可预测且不重复所以不用对IV做特殊保护。4.3 CString与字节数组的转换陷阱这是MFC环境特有的坑。MFC默认工程可能是Unicode编码CString内部是wchar_t数组而DES处理的是unsigned char。如果直接把CString的LPCTSTR指针强转成unsigned char*去加密结果完全不是你想要的那份数据的密文因为每个字符占了两个字节。我封装了两个辅助函数// CStringA是ANSI版本字符串按单字节处理 std::vectorunsigned char StringToBytes(const CString str) { CStringA ansiStr(str); // Unicode转ANSI std::vectorunsigned char bytes(ansiStr.GetLength()); memcpy(bytes.data(), ansiStr.GetString(), ansiStr.GetLength()); return bytes; } CString BytesToHexString(const std::vectorunsigned char bytes) { CString result; for (size_t i 0; i bytes.size(); i) { result.AppendFormat(_T(%02X), bytes[i]); } return result; }加密前统一把输入的CString转成ANSI字节数组加密后把密文转成十六进制字符串显示。之所以转十六进制而不是直接转CString是因为密文是二进制数据可能包含不可见字符直接转成字符串会出现截断或显示乱码。十六进制既能完整保存密文又方便人工比对结果。5. MFC界面联调中的典型坑与排查经验在MFC对话框里把代码串起来之后我遇到了一些和界面相关的问题。这些问题单独看都不难但组合在一起很容易让人怀疑算法实现是不是错了这里列出来算是给后来者排雷。5.1 界面卡死与数据错乱第一个坑是大文件加密时界面卡死。DES本身速度并不快在Debug模式下加密一个几十MB的文件需要几秒甚至更久而我在最开始的版本里直接在按钮响应函数里同步处理界面完全无响应看起来像程序死了。解决方法是把加密放到工作线程里或者对大文件分块处理并在循环里调用一次消息泵二选一。这个既是教训也是常识但还是有人踩。另一个坑是Edit Control控件的换行符。在MFC的Edit控件里显示多行密文时\r\n会被自动处理但如果你把十六进制密文按每行64个字符分段显示再复制出来复制的内容可能带有额外的\r导致拿去解密时数据长度不对。稳妥的办法是界面上一律用无换行的连续十六进制字符串需要分段展示时再单独写一个显示逻辑不要直接操作密文字节。5.2 调试技巧拿标准测试向量对拍排错时最有效的工具就是标准测试向量。NIST的SP 800-38A文档里提供了DES三个模式的示例网上也很容易找到覆盖五种模式的测试数据。我的做法是在DesMode里加一个自检函数bool DesMode::SelfTest() { // 用标准向量校验CBC std::vectorunsigned char key {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; std::vectorunsigned char iv {0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}; std::vectorunsigned char plain {0x4E, 0x6F, 0x77, 0x20, 0x69, 0x73, 0x20, 0x74}; // 期望密文...查标准文档 ... }在程序启动或者调试版本里调用SelfTest确认基础实现正确后再去做MFC界面联调能分离“算法写错”和“界面转换写错”两类问题。我调试的时候基本上先用这个自检把DesCore和DesMode排除掉然后再查CString转换的代码。5.3 关于DES强度与模式选型的一点看法最后聊一点题外话。从密码学发展史的角度看DES的56位密钥强度在硬件算力飞速发展的今天已经被证实可以在可接受时间内被暴力破解新项目直接用DES确实不合适。但在MFC老项目维护、嵌入式系统兼容、以及教学研究场景里理解DES五种模式的实现思路仍然非常值得因为这些模式的设计思想在AES上完全复用CBC、CTR这些词在今天的工程里依然天天遇到。我在实际维护中更多是把这套实现作为一个学习跳板先在MFC里跑通DES的五种模式再把DesCore替换成AES的核心实现模式封装层几乎一行不用改。这也是为什么我愿意把模式层和算法层分那么清楚——换算法只换一层模式逻辑一次写好到处复用。你可以沿着这个思路把加密模块继续扩展成支持AES或者在界面层增加文件加解密、Base64编码等功能都是一个套路。如果你在MFC下写完这套代码建议保留一份命令行测试工具别只依赖界面。加密这种东西出错往往是静悄悄的没有明文比对光看界面上的密文很难发现问题。这个习惯帮我省下的时间远超写测试工具本身花费的时间。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门