首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >当加密ID需要变成Guid:为什么我选择了AES-CBC而非GCM?

当加密ID需要变成Guid:为什么我选择了AES-CBC而非GCM?

作者头像
郑子铭
发布2025-09-02 18:11:59
发布2025-09-02 18:11:59
10200
代码可运行
举报
运行总次数:0
代码可运行

在当代的密码学工程中,有一个非常主流的建议:“GCM 是现代加密的首选,应该优先考虑它,而不是像 CBC 这样的传统模式。” 这个建议在绝大多数情况下都很有道理。AES-GCM (Galois/Counter Mode) 凭借其卓越的性能、并行处理能力以及内置的认证加密 (AEAD) 特性,确实能提供远超 CBC (Cipher Block Chaining) 的机密性与完整性保障。

然而,作为在真实世界中构建软件的工程师,我们深知技术选型并非简单的“非黑即白”。在某些特定的、带有约束条件的场景下,我们是否真的只能选择 GCM?会不会存在一些“灰色地带”,让看似“过时”的 CBC 反而成为更务实、更巧妙的解决方案?

在我开发开源项目 Sdcb.Chats 的过程中,就遇到了这样一个有趣的场景。这段经历让我深刻体会到,真正的工程决策,是在深刻理解原理之后,基于具体需求所做的权衡与取舍(Trade-off)。本文将结合这段实践,深入探讨 GCM 和 CBC 之间那些不常被提及的选择考量。

GCM 的光环:为何它被誉为黄金标准?

在深入探讨“特例”之前,我们必须先充分肯定 GCM 的普适优势。简单回顾一下,GCM 之所以强大,主要在于:

  1. 「认证加密 (AEAD)」:这是 GCM 最核心的优势。它在加密数据(提供机密性)的同时,会生成一个「认证标签(Authentication Tag)」。这个标签能保证数据在传输过程中未被篡改(提供完整性)。任何对密文的修改都会导致标签验证失败,解密操作会直接抛出异常,从根本上杜绝了篡改风险,也让“填充预言攻击”等针对 CBC 的攻击方式成为历史。
  2. 「高性能」:GCM 的核心是 CTR (Counter) 模式,其加密过程可以被高度并行化。在支持 AES-NI 指令集的现代 CPU 上,GCM 的吞吐量通常远超需要串行加密的 CBC 模式。
  3. 「无需填充」:作为一种流加密模式,GCM 不需要对明文进行填充(Padding),可以直接处理任意长度的数据,代码实现更简洁,也避免了与填充相关的潜在安全问题。

总而言之,当你需要为一个新系统设计通用的、安全的网络通信协议或数据存储加密时,「请毫不犹豫地选择 AES-GCM」

现实的骨感:当 GCM 的要求与需求冲突

Sdcb.Chats 项目中,我遇到了一个需求:将数据库中的自增 intlong 类型的 ID,在 API 和前端 URL 中展示为一个看起来随机、无规律的标识符,以防止信息泄露(如系统规模)和恶意猜测。同时,这个标识符最好能保持统一、简洁的格式。

这看似简单的需求,却让 GCM 的两个核心要求显得格外“碍事”。

冲突一:固定的 IV/Nonce 与 GCM 的“灾难性”后果

为了保证前端逻辑的稳定性(例如,基于 ID 的缓存和状态管理),我需要一个「确定性的加密」:对于同一个输入的整数 ID,加密后的字符串结果必须永远相同。

这意味着,我不能在每次加密时都使用随机生成的 「Nonce」 (Number used once)。我必须为每种加密目的(如 ChatId, MessageId)使用一个固定的初始向量(IV),或者说,一个固定的 Nonce。

这对于 GCM 来说是「绝对禁止」的操作。GCM 的安全性基石在于,「对于同一个密钥,Nonce 绝不能重复使用」。一旦你用相同的密钥和 Nonce 加密了不同的明文(哪怕明文之间只有微小的差异,比如连续的整数 ID 1, 2, 3...),攻击者就可以通过简单的计算破解出密钥流,进而恢复所有明文。

让我们用代码直观地看一下后果。假设我们使用固定的 Nonce 来加密连续的整数:

代码语言:javascript
代码运行次数:0
运行
复制
using System.Security.Cryptography;
using System.Text;

// 假设我们为某个加密目的,固定使用一个 Nonce
byte[] key = RandomNumberGenerator.GetBytes(16);
byte[] fixedNonce = RandomNumberGenerator.GetBytes(12);

