AES加密模式演进:从ECB到GCM的C#实践指南

做了五年半的开发者,我碰过最多的加密需求就是 AES。从最初的 ECB 一行代码完事,到后来被安全扫描打回重写,再到在现代 .NET 生态里老老实实用 GCM,这条路几乎是每个做数据安全的人都要走一遍的。这篇就围绕 AES 加密模式演进这条主线,把我这几年在 C# 里从 ECB、CBC 到 GCM 的实操经验、踩坑记录和代码样板一次整理出来。适合刚接手加密需求的后端开发,也适合被审计要求搞到头大、准备从旧模式迁移到认证加密的同学。

1. 加密模式演进背后的逻辑:为什么 ECB、CBC 最终都要走向 GCM

1.1 分组密码与工作模式:先搞懂 AES 到底加密了什么

AES 本身属于分组密码,一次只能处理固定长度的数据块,AES 的分组长度固定是 16 字节。无论要加密的是用户密码、身份证号,还是一整份 JSON 文件,实际使用时都要先把数据切成一个个 16 字节的块,再逐块交给 AES 处理。

分组密码原生的操作单元只有“一个块”,但真实业务里没有人只加密 16 字节。于是就有了工作模式(Mode of Operation),它决定了多个分组怎么串联、怎么加盐、怎么防止同一份明文在不同位置产生相同密文。可以把 AES 当成一把质量很好的锁,工作模式则是“用这把锁锁一排柜子”的方式。ECB 是每个柜子都单独锁,锁法完全一样;CBC 是锁完上一个柜子后,结果会影响下一个柜子的锁法;GCM 则在锁柜子的同时,给整排柜子加了一道封条,谁动过都会留痕。

AES 的密钥长度还会影响轮数:128 位密钥的 AES-128 是 10 轮,192 位是 12 轮,256 位是 14 轮。这里不展开数学细节,实践上默认选 AES-256。至于工作模式,绝大部分新代码应该直接考虑认证加密,也就是 AEAD,GCM 就是目前最常见的 AEAD 选择。

1.2 ECB 模式:简单到让人放松警惕,也危险到不该再碰

ECB 的工作方式最直白:每个 16 字节明文块独立使用相同的密钥加密,块与块之间没有任何关联。加密阶段可以完全并行,解密也是,代码写起来一行就能搞定。

但问题恰好出在这个“独立”上。既然每个块都用同一个密钥独立加密,那么同样的明文块,在任意位置加密后都会得到一模一样的密文块。这不是数学故障,而是模式本身保留了大量明文统计特征。我看过一个非常直观的测试:一张大片纯色块的图片用 ECB 加密后,视觉轮廓依然清晰可辨。换成一段结构固定的业务报文,重复的头部字段照样会暴露。

C# 里实现 ECB 非常简单:

using System.Security.Cryptography; byte[] key = RandomNumberGenerator.GetBytes(32); byte[] plaintext = Encoding.UTF8.GetBytes("这段内容不应保留统计特征"); using var aes = Aes.Create(); aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.PKCS7; aes.Key = key; using var encryptor = aes.CreateEncryptor(); byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);

ECB 连 IV 都不需要,因为没有“链”。看起来少了参数、少了出错机会,实际上是把最重要的随机性丢掉了。任何生产系统都不应该把 ECB 作为默认选项。如果只是本地测试算法链路,可以用;一旦数据会跨网络传输、会落库、会涉及多用户,请直接绕开它。

1.3 CBC 模式:链式反馈虽然解决了重复块,却不解决完整性

CBC 引入了一条链:每个明文块在加密之前,先与上一个密文块做异或,然后再交给 AES。第一个明文块没有“上一个块”,于是引入一个初始向量 IV。

因为有这条链,相同的明文块在不同位置会得到不同的密文块,ECB 的重复模式问题被解决了。CBC 解密则是反向操作:先解出当前块,再与上一个密文块异或,因此解密天然支持并行,加密则在链上串行。这一点在后面对比性能时非常重要。

CBC 的隐患在于:它只提供保密性,不提供完整性。攻击者可以修改第 N 个密文块,从而让第 N+1 个明文块产生可控变化。配合某些语言里解密时的 padding 报错,甚至能发展出 padding oracle 攻击,逐字节还原出明文。所以裸 CBC 根本不够,真正的生产方案应该是 CBC + HMAC,也就是先加密,再对密文做消息认证码。

