ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Visual C++客户端DES加密通信实现与报文封装详解

Visual C++客户端DES加密通信实现与报文封装详解 简介这是基于 Visual C 实现的 DES 加密通信程序包包含完整可运行的客户端与服务端源码适合学习对称加密、网络编程及 Win32 控制台应用的开发者。压缩包共 26 个文件以 cpp 源码、dsp/dsw 工程文件、exe 可执行程序及 pdb/obj 等调试生成文件为主整体 3.57MB结构清晰便于直接打开工程对照学习。DES 作为经典对称加密算法采用 64 位密钥分组和 Feistel 结构尽管目前安全性已不及 AES但其实现思路仍是理解现代加密体系的重要基础。资料通过客户端与服务端间的连接建立、密钥交换、加密传输、解密还原等流程直观展示了 DES 在真实通信场景中的应用方式同时可借此观察 Visual C 工程组织与调试文件构成。已有 131 人学习适合初次接触加密通信或希望快速运行 Demo 的 C 初学者与实践者。1. DES 客户端加密通信被判过时的算法为什么还在工程里运行DES 加密算法在密码学教材里早就被判了可暴力破解的死刑但真实工程里它并没有退场工业上位机、MFC 遗留系统、内部工具链以及大量教学型通信程序依然用 DES 在客户端与服务端之间搭建最小密文通道。标题里 visual c 和客户端加密组合起来指向的正是这样一个典型需求在 VC 客户端里把明文在 send 之前用 DES 加密对端用同一把密钥解密从而让抓包的人看到的只是一串十六进制密文。这类代码的问题通常不在算法本身而在模式选择、IV 传递、填充处理和字节序。这篇文章按选型→实现→组包→验证的顺序把这套方案讲透。2. 动手前先定参数DES 的模式、填充与 Visual C 三条实现路线2.1 8 字节密钥里只有 56 位有效字符串密钥先统一编码DES 加密算法以 64 位为分组密钥同样是 64 位但每字节的最高位是奇偶校验位实际参与运算的只有 56 位。这意味着在代码里写BYTE key[8] {...}时哪怕某一个校验位写反加密结果也不会变错误会被静默吞掉。更常见的坑是密钥来源如果密钥是字符串Visual C 项目在 Unicode 字符集下会把字符存成 2 字节的 wchar_t直接 memcpy 进 key buffer 会混入 0x00导致两端密钥字节不一致。我一般会先约定密钥的载体要么用纯字节数组要么先转成 ANSI 或 UTF-8 再取字节。如果对接的 DES 内核是自实现或者从别的语言迁移过来的校验位会影响DES_set_key_unchecked这类接口的行为。可以用下面这段把任意 8 字节整理成合法奇校验密钥void NormalizeDesKey(BYTE key[8]) { for (int i 0; i 8; i) { int parity 0; for (int b 0; b 7; b) // 统计低 7 位中 1 的个数 parity ^ (key[i] b) 1; key[i] (key[i] 0xFE) | (parity ? 0 : 1); // 使整字节 1 的个数为奇数 } }逻辑说明DES 要求每个密钥字节按奇校验即整个字节里 1 的个数必须是奇数。这段代码先统计低 7 位的奇偶性再设置 bit0 把奇偶补齐。Windows CryptoAPI 导入 DES 密钥时忽略校验位所以这个函数主要用于跨库、跨语言对接时让密钥的字节表示完全一致避免明明同一个密钥却解不开的诡异问题。2.2 ECB 与 CBC 的取舍固定 IV 会让 CBC 退化成 ECBDES 的经典分组模式里客户端通信常用的是 ECB 和 CBC。ECB 模式下每个明文分组独立加密相同明文块永远产生相同密文块收发长数据时密文会出现重复模式攻击者看分组边界就能猜出结构我基本只在加密单块数据比如一个设备 ID时用它。CBC 模式则把每个明文分组与上一块密文异或后再加密第一块与 8 字节 IV 异或。IV 不需要保密但同一把密钥下绝对不要复用同一个 IV两个密文块做异或后 IV 的影响会被消掉相同前缀的明文就会暴露出来。每次会话随机生成 IV、随报文一起传给对端是标准做法。网上搜DES 使用 IV 及密钥在线加解密能找到很多网页对照工具适合在联调前先用已知 IV 和密钥验证两端的 CBC 结果是否一致。调试阶段用固定 IV 方便抓包比对上线前务必改成随机 IV否则加密强度会明显下降。2.3 CryptoAPI、OpenSSL 还是自实现三条路线的取舍在 Visual C 里实现 DES常见做法分三条路线取舍很直接路线依赖维护成本典型场景Windows CryptoAPI系统自带零部署低API 老旧但稳定MFC/Win32 老项目、只跑 Windows 的客户端OpenSSL需随程序带 DLL中1.1.1 与 3.x 接口差异大跨平台、已有 OpenSSL 依赖的新项目自实现 C 版 DES无高需要大量测试向量验证教学、无第三方库的受限环境我给存量 VC 客户端做加密改造时会优先选 CryptoAPI目标机器不需要额外部署加密库一个 advapi32.lib 就能编过改造成本最低。新项目可以考虑 CNG 的 BCryptEncrypt但 CNG 里 DES 需要手动打开算法句柄代码量并不少。选 OpenSSL 的话要留意版本差异OpenSSL 3.0 起 DES 接口被移入 legacy provider默认配置下DES_ecb_encrypt直接调用会失败得在初始化时显式加载 legacy provider这是老代码升级时最容易被忽略的隐性坑。提示DES 解密算法与加密算法共用同一个 56 位密钥只是 16 轮子密钥的使用顺序相反。选型阶段先确认两端平台和加密库版本再决定接口层怎么写别让算法一致掩盖实现不一致。3. 在 Visual C 里封装一个可复用的 DES 加密模块3.1 用 CryptoAPI 完成 CBC 加密的完整函数下面封装的是带 IV 的 CBC 模式 DES 加密输入密钥、IV 和明文输出 malloc 出来的密文缓冲区。这套代码可以直接粘进一个 Win32 控制台工程编译#include windows.h #include wincrypt.h #pragma comment(lib, advapi32.lib) BOOL DesEncrypt(const BYTE key[8], const BYTE iv[8], const BYTE* plain, DWORD plainLen, BYTE** cipher, DWORD* cipherLen) { HCRYPTPROV hProv 0; HCRYPTKEY hKey 0; BOOL ok FALSE; if (!CryptAcquireContext(hProv, NULL, MS_ENHANCED_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) goto done; if (!CryptImportKey(hProv, key, 8, 0, 0, hKey)) // 导入 8 字节密钥 goto done; if (!CryptSetKeyParam(hKey, KP_IV, (BYTE*)iv, 0)) // 设置 CBC 初始向量 goto done; DWORD bufLen plainLen 8; // 预留一个分组的填充空间 BYTE* buf (BYTE*)malloc(bufLen); if (!buf) goto done; memcpy(buf, plain, plainLen); DWORD dataLen plainLen; if (!CryptEncrypt(hKey, 0, TRUE, 0, buf, dataLen, bufLen)) { free(buf); goto done; } *cipher buf; // 调用方负责 free *cipherLen dataLen; // 加密后的实际长度 ok TRUE; done: if (hKey) CryptDestroyKey(hKey); if (hProv) CryptReleaseContext(hProv, 0); return ok; }参数说明key 是 8 字节 DES 密钥iv 是 8 字节 CBC 初始向量CryptEncrypt 是原地操作所以先把明文拷贝进 buf再在同一个缓冲区上加密。bFinal 传 TRUE 表示这是最后一块明文让系统在字节不足 8 时做补齐bufLen 因此比明文多留 8 字节。函数结束后密文长度在 cipherLen 里它可能等于、也可能大于 plainLen取决于填充行为。关键参数表如下CryptEncrypt 参数本例取值作用hKeyCryptImportKey 返回的句柄绑定算法与密钥hHash0不附加哈希保持密文可逆bFinalTRUE本缓冲区为最后一块允许补齐pdwDataLendataLen输入明文长度输出密文长度3.2 CryptoAPI 的填充行为和标准 PKCS#7 并不等价这里有一个我踩过的坑CryptoAPI 的 CryptEncrypt 在 bFinalTRUE 时只会在明文不是 8 字节整数倍时补填充如果明文长度正好是 8 的倍数它不会追加一个完整的填充块。也就是说它的补齐规则和 OpenSSL 等库实现的 PKCS#7 并不一致两端如果一方用 CryptoAPI、一方用 OpenSSL解密结果会出现尾部多一块或者少一块。常见做法是绕开填充差异加密前把原始明文长度放到报文头里下一章会讲解密后按这个长度截断不依赖填充块判断原始长度。如果一定要跨库互通就自己实现 PKCS#7 填充在明文尾部追加 n 个值为 n 的字节n 8 - (plainLen % 8)再交给 CryptEncrypt解密后按末尾字节的值去掉填充。这样底层库怎么处理都不影响数据语义。3.3 配套解密函数与句柄释放顺序解密函数与加密完全对称差异只在 CryptDecrypt 本身BOOL DesDecrypt(const BYTE key[8], const BYTE iv[8], const BYTE* cipher, DWORD cipherLen, BYTE** plain, DWORD* plainLen) { HCRYPTPROV hProv 0; HCRYPTKEY hKey 0; BOOL ok FALSE; if (!CryptAcquireContext(hProv, NULL, MS_ENHANCED_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) goto done; if (!CryptImportKey(hProv, key, 8, 0, 0, hKey)) goto done; if (!CryptSetKeyParam(hKey, KP_IV, (BYTE*)iv, 0)) goto done; BYTE* buf (BYTE*)malloc(cipherLen); if (!buf) goto done; memcpy(buf, cipher, cipherLen); DWORD dataLen cipherLen; if (!CryptDecrypt(hKey, 0, TRUE, 0, buf, dataLen)) { free(buf); goto done; } *plain buf; *plainLen dataLen; ok TRUE; done: if (hKey) CryptDestroyKey(hKey); if (hProv) CryptReleaseContext(hProv, 0); return ok; }调用方拿到 plain 后用完后 free。注意 CryptDecrypt 输出的 dataLen 是解密后缓冲区里的有效数据长度如果你按 3.2 说的自己实现了 PKCS#7 填充这里还需要按明文末尾的填充值再截断一次。密钥句柄和 CSP 句柄在出错路径上都要释放函数里用 goto done 集中收尾避免每个分支各写一遍销毁代码这是 Windows 加密代码里比较稳妥的结构化写法。4. 把密文送上 Socket客户端与服务端的报文封装与解析4.1 自定义报文头为什么长度和 IV 必须随包带走DES 密文是二进制数据TCP 是字节流接收方拿到的数据没有天然边界。客户端向服务端发送加密数据时必须自己定义帧格式。我用的结构包含四个字段偏移字段长度说明0魔数2 字节固定 0xDE 0x53用于粗过滤2密文长度2 字节网络字节序避免粘包半包4IV8 字节本次加密的初始向量12密文不定DES 加密后的字节流魔数纯粹是协议标识不承担安全作用密文长度用 htons 转成网络字节序是为了在不同 CPU 字节序的机器之间传值时不会读反。IV 放在包里随密文一起传是 CBC 模式的常规做法——IV 的保密性本来就不需要关键是随机和唯一。4.2 客户端组包发送的完整片段SOCKET s; // 已 connect 的 TCP 套接字 BYTE key[8] {0x13,0x34,0x57,0x79,0x9B,0xBC,0xDF,0xF1}; BYTE iv[8] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; char* msg heartbeat:alive; BYTE* cipher NULL; DWORD cipherLen 0; DesEncrypt(key, iv, (BYTE*)msg, (DWORD)strlen(msg), cipher, cipherLen); BYTE header[12]; header[0] 0xDE; header[1] 0x53; USHORT netLen htons((USHORT)cipherLen); memcpy(header 2, netLen, 2); memcpy(header 4, iv, 8); send(s, (char*)header, 12, 0); // 先发头部 send(s, (char*)cipher, cipherLen, 0); // 再发密文逻辑说明发送前先调用第 3 章的 DesEncrypt再把加密结果与 IV 组装成一帧。CBC 模式下接收方必须拿到同一个 IV 才能解密所以 IV 必须在发送方封装进报头。代码里 IV 是演示用的固定值真实会话应该用 CryptGenRandom 生成。另外 send 在 TCP 下不保证一次发完Windows 上返回 SOCKET_ERROR 或发送部分字节时需要循环处理剩余数据这里只展示了最小路径。4.3 服务端按长度字段切帧recv 循环的写法接收端的核心是把字节流恢复成帧。一次 recv 可能只收到半个帧也可能一个包里塞了多帧判断依据就是报文头里的 cipherLen。先 recv 满 12 字节头ntohs 出长度再循环 recv 直到收满该长度的密文最后用头里的 IV 和本地密钥调用 DesDecrypt。调试阶段可以在另一台机器上用网络调试助手模拟服务端把12 字节头 密文按原始字节发过来观察你的收包逻辑对粘包和半包的处理是否符合预期。网络调试助手的十六进制视图比直接打印 char* 靠谱得多因为密文里大概率有 0x00 和不可见字符printf 会在第一个 0x00 处截断。4.4 字符集和字节序Visual C 项目两个高频翻车点第 4.2 节的 htons 只处理了长度字段CBC 密文本身是字节流不需要再做字节序转换。真正容易翻车的是字符集Visual C 默认工程可能是 Unicode 字符集CString 内部是 wchar_t每个字符占 2 字节。如果把 CString 直接强转成 char* 再交给加密函数末尾的 0x00 会被当成数据一起加密而服务端按不同方式截断就会解出一堆乱码。我一般的规定是进加密函数前统一转成 UTF-8 字节数组或 ANSI 字节数组长度用转换后的 strlen 计算绝不用 sizeof(CString) 或 wcslen。5. 用标准测试向量和边界用例验证 DES 客户端模块5.1 两分钟跑通的教材测试向量无论用 CryptoAPI、OpenSSL 还是自实现先别急着联调用它验一下算法内核。DES 最经典的测试向量是密钥 0x133457799BBCDFF1明文 0x0123456789ABCDEFECB 加密输出 0x85E813540F0AB405。用第 3 章的 CBC 函数IV 全 0 时单块加密结果与 ECB 完全一致所以可以直接复用BYTE key[8] {0x13,0x34,0x57,0x79,0x9B,0xBC,0xDF,0xF1}; BYTE iv[8] {0}; BYTE plain[8] {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF}; // 期望密文85 E8 13 54 0F 0A B4 05如果输出和期望不符先检查内存字节序x86 平台写 0x13345779 这样的常量时内存里是 79 57 34 13 的顺序而 DES 算法要求密钥从左到右逐字节参与运算。拿DES 使用 IV 及密钥在线加解密的在线工具做对照也能快速定位是算法问题还是参数传递问题。5.2 边界用例清单与回归验证我一般把下面几组用例写成一个控制台测试函数每次改动加密模块后都跑一遍明文长度为 0明文长度为 8 字节块对齐确认尾部是否被填充明文长度为 16 字节两个分组同一个明文用两个不同的 IV密文必须不同密文翻转一个字节后解密确认从该分组开始到结尾全部乱码但程序不崩溃。最后一组验证的是 CBC 的错误传播特性如果你的实现只有当前块乱码说明分组链接逻辑有问题。把这些用例固化进工程以后从 DES 升级到 3DES、或者换用 CNG 接口改完跑一遍就知道兼容边界在哪里。本文还有配套的精品资源点击获取
返回列表