news 2026/10/14 1:03:47

AES-128 CMAC完整实现:从原理到C#代码实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AES-128 CMAC完整实现:从原理到C#代码实战

做嵌入式固件升级、金融终端报文对接、或者车机ECU通信的时候,经常会遇到一个绕不开的需求:报文完整性校验。AES加密大家都能写,但加密和防篡改是两码事——密文被攻击者翻转几个比特,解密端解出来的业务字段就完全变了,没有校验机制根本发现不了。所以很多协议会在数据帧尾部附加一个认证标签,用密钥对整个报文算一个固定长度的MAC值,接收方先验证MAC再处理数据。AES-128 CMAC就是这类场景里的经典算法,它基于AES-128分组密码,不依赖额外的哈希模块,在只有AES加密芯片的硬件上也能直接跑。这篇文章把我在实际项目里从零实现AES-128 CMAC的完整过程写出来,包括算法原理、C#实现代码、标准向量验证和踩过的坑,适合所有需要手写消息认证算法的读者参考。

1. AES加密解决不了防篡改,CMAC补的是什么

1.1 加密只保证机密性,不保证完整性

很多刚接触密码学的开发者有个误区:数据被AES加密过了,别人看不到明文,那安全就够了。可实际场景里,“防止被读”和“防止被改”是两个独立的安全目标。打个比方,你把重要文件放进保险柜,保险柜能防止别人偷看,但没法防止别人在柜门上喷漆、贴纸条。密文也一样,攻击者不需要知道密钥,他只要动过密文中的某些字节,接收方解密后就会得到一份被破坏或者被替换过的明文——如果系统没有校验,这份坏数据可能直接被当成正常业务处理。

消息认证码MAC(Message Authentication Code)解决的就是这个问题。发送方用密钥K对消息M计算出一个定长的认证标签,然后发送(M, MAC);接收方拿到数据后,用同一个密钥对M重新计算MAC,比对是否一致。一致说明数据没有被改动过,也说明数据确实来自持有密钥的那一方。MAC有很多种构造方式,常见的有基于哈希函数的HMAC,以及基于分组密码的CMAC。后者全称是Cipher-based Message Authentication Code,标准文档里有一个更老的别名叫OMAC,是2003年前后对之前一系列CBC-MAC改进方案整合后的最终版本。随着标准公开,它成了很多汽车总线、传感器网络、金融协议里的默认认证算法。

当底层分组密码选AES-128时,输入密钥16字节,输出认证标签也是16字节(128位),这个组合就是AES-128 CMAC。它和AES加解密共享同一个分组密码内核,所以只要系统里已经有AES硬件加速模块,计算CMAC几乎不增加额外成本,这是它在嵌入式环境里特别受欢迎的根本原因。

1.2 朴素CBC-MAC的最大问题是长度扩展攻击

CMAC不是凭空发明的。最朴素的“用分组密码构造MAC”思路,就是直接把消息丢进CBC模式:设C_0全零,然后对每个消息块依次执行C_i = AES_K(C_{i-1} XOR M_i),最后取C_n作为MAC。这就是流传很广的CBC-MAC。

CBC-MAC对固定长度消息是正确且安全的,但一旦消息长度可变,立刻会爆出漏洞,典型的是长度扩展攻击。攻击者拿到一条消息M和它的MAC值T之后,不需要知道密钥,就可以构造出一条更长的消息M',让它也拥有合法的MAC。原理说起来也不复杂:上一次计算得到的T,恰好会成为下一块消息进入CBC链时的“初始向量”。攻击者只要把T当作新消息开头异或的对象,就能继续往后拼数据,最终伪造出一个全新的、看起来完全合法的消息和标签对。这在现实中意味着,接收方完全无法区分“发送方确实发出过这条更长消息”和“攻击者篡改拼接出来的假消息”。

CMAC的修补思路非常简洁:在最后一块消息进入最后一轮加密之前,根据最后一块是否满块,给它异或一个不同的子密钥K1或K2。K1和K2由主密钥派生,攻击者不知道主密钥,自然无法预先伪造出这种异或效果。这样一来,消息的分组边界和长度信息被“固化”进了MAC计算过程,长度扩展攻击的路就堵死了。后面能看到,真正实现的时候,这套方案只比普通CBC-MAC多了子密钥生成和最后一块的异或两步。

1.3 CMAC和HMAC怎么选