用 C# 写 CBC 也很常规:

byte[] key = RandomNumberGenerator.GetBytes(32); byte[] iv = RandomNumberGenerator.GetBytes(16); byte[] plaintext = Encoding.UTF8.GetBytes("CBC 示例数据"); using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = key; aes.IV = iv; using var encryptor = aes.CreateEncryptor(); byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);

这段代码本身没问题,但留着“裸 CBC”在项目里,就像给门上了锁却把钥匙挂在门框上。正常做法是在外面再套 HMAC,而“套 HMAC”这件事,恰恰是团队最容易做错的地方。

1.4 GCM 模式:把加密和认证放到同一个框架里

GCM(Galois/Counter Mode)是把 CTR 模式加密和 GHASH 认证组合在一起。先由 nonce 生成初始计数器,通过 AES 加密计数器得到密钥流,明文与密钥流异或得到密文;与此同时,GHASH 在伽罗华域上对密文和附加数据 AAD 计算认证标签。最终输出的包除了密文,还会多出一段固定长度的 tag。

GCM 最关键的特点是同时解决保密性、完整性和真实性。解密时会重新计算 tag 并与随包携带的 tag 比对,一旦密文、nonce、AAD 任何一个字节被篡改,验证立刻失败,不会给你任何“部分解密结果”。这就是认证加密 AEAD 带来的好处:把“加密”和“防篡改”两件事合并成了一件事。

在 .NET 里,从 .NET Core 3.0 开始提供AesGcm类,到 .NET 5/6 之后 API 逐渐稳定。和手动组合 CBC + HMAC 相比,GCM 让普通开发者在默认情况下就能写出安全方案。但 GCM 也有一个绝对红线:nonce 绝不能复用。同一把密钥下,两次加密用了同一个 nonce,密钥流就会重复,攻击者能够通过异或关系直接破坏保密性,甚至进一步伪造认证标签。这一点我在 5.3 节还会再强调。

2. C# 落地 AES 实践:从 Aes 类到 AesGcm 类的关键差异

2.1 密钥、IV 与随机数生成:第一步最容易埋雷

不管是 ECB、CBC 还是 GCM,第一行代码几乎都是生成密钥、生成随机数。这里有一个很多人踩过的坑:用new Random()或Random.NextBytes生成密钥。Random不是密码学安全的随机数生成器,生成出的字节序列理论上可预测,一旦密钥被还原,整个加密体系直接失效。

正确做法是用RandomNumberGenerator.GetBytes:

byte[] key = RandomNumberGenerator.GetBytes(32); byte[] iv = RandomNumberGenerator.GetBytes(16); // CBC 用 byte[] nonce = RandomNumberGenerator.GetBytes(12); // GCM 用
  • 密钥长度:推荐 32 字节,也就是 AES-256。
  • CBC 的 IV:固定需要 16 字节,与 AES 分组等长。
  • GCM 的 nonce:标准推荐 12 字节,正好 96 位,这是 GCM 最推荐的长度。如果用 16 字节 nonce,要通过 GHASH 转换,存在些许额外风险,实践上没必要。

密钥不应该硬编码在源码里,更不应该提交到 Git。可以从环境变量、密钥管理服务或配置中心读取。开发环境可以用本地生成密钥,生产环境必须走可轮换的密钥管理通道。我们后面封装服务时,会直接让密钥从外部传入,而不是在类内部写死。

还有一个细节:IV 和 nonce 不需要保密,可以随着密文一起传输,但绝不能固定不变。固定 IV 会让 CBC 下相同明文始终生成相同密文,本质上是把 CBC 退化成了 ECB 的行为模式。

2.2 ECB 与 CBC 的 C# 实现:兼容旧系统的必经之路

虽然新系统应该用 GCM,但很多存量系统还在运行 CBC,甚至 ECB。为了兼容旧数据,你仍然需要知道怎么用Aes类写这些模式。核心就是设置Mode和Padding。

封装一个比较完整的 AES-CBC 加密方法:

public static byte[] EncryptCbc(byte[] plaintext, byte[] key, byte[] iv) { using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = key; aes.IV = iv; using var encryptor = aes.CreateEncryptor(); return encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length); } public static byte[] DecryptCbc(byte[] ciphertext, byte[] key, byte[] iv) { using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = key; aes.IV = iv; using var decryptor = aes.CreateDecryptor(); return decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length); }

