MFC字符串加解密工具实战:用XOR+Base64保护配置敏感信息
发布时间:2026/9/9 9:24:52 锦皓数字建站

简介这是一套基于VC6.0环境、面向MFC开发者的CString字符串加解密示例工程由作者xugqsky整理重点解决MFC程序中敏感字符串的可逆加密与解密需求。资源共21个文件以C源文件为主5个.h、3个.cpp同时包含MFC工程所需的.dsp/.dsw/.rc/.rc2等配置与资源文件、ReadMe.txt和新建文本文档.txt两份说明文档以及编译好的EnCry.exe可执行程序整个RAR压缩包仅33KB非常轻量便于下载后直接运行查看效果。核心代码对EnCryandUnEncry.h等模块进行了封装演示了在CString上集成AES、DES或XOR等对称加密算法的基本流程并涉及密钥注入、字节数组转换和可逆解密验证等关键环节文档部分则适合快速理解工程结构。该资源已有598人学习/下载适合正在学习MFC字符串处理或希望为CString补充加密能力的Windows开发者作为入门参考。 最近在维护一个老项目时碰上了典型需求MFC界面里的数据库连接密码、调用外部接口的Token原本以明文写在ini配置里测试时没人在意交付给现场后客户打开配置文件就能看到一堆敏感信息。领导的要求很明确——不动架构、不加第三方依赖、快速把字符串加解密这套能力补上。于是就有了这篇文章要整理的“MFC字符串加解密”小工具从方案选型到代码实现再到实际踩坑完整记录一遍。如果你也在做MFC界面开发或者正在写类似需要保护本地配置信息的桌面程序这篇文章应该能帮你省掉不少弯路。我先把结论放在前面这个项目最终的落地方案是“XOR异或加密 Base64编码”的组合整体代码量不大但牵扯到MFC中CString的字符集问题、Windows编辑框与字节流的交互、以及加密后特殊字符的传输安全每一个环节都有隐藏的坑。下面按实际推进的顺序从思路到实现再到排查逐步拆开讲。1. 整体思路与方案选型1.1 为什么不用AES、DES这类“正规军”做加解密方案时第一时间想到的往往是AES、3DES这些成熟算法。如果是新项目、有专门的加密库可用我肯定直接用AES没必要自己造轮子。但这次的情况不同目标程序是多年前的MFC工程编译环境老旧运行环境又是客户现场的Windows工控机没有联网安装依赖的条件。要扛着一个额外的Crypto或者OpenSSL库进去光是解决静态编译、运行时库冲突就够折腾半天。更关键的是这类场景要防的不是专业攻击者而是“防止有人随手打开配置文件就看到明文”。针对这种低烈度威胁一个实现简单、逻辑完全可控的自定义加密方案反而更合适。XOR异或加密正是这样的选择原理简单到几行代码能讲清楚执行效率高密钥可以自己控制出问题时排查成本极低。要是直接上AES单是密钥管理和初始向量(IV)的处理方式就够在不熟悉密码学的同事那里引发一连串误解。另外还有一个实操层面的原因MFC项目里最常处理的字符串是CString而CString在工程字符集不同的情况下会对应宽字符或窄字符两种底层类型。AES这类算法操作的是字节数组拿CString直接喂进去还得小心翼翼地做字符集转换和长度处理。XOR逐字节运算的方式和字节流的契合度反而更高处理起来更顺手。1.2 最终方案XOR加密与Base64编码组合单纯用XOR加密出来的结果有个问题它是一堆不可见的二进制字节甚至可能包含0x00这种字符串结束符。如果再把这些字节塞回CString或者写入配置文件轻则显示乱码重则在复制、传输过程中直接截断数据。所以要给加密结果加一层Base64编码把所有字节映射成大小写字母、数字和“” “/”这类安全字符。整体流程就是两条线加密明文CStringA → 逐字节异或 → 得到密文字节流 → Base64编码 → 得到可见字符串解密Base64字符串 → Base64解码 → 恢复密文字节流 → 用相同密钥逐字节异或 → 还原明文这个组合在效率上完全够用。Base64编码会把数据体积扩大约33%但配置文件里那点密码、Token数据量极小这点开销可以忽略。作为对照我也列一下实际评估过的几个方案方案优点缺点结论纯XOR实现最简单解密速度快加密后含不可见字符直接存储会截断或乱码需配合编码不能单独用XOR Base64防止截断结果可打印可复制加密强度有限仅适合低敏感场景本方案采用AES/CBC加密强度高需引入加密库密钥和IV管理复杂本项目环境不适合RC4流密码实现简单存在已知弱点官方不推荐不建议新代码使用DPAPICryptProtectDataWindows自带与用户账户绑定换机器或换账户后无法解密可作进阶替代方案从表里能看出来选型不是“哪个最强选哪个”而是在“当前工程约束下哪个最合适”。XORBase64的组合在代码量、可控性、维护成本几个维度上都占优最后就定了它。2. MFC字符串与编码的核心细节2.1 CString的“双面人生”宽字符与窄字符MFC开发中最折磨人的点之一就是CString在工程字符集不同时底层类型会跟着变。新建MFC项目时VS默认会勾选Unicode字符集这时候CString的底层是CStringW每个字符占据2个字节对应Windows的UTF-16编码。如果项目改成“使用多字节字符集”CString底层就变成CStringA一个字符一字节对应系统的ANSI代码页。做加解密这类字节级操作时我强烈建议统一使用CStringA即窄字符版本。原因很好理解加密函数处理的是“字节流”而字节流天然就是单字节的。如果不做统一会出现这样的情况——界面编辑框输入“abc”拿到的是CStringW宽字符数据里藏着0x00字节逐字节异或时数据长度翻倍、内容还含空洞最后加密结果和解密结果完全对不上。我在代码里写函数签名时参数和返回值全部用CStringA界面层在获取和回显数据时也显式使用GetWindowTextA和SetWindowTextA。这样整个链路里不出现宽窄混用问题当场就消失了大半。2.2 加密结果中的0x00字节为什么必须处理这是新手第一次写XOR时最容易踩的雷明文“test”和密钥“key”逐字节异或后得到的字节流极有可能包含0x00。如果用CString直接保存这段数据CString内部按C风格字符串处理遇到0x00就会认为字符串结束后面的数据全部丢失解出来的内容自然残缺不全。Base64的作用在这里就体现出来了。它把任意的3个字节映射成4个可打印ASCII字符输出结果里绝对不会出现0x00、回车换行这些控制字符。加密后无论内容是什么都能安全地存进ini文件、放进编辑框、甚至通过复制粘贴传递。这一点要记住对XOR加密做Base64编码不是可选项是必须项。2.3 std::string与CStringA的互通MFC代码里常常混用标准库的std::string和MFC的CString。在项目使用Unicode字符集时直接CString转std::string会出现编译错误需要借助CT2A这类转换宏。但既然我们统一用CStringA情况就简单得多CStringA提供了.GetString()方法返回const char*指针可以直接构造std::string反过来std::string可以通过.c_str()构造CStringA。简而言之在加解密函数内部不要碰CStringW也不要依赖_T()宏做字符串字面量转换。固定用CStringA和std::string配合数据类型一目了然能省掉一大半字符集相关的编译错误和运行期乱码。3. 完整实操在MFC对话框中实现加解密3.1 建立工程与界面布局在VS中新建一个“MFC应用程序”应用类型选择“基于对话框”项目名称随意。工程建好后把默认生成的对话框资源打开往上面拖几个控件完成下面的布局一个静态文本“明文/密文”一个编辑框ID设为IDC_EDIT_INPUT一个静态文本“密钥”一个编辑框ID设为IDC_EDIT_KEY一个静态文本“输出结果”一个编辑框ID设为IDC_EDIT_OUTPUT属性里勾选“Multiline”多行方便看长文本两个按钮一个“加密”ID为IDC_BTN_ENCRYPT一个“解密”ID为IDC_BTN_DECRYPT界面不需要做得花哨关键是配合类向导给两个按钮添加BN_CLICKED消息处理函数。实际项目中如果想更顺手可以给编辑框加右键菜单或者用UpdateData体系来读写控件值但这里为了后面代码和流程更清晰我直接选用GetWindowTextA和SetWindowTextA避开字符集宏的干扰。3.2 核心加解密函数封装加解密逻辑不适合直接丢在对话框的消息响应里我单独写了一个StringCrypto类来封装。头文件StringCrypto.h内容如下// StringCrypto.h #pragma once #include string class StringCrypto { public: // 加密明文 - Base64(XOR(明文)) static CStringA Encrypt(const CStringA strData, const CStringA strKey); // 解密Base64字符串 - XOR还原 - 明文 static CStringA Decrypt(const CStringA strEncData, const CStringA strKey); private: // XOR核心逻辑bothEncode true 表示加密false表示解密 static std::string XorCipher(const std::string data, const std::string key); // Base64编码 static std::string Base64Encode(const unsigned char* src, int srcLen); // Base64解码 static std::string Base64Decode(const char* src, int srcLen); };实现文件StringCrypto.cpp里关键是XOR部分的代码// StringCrypto.cpp #include pch.h #include StringCrypto.h std::string StringCrypto::XorCipher(const std::string data, const std::string key) { std::string result data; size_t keyLen key.length(); if (keyLen 0) return result; // 无密钥时不加密 for (size_t i 0; i result.size(); i) { // 核心取模循环使用密钥逐字节异或 result[i] (char)((unsigned char)result[i] ^ (unsigned char)key[i % keyLen]); } return result; }这段代码里有几个细节值得说明。第一key[i % keyLen]让密钥循环参与运算不管明文多长密钥都会从头开始反复使用这正是XOR加密的标准写法。第二异或前把char强转成unsigned char防止符号位扩展带来的异常结果。第三无密钥时直接返回原始数据这在调试时需要用到但正常使用时我会在调用端做校验避免用户忘了填密钥。3.3 Base64编码与解码的实现Base64的编码逻辑并不复杂。它把每3个字节24位拆成4组6位的数值每组对应一个编码表字符最终长度按(原长度 2) / 3 * 4计算不足的部分补等号。std::string StringCrypto::Base64Encode(const unsigned char* src, int srcLen) { static const char* base64Table ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/; std::string result; result.reserve(((srcLen 2) / 3) * 4); int i 0; while (i 2 srcLen) { unsigned int val ((unsigned int)src[i] 16) | ((unsigned int)src[i 1] 8) | ((unsigned int)src[i 2]); result.push_back(base64Table[(val 18) 0x3F]); result.push_back(base64Table[(val 12) 0x3F]); result.push_back(base64Table[(val 6) 0x3F]); result.push_back(base64Table[val 0x3F]); i 3; } int remain srcLen - i; if (remain 1) { unsigned int val (unsigned int)src[i] 16; result.push_back(base64Table[(val 18) 0x3F]); result.push_back(base64Table[(val 12) 0x3F]); result.push_back(); result.push_back(); } else if (remain 2) { unsigned int val ((unsigned int)src[i] 16) | ((unsigned int)src[i 1] 8); result.push_back(base64Table[(val 18) 0x3F]); result.push_back(base64Table[(val 12) 0x3F]); result.push_back(base64Table[(val 6) 0x3F]); result.push_back(); } return result; }解码是编码的逆过程把每个Base64字符映射回6位数值然后拼装成字节序列。具体实现时要注意过滤掉字符串中的换行回车字符因为Base64编码结果若被写入配置文件常见的做法是每76个字符加一个换行。解码端如果不去除这些空白符就会导致字节错位。这段代码略长但本质上就是查表和位运算的组合这里不再全文贴出项目中完整保留即可。3.4 对话框按钮事件绑定封装类的两个对外接口内部组合了XorCipher与Base64Encode/Base64DecodeCStringA StringCrypto::Encrypt(const CStringA strData, const CStringA strKey) { std::string data(strData.GetString()); std::string key(strKey.GetString()); std::string xorData XorCipher(data, key); // 1. 逐字节异或 if (xorData.empty()) return CStringA(); return CStringA(Base64Encode((const unsigned char*)xorData.data(), (int)xorData.size()).c_str()); }解密则先做Base64解码再对字节流做一次XORXOR操作自身的特点决定了加密和解密是同一个函数。按钮的响应处理就变得很简单void CStrEncryptDlg::OnBnClickedBtnEncrypt() { CStringA key; m_ctlKey.GetWindowTextA(key); if (key.IsEmpty()) { AfxMessageBox(_T(请输入密钥)); return; } CStringA input; m_ctlInput.GetWindowTextA(input); if (input.IsEmpty()) { AfxMessageBox(_T(请输入要加密的内容)); return; } CStringA encrypted StringCrypto::Encrypt(input, key); m_ctlOutput.SetWindowTextA(encrypted); }解密按钮的代码结构完全一样只需要把Encrypt换成Decrypt。注意整个过程的操作对象都是CStringA控件变量我直接在资源编辑器里关联成了CEdit类型的m_ctlKey、m_ctlInput、m_ctlOutput。3.5 编译与运行验证编译运行后在“明文/密文”编辑框输入Hello MFC密钥填abc123点击加密输出框出现一段类似SG9sZXQgT80ID3w/Tw的Base64串。再把这个串复制到输入框点击解密能正常还原出Hello MFC逻辑闭环就验证通过了。验证时我建议先测纯ASCII英文再测中文。中文在CStringA中对应的是当前系统代码页的多字节序列只要前后数据链路保持一致中文加解密不会乱码。但如果中途混入了宽字符转换中文就很容易出问题这也是我在上一节反复强调字符集统一的原因。4. 实战中的典型问题与排查方法4.1 明文字符串里包含0x00导致截断加密本身没问题但拿到明文时就已经被截断了。这种情况常见于从文件或网络缓冲区读数据CString的构造或赋值依据的是C风格字符串到0x00就停后面的字节被丢弃。排查时用调试器检查原始缓冲区长度和CString内部的长度如果二者对不上就说明源头就已经丢了数据。解决方法是尽量从源头就使用CStringA并且每一步都通过GetLength()、size()这类显式长度来传递数据不要依赖底层字符串结束符。4.2 中文内容加密后再解密出现乱码大多是因为工程是Unicode字符集输入框取出的是宽字符而加密函数处理的是窄字符。宽窄字符互转时如果选择的代码页不对中文就会变成问号或乱码。我采取的做法是所有控件取值和赋值都显式用GetWindowTextA/SetWindowTextA。这样一来编辑框的宽字符数据在进入CStringA时由Windows内部按系统代码页完成转换再往后就是一路窄字符操作不乱码。4.3 Base64解码后长度不对、解密内容错乱先确认编码时的数据源长度是否按字节数而不是字符数算。例如std::string的size()是正确的但有人会误用strlen遇到0x00就提前截断。再检查解码函数是否正确地忽略了空白字符非法字符会让位运算错位导致解密结果面目全非。建议在解码循环里加一个isspace判断见到空白直接跳过。4.4 常见问题速查表现象可能原因解决建议加密后编辑框一片空白或内容很短密文中含0x00被当成字符串结束确认是否做了Base64编码解密结果乱码Unicode/多字节字符集混乱全部改用CStringA与Get/SetWindowTextA中文解密后变成“??”宽窄字符转换代码页不对统一走系统代码页不做手工转换Base64结果里混有换行导致解密错乱编码时添加了换行解码前过滤所有空白字符同一明文每次加密结果一致这是XOR特性无随机IV预期行为敏感场景需换方案错误的数据字节数导致解密失败长度用了字符数而非字节数用size()/GetLength()获取字节数4.5 关于密钥与效率的进阶建议密钥管理这块直接把明文密钥写死在代码里风险较大哪怕代码做了混淆用调试器一搜内存就能找出来。我的做法是把密钥拆成几段分散到代码的不同位置用字符串拼接的方式在运行时组装或者用密钥做一次简单变换后再参与异或。这种方式不提供绝对安全但能挡住大部分“好奇式偷看”。在效率方面XOR加Base64非常快。实测对10KB字符串做完整加解密耗时都在毫秒级放在按钮响应里完全不会卡界面。如果配置信息很长建议密钥长度选16字节以上并尽量使用随机生成的密钥避免弱密钥重复模式被攻击者利用。5. 写在项目之后还有哪些可扩展方向这个加解密工具做完后我又顺手在MFC项目里扩展了几处这里一并分享。如果你拿到的是存量代码可以参考。一个是把配置文件的读取写统一封装成类写入时自动加密读取时自动解密。这样业务方不用关心加密细节看到的是和以前一样的GetConfig/SetConfig接口。另一个是加密结果可以再套一层格式校验比如在明文字节流末尾追加一个固定魔数的加密副本解密后校验魔数可以快速判断密钥是否输入错误避免解出一堆乱码还看不出原因。如果对安全性要求进一步升级比如现场有专门的IT审计要求可以调研Windows自带的DPAPI即CryptProtectData/CryptUnprotectData它不需要额外依赖、和Windows操作系统深度集成主密钥由系统按当前用户管理安全性远高于自定义XOR方案。缺点是加密结果和用户账号绑定换用户或换机器就解不开这是它的自身属性选型时要评估清楚。最后再提一个我在处理“MFC控件自适应屏幕分辨率”时观察到的小点当对话框较长、加密结果反复粘贴查看时给输出编辑框加上垂直滚动条会让体验好很多。这个和加解密本身无关但恰恰是这类小细节决定了工具交付给现场后好用不好用。技术方案固然重要落在实际界面上的每一个顺手之处才真正影响一个功能在存量环境里能走多远。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。