☰
DES五种加密模式MFC实现与避坑:ECB/CBC/CFB/OFB/CTR
2026/10/26 4:42:50 网站建设 项目流程

简介:面向具备C++与MFC基础、希望理解分组密码工作模式的开发者,这份源码在MFC框架下实现了一重DES的ECB、CBC、CFB、OFB、CTR五种加密模式,并提供友好界面与较完整注释,可用来对照学习各模式的加解密流程与差异,也便于在此基础上扩展为三重DES。压缩包共18个文件、约57KB,包含4个h头文件与3个cpp源文件承载核心算法和对话框逻辑,rc资源脚本、ico图标及dsw/dsp工程文件构成可直接打开的VC工程,exe与txt分别方便试运行和阅读说明,ncb、aps、clw、opt等为IDE辅助文件,整体体量轻便。目前已有1091人学习下载。读者可从中获得一套可编译运行的DES多模式演示程序,通过界面操作直观比较五种模式的密文差异,并借助注释理解密钥调度、初始置换、轮函数及模式串联方式;退出时的提示不影响文件加解密,可放心用于课程实验、算法验证与二次开发。

1. 从一只旧串口工具说起:DES 五种模式在 MFC 里到底怎么落地

DES 五种加密模式(MFC 实现)源码,这个名字看起来像课程设计,实际拆开后更像一套给旧桌面工具续命的加密工具箱。很多 MFC 上位机不是因为喜欢 DES 才用它,而是协议、历史数据和第三方接口把密钥长度和分组模式钉死了,换算法意味着整条链路重签。这份源码把 ECB、CBC、CFB、OFB、CTR 五种模式放在同一个工程里,核心价值不是“再造一个 DES”,而是让你能对着对话框、字节数组和 IV 把每种模式的边界跑一遍。适合维护老 MFC 项目、对接固定加密协议、以及想弄明白分组模式差异的人;新手可以照着按钮走,熟手重点看模式封装和填充处理。血泪经验是:先别改业务代码,先把五种模式各跑一组边界用例,后面能少熬两个晚上。

2. 拆开 DES 核心:64 位分组、16 轮迭代与 MFC 工程边界

DES 的算法骨架并不复杂:输入 64 位明文,经过初始置换、16 轮 Feistel 迭代、逆置换得到 64 位密文;密钥表面 64 位,实际有效 56 位,每第 8 位是奇偶校验位。真正让 MFC 工程翻车的往往不是轮函数,而是 CString 到字节数组的转换、密钥的十六进制解析、以及 Unicode 和多字节字符集混用。先把这个边界钉死,后面五种模式才有讨论意义。

2.1 密钥调度和 S 盒:先确认字节序再谈加密

DES 的密钥调度会先做 PC-1 置换,把 64 位密钥压成 56 位,再分成左右各 28 位的 C、D 寄存器;每一轮按轮次循环左移 1 或 2 位,然后做 PC-2 置换产出 48 位子密钥。16 轮下来得到 16 个子密钥。轮函数里明文右半部分先扩展成 48 位,和子密钥异或,再进 8 个 S 盒压回 32 位,最后做 P 置换。听上去是标准流程,但落地时你要盯住两件事:密钥到底按什么字节序写入数组,以及 CString 里的“8 个字符”到底占几个字节。

MFC 工程常见两种密钥输入:一种是对接方给 8 个 ASCII 字符,另一种是给 16 位十六进制字符串。前者在 Unicode 工程里不能直接memcpyCString 的缓冲区,因为CString内部是wchar_t,一个字符占两个字节;后者要按从左到右、高字节到低字节写入k[0]到k[7],不要先转成整数再memcpy,整数大小端会把顺序颠倒。我一般会把密钥解析单独抽成一个函数,失败时把错误原因带回对话框,而不是在加密函数里静默补零。