这里有个值得解释的点:Aes.Create()返回实例后,默认模式其实已经就是 CBC,默认填充也是 PKCS7,但显式写出来更容易读,也避免后期有人误改成别的模式。

如果你还要兼容 ECB,只需要把aes.Mode = CipherMode.ECB,并且不设置IV。但请只在读取旧数据时使用,不要在写入路径上新增 ECB 数据。每次你在代码里看到CipherMode.ECB,都应该把它当成一个待偿还的技术债。

2.3 AesGcm 的正确用法:.NET 中真正推荐的模式

AesGcm的 API 和传统Aes类差别很大。它不是用一个TransformFinalBlock把数据“流式”处理完,而是把加密后的密文和认证标签分别放到两个输出缓冲区里。

基础用法:

using System.Security.Cryptography; using System.Text; byte[] key = Convert.FromBase64String("base64-encoded-key"); byte[] nonce = RandomNumberGenerator.GetBytes(12); byte[] plaintext = Encoding.UTF8.GetBytes("需要保护的内容"); using var aesGcm = new AesGcm(key, 16); byte[] ciphertext = new byte[plaintext.Length]; byte[] tag = new byte[16]; aesGcm.Encrypt(nonce, plaintext, ciphertext, tag); // 解密 byte[] decrypted = new byte[ciphertext.Length]; aesGcm.Decrypt(nonce, ciphertext, tag, decrypted);

注意几点:

  • AesGcm的第二个参数是 tag 长度,单位是字节。官方允许 12、13、14、15、16 字节,但实践中直接用 16 字节,也就是 128 位认证标签。
  • ciphertext的长度必须和plaintext完全一致,因为 GCM 本质是流式异或,密文长度等于明文长度。
  • Decrypt如果验证失败,会抛出AuthenticationTagMismatchException,而不返回错误码。生产封装时,通常会在这一层捕获并转换成业务返回值。
  • 如果要在 .NET Core 3.0 上运行,AesGcm构造函数只支持new AesGcm(key),默认 tag 就是 16 字节。从 .NET 5 开始才提供带tagSizeInBytes的构造函数。

2.4 CBC+HMAC 方案为什么我最终弃用了

有人会问:既然老系统已经在用 CBC,那我只要在外面加一层 HMAC,不也能实现认证吗?理论上可以,而且 encrypt-then-MAC 是一种经典安全结构。但组合方案里容易犯的错误非常多:

第一,加密密钥和 HMAC 密钥必须分离。直接用同一个密钥既做 AES 又做 HMAC,属于高危操作。正确的做法是通过 HKDF 从主密钥派生出两个独立子密钥,很多人直接忽略。

第二,HMAC 校验必须使用常量时间比较。用普通字符串比较或数组比较,可能被时间侧信道攻击,攻击者可以通过响应时间差异逐字节猜出 MAC。.NET 里应该用CryptographicOperations.FixedTimeEquals。

第三,HMAC 计算范围很容易漏。有些人只对密文做 HMAC,没有覆盖 IV 或关联元数据,攻击者可以替换 IV 或改变字段含义而不触发告警。

第四,组合方案是把“安全责任”分摊给写代码的人,而 GCM 把这些细节收进标准库里。同样的精力,放到密钥管理和 nonce 管理上,收益高得多。

所以我自己的结论是:新代码默认 GCM;CBC+HMAC 只作为兼容层存在,不为新功能引入。

3. 安全、性能与选型的横向对比:别再迷信“高级加密”

3.1 安全维度上的分水岭

从安全角度,这三种模式的差异是质的,不是量的。下面这张表可以贴在项目文档里:

模式保密性完整性典型风险推荐程度
ECB弱无相同明文产生相同密文,模式可被识别不应使用
CBC中无需要额外 HMAC,否则可篡改、可 padding oracle仅在旧系统
GCM强有nonce 复用会导致灾难新系统默认

关键在于“完整性”这一列。没有完整性保护时,攻击者不需要拿到密钥,只要拦截密文并修改,就可能造成业务逻辑被篡改。比如一份密文代表转账金额,CBC 下攻击者通过翻转某些位,可能把金额改成可控值。GCM 的 tag 就是用来拦截这种操作的。

GCM 的已知薄弱点比较集中:nonce 复用、AAD 使用错误、tag 被截断。这些问题都可以通过规范设计避免,整体风险比前两者低很多。

3.2 性能与并行化:GCM 并没有想象中的慢