Console.WriteLine($"Key: {Convert.ToHexString(key)}");
Console.WriteLine($"Fixed Nonce: {Convert.ToHexString(fixedNonce)}\n");

using AesGcm aesGcm = new AesGcm(key, tagSizeInBytes: 16);

for (int id = 1; id <= 5; id++)
{
    byte[] plaintext = BitConverter.GetBytes(id);
    byte[] ciphertext = new byte[plaintext.Length];
    byte[] tag = new byte[16];

    // 每次都使用相同的 Nonce!这是非常危险的!
    aesGcm.Encrypt(fixedNonce, plaintext, ciphertext, tag);

    Console.WriteLine($"ID: {id}, Plaintext: {Convert.ToHexString(plaintext)}");
    Console.WriteLine($"Ciphertext: {Convert.ToHexString(ciphertext)}");
    Console.WriteLine();
}

「输出结果可能如下:」

代码语言:javascript
代码运行次数:0
运行
复制
Key: DE66C08C3C22D646422DD28D9E539912
Fixed Nonce: 23B5E92983623712E943B6DB

ID: 1, Plaintext: 01000000
Ciphertext: 75749629

ID: 2, Plaintext: 02000000
Ciphertext: 76749629

ID: 3, Plaintext: 03000000
Ciphertext: 77749629

ID: 4, Plaintext: 04000000
Ciphertext: 70749629

ID: 5, Plaintext: 05000000
Ciphertext: 71749629

请仔细观察 Ciphertext!虽然输入的 Plaintext 只有第一个字节在变,但输出的 Ciphertext 也呈现出极其明显的规律性(只有第一个字节在变化)。这完全违背了加密的初衷,攻击者可以轻易地利用这种模式。

「那么,CBC 在这种场景下表现如何呢?」

CBC 模式虽然也建议每次使用随机的 IV,但即使 IV 固定,其“链式”的内在结构也提供了更好的扩散性。每个明文块都会与「前一个密文块」进行异或,这使得即使输入数据有规律,输出的密文块也会显得非常混乱。

代码语言:javascript
代码运行次数:0
运行
复制
byte[] key = RandomNumberGenerator.GetBytes(16);
Console.WriteLine($"Key: {Convert.ToHexString(key)}");
using (Aes aes = Aes.Create())
{
    aes.Key = key;
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;
    // 使用固定的 IV
    aes.IV = new byte[16]; 

    Console.WriteLine("\n--- Testing CBC with Fixed IV ---\n");
    for (int id = 1; id <= 5; id++)
    {
        byte[] plaintext = BitConverter.GetBytes(id);
        byte[] ciphertext = aes.EncryptCbc(plaintext, aes.IV);

        Console.WriteLine($"ID: {id}, Plaintext: {Convert.ToHexString(plaintext)}");
        // CBC + PKCS7 padding on a 4-byte input results in a 16-byte output
        Console.WriteLine($"Ciphertext: {Convert.ToHexString(ciphertext)}");
        Console.WriteLine();
 }
}

「CBC 的输出结果:」

代码语言:javascript
代码运行次数:0
运行
复制
Key: 2255D210C5397DB4454C73DC190DE821

--- Testing CBC with Fixed IV ---

ID: 1, Plaintext: 01000000
Ciphertext: 595F8EFF602FD258C59BE8F0D94D57ED

ID: 2, Plaintext: 02000000
Ciphertext: B9D180464306DF29EE58EEB2086C2C54

ID: 3, Plaintext: 03000000
Ciphertext: 0332B6765638FF5AEA3D64755AA150B9

ID: 4, Plaintext: 04000000
Ciphertext: C8A67BC6F9E5C479EE77B54ADA5BF553

ID: 5, Plaintext: 05000000
Ciphertext: 42C69ABACAB18B35B2A3A8837EB4C17C

看到了吗?尽管输入 ID 是连续的,并且 IV 是固定的,但输出的密文看起来完全是随机和无规律的,成功地隐藏了原始数据的模式。

「结论一:在必须使用固定 IV/Nonce 的确定性加密场景下,CBC 的安全性表现远优于 GCM。」

冲突二:输出长度的限制与 GCM 的“累赘”

我的另一个需求,是将加密后的 ID 能够方便地表示为一个 Guid。一个标准的 Guid 是一个 16 字节(128位)的数据结构。

这给 GCM 带来了第二个无法解决的问题。GCM 的输出负载「必然」包含三部分:「Nonce」「认证标签 (Tag)」「密文」