// DesKey.h:密钥解析,支持 8 个 ASCII 字符或 16 位十六进制 #pragma once #include <string> struct DesKey8 { unsigned char k[8]; }; static int HexVal(TCHAR c) { if (c >= _T('0') && c <= _T('9')) return c - _T('0'); if (c >= _T('a') && c <= _T('f')) return c - _T('a') + 10; if (c >= _T('A') && c <= _T('F')) return c - _T('A') + 10; return -1; } bool ParseDesKey(const CString& text, DesKey8& out, CString& err) { CString s = text; s.Trim(); if (s.GetLength() == 16) { for (int i = 0; i < 8; ++i) { int hi = HexVal(s[i * 2]); int lo = HexVal(s[i * 2 + 1]); if (hi < 0 || lo < 0) { err = _T("密钥不是合法十六进制"); return false; } out.k[i] = (unsigned char)((hi << 4) | lo); } return true; } if (s.GetLength() == 8) { // 只处理单字节字符集;Unicode 工程请显式转 UTF-8/GBK, // 不建议用中文当 DES 密钥,字节长度不可控。 CT2A a(s); if (strlen((const char*)a) != 8) { err = _T("密钥包含多字节字符,请改用 16 位十六进制"); return false; } memcpy(out.k, (const char*)a, 8); return true; } err = _T("密钥长度必须是 8 字符或 16 位十六进制"); return false; }

这段代码的关键点不在ParseDesKey本身,而在两个参数约束:text是用户输入,允许前后空格;out.k固定 8 字节,不额外补零。CT2A依赖当前代码页,Unicode 工程里中文会被转成多字节,长度很容易超过 8,所以我在函数里直接拦掉。对接方如果给的是 16 位 hex,按k[0]对应最左边两位、k[7]对应最右边两位,不要用std::stoul转成 64 位整数再倒序,那种写法在跨平台比对时最容易出玄学问题。

DES 核心的 S 盒数据通常直接以常量表放在源码里,不需要在运行时计算。你拿到源码后先看 S 盒表的行、列索引方式:标准 DES 是 6 位输入,最高位和最低位组成行号,中间 4 位组成列号。如果实现里把行号写成(x >> 4) & 0x03,那 S 盒索引就错了,表现是加密能跑、解密也能跑,但和任何独立实现都对不上。这类错误不会崩溃,只会让结果像随机数,排查时优先用固定 key、固定明文跑一遍,再和另一套实现比对第一轮输出。

2.2 工程编译与入口:把控制台验证塞进 MFC 对话框

这份源码是 MFC 工程,入口通常是一个对话框。不要一上来就在按钮里写完整加密流程,先把 DES 核心编译通过,再让对话框只做参数收集和结果展示。开发环境里新建或打开 MFC 工程时,先确认字符集设置和源码一致:源码按 Unicode 写,工程却设成多字节,CString到char*的转换会到处报错;反过来,源码里大量char缓冲区,工程设成 Unicode,AfxMessageBox和CString拼接也会出问题。常见做法是保持工程默认字符集,然后在边界处统一转std::vector<BYTE>。

编译顺序我一般分三步:先把 DES 核心单独编译成静态库或至少独立成DesCore.cpp,确保没有 MFC 依赖;再把五种模式封装成DesCrypt.cpp,只依赖标准库和核心;最后在对话框里调用封装层。这样做的原因是,模式封装里的 IV、填充、计数器都不应该和界面控件绑在一起,否则你换一个协议就要改对话框。

// DesDemoDlg.cpp:按钮只负责收集参数,加密走封装层 void CDesDemoDlg::OnBnClickedBtnRun() { UpdateData(TRUE); DesKey8 key; CString err; if (!ParseDesKey(m_keyText, key, err)) { AfxMessageBox(err); return; } // m_plainText 是 CString,先明确编码再转字节 std::vector<BYTE> in; if (!CStringToUtf8Bytes(m_plainText, in)) { AfxMessageBox(_T("明文转 UTF-8 失败")); return; } BYTE iv[8] = {0}; if (m_mode == DesMode::CBC || m_mode == DesMode::CFB || m_mode == DesMode::OFB || m_mode == DesMode::CTR) { if (!ParseHex8(m_ivText, iv, err)) { AfxMessageBox(err); return; } } std::vector<BYTE> out; DesPadding pad = (m_mode == DesMode::ECB || m_mode == DesMode::CBC) ? DesPadding::PKCS7 : DesPadding::None; if (!DesEncrypt(m_mode, key, iv, in, out, pad, err)) { AfxMessageBox(err); return; } m_cipherHex = BytesToHex(out); UpdateData(FALSE); }

