You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET中使用AesGcm加密时是否需要设置迭代次数?

问题

我了解到RijndaelManaged已被弃用,.NET Core 3.1引入的AesGcm相比AesManaged是更推荐的实现,因此我参考相关教程与Stack Overflow解答尝试实现AesGcm加解密,代码如下:

以下是我的实现代码:

/// Perform AES Encryption, returning the result as a byte array.
/// </summary>
/// <param name="bytesToEncrypt">string, file or data represented as byte array</param>
/// <param name="passwordBytes">A unique password for the encryption (must be 32 bytes?)</param>
/// <returns>The data encrypted</returns>
public byte[] EncryptData(byte[] bytesToEncrypt, byte[] passwordBytes)
{
    // 参考公开实现逻辑
    // Get parameter sizes
    int nonceSize = AesGcm.NonceByteSizes.MaxSize;
    int tagSize = AesGcm.TagByteSizes.MaxSize;
    int cipherSize = bytesToEncrypt.Length;

    // We write everything into one big array for easier encoding
    int encryptedDataLength = 4 + nonceSize + 4 + tagSize + cipherSize;
    Span<byte> encryptedData = encryptedDataLength < 1024
                             ? stackalloc byte[encryptedDataLength]
                             : new byte[encryptedDataLength].AsSpan();

    // Copy parameters
    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(0, 4), nonceSize);
    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4), tagSize);
    var nonce = encryptedData.Slice(4, nonceSize);
    var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
    var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);

    // Generate secure nonce
    RandomNumberGenerator.Fill(nonce);

    // Encrypt
    using (var aes = new AesGcm(passwordBytes))
    {
      aes.Encrypt(nonce, bytesToEncrypt.AsSpan(), cipherBytes, tag);
    }

    return encryptedData.ToArray();
}


/// <summary>
/// Takes in an AES encrypted byte array, decrypts it and returns the resulting unencrypted byte array.
/// </summary>
/// <param name="encryptedBytes">A string, file or object represented as a byte array that's previously been encrypted.</param>
/// <param name="passwordBytes">The password used to encrypt the data. </param>
/// <returns></returns>
public byte[] DecryptData(byte[] encryptedBytes, byte[] passwordBytes)
{
    // Decode
    Span<byte> encryptedData = encryptedBytes.AsSpan();

    // Extract parameter sizes
    int nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(0, 4));
    int tagSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData.Slice(4 + nonceSize, 4));
    int cipherSize = encryptedData.Length - 4 - nonceSize - 4 - tagSize;

    // Extract parameters
    var nonce = encryptedData.Slice(4, nonceSize);
    var tag = encryptedData.Slice(4 + nonceSize + 4, tagSize);
    var cipherBytes = encryptedData.Slice(4 + nonceSize + 4 + tagSize, cipherSize);

    // Decrypt
    Span<byte> plainBytes = cipherSize < 1024
                          ? stackalloc byte[cipherSize]
                          : new byte[cipherSize];
    using (var aes = new AesGcm(passwordBytes))
    {
      aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
    }

    // Convert plain bytes back into string
    return plainBytes.ToArray();
}

我注意到当前实现中没有设置迭代次数的位置。

例如我之前使用AesManaged时,一直会按照如下方式设置迭代,迭代可以提升攻击难度,我在实现密码哈希时也采用了类似模式:

// Set Symmetric encryption algorithm
var AES = Aes.Create("AesManaged");
AES.KeySize = 256;
AES.BlockSize = 128;
AES.Padding = PaddingMode.PKCS7;

// 多轮哈希拉伸密钥,提升暴力破解成本
var key = new Rfc2898DeriveBytes(passwordBytes, salt, 100000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
...

我知道nonce会影响加密安全性,但我之前的所有实现都依赖多轮迭代,AesGcm中不设置迭代显得很反常。
AesGcm似乎没有内置迭代配置入口,请问使用AesGcm时是否需要在流程中加入迭代?如果需要的话应当如何正确实现?


回答

首先要明确核心逻辑:你之前用的迭代根本不是AES算法本身的配置,是密钥派生阶段的操作,和你用AesManaged还是AesGcm没有任何关系。

AES(不管是CBC、GCM哪种工作模式)本身的输入要求是固定长度的高熵密钥,算法自身没有迭代的概念。你之前写的100000次迭代,是Rfc2898DeriveBytes也就是PBKDF2密钥派生函数的参数,作用是把用户输入的低熵、长度不固定的密码,通过多轮哈希拉伸成符合AES要求的定长高熵密钥,增加暴力破解密码的成本。

你现在写的AesGcm代码相当于直接把传入的passwordBytes当成了AES密钥使用,本身存在两个明显问题:

  • 如果传入的是用户自行设置的短密码、低熵密码,没有经过密钥派生,暴力破解成本极低,完全达不到安全要求
  • AesGcm要求密钥必须是16/24/32字节(对应128/192/256位AES),如果传入的字节长度不符合要求,运行时会直接抛出异常

所以当然要加迭代,但迭代不是加在AesGcm的加解密逻辑里,是加在把用户密码转换成AES密钥的密钥派生步骤里,和你之前用AesManaged时的逻辑位置完全一致。

正确的实现流程如下:

  1. 加密时,先生成一个随机盐(至少16字节,用加密安全的随机数生成器生成)
  2. 用PBKDF2(.NET 8+更推荐用抗破解能力更强的Argon2id)搭配高迭代次数/高计算成本,把用户输入的密码+盐派生出32字节(256位)的AES密钥
    • 注意:AesGcm的nonce是每次加密单独随机生成的,不要从密钥派生函数里取nonce。nonce不需要保密,只要保证同一密钥下不重复即可,可以和盐、密文、认证标签拼接在一起存储
  3. 用派生出来的合法密钥初始化AesGcm,完成加密
  4. 把盐、nonce长度、tag长度、nonce、tag、密文按固定格式拼接成最终的加密结果返回
  5. 解密时,先从加密结果里按固定偏移拆出盐、nonce、tag、密文,用和加密时完全一致的密钥派生参数(相同迭代次数、相同哈希算法),搭配用户输入的密码+拆出的盐派生出一致的密钥,再用这个密钥初始化AesGcm完成解密

核心加密逻辑的参考实现如下,可以对照调整你的代码:

// 2024年PBKDF2-SHA256建议迭代次数不低于31万,可以根据服务器性能适当调高
private const int KeyDerivationIterations = 310000;
private const int SaltSize = 16;
private const int KeySize = 32; // 对应256位AES密钥

public byte[] EncryptData(byte[] bytesToEncrypt, byte[] userPassword)
{
    int nonceSize = AesGcm.NonceByteSizes.MaxSize;
    int tagSize = AesGcm.TagByteSizes.MaxSize;
    int cipherSize = bytesToEncrypt.Length;

    // 最终结果结构:盐(16字节) + nonce长度(4字节) + tag长度(4字节) + nonce + tag + 密文
    int encryptedDataLength = SaltSize + 4 + nonceSize + 4 + tagSize + cipherSize;
    Span<byte> encryptedData = encryptedDataLength < 1024
                             ? stackalloc byte[encryptedDataLength]
                             : new byte[encryptedDataLength].AsSpan();

    // 生成随机盐
    var salt = encryptedData.Slice(0, SaltSize);
    RandomNumberGenerator.Fill(salt);

    // 从用户密码派生符合要求的AES密钥
    byte[] aesKey = Rfc2898DeriveBytes.Pbkdf2(
        userPassword, 
        salt, 
        KeyDerivationIterations, 
        HashAlgorithmName.SHA256, 
        KeySize);

    // 写入参数、生成随机nonce、完成加密,注意偏移量要加上盐的长度
    int offset = SaltSize;
    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(offset, 4), nonceSize);
    offset +=4;
    var nonce = encryptedData.Slice(offset, nonceSize);
    RandomNumberGenerator.Fill(nonce);
    offset += nonceSize;

    BinaryPrimitives.WriteInt32LittleEndian(encryptedData.Slice(offset,4), tagSize);
    offset +=4;
    var tag = encryptedData.Slice(offset, tagSize);
    offset += tagSize;
    var cipherBytes = encryptedData.Slice(offset, cipherSize);

    using var aes = new AesGcm(aesKey);
    aes.Encrypt(nonce, bytesToEncrypt.AsSpan(), cipherBytes, tag);

    // 清空内存中存储的密钥,避免敏感数据泄露
    CryptographicOperations.ZeroMemory(aesKey);

    return encryptedData.ToArray();
}

解密逻辑对应调整偏移量、增加密钥派生步骤即可,解密完成后也要记得清空内存中的密钥。

额外提两个安全注意点:

  • 不要复用nonce,AesGcm对同一密钥下的nonce唯一性要求极高,只要出现一次nonce重复,就会直接泄露密钥,每次加密必须生成全新的随机nonce
  • PBKDF2属于比较老的密钥派生算法,抗GPU/ASIC暴力破解的能力一般,如果项目使用.NET 8及以上版本,建议替换为Argon2id实现,安全强度更高。

内容的提问来源于stack exchange,提问作者JsAndDotNet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 10:24:22