不少读者会问:要算MAC,我直接用HMAC-SHA256不就好了?确实,在通用开发环境里HMAC更常见,因为相关代码样本多、库更普及。但CMAC有它不可替代的位置,尤其在硬件环境里。我把两者的差异整理了一张表:

对比维度CMAC(AES-128)HMAC-SHA256
底层原语AES分组密码SHA-256哈希函数
密钥长度16字节固定任意长度,常用32字节
硬件适配大量芯片有AES加速,速度极快很多MCU没有哈希加速
输出标签长度128位,可截断256位,可截断
依赖模块只需要AES加解密需要SHA-256实现
典型场景汽车总线、传感器网络、金融报文API签名、Token、JWT

我的选择经验是:如果系统里已经有AES模块,或者运行环境是资源受限的单片机,优先用CMAC;如果做的是互联网API、Token这类业务,身边也已经有成熟的哈希库,用HMAC就行。两者安全性都经过充分验证,没有谁绝对优于谁,关键是场景适配。CMAC输出的128位标签虽然比HMAC-SHA256短,但对绝大多数防篡改场景已经足够,截断到96位也是常见的做法。

2. CMAC的核心机制:子密钥K1/K2与末端块处理

2.1 从零块加密到K1、K2的完整派生过程

CMAC的第一步不是处理消息,而是派生子密钥。标准做法是:先构造一个全零的128位块Z,用主密钥K对它做一次AES加密,得到中间值L:

L = AES_K(Z)

然后根据L的最高位(最左侧那个比特)决定左移后是否异或常数0x87。规则如下:

  • 如果L的最高位是0:K1 = L << 1
  • 如果L的最高位是1:K1 = (L << 1) XOR 0x87

K2再用同样的规则从K1派生出来:

  • 如果K1的最高位是0:K2 = K1 << 1
  • 如果K1的最高位是1:K2 = (K1 << 1) XOR 0x87

这里的“<< 1”是对整个128位二进制数按大端位序左移一位。很多初学者会把0x87误当成一个随便选的魔法常数,其实它是一个有明确数学背景的固定值:底层二进制域GF(2^128)中不可约多项式x^128 + x^7 + x^6 + x + 1的低8位后缀。左移一位相当于多项式乘以x,最高位溢出后,通过异或0x87把溢出部分“折叠”回128位内部。理解这一点后,你就不会记错为什么常数是0x87而不是别的,也不会在实现时把判断条件搞反。

整个推导过程只依赖一次AES加密,所以开销非常小。实际项目里,如果使用同一个密钥计算大量消息的CMAC,完全可以把K1和K2提前算好缓存,每次调用时直接使用,这是最直观也最有效的优化手段。

2.2 完整块与不完整块的分叉规则

子密钥生成之后才是消息处理阶段。假设消息M被切成n个16字节块M_1到M_n,分叉规则集中在最后一块:

  • 如果最后一块恰好16字节(完整块):M_n' = M_n XOR K1,不需要补位。
  • 如果最后一块不足16字节(包括空消息):先在消息末尾补一个比特“1”,字节层面就是0x80,再继续补0x00直到凑满16字节,然后 M_n' = 补位后的块 XOR K2。

这里最容易被忽略的是空消息。消息长度为0时,很多实现容易进入“长度是16的倍数”这个分支,这是错的。标准明确规定,空消息被当作一个特殊的不足一块的消息处理——补出来一整块“0x80 00 00 … 00”,然后异或K2再加密。之所以这样设计,就是为了让“长度恰好为16整数倍”和“长度不足16字节”这两类消息在最后一轮使用不同的子密钥,从而防止攻击者利用块边界做文章。

前N-1块的处理则是普通的CBC链,没有任何特殊之处。整个过程可以概括为:普通块走标准CBC迭代,最终块引入K1或K2做差异化处理,然后做最后一轮AES加密。这个设计既保留了CBC-MAC的计算效率,又解决了变长消息的安全问题。

2.3 一段伪代码看懂整体流程

把上面的文字描述压缩成一段伪代码,整个算法结构一目了然:

L = AES_K(0^128) K1 = 根据L最高位左移并可能异或0x87 K2 = 根据K1最高位左移并可能异或0x87 C = 0^128 for each 非最后一个块 M_i: C = AES_K(C XOR M_i) if len(last_block) == 16: last_block = last_block XOR K1 else: last_block = pad(last_block) XOR K2 // 补0x80和0x00 MAC = AES_K(C XOR last_block)