UpdateData(TRUE)把控件内容刷到成员变量,UpdateData(FALSE)把成员变量刷回控件;这两个调用顺序反了,你会看到“点了按钮但结果没变”。m_mode是模式下拉框的枚举值,m_ivText是 16 位十六进制 IV 输入框。DesEncrypt的pad参数在 ECB/CBC 下用 PKCS#7,在 CFB/OFB/CTR 下用None,因为后三种模式本质上是流式处理,不需要按 8 字节补齐。CStringToUtf8Bytes必须显式处理编码,不能直接CStringA强转,否则中文会在解密后变成乱码。

对话框里展示结果时,用十六进制字符串,不要直接把二进制塞进AfxMessageBox。二进制里遇到0x00会被当成字符串结束,你看到的结果会短一截,然后误以为是解密截断。源码里如果有“复制结果”按钮,也按十六进制复制,别复制原始字节。

3. 五种模式封装:ECB、CBC、CFB、OFB、CTR 的代码骨架

DES 核心只负责“8 字节进、8 字节出”,五种模式负责把任意长度数据切块、串联、反馈或计数。ECB 最简单,每块独立加密;CBC 把上一块密文异或进当前明文;CFB 用密文反馈生成密钥流;OFB 用输出反馈生成密钥流;CTR 用计数器加密后异或。封装层要解决三个问题:模式状态放哪、IV 怎么传、填充什么时候做。把这三件事分开,代码才不会写成意大利面。

3.1 分组模式的状态机:IV、反馈与计数器

ECB 没有状态,相同明文块永远得到相同密文块,所以它不适合有重复结构的业务数据,但适合加密随机密钥、短标识或者做回归测试。CBC 的状态是上一块密文,加密时C_i = E(P_i xor C_{i-1}),解密时P_i = D(C_i) xor C_{i-1},IV 只在第一块参与异或。CFB 的状态是一个移位寄存器,加密时用E(寄存器)产生密钥流,和明文异或得到密文,再把密文反馈进寄存器;解密时同样用E(寄存器),但反馈的是收到的密文。OFB 的状态是上一次的输出块,O_i = E(O_{i-1}),加解密都用同一个密钥流异或。CTR 的状态是计数器,KS_i = E(counter_i),每处理一块计数器加一。

// DesMode.h:把模式、填充和上下文分开 enum class DesMode { ECB, CBC, CFB, OFB, CTR }; enum class DesPadding { None, PKCS7, Zero }; struct DesCtx { DesMode mode; DesPadding pad; DesKey8 key; BYTE iv[8]; // CBC/CFB/OFB 的初始向量,CTR 的初始计数器 BYTE chain[8]; // CBC 的上一块密文,CFB/OFB 的反馈寄存器 BYTE counter[8]; // CTR 当前计数器 int cfbSegmentBits; // 64 或 8,必须和对接方一致 };

iv和chain要分开:CBC 加密时chain初始等于iv,之后每块更新为当前密文;CFB 的chain是移位寄存器,每处理一个段就移位并填入密文;OFB 的chain是上一次的输出块。cfbSegmentBits是很容易被忽略的参数,CFB-64 一次处理 8 字节并整体反馈,CFB-8 一次处理 1 字节并移位反馈,两者密文完全不同。对接方只说“CFB”时,必须追问段大小,否则你解出来的就是一堆错位数据。

CBC 的解密要特别注意:IV 只参与第一块,后续用密文块做异或。很多人写解密时把chain更新成明文块,结果第一块对、第二块开始全错。正确顺序是先用当前密文块解密,再和chain异或得到明文,最后把chain更新成当前密文块。CFB 解密则相反,先用E(chain)生成密钥流,和密文异或得到明文,再把密文反馈进chain。

3.2 填充与流式接口:PKCS#7、ZeroPadding 和按块喂数据

ECB 和 CBC 是分组模式,输入长度不是 8 的倍数时要填充。PKCS#7 的规则是缺几个字节就填几个字节,填的值等于填充长度;如果原文正好是 8 的倍数,也要额外填一个 8 字节块,值为0x08。这样解密后才能可靠地去填充。ZeroPadding 是末尾补0x00,缺点很明显:原文本身以0x00结尾时,去填充无法区分“补的零”和“数据里的零”,只适合文本协议,不适合二进制报文。