很多人觉得 GCM 又是加密又是认证,肯定会比 CBC 慢。实际性能取决于硬件指令和并行方式。

  • ECB 加密并行度最高,因为块之间完全独立。
  • CBC 加密是串行的,因为每一块都要依赖上一块密文;但解密时可以并行。
  • CTR 模式的加密解密都可以并行,GCM 继承了这个优点,加密和解密都能预先生成密钥流。
  • GHASH 认证部分在现代 CPU 上有PCLMULQDQ指令支持,配合 AES-NI,GCM 的性能在实际项目中非常可观。

如果你做基准测试,在支持 AES-NI 的机器上,AES-GCM 通常不会成为性能瓶颈。真正的性能开销往往来自网络传输、数据库读写、序列化,而不是这十几字节的 tag 计算。所以选型时我一直坚持:先用安全把模式定下来,再考虑优化,不要反过来为了“少几毫秒”用 ECB。

3.3 场景选型建议:什么情况下我能接受 CBC,什么情况坚决不碰

根据我的实践,场景可以这样划分:

  • 新项目、新接口、新数据库表:一律 AES-GCM。
  • 需要和第三方系统对接,第三方只支持 CBC:可以接受 CBC,但必须在协议层做 HMAC 或签名,最好由对方提供明确的认证方案。
  • 老系统迁移:先兼容读旧数据,再逐步切到 GCM,不要一改了之。
  • 数据量极小比如 token、设备序列号:一样用 GCM,别因为数据“不敏感”就放松。
  • 合规审计或安全评审:直接选 GCM,并且把 nonce 策略写清楚,评审通过率明显更高。

ECB 我认为没有任何新业务场景应该使用。即使是加密随机值,ECB 也会暴露“相同随机值出现”的信息,这是多余的泄露。

4. 实操全过程:用 C# 封装一个健壮的 AES-GCM 加密工具

4.1 明确封包格式:为未来演进留下空间

真实项目里,我们不能只把密文返回给调用方,否则解密时 nonce、tag 无从说起。常见的做法是自定义一个封包格式,把密文、nonce、tag、版本信息一起打包。

我设计的封包格式如下:

字段长度说明
Magic2 字节固定标记,便于识别包类型
Version1 字节版本号,用于未来算法演进
Nonce12 字节GCM nonce
Tag16 字节GCM 认证标签
Ciphertext变长实际密文,长度与明文一致

把 Magic 和 Version 作为 AAD 传到AesGcm.Encrypt,解密时再校验它们。这样做的好处是:如果有人把密文包里的版本号从 0x01 改成 0x02,tag 校验就会失败,因为 AAD 变了。这能有效防止降级攻击和字段混淆。

4.2 完整代码实现与逐段解读

下面是我自己在项目中使用的封装版本,做了一些简化,但结构完整,可以直接抄:

using System; using System.Security.Cryptography; public sealed class AesGcmService : IDisposable { private const int NonceSize = 12; private const int TagSize = 16; private const int HeaderSize = 3; private static readonly byte[] Magic = { 0x01, 0x0A }; private const byte Version = 0x01; private readonly byte[] _key; private bool _disposed; public AesGcmService(byte[] key) { _key = key ?? throw new ArgumentNullException(nameof(key)); if (key.Length != 32) throw new ArgumentException("AES-256 需要 32 字节密钥", nameof(key)); } public static byte[] GenerateKey() => RandomNumberGenerator.GetBytes(32); public byte[] Encrypt(byte[] plaintext) { ThrowIfDisposed(); var nonce = RandomNumberGenerator.GetBytes(NonceSize); var ciphertext = new byte[plaintext.Length]; var tag = new byte[TagSize]; var header = BuildHeader(); using (var aes = new AesGcm(_key, TagSize)) { aes.Encrypt(nonce, plaintext, ciphertext, tag, header); } var result = new byte[HeaderSize + NonceSize + TagSize + ciphertext.Length]; Buffer.BlockCopy(header, 0, result, 0, header.Length); Buffer.BlockCopy(nonce, 0, result, HeaderSize, nonce.Length); Buffer.BlockCopy(tag, 0, result, HeaderSize + NonceSize, tag.Length); Buffer.BlockCopy(ciphertext, 0, result, HeaderSize + NonceSize + TagSize, ciphertext.Length); return result; } public byte[]? Decrypt(byte[] package) { ThrowIfDisposed(); if (package.Length < HeaderSize + NonceSize + TagSize) return null; var header = new byte[HeaderSize]; var nonce = new byte[NonceSize]; var tag = new byte[TagSize]; var ciphertextLength = package.Length - HeaderSize - NonceSize - TagSize; var ciphertext = new byte[ciphertextLength]; Buffer.BlockCopy(package, 0, header, 0, header.Length); Buffer.BlockCopy(package, HeaderSize, nonce, 0, nonce.Length); Buffer.BlockCopy(package, HeaderSize + NonceSize, tag, 0, tag.Length); Buffer.BlockCopy(package, HeaderSize + NonceSize + TagSize, ciphertext, 0, ciphertextLength); if (!CryptographicOperations.FixedTimeEquals(header, BuildHeader())) return null; var plaintext = new byte[ciphertextLength]; try { using var aes = new AesGcm(_key, TagSize); aes.Decrypt(nonce, ciphertext, tag, plaintext, header); } catch (AuthenticationTagMismatchException) { return null; } return plaintext; } private static byte[] BuildHeader() { return new byte[] { Magic[0], Magic[1], Version }; } private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(AesGcmService)); } public void Dispose() { if (!_disposed) { CryptographicOperations.ZeroMemory(_key); _disposed = true; } } }

