.NET8中AES加密改用SHA512后解密长文本被截断问题
AES长文本解密截断问题排查(.NET8 SHA512替换SHA1后)
我在应用中使用基于Stack Overflow的C# AES加密代码,原代码在.NET6下运行正常。升级到.NET8后,因SHA1已废弃,将哈希算法改为SHA512。目前短文本解密正常,但长文本解密仅返回前几个字符。附上完整代码及测试输出,请求排查原因:
using System.Security.Cryptography; using System.Security.Principal; using System.Text; namespace AesEncryption { /// <summary> /// Extension class for encryption based on an algorithm described in https://stackoverflow.com/questions/10168240/encrypting-decrypting-a-string-in-c-sharp /// Changed the fixed derivationIterations to a random short-number and algorithm to AesCng. /// Changed HashAlgorithm to SHA512, as SHA1 became deprecated /// </summary> public static class GlobalEncryptionAes { public static string Encrypt(this string plainText) { return Encrypt(plainText, GeneratePassPhrase()); } public static string Encrypt(this string plainText, string passPhrase) { // DerivationIterations, Salt and IV are randomly generated each time, but is prepended to encrypted cipher text, so they can be used for decryption byte[] derivationIterationsBytes = GenerateBitsOfRandomEntropy(2); int derivationIterations = (int)BitConverter.ToUInt16(derivationIterationsBytes); if (derivationIterations == 0) derivationIterations++; byte[] saltStringBytes = GenerateBitsOfRandomEntropy(32); byte[] ivStringBytes = GenerateBitsOfRandomEntropy(16); byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText); using (Rfc2898DeriveBytes password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, derivationIterations, HashAlgorithmName.SHA512)) { var keyBytes = password.GetBytes(32); using (AesCng symmetricKey = new AesCng()) { symmetricKey.KeySize = 256; symmetricKey.BlockSize = 128; symmetricKey.Mode = CipherMode.CBC; symmetricKey.Padding = PaddingMode.PKCS7; using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes)) { using (MemoryStream memoryStream = new MemoryStream()) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); cryptoStream.FlushFinalBlock(); // Create the final bytes as a concatenation of the random derivation iteration bytes, the random salt bytes, the random iv bytes and the cipher bytes. byte[] cipherTextBytes; cipherTextBytes = derivationIterationsBytes; cipherTextBytes = cipherTextBytes.Concat(saltStringBytes).ToArray(); cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray(); cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray(); memoryStream.Close(); cryptoStream.Close(); return Convert.ToBase64String(cipherTextBytes); } } } } } } /// <summary> /// decrypts a string /// </summary> /// <param name="cipherText">encrypted string</param> /// <returns></returns> public static string Decrypt(this string cipherText) { return Decrypt(cipherText, GeneratePassPhrase()); } /// <summary> /// decrypts a string /// </summary> /// <param name="cipherText">encrypted string</param> /// <param name="passPhrase">encryption key</param> /// <returns></returns> public static string Decrypt(this string cipherText, string passPhrase) { // Get the complete stream of bytes that represent: // [2 byte of Derivation Iteration], [32 bytes of Salt] + [16 bytes of IV] + [n bytes of CipherText] byte[] cipherTextBytesWithDerivationIterationSaltIv = Convert.FromBase64String(cipherText); if (cipherTextBytesWithDerivationIterationSaltIv.Length <= 50) { throw new ArgumentException("cipherText is not valid"); } // Get the DerivationIterationBytes by extracting the first 2 bytes from the supplied cipherText bytes. byte[] derivationIterationsBytes = cipherTextBytesWithDerivationIterationSaltIv.Take(2).ToArray(); int derivationIterations = (int)BitConverter.ToUInt16(derivationIterationsBytes); // Get the saltStringBytes by extracting the next 32 bytes from the supplied cipherText bytes. byte[] saltStringBytes = cipherTextBytesWithDerivationIterationSaltIv.Skip(2).Take(32).ToArray(); // Get the ivStringBytes by extracting the next 24 bytes from the supplied cipherText bytes. byte[] ivStringBytes = cipherTextBytesWithDerivationIterationSaltIv.Skip(34).Take(16).ToArray(); // Get the actual cipher text bytes by removing the first 58 bytes (2 + 32 + 16) from the cipherText string. byte[] cipherTextBytes = cipherTextBytesWithDerivationIterationSaltIv.Skip(50).Take(cipherTextBytesWithDerivationIterationSaltIv.Length - 50).ToArray(); using (Rfc2898DeriveBytes password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, derivationIterations, HashAlgorithmName.SHA512)) { var keyBytes = password.GetBytes(32); using (AesCng symmetricKey = new AesCng()) { symmetricKey.KeySize = 256; symmetricKey.BlockSize = 128; symmetricKey.Mode = CipherMode.CBC; symmetricKey.Padding = PaddingMode.PKCS7; using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)) { using (MemoryStream memoryStream = new MemoryStream(cipherTextBytes)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { byte[] plainTextBytes = new byte[cipherTextBytes.Length]; var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); memoryStream.Close(); cryptoStream.Close(); return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); } } } } } } /// <summary> /// generates string dependent on machine name and user name and SID of user /// </summary> /// <returns>passphrase</returns> private static string GeneratePassPhrase() { string machineName; string currentUserName; SecurityIdentifier? currentUserSecurityIdentifier; string currentUserSecurityIdentifierString; string passPhrasString; byte[] passPhraseBytes; machineName = Environment.MachineName; currentUserName = WindowsIdentity.GetCurrent().Name; currentUserSecurityIdentifier = WindowsIdentity.GetCurrent().User; if (currentUserSecurityIdentifier is not null) { currentUserSecurityIdentifierString = currentUserSecurityIdentifier.Value; } else { currentUserSecurityIdentifierString = ""; } passPhrasString = machineName + currentUserName + currentUserSecurityIdentifierString; passPhraseBytes = Encoding.UTF8.GetBytes(passPhrasString); return Convert.ToBase64String(passPhraseBytes); } /// <summary> /// generates entropy /// </summary> /// <returns>random entropy of length</returns> public static byte[] GenerateBitsOfRandomEntropy(byte numberOfBytes) { byte[] randomBytes = new byte[numberOfBytes]; RandomNumberGenerator.Fill(randomBytes); return randomBytes; } } public class Program { static void Main(string[] args) { string plainText = "abc"; Console.WriteLine(plainText); string cypherText = plainText.Encrypt(); Console.WriteLine(cypherText); plainText = cypherText.Decrypt(); Console.WriteLine(plainText); plainText = "abcdefghijklmnopqrstuvwxyz"; Console.WriteLine(plainText); cypherText = plainText.Encrypt(); Console.WriteLine(cypherText); plainText = cypherText.Decrypt(); Console.WriteLine(plainText); } } }
测试输出
短文本(正常)
abc rsWFdjY34byN8xxMt/pJxXc4s0Nvi/HwZEW9g0KFhAS+qqi+qlTlbHg73WN49HNOxSB4jXPqqev7MucKwyLvUepR abc
长文本(解密截断)
abcdefghijklmnopqrstuvwxyz xoTpfRH4GIMrB4KrrjWxAy872n3dCAvGZteJCb1W+xfX3jIa9ZFFn94lUhhxwQWaJzh1lAi/B44ZXrYCQK67u3z4BZIelPxEBHfuFeEXKsIJiA== abcdefghijklmnop
原SHA1版本无此问题,期望长文本解密后完整返回原内容。
问题原因及解决方案
问题根源
解密方法中使用CryptoStream.Read仅读取了一次数据,但Read方法不能保证一次性读取所有可用数据,它返回的是实际读取的字节数。短文本解密时巧合一次读完,但长文本或SHA512生成的密钥导致解密数据流需要多次读取才能完成,只调用一次就会导致数据截断。
修复代码
修改Decrypt方法中读取解密数据的部分,循环读取直到Read返回0:
// 替换原解密方法中CryptoStream内的读取逻辑 using (MemoryStream memoryStream = new MemoryStream(cipherTextBytes)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { using (MemoryStream plainStream = new MemoryStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = cryptoStream.Read(buffer, 0, buffer.Length)) > 0) { plainStream.Write(buffer, 0, bytesRead); } return Encoding.UTF8.GetString(plainStream.ToArray()); } } }
说明
- 使用循环读取
CryptoStream,确保所有解密后的数据都被读取 - 用临时
MemoryStream存储所有解密字节,最后一次性转换为字符串 - 原代码中依赖
cipherTextBytes.Length初始化输出数组的方式不可靠,因为解密后的明文长度可能小于密文长度(PKCS7 padding会被移除)
内容的提问来源于stack exchange,提问作者Holger Kühn
相关产品推荐
相关产品推荐