// PKCS#7 填充:空串也会填 8 个 0x08 bool PadPkcs7(const BYTE* in, int len, std::vector<BYTE>& out, CString& err) { if (len < 0 || (len > 0 && in == nullptr)) { err = _T("输入缓冲区无效"); return false; } int pad = 8 - (len % 8); out.assign(in, in + len); out.insert(out.end(), (size_t)pad, (BYTE)pad); return true; } // 去填充:检查尾部 1~8 个字节是否一致 bool UnpadPkcs7(const std::vector<BYTE>& in, std::vector<BYTE>& out, CString& err) { if (in.empty() || in.size() % 8 != 0) { err = _T("密文长度不是 8 的倍数"); return false; } BYTE pad = in.back(); if (pad < 1 || pad > 8 || pad > in.size()) { err = _T("填充值非法"); return false; } for (size_t i = in.size() - pad; i < in.size(); ++i) { if (in[i] != pad) { err = _T("填充字节不一致"); return false; } } out.assign(in.begin(), in.end() - pad); return true; }

PadPkcs7的len是字节长度,不是字符长度;out先拷贝原文再追加填充,避免修改调用方缓冲区。UnpadPkcs7先检查总长度是不是 8 的倍数,再检查填充值范围,最后逐个比对填充字节。只检查最后一个字节是不够的,错误密文可能碰巧最后一个字节是0x01,逐个比对能把大部分填充错误挡在业务层之外。

CFB、OFB、CTR 不需要填充,但如果你把上层接口统一成“输入任意长度、输出任意长度”,封装层要保留一个“流式”分支。CTR 可以按字节生成密钥流:先加密计数器得到 8 字节密钥流,用几个字节就取几个字节,用完后计数器加一。CFB-8 也是按字节处理,每处理一个字节就移位一次寄存器。OFB 可以按块生成密钥流,也可以按字节取用,取决于源码里的实现粒度。统一接口里不要把流式模式硬塞进分组填充逻辑,否则密文长度会多出 8 字节,对接方一验长度就露馅。

3.3 模式对比与选型:别让 ECB 背所有锅

模式需要 IV/计数器加解密是否同构错误传播是否填充典型场景
ECB否是单块错误只影响单块是短随机标识、回归测试
CBC是否一块错误影响当前块和下一块是报文、文件、旧协议
CFB是否错误影响当前段和后续反馈否流式数据、逐字节到达
OFB是是单字节错误只影响对应字节否噪声信道、流式加密
CTR是是单字节错误只影响对应字节否随机访问、分块处理

选型时先看对接方给的参数,不要自己“优化”。ECB 不是绝对不能用,它在加密随机密钥、短 token、或者做固定向量回归时很方便;但它不能用于有重复结构的业务数据,因为相同明文块会产生相同密文块,肉眼就能看出模式。CBC 需要随机 IV,并且 IV 要随密文一起传输;如果协议要求固定 IV,那安全性会打折,但兼容优先。CFB 和 OFB 把分组密码变成流式,适合数据一块一块到达的场景;CTR 适合随机访问,因为你可以直接跳到第 N 块计算计数器,但计数器绝对不能重复。

DES 本身的密钥只有 56 位有效长度,放在今天不适合作为新系统的唯一安全方案。这份源码的定位是理解分组模式、维护旧协议、做兼容层,不是让你拿 DES 去设计新系统。把这一点想清楚,后面调参和排错才不会跑偏。

4. 避坑与排查:五条血泪记录

DES 五种模式的代码量不大,但坑很集中:编码、IV、段大小、计数器、缓冲区。下面五条都是“代码能跑、结果不对”的类型,每条按现象、原因、解决写,照着排查能省不少时间。

4.1 现象:中文解密后乱码,长度还短一截

现象很典型:英文加解密正常,换成中文后解密结果乱码,而且比原文短。原因通常不是 DES 算错了,而是CString按字符长度处理,加密时却按字节长度切块。Unicode 工程里一个中文CString字符占两个wchar_t字节,转成 UTF-8 后又占三个字节;如果你用GetLength()当字节长度传给加密函数,输入缓冲区实际只读了一部分,解密后自然短一截。另一个常见原因是解密后直接用CString构造二进制结果,遇到0x00就截断。

解决方法是把“文本”和“字节”彻底分开:对话框负责把文本转成明确编码的字节数组,加密层只认BYTE*和字节长度,解密层返回字节数组,最后由显示层决定按 UTF-8 还是 GBK 转回文本。