这个类有几点值得展开:

  • 构造函数要求密钥长度必须是 32 字节,这是强制 AES-256,避免调用方传入短密钥而不自知。
  • Encrypt里每次都要重新生成 nonce,保证同一把密钥下几乎不可能复用。
  • Decrypt的返回值是byte[]?,当包格式不对或认证失败时返回null,业务层不用手工捕获细粒度异常。
  • CryptographicOperations.FixedTimeEquals用来比较 header,防止时间侧信道。header 是公开字段,这个比较不是必需的,但养成用常量时间比较的习惯没坏处。
  • Dispose里清空密钥字节。虽然托管内存里可能还有残留副本,但至少能降低被转储的风险。

使用示例:

byte[] key = AesGcmService.GenerateKey(); using var service = new AesGcmService(key); byte[] encrypted = service.Encrypt(Encoding.UTF8.GetBytes("你好,GCM")); byte[] decrypted = service.Decrypt(encrypted); string text = Encoding.UTF8.GetString(decrypted!);

4.3 单元测试与兼容性验证思路

封装完之后,不要急着接入业务,先写几个针对性测试:

  • 加密再解密,得到的内容必须与原文完全一致。
  • 对密文任意翻转一个字节,解密必须返回null,而不是抛异常或返回乱码。
  • 手动修改包里的版本字节,解密必须返回null,因为 header 是 AAD。
  • 对同一段明文连续加密两次,生成的封包必须不同,因为 nonce 不同。
  • 空明文也要测:Encrypt(Array.Empty<byte>())应该返回长度为 31 的封包,解密后得到空数组。
  • 长明文要测:比如 1MB 随机字节,确保不会因为缓冲区长度问题崩溃。

兼容性验证上,要注意运行框架。AesGcm不是 .NET Framework 4.8 的标准 API,如果项目还在老框架上,需要用 BouncyCastle 等第三方库实现 GCM。迁移的时候不要直接把代码复制过去,要单独做一轮跨平台加解密验证,确认你在 .NET 8 里加密的包,能被 BouncyCastle 正确解开;反过来也一样。跨语言对接时尤其要确认 nonce、tag 的拼接顺序,不同库返回的顺序可能有差异。

5. 常见问题与排查技巧实录

5.1 Padding 模式与块对齐:为什么明文同样长度密文却不同

GCM 模式下不需要设置 Padding,因为密文长度等于明文长度,任何多余字节都会破坏解密。而使用 ECB/CBC 的TransformFinalBlock时,默认 PKCS7 会在最后一个块补齐到 16 字节。如果明文恰好是 16 的倍数,PKCS7 也会额外追加一个完整的 16 字节填充块。这不是 bug,是为了解密时能明确判断填充长度。所以有些场景下,同样 16 字节明文,CBC 加密后得到 32 字节,初学者会觉得奇怪。

调试时如果发现 GCM 封包长度比预期多出 28 字节,先算账:3 字节 header + 12 字节 nonce + 16 字节 tag = 31 字节。如果密文长度还是多出 31,说明确实是 GCM 封包;如果只是多出 16,可能是误用了 CBC 的 PKCS7。

5.2 解密乱码的排查:IV 保存与传递问题