让我们算一笔账。即使我们加密一个仅 4 字节的 int ID:

  • 「密文」:4 字节
  • 「Nonce」:通常至少 12 字节
  • 「Tag」:通常至少 12 字节(推荐 16 字节)

总长度 = 4 + 12 + 12 = 「28 字节」。这个长度远远超过了 Guid 所能容纳的 16 字节。我们无法在不破坏 GCM 安全模型的前提下,将它的输出“塞”进一个 Guid 里。

「而这,恰恰是 AES-CBC 的“高光时刻”。」

AES 本身是一个块加密算法,其块大小固定为 「16 字节」。当我们使用 CBC 模式配合 PKCS7 填充来加密一个小于 16 字节的数据(比如一个 4 字节的 int 或 8 字节的 long)时,算法会自动将其填充到 16 字节,然后进行加密,最终输出的密文「恰好就是 16 字节」

这简直是为 Guid 量身定做的!

代码语言:javascript
代码运行次数:0
运行
复制
byte[] key = RandomNumberGenerator.GetBytes(16);
Console.WriteLine($"Key: {Convert.ToHexString(key)}");

int idToEncrypt = 12345;
byte[] idBytes = BitConverter.GetBytes(idToEncrypt);

byte[] encryptedBytes;
using (Aes aes = System.Security.Cryptography.Aes.Create())
{
    aes.Key = key;
    aes.IV = new byte[16]; // 固定 IV
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;
    encryptedBytes = aes.EncryptCbc(idBytes, aes.IV);
}

Console.WriteLine($"Input is {idBytes.Length} bytes.");
Console.WriteLine($"Encrypted output is {encryptedBytes.Length} bytes.");

// 完美转换为 Guid
Guid finalGuid = new Guid(encryptedBytes);
Console.WriteLine($"Final Guid: {finalGuid}");

「输出:」

代码语言:javascript
代码运行次数:0
运行
复制
Key: 4B8D859D12AFE340018562C8F70258D5
Input is 4 bytes.
Encrypted output is 16 bytes.
Final Guid: 84a873bb-6bb1-01b1-216c-1fba73400fda

「结论二:当需要将加密结果限制在固定长度(特别是 16 字节以适配 Guid)时,AES-CBC 是一个完美且自然的选择,而 GCM 则完全不适用。」

安全性的再思考:我们放弃了什么?

选择 CBC,意味着我们放弃了 GCM 提供的「内置完整性验证」。攻击者理论上可以篡改我们生成的 Guid。

但这在我的场景下是可接受的风险,原因如下:

  1. 「低碰撞概率」:篡改后的 16 字节数据,在解密后,需要恰好能解析为一个有效的、存在于数据库中的整数 ID。这个概率极低。
  2. 「应用层验证」:即使碰巧解密出了一个有效的 ID,后续的业务逻辑和权限验证层(例如,验证当前用户是否有权访问该 ChatId)会成为第二道、也是更坚固的防线。
  3. 「风险收益不对等」:我们场景的核心目标是「防止信息泄露和批量扫描」,而不是保护像银行交易那样的高价值数据免于定点攻击。为了这个目标,牺牲 GCM 的完整性保护,换取确定性加密和固定的 Guid 输出格式,是一个非常划算的买卖。

总结: 务实主义胜于教条主义

通过 Sdcb.Chats 项目的这次实践,我想分享的核心观点是:

  • 「AES-GCM 依然是现代加密的首选和黄金标准。」 对于绝大多数需要同时保证机密性和完整性的新应用,你应该毫不犹豫地选择它。
  • 然而,技术世界没有“银弹”。「我们不应将“最佳实践”奉为不可违背的教条。」
  • 在遇到特殊约束条件时——例如「需要确定性加密(固定 IV/Nonce)或对输出长度有严格限制(如适配 Guid)」——我们应该深入思考,并勇敢地选择更适合当前场景的工具。

在这种情况下,古老的 AES-CBC 模式,在充分理解其安全边界并做好应用层风险规避的前提下,可以焕发出新的生命力,成为一个更优雅、更务实的解决方案。

作为工程师,我们的价值不仅在于知道“什么是最好的”,更在于知道“在何种情况下,什么是最合适的”。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • GCM 的光环:为何它被誉为黄金标准?
  • 现实的骨感:当 GCM 的要求与需求冲突
    • 冲突一:固定的 IV/Nonce 与 GCM 的“灾难性”后果
    • 冲突二:输出长度的限制与 GCM 的“累赘”
  • 安全性的再思考:我们放弃了什么?
  • 总结: 务实主义胜于教条主义
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档