// CString -> UTF-8 字节数组,长度按字节算 bool CStringToUtf8Bytes(const CString& s, std::vector<BYTE>& out) { int n = WideCharToMultiByte(CP_UTF8, 0, s, -1, nullptr, 0, nullptr, nullptr); if (n <= 0) return false; out.resize((size_t)n - 1); // 去掉结尾 '\0' if (n > 1) { WideCharToMultiByte(CP_UTF8, 0, s, -1, (char*)out.data(), n - 1, nullptr, nullptr); } return true; }

CP_UTF8是目标编码,-1表示输入以\0结尾,返回值包含结尾\0,所以out要减一。解密后如果要显示,再用MultiByteToWideChar(CP_UTF8, ...)转回CString;如果对接方用 GBK,就把CP_UTF8换成CP_ACP或显式936,但一定要在协议文档里写死。不要用CStringA直接接二进制密文,也不要用strlen量密文长度。

4.2 现象:同一明文两次加密结果不同

现象是回归测试突然失败:同样的 key、同样的明文,今天加密出来的密文和昨天不一样。原因多半是 CBC、CFB、OFB、CTR 使用了随机 IV 或随机计数器。ECB 没有 IV,所以结果稳定;CBC 每次加密如果重新生成 IV,密文当然不同。这不是 bug,反而是正确用法,但测试用例必须固定 IV,否则你无法比对。

解决分两个场景:生产环境加密时生成随机 IV,并把 IV 和密文一起发给对方,IV 不需要保密,但要保证不可预测;测试环境固定 IV,单独做一组“固定向量回归”。如果对接方要求“同一明文必须得到同一密文”,那就要确认协议是不是 ECB,或者是不是固定 IV 的 CBC;不要为了通过测试把随机 IV 改成固定零 IV,那会在生产环境留下隐患。

// 测试用固定 IV;生产环境请换成系统随机源 void MakeTestIv(BYTE iv[8]) { const BYTE fixed[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; memcpy(iv, fixed, 8); }

这个函数只适合测试,不要在生产代码里用rand()拼 IV。rand()的随机性不够,而且没有播种时序列固定。生产环境用系统随机源生成 8 字节,把 IV 放在密文前面或协议指定字段里传输,接收方先拆 IV 再解密。

4.3 现象:CFB/OFB 解密结果整体错位

现象是第一块可能对,从第二块开始整体错位,或者只有前几个字节对。原因通常是三个:IV 不一致、CFB 段大小不一致、反馈方向写反。CFB-64 一次处理 8 字节,寄存器整体替换;CFB-8 一次处理 1 字节,寄存器左移 1 字节再填入密文。对接方说“CFB”但没说是 CFB-64 还是 CFB-8,你按 64 写,对方按 8 解,结果就是错位。

排查时先把cfbSegmentBits打印出来,再用固定 IV 和全零明文跑一组:全零明文下,CFB 的密文就是密钥流,直接和对方给的密钥流比对。OFB 类似,全零明文下密文等于 OFB 输出流,错位一眼就能看出来。解决方法是把段大小做成显式参数,不要写死在循环里;CFB-8 的寄存器移位要用memmove或手动循环,注意不要用memcpy处理重叠区域。

// CFB-8 的反馈:处理 1 字节后寄存器左移,末字节填密文 void Cfb8Feedback(BYTE reg[8], BYTE cipherByte) { memmove(reg, reg + 1, 7); reg[7] = cipherByte; }

reg是 8 字节寄存器,cipherByte是当前密文字节。加密和解密都要调用这个反馈,但解密时填的是收到的密文,不是计算出的明文。如果源码里解密反馈填了明文,第一字节可能对,后续全错。

4.4 现象:CTR 计数器溢出后前 16 字节正常后面全错

现象是短报文正常,长报文从某个位置开始全错,而且错误位置很规律。原因通常是计数器只递增了低 32 位,低 32 位溢出后没有向高位进位,或者计数器字节序反了。CTR 的计数器是一个大整数,DES 分组只有 8 字节,所以计数器通常是 8 字节大端;每加密一块,从最低字节开始加一,进位向高位传。如果只写counter[7]++,加到 255 就回绕,后面的密钥流和对方完全不一致。