我遇到过不少“加密没问题、解密乱码”的情况,最后定位基本都是 IV 或 nonce 保存错了。比如加密时随机生成了 nonce,但只把密文存进数据库,解密时却用了一个全零 nonce。GCM 虽然不会因为 nonce 错误而抛异常,但解密结果会完全不同。

排查建议:

  • 确认 nonce 是否在加密时被放入封包,并原样取出。
  • 确认没有做双重编码:比如先对密文做 Base64,再整体拼接,再用 Base64,导致切分错位。
  • 确认Buffer.BlockCopy的偏移量正确,尤其是封包里有 tag 时,tag 和密文的先后顺序容易搞错。
  • 用固定的测试向量验证:已知密钥、已知 nonce、已知明文,比对官方示例输出,确认算法链路没问题。

5.3 标签验证失败的几个诱因

AuthenticationTagMismatchException是所有 GCM 开发者迟早要面对的问题。它表示解密时重新计算的 tag 和传入的 tag 不一致。常见原因有:

  • 密文在传输或存储中被截断、改位。
  • 解密时传入的 AAD 和加密时不一致。比如加密时把版本号作为 AAD,但解密时没有传,或者后面对应不上。
  • nonce 不一致。同一个封包,加密时用 nonce A,解密时用了 nonce B。
  • tag 切分位置不对。从封包取 tag 时,偏移量少算或多算了一个字节。
  • 密钥不一致。换过密钥但旧数据还用旧密钥解,必须按照版本进行密钥路由。
  • tag 长度不匹配。某些库默认 16 字节 tag,你自己把 tag 存储为 12 字节,解密时长度不对。

排查时不要只盯着“是不是网络传输坏了”,先用Decrypt方法分别尝试“篡改密文”“篡改 AAD”“篡改 nonce”三个实验,确认哪一类变化触发了失败,往往很快就能定位到逻辑错误。

5.4 从旧系统迁移到 GCM 的经验

迁移旧系统比新写一个 GCM 工具麻烦得多。我的经验是分四步走:

第一,兼容读旧数据。上线后先保证代码能解密历史数据,旧 ECB/CBC 格式继续保留,新写入的数据一律用 GCM。可以靠封包里的 Magic 或 Version 区分格式。

第二,数据库字段扩容。GCM 的封包长度是明文长度加 31,而 CBC 密文长度基本是明文长度按 16 字节填充后再加一个可能存在的块,两者不相等。如果原字段长度是固定varbinary(256),要先改成varbinary(max)或至少varbinary(512),否则新数据会写不进去。

第三,尽量在迁移期间轮换密钥。GCM 本身很好,但如果继续沿用当年硬编码在代码里的老密钥,安全性提升有限。上线迁移时,同时切换新的密钥管理方案。

第四,对另一端的解析方同步升级。如果密文要发给第三方系统,一定提前确认对方是否支持 AEAD,以及对方封包格式里的 nonce、tag 顺序是否需要做转换。我见过两个系统各自实现了 GCM,但一个把 nonce 放在最前面,另一个把 tag 放在最前面,结果联调时各种不匹配。

我个人踩过几次坑之后,最大的体会是加密方案真不在于炫技,而在于让错误发生的概率降到最低。GCM 没有把所有问题解决掉,但它把认证功能内置,让普通开发者不会因为组合 HMAC 时顺序错漏而埋下隐患。如果你手头还有 ECB 或者裸 CBC 的代码,别等安全扫描来提醒,今天就可以按文中的方案重构一版。哪怕只是把解密部分先兼容好,也是向前迈了一大步。

相关新闻

Spring Boot电子病历系统毕设:数据库建模与权限控制实战解析

Spring Boot电子病历系统毕设:数据库建模与权限控制实战解析

每次有同学拿医院信息系统的选题来找我&#xff0c;我先问一句&#xff1a;你是把它当一个“管理后台”做&#xff0c;还是当一个“医疗业务系统”做&#xff1f;这个回答基本决定了作品最后能拿多少分。Spring Boot 电子病历管理系统&#xff08;毕业设计源码编号23540&#x…

2026/10/20 16:18:47 阅读更多 →
AI去杂物后为何总不自然?纹理光影语义三层拆解与修复指南

AI去杂物后为何总不自然?纹理光影语义三层拆解与修复指南