伪代码展示了一个关键事实:CMAC前99%的计算都只是普通CBC迭代。所以在C#里实现时,根本不需要真的创建CBC模式,只要拿到ECB模式下的单块加密能力,逐块迭代即可。这也让代码量和出错概率都大大降低。

3. 用C#从零搭建AES-128 CMAC实现

3.1 两个必须写对的底层工具函数

实现CMAC之前,先写两个底层操作:128位字节数组的异或,以及大端左移一位。异或很直接:

private static void XorInPlace(byte[] target, byte[] source) { for (var i = 0; i < 16; i++) target[i] ^= source[i]; }

需要注意,每次异或的对象都是16字节定长块,所以循环16次足够。另一个工具方法是左移一位,这是整个实现里最容易写错的地方:

private static byte[] LeftShiftOneBit(byte[] block) { var result = new byte[16]; var carry = 0; for (var i = 0; i < 16; i++) { var b = block[i]; result[i] = (byte)((b << 1) | carry); carry = (b & 0x80) >> 7; } return result; }

我之所以从索引0往索引15遍历,是因为AES标准把字节数组视为大端序:索引0对应128位数值的最高有效字节。左移一位等价于整个数乘以2,也就是每个字节内部左移,同时把前一个更高位字节溢出的最高位,作为当前字节的最低位。carry这个变量承担的就是“把上一字节溢出位搬到下一字节最低位”的职责。循环结束后,最左侧字节的最高位自然丢弃,如果它是1,就由后续的子密钥生成函数决定是否异或0x87回收。

3.2 子密钥生成:加密零块后分两支

有了左移和异或函数,K1和K2的生成代码就非常简短:

private static (byte[], byte[]) GenerateSubkeys(ICryptoTransform encryptor) { var zeroBlock = new byte[16]; var l = EncryptSingleBlock(encryptor, zeroBlock); var k1 = LeftShiftOneBit(l); if ((l[0] & 0x80) == 0x80) k1[15] ^= 0x87; var k2 = LeftShiftOneBit(k1); if ((k1[0] & 0x80) == 0x80) k2[15] ^= 0x87; return (k1, k2); }

判断最高位只需要检查字节数组下标0的最高位,也就是l[0] & 0x80。为什么异或0x87只作用在最后一个字节上?因为0x87作为常数出现在128位数的最低位区间,对应字节数组里的最低有效字节。如果你喜欢,也可以把0x87展开成完整的16字节数组再做异或,效果完全一样,但k[15] ^= 0x87更简洁,也更容易看出代码意图。

这里的EncryptSingleBlock负责单块AES加密。它依赖.NET自带的Aes类,但必须设置成ECB模式并关闭填充。具体设置我会在下一节完整源码里给出,这也是最容易踩坑的地方之一。

3.3 主循环和最终块处理逻辑

核心的ComputeMac方法可以这样设计:函数签名为输入16字节密钥和任意长度消息,输出16字节CMAC标签。代码的关键在于正确处理“到底哪些块走普通CBC链,哪个块走最终分支”:

public static byte[] ComputeMac(byte[] key, byte[] message) { if (key == null || key.Length != 16) throw new ArgumentException("AES-128 CMAC要求16字节密钥。", nameof(key)); message ??= Array.Empty<byte>(); using var aes = Aes.Create(); aes.Key = key; aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.None; using var encryptor = aes.CreateEncryptor(); var (k1, k2) = GenerateSubkeys(encryptor); var blockCount = message.Length / 16; var remainder = message.Length % 16; var state = new byte[16]; var offset = 0; // 前缀块数量:如果最后是完整块,则留出最后一块;否则处理所有完整块 var prefixCount = (remainder == 0) ? Math.Max(0, blockCount - 1) : blockCount; for (var i = 0; i < prefixCount; i++) { for (var j = 0; j < 16; j++) state[j] ^= message[offset + j]; state = EncryptSingleBlock(encryptor, state); offset += 16; } byte[] finalBlock; if (remainder == 0 && blockCount > 0) { // 非空且最后一块完整:复制最后一块,异或K1 finalBlock = new byte[16]; Buffer.BlockCopy(message, offset, finalBlock, 0, 16); XorInPlace(finalBlock, k1); } else { // 空消息或最后一块不完整:补0x80和0x00,异或K2 finalBlock = new byte[16]; if (remainder > 0) Buffer.BlockCopy(message, offset, finalBlock, 0, remainder); finalBlock[remainder] = 0x80; XorInPlace(finalBlock, k2); } XorInPlace(state, finalBlock); return EncryptSingleBlock(encryptor, state); }

这段逻辑里最需要看明白的是prefixCount的计算。当消息长度恰好是16的整数倍且不为空时,所有块都是完整块,前N-1块走普通CBC链,最后一块异或K1后再进入最后一轮加密。当消息不是16的整数倍时,前N个完整块都走普通链,最后剩下的不完整块补位后异或K2。当消息为空时,preBlockCount为0,直接构造“0x80 00…00”异或K2。这三个分支一定要区分开,很多网上流传的错误实现就是在空消息上翻的车。

4. 最容易翻车的地方:字节序、补位和边界条件

4.1 大端左移和“看起来的左移”不是一回事

左移函数是整个实现里最反直觉的部分。我见过不少实现用从后往前遍历的方式来移位,结果算出来的K1、K2和标准完全对不上。核心原因在于,字节数组在AES标准语境下是大端序表示的:下标0是最高位字节。如果你把一个字节数组当成普通数组去做移位,比如先移动最后一个字节,再把进位往前面推,那实际上是在做小端左移,结果自然不对。

我给一个具体例子感受一下。假设128位块的最高字节是0x80,其余全是0x00,这个大端数字左移一位后,应该是0x01移到了第二字节的位置,第一字节变成0x00。正确的大端左移结果为 [0x00, 0x01, 0x00, 0x00, ...]。如果你从下标15往0反向处理,第一轮处理的是最低字节,等处理到最高字节时,进位方向完全反了,结果就变成另一个数。调试这类问题最好的方法是打印出L、K1、K2的十六进制值,和标准向量比对,几秒钟就能定位。

4.2 三种边界情况:空消息、单块消息、跨块消息

边界条件是CMAC实现里最容易埋雷的地方。我把三种典型情况列成表格:

消息长度最终块构造使用的子密钥
0字节(空消息)补出 0x80 00 00 … 00 一整块K2
大于0且为16的整数倍直接取最后一块K1
非16的整数倍复制剩余字节 + 0x80 + 补0x00K2

特别是第一条,空消息虽然形式上满足“长度为16的0倍”,但绝不能用K1分支。原因是标准设计时希望任何长度不同的消息都对应不同分组关系,空消息如果走完整块分支,那就没有任何比特被处理,MAC直接变成AES_K(0),既不符合语义也会带来安全隐患。实现完成后,测试用例里一定要覆盖空消息、16字节、17字节、32字节这四个长度,能跑通就说明边界处理基本过关。

4.3 密钥长度、并发安全和密钥复用的注意点

AES-128要求密钥固定16字节。有些协议会把密钥写成Hex字符串传递,调用前先转成字节数组,调完后不要留着原始字符串在日志里。密钥长度不对时,Aes对象创建本身就会抛异常,但为了给调用者更明确的反馈,建议在方法入口显式检查一次。

并发场景也要想清楚。如果直接写成一个工具类,内部保存了Aes实例和K1、K2,那多个线程同时调用ComputeMac就会共享加密器和可变状态,很容易出现间歇性错误。我上面的静态方法每次调用都新建Aes实例,天然线程安全。牺牲的是一点点性能,但换来的是可预测性。

另外,生产环境要注意密钥分离原则:同一把密钥不要既做AES加解密,又做CMAC计算。虽然CMAC算法本身经过了严谨的安全分析,但从密钥管理的角度,不同用途使用不同密钥是基本的安全卫生习惯,能避免很多交叉攻击场景。

5. 用标准向量和现有库交叉验证实现

5.1 第一组标准测试向量:空消息

写完算法后,第一件事是验证正确性。CMAC有一组公开的标准测试向量,密钥固定为:

Key = 2b7e151628aed2a6abf7158809cf4f3c

空消息的预期CMAC输出是:

MAC = bb1d6929e95937287fa37d129d756a35

我在本地跑验证代码时,直接用了一个Hex转换方法:

var key = Convert.FromHexString("2b7e151628aed2a6abf7158809cf4f3c"); var mac = Aes128CmacUtil.ComputeMac(key, Array.Empty<byte>()); Console.WriteLine(Convert.ToHexString(mac).ToLowerInvariant()); // 期望输出:bb1d6929e95937287fa37d129d756a35

如果你的输出不是这个值,问题基本集中在两个方面:要么是K1/K2生成有误,要么是空消息走了错误的子密钥分支。此时单独把L、K1、K2打印出来和标准对比,是最快的定位手段。

5.2 第二组测试向量:单块消息和跨块消息

空消息验证通过后,再测一个16字节消息,密钥不变:

Message = 6bc1bee22e409f96e93d7e117393ea04 MAC = 070a16b46b4d4144f79bdd9dd04a287c

这组向量专门验证“最后一个块恰好完整”的分支。接下来你可以自己构造一个40字节消息(比如把上面16字节重复两次再加上前8字节),预期输出可以从标准文档中查。我通常还会把消息补成48字节,让“最后一块完整、但消息很长”的情况也覆盖到。三个长度都通过,算法基本可以放心往后走。

5.3 用.NET内置AesCmac和OpenSSL交叉验证

如果你的开发环境是.NET 6或更高版本,还有更省事的交叉验证方式:直接用框架内置的AesCmac类对拍。测试代码非常简单:

using System.Security.Cryptography; var key = Convert.FromHexString("2b7e151628aed2a6abf7158809cf4f3c"); var message = Convert.FromHexString("6bc1bee22e409f96e93d7e117393ea04"); using var cmac = new AesCmac(key); var builtInTag = cmac.ComputeHash(message); var myTag = Aes128CmacUtil.ComputeMac(key, message); Console.WriteLine(Convert.ToHexString(builtInTag).ToLowerInvariant()); Console.WriteLine(Convert.ToHexString(myTag).ToLowerInvariant());

如果两边输出完全一致,说明实现没问题。注意类名可能和自己定义的工具类撞名,必要时用全限定名加以区分。在更早的.NET Framework环境里没有内置AesCmac,也没关系,可以用标准向量加一个跨语言命令来验证。如果你的机器有OpenSSL 3.x,也可以直接用命令行:

openssl mac -algorithm CMAC -cipher AES-128-CBC \ -hexkey 2b7e151628aed2a6abf7158809cf4f3c \ -in message.bin

这个命令里的-cipher AES-128-CBC只是OpenSSL命令行里指定底层AES分组密码的写法,实际算法是CMAC而不是CBC,不要被参数名迷惑。不同版本的OpenSSL语法略有差异,以openssl mac -help输出为准。生产数据不要拿到在线工具去算,本地跑标准向量足够。

6. 完整可复用的类源码与踩坑过程记录

6.1 可直接复制使用的完整类

下面是整理后的完整实现,静态工具类,线程安全,不依赖任何第三方包。代码里把单块加密用的ICryptoTransform对象复用了,避免每个块都创建加密器:

using System; using System.Security.Cryptography; public static class Aes128CmacUtil { public static byte[] ComputeMac(byte[] key, byte[] message) { if (key == null || key.Length != 16) throw new ArgumentException("AES-128 CMAC要求16字节密钥。", nameof(key)); message ??= Array.Empty<byte>(); using var aes = Aes.Create(); aes.Key = key; aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.None; using var encryptor = aes.CreateEncryptor(); var (k1, k2) = GenerateSubkeys(encryptor); var blockCount = message.Length / 16; var remainder = message.Length % 16; var state = new byte[16]; var offset = 0; var prefixCount = (remainder == 0) ? Math.Max(0, blockCount - 1) : blockCount; for (var i = 0; i < prefixCount; i++) { for (var j = 0; j < 16; j++) state[j] ^= message[offset + j]; state = EncryptSingleBlock(encryptor, state); offset += 16; } byte[] finalBlock; if (remainder == 0 && blockCount > 0) { finalBlock = new byte[16]; Buffer.BlockCopy(message, offset, finalBlock, 0, 16); XorInPlace(finalBlock, k1); } else { finalBlock = new byte[16]; if (remainder > 0) Buffer.BlockCopy(message, offset, finalBlock, 0, remainder); finalBlock[remainder] = 0x80; XorInPlace(finalBlock, k2); } XorInPlace(state, finalBlock); return EncryptSingleBlock(encryptor, state); } private static (byte[], byte[]) GenerateSubkeys(ICryptoTransform encryptor) { var zeroBlock = new byte[16]; var l = EncryptSingleBlock(encryptor, zeroBlock); var k1 = LeftShiftOneBit(l); if ((l[0] & 0x80) == 0x80) k1[15] ^= 0x87; var k2 = LeftShiftOneBit(k1); if ((k1[0] & 0x80) == 0x80) k2[15] ^= 0x87; return (k1, k2); } private static byte[] EncryptSingleBlock(ICryptoTransform encryptor, byte[] block) { return encryptor.TransformFinalBlock(block, 0, 16); } private static byte[] LeftShiftOneBit(byte[] block) { var result = new byte[16]; var carry = 0; for (var i = 0; i < 16; i++) { var b = block[i]; result[i] = (byte)((b << 1) | carry); carry = (b & 0x80) >> 7; } return result; } private static void XorInPlace(byte[] target, byte[] source) { for (var i = 0; i < 16; i++) target[i] ^= source[i]; } }

这段代码在.NET Core 3.1和.NET 6以上环境可以直接编译运行。如果项目还在.NET Framework,把using var换成传统的try/finally写法,Convert.FromHexString也需要替换成自定义的Hex解析方法,核心算法逻辑完全不受影响。

6.2 我踩过的三个坑

第一个坑是Padding默认值。Aes类默认的PaddingMode是PKCS7,也就是说你输入一个16字节块,它可能输出32字节的填充后密文。我最初写EncryptSingleBlock时没有设置aes.Padding = PaddingMode.None,结果每个加密结果都比预期长一倍,异或状态链很快就乱了。排查了半天才发现是填充设置的问题。设置成None之后,一切突然就顺了。

第二个坑是空消息的分支。当时我的代码先判断“消息长度是不是16的倍数”,是就走K1分支,这看起来合情合理,但空消息的blockCount为0,根本不存在“最后一块”。输出和标准向量一比,差了十万八千里。后来查文档才确认空消息必须走补位分支,也就是构造“0x80 00 … 00”一整块再异或K2。这个分支错一次就够你记一辈子。

第三个坑是判断最高位时看错了字节。子密钥生成规则里说“如果最高位为1”,我一开始理解为“最后一个字节的最低位”,用了l[15] & 0x01来判断,结果K1、K2全部生成反了。后来把L的十六进制打印出来,对照标准文档里的中间值,才发现自己把大端序理解成了小端序。AES语境下的“最高位”永远是字节数组第0字节的最高位,也就是l[0] & 0x80。

6.3 性能优化和协议使用建议

完整类已经为每个块复用了ICryptoTransform,这是第一层优化。如果你需要用同一个密钥计算大量消息,还可以把Aes实例、ICryptoTransform、K1、K2都缓存在一个对象里,计算时加锁复用,进一步减少对象创建开销。不过要注意加锁,否则多线程并发调用会出问题。

协议设计层面,有些协议不需要完整的128位CMAC标签,可能只需要前8字节或前12字节。CMAC标准允许截断输出,截断后的安全性取决于剩余长度。我的建议是:能保留全128位就保留全128位;如果因为报文长度限制不得不截断,至少保留96位(12字节),低于64位的截断强度在大多数场景下都不够可靠。

另外,计算CMAC的密钥和加密密钥一定要分开使用。很多协议只给了一把密钥AES Key,既做数据加密又做MAC计算,这在纯算法层面也许能工作,但从安全工程角度看属于偷懒做法。严谨的设计应该是两把独立密钥或者通过密钥派生函数推导出两个不同用途的子密钥。

最后分享一点个人体会。这个算法我在好几个项目里都写过,第一遍写代码大概花了一个小时,但调试花了大半天。回头看,最耗时间的不是AES调用,而是对“空消息到底走哪个分支”“大端左移从哪头开始”这类规范边缘细节的理解。如果你也准备自己实现CMAC,建议不要直接抄代码,先拿标准测试向量跑通空消息、单块、跨块三种用例,再根据自己业务的协议裁剪。算法本身不复杂,复杂的是边界条件,把边界条件处理对了,这个实现基本就不会再出问题。

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

Modbus TCP工业通讯程序实战:从连通到产线级稳定

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:02:51

Java数组内存机制全解析:从对象头到性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:02:50

Matlab与V-REP激光雷达2D动态建图仿真链路搭建指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:02:44

Shiro权限动态刷新:缓存一致性与实时生效实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:02:41

M4C-Hat NPU扩展板实战:Pulsar2工具链与边缘推理性能测试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华