// 8 字节大端计数器加一,处理进位 void IncrementBigEndian(BYTE* ctr, int n) { for (int i = n - 1; i >= 0; --i) { ctr[i] = (BYTE)(ctr[i] + 1); if (ctr[i] != 0) break; // 没有溢出,停止进位 } }

n是计数器字节数,通常是 8;ctr从高位到低位存放。这个写法在最低字节0xFF时会变成0x00并继续向高位进位,和普通整数加法一致。对接方如果用的计数器不是从 0 开始,而是 nonce 加序号,你要确认 nonce 占几个字节、序号占几个字节、序号是大端还是小端。最稳妥的办法是让对方给一组“计数器初值 + 前两块密钥流”,你本地比对,比猜字节序快得多。

4.5 现象:Release 正常 Debug 崩溃

现象是 Release 跑得好好的,Debug 一跑就崩,或者反过来。原因通常是未初始化缓冲区、数组越界、CString临时对象生命周期、MFC 断言。比如BYTE iv[8];没有初始化就直接传给 CBC,Debug 下未初始化内存被填充成0xCC,Release 下碰巧是零,结果表现不一致。再比如用new BYTE[len]分配输出缓冲区,长度算少了一个填充块,Release 下越界写不崩,Debug 下堆检查直接报错。

解决方法是把所有缓冲区改成std::vector<BYTE>,长度按“输入长度 + 8”预留;所有 IV、计数器、寄存器在声明时初始化为零;不要在 Debug 下依赖未初始化内存的“碰巧正确”。MFC 对话框里如果用了临时CString取GetBuffer(),记得成对调用ReleaseBuffer(),或者干脆转成std::vector<BYTE>再传下去。

// 输出缓冲区按最坏情况预留:输入长度 + 一个填充块 std::vector<BYTE> out; out.resize(in.size() + 8);

in.size() + 8是分组模式的上界,PKCS#7 最多多出一个 8 字节块;流式模式不会超过这个上界。真正写入时用out.resize(actualLen)截断,不要直接把out.data()传给加密函数却不告诉它容量。Debug 崩溃很多时候不是算法问题,而是缓冲区边界问题,先把这部分收紧,再谈模式对不对。

5. 接口对接与验证:用固定向量和边界用例把黑匣子撬开

对接加密协议时,最怕对方只给一句“DES/CBC/PKCS7”。密钥怎么给、IV 放哪、编码是 UTF-8 还是 GBK、CFB 段大小是多少、CTR 计数器几字节,这些参数少一个都可能让你调一下午。把这份源码当黑匣子用之前,先建立一套自己的验证流程:固定向量做回归、边界用例查填充、参数清单对齐对接方。

5.1 测试向量怎么选:自洽回归与跨实现比对

网上能找到不少 DES 的十六进制向量,但很多没有注明模式、填充和 IV,直接拿来比对很容易误判。我的做法是分两层:第一层做自洽回归,固定 key、固定 IV、固定明文,加密再解密,检查能否回到原文;第二层做跨实现比对,用另一套独立实现加密同一组数据,逐字节比对密文。第一层能抓住填充和缓冲区错误,第二层能抓住字节序、S 盒、模式状态错误。

// 自洽回归:同一组参数加密再解密,必须回到原文 bool RoundTrip(DesMode mode, const std::vector<BYTE>& plain, CString& err) { DesKey8 key; const BYTE k[8] = {0x13, 0x34, 0x57, 0x79, 0x9B, 0xBC, 0xDF, 0xF1}; memcpy(key.k, k, 8); BYTE iv[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; DesPadding pad = (mode == DesMode::ECB || mode == DesMode::CBC) ? DesPadding::PKCS7 : DesPadding::None; std::vector<BYTE> cipher, back; if (!DesEncrypt(mode, key, iv, plain, cipher, pad, err)) return false; if (!DesDecrypt(mode, key, iv, cipher, back, pad, err)) return false; return back == plain; }

key和iv是固定测试值,不要在生产代码里复用;pad根据模式选择,ECB/CBC 用 PKCS#7,流式模式用None。RoundTrip只验证“自己能解自己”,不能证明你和对接方一致,所以第二层比对不能省。跨实现比对时,先比第一块密文,再比第二块;第一块就错,优先查密钥、IV、字节序;第一块对第二块错,优先查模式状态更新,比如 CBC 的chain有没有更新成

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询