1. 问题到底出在哪&#xff1a;从一张“修完更假”的照片说起 你肯定遇到过这种情况&#xff1a;用AI工具把照片里乱入的路人、垃圾桶、电线杆抹掉&#xff0c;乍一看挺干净&#xff0c;但放大一瞧&#xff0c;那块区域总像贴了块膏药——颜色对但纹理糊&#xff0c;边缘对但光…

2026/10/20 16:18:47 阅读更多 →
Unity安卓APK版本更新完整方案:检测、下载、MD5校验与安装适配

Unity安卓APK版本更新完整方案:检测、下载、MD5校验与安装适配

做Unity安卓项目&#xff0c;最躲不开的东西就是“更新”。无论是修复线上崩溃、上线新玩法&#xff0c;还是替换过期的配置文件&#xff0c;只要应用发出去&#xff0c;官方商店审核的节奏往往是跟着版本周期走的&#xff0c;自有渠道或者测试包反而等不起。直接让玩家点链接下…

2026/10/20 16:18:47 阅读更多 →

最新新闻

大模型入门必读:从深度学习原理到工程实战的系统书单

大模型入门必读:从深度学习原理到工程实战的系统书单

最近这半年&#xff0c;总有人在后台问我&#xff1a;“我想入门大模型&#xff0c;该看什么书&#xff1f;”老实说&#xff0c;每次被问到这个问题&#xff0c;我都挺为难的。现在网上关于大模型的书单五花八门&#xff0c;有讲算法的、有讲应用的、有讲产业趋势的&#xff0…

2026/10/20 17:54:48 阅读更多 →
拆解DeepSeek Agent框架:插件化架构的设计与实战踩坑记录

拆解DeepSeek Agent框架:插件化架构的设计与实战踩坑记录

我一直有个习惯&#xff1a;拿到一个开源项目&#xff0c;先不看 README 也不看官方博客&#xff0c;直接把仓库 clone 到本地&#xff0c;先翻目录结构、再读接口定义&#xff0c;最后才回去对照文档验证自己的猜想。这次拆 DeepSeek 开源的 Agent 框架&#xff0c;起初只是好…

2026/10/20 17:54:48 阅读更多 →
AI助理代登录的权限设计:Poppy草案的四层授权关卡与落地实践

AI助理代登录的权限设计:Poppy草案的四层授权关卡与落地实践

你有没有想过&#xff0c;有一天你的AI助理会替你登录各种账号&#xff1f;不是那种简单的“帮我查一下天气”&#xff0c;而是真的拿着你的身份&#xff0c;去另一个平台做事情——比如帮你把昨天的报销单提交到财务系统&#xff0c;帮你把一个长邮件回复整理好并发送出去&…

2026/10/20 17:54:48 阅读更多 →
腾讯云AI语音识别:自动生成会议纪要的实践与调优

腾讯云AI语音识别:自动生成会议纪要的实践与调优

我们每次开会一小时&#xff0c;纪要却要整理半小时&#xff0c;这句话我从不止一个团队嘴里听到过。会议内容总要有人记&#xff0c;记完还要提炼待办、确认负责人&#xff0c;稍微复杂一点的讨论&#xff0c;光靠人工回忆很容易漏掉关键信息。所以这两年我一直建议有条件的团…

2026/10/20 17:54:48 阅读更多 →
Kubernetes资源清单实战:从YAML编写到Pod、Service与ConfigMap

Kubernetes资源清单实战:从YAML编写到Pod、Service与ConfigMap

1. 资源清单是什么&#xff0c;为什么是Kubernetes的第一道门槛很多刚接触Kubernetes的开发者&#xff0c;第一个卡住的地方往往不是Pod、Deployment这些概念本身&#xff0c;而是搞不懂"资源清单"到底是个什么东西、为什么要写这么一长串YAML。我最早学K8s的时候也踩…

2026/10/20 17:54:48 阅读更多 →
Qt课程进度导航栏G03:从数据模型到自绘渲染的完整实践

Qt课程进度导航栏G03:从数据模型到自绘渲染的完整实践

做教育类桌面客户端的时候&#xff0c;课程导航一直是个绕不开的活儿。用户要在一个窗口里看到章、节、课时的层级关系&#xff0c;要清楚自己学到哪了&#xff0c;还得能一键跳回上次退出的位置。市面上的现成组件大多只解决“展示目录”这一层&#xff0c;进度状态、学习记忆…

2026/10/20 17:53:47 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/20 9:50:45 阅读更多 →