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

