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

AesCng不同实例加密结果不一致问题及方案适用性咨询

AES跨实例加密不一致及解密失败问题分析与修复

问题场景

实现了AES加密包装类AesCngWithSalt,使用相同密码和盐构造两个实例时,同一明文的加密结果完全不同;将加密值存入数据库后,用新实例解密会失败,仅同一实例内的加解密能正常工作。

原实现代码

public class AesCngWithSalt
{
    public AesCngWithSalt() { }
    public AesCngWithSalt(string password, string salt)
    {
        Password = password;
        Salt = salt;
        Build();
    }

    public string Password {  get; set; }
    public string Salt { get; set; }
    protected Aes Encryptor { get; set; }
    protected Aes Decryptor { get; set; }

    public void Build()
    {
        if (string.IsNullOrWhiteSpace(Password) || string.IsNullOrWhiteSpace(Salt))
        {
            throw new ArgumentException("Password and Salt must be set first");
        }
        Rfc2898DeriveBytes rfcKey1 = new Rfc2898DeriveBytes(Password, Encoding.Unicode.GetBytes(Salt), 1000);
        Rfc2898DeriveBytes rfcKey2 = new Rfc2898DeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));
        Encryptor = AesCng.Create();
        Encryptor.Padding = PaddingMode.PKCS7;
        Encryptor.KeySize = 256;
        Encryptor.Key = rfcKey1.GetBytes(32);
        Decryptor = AesCng.Create();
        Decryptor.Padding = PaddingMode.PKCS7;
        Decryptor.KeySize = 256;
        Decryptor.Key = rfcKey2.GetBytes(32);
        Decryptor.IV = Encryptor.IV;
    }

    public string Encrypt(string text)
    {
        try
        {
            MemoryStream encryptionStream = new MemoryStream();
            CryptoStream encrypt = new CryptoStream(encryptionStream, Encryptor.CreateEncryptor(), CryptoStreamMode.Write);
            byte[] utfD1 = new System.Text.UTF8Encoding(false).GetBytes(text);
            encrypt.Write(utfD1, 0, utfD1.Length);
            encrypt.FlushFinalBlock();
            encrypt.Close();
            byte[] edata1 = encryptionStream.ToArray();
            return Convert.ToBase64String(edata1);
        }
        catch
        {
            return string.Empty;
        }
    }

    public string Decrypt(string text)
    {
        try
        {
            MemoryStream decryptionStreamBacking = new MemoryStream();
            CryptoStream decrypt = new CryptoStream(decryptionStreamBacking, Decryptor.CreateDecryptor(), CryptoStreamMode.Write);
            char[] data = text.ToArray();
            byte[] bdata = Convert.FromBase64CharArray(data, 0, data.Length);
            decrypt.Write(bdata, 0, bdata.Length);
            decrypt.Flush();
            decrypt.Close();
            return new UTF8Encoding(false).GetString(decryptionStreamBacking.ToArray());
        }
        catch
        {
            return string.Empty;
        }
    }
}

问题根源

  1. IV(初始化向量)未持久化传递:
    每次调用AesCng.Create()都会生成随机IV,这是安全设计,但原代码仅在Build()方法中将当前实例的EncryptorIV赋值给Decryptor。创建新实例时,新实例的Encryptor会生成全新的随机IV,解密时未使用加密对应的IV,导致解密失败。
  2. 加密结果未包含IV:AES默认CBC模式要求加解密必须使用相同IV才能还原明文,原代码仅存储密文,丢失了关键的IV信息。
  3. 异常处理不合理:直接吞异常返回空字符串,掩盖实际错误,增加排查难度。

当前实现是否合适?

当前实现完全不合适,违反了AES加密的核心要求,无法支持跨实例的加解密,不适合需要持久化密文的场景(比如存入数据库)。

修复方案

核心改进点

  • 加密时将IV与密文一起存储(IV无需加密,可明文携带)
  • 解密时先从存储的加密数据中分离出IV,再用该IV解密
  • 提升密钥派生的迭代次数,符合当前安全标准
  • 优化实例管理,每次加解密创建新的Aes实例,避免状态复用风险
  • 改进异常处理,明确抛出错误而非返回空字符串

修复后的代码

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class AesCngWithSalt
{
    private const int KeyDerivationIterations = 10000;
    private const int AesKeySize = 256;
    private readonly byte[] _encryptionKey;

    public AesCngWithSalt(string password, string salt)
    {
        if (string.IsNullOrWhiteSpace(password))
            throw new ArgumentNullException(nameof(password));
        if (string.IsNullOrWhiteSpace(salt))
            throw new ArgumentNullException(nameof(salt));

        // 使用单实例Rfc2898DeriveBytes生成密钥,改用SHA256提升安全性
        using var keyDeriver = new Rfc2898DeriveBytes(
            password, 
            Encoding.Unicode.GetBytes(salt), 
            KeyDerivationIterations, 
            HashAlgorithmName.SHA256);
        _encryptionKey = keyDeriver.GetBytes(AesKeySize / 8);
    }

    public string Encrypt(string plainText)
    {
        if (string.IsNullOrEmpty(plainText))
            throw new ArgumentNullException(nameof(plainText));

        using var aes = AesCng.Create();
        aes.Key = _encryptionKey;
        aes.Padding = PaddingMode.PKCS7;
        aes.GenerateIV(); // 生成随机IV

        using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
        using var memoryStream = new MemoryStream();

        // 先写入IV,再写入加密后的密文
        memoryStream.Write(aes.IV, 0, aes.IV.Length);
        using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
        var plainBytes = Encoding.UTF8.GetBytes(plainText);
        cryptoStream.Write(plainBytes, 0, plainBytes.Length);
        cryptoStream.FlushFinalBlock();

        return Convert.ToBase64String(memoryStream.ToArray());
    }

    public string Decrypt(string encryptedText)
    {
        if (string.IsNullOrEmpty(encryptedText))
            throw new ArgumentNullException(nameof(encryptedText));

        var encryptedBytes = Convert.FromBase64String(encryptedText);
        using var aes = AesCng.Create();
        aes.Key = _encryptionKey;
        aes.Padding = PaddingMode.PKCS7;

        // 提取IV(AES块大小固定为16字节,对应IV长度16字节)
        var ivLength = aes.BlockSize / 8;
        var iv = new byte[ivLength];
        Array.Copy(encryptedBytes, 0, iv, 0, ivLength);
        aes.IV = iv;

        // 提取实际密文部分
        var cipherBytes = new byte[encryptedBytes.Length - ivLength];
        Array.Copy(encryptedBytes, ivLength, cipherBytes, 0, cipherBytes.Length);

        using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
        using var memoryStream = new MemoryStream();
        using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write);
        cryptoStream.Write(cipherBytes, 0, cipherBytes.Length);
        cryptoStream.FlushFinalBlock();

        return Encoding.UTF8.GetString(memoryStream.ToArray());
    }
}

使用示例

// 加密
var aes1 = new AesCngWithSalt("MyPassword", "ASaltValue");
string encrypted = aes1.Encrypt("Test Text");

// 解密(新实例)
var aes2 = new AesCngWithSalt("MyPassword", "ASaltValue");
string decrypted = aes2.Decrypt(encrypted);
// decrypted 会正确返回 "Test Text"

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 08:32:03