问题描述
我正在使用类似于提供的字符串加密/解密类 这里作为解决方案.
I'm using a string Encryption/Decryption class similar to the one provided here as a solution.
这在 .Net 5 中对我很有效.
现在我想将我的项目更新到 .Net 6.
This worked well for me in .Net 5.
Now I wanted to update my project to .Net 6.
使用 .Net 6 时,解密后的字符串确实会根据输入字符串的长度被截断.
▶️ 为了便于调试/重现我的问题,我在此处创建了一个公共 repro 存储库.
▶️ To make it easy to debug/reproduce my issue, I created a public repro Repository here.
- 加密代码是在标准 2.0 项目中故意使用的.
- 引用此项目既是 .Net 6 也是 .Net 5 控制台项目.
两者都使用完全相同的输入12345678901234567890"
调用加密方法,路径短语为nzv86ri4H2qYHqc&m6rL"
.
Both are calling the encryption methods with the exact same input of "12345678901234567890"
with the path phrase of "nzv86ri4H2qYHqc&m6rL"
.
.Net 5 输出:12345678901234567890"
.Net 6 输出:1234567890123456"
长度差是4
.
我还查看了 .Net 6 的重大更改,但找不到能引导我找到解决方案的东西.
I also looked at the breaking changes for .Net 6, but could not find something which guided me to a solution.
很高兴收到有关我的问题的任何建议,谢谢!
I'm glad for any suggestions regarding my issue, thanks!
加密类
public static class StringCipher
{
// This constant is used to determine the keysize of the encryption algorithm in bits.
// We divide this by 8 within the code below to get the equivalent number of bytes.
private const int Keysize = 128;
// This constant determines the number of iterations for the password bytes generation function.
private const int DerivationIterations = 1000;
public static string Encrypt(string plainText, string passPhrase)
{
// Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
// so that the same Salt and IV values can be used when decrypting.
var saltStringBytes = Generate128BitsOfRandomEntropy();
var ivStringBytes = Generate128BitsOfRandomEntropy();
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
{
var keyBytes = password.GetBytes(Keysize / 8);
using (var symmetricKey = Aes.Create())
{
symmetricKey.BlockSize = 128;
symmetricKey.Mode = CipherMode.CBC;
symmetricKey.Padding = PaddingMode.PKCS7;
using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
{
using (var memoryStream = new MemoryStream())
{
using (var 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 salt bytes, the random iv bytes and the cipher bytes.
var cipherTextBytes = saltStringBytes;
cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
memoryStream.Close();
cryptoStream.Close();
return Convert.ToBase64String(cipherTextBytes);
}
}
}
}
}
}
public static string Decrypt(string cipherText, string passPhrase)
{
// Get the complete stream of bytes that represent:
// [32 bytes of Salt] + [16 bytes of IV] + [n bytes of CipherText]
var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
// Get the saltbytes by extracting the first 16 bytes from the supplied cipherText bytes.
var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
// Get the IV bytes by extracting the next 16 bytes from the supplied cipherText bytes.
var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
// Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray();
using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
{
var keyBytes = password.GetBytes(Keysize / 8);
using (var symmetricKey = Aes.Create())
{
symmetricKey.BlockSize = 128;
symmetricKey.Mode = CipherMode.CBC;
symmetricKey.Padding = PaddingMode.PKCS7;
using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
{
using (var memoryStream = new MemoryStream(cipherTextBytes))
{
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
var plainTextBytes = new byte[cipherTextBytes.Length];
var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
memoryStream.Close();
cryptoStream.Close();
return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
}
}
}
}
}
}
private static byte[] Generate128BitsOfRandomEntropy()
{
var randomBytes = new byte[16]; // 16 Bytes will give us 128 bits.
using (var rngCsp = RandomNumberGenerator.Create())
{
// Fill the array with cryptographically secure random bytes.
rngCsp.GetBytes(randomBytes);
}
return randomBytes;
}
}
调用代码
var input = "12345678901234567890";
var inputLength = input.Length;
var inputBytes = Encoding.UTF8.GetBytes(input);
var encrypted = StringCipher.Encrypt(input, "nzv86ri4H2qYHqc&m6rL");
var output = StringCipher.Decrypt(encrypted, "nzv86ri4H2qYHqc&m6rL");
var outputLength = output.Length;
var outputBytes = Encoding.UTF8.GetBytes(output);
var lengthDiff = inputLength - outputLength;
推荐答案
原因是这个重大变化:
DeflateStream、GZipStream 和 CryptoStream 不同于典型的Stream.Read 和 Stream.ReadAsync 行为的两种方式:
DeflateStream, GZipStream, and CryptoStream diverged from typical Stream.Read and Stream.ReadAsync behavior in two ways:
直到缓冲区通过后,他们才完成读取操作到读操作被完全填满或流结束已到达.
They didn't complete the read operation until either the buffer passed to the read operation was completely filled or the end of the stream was reached.
而新的行为是:
从 .NET 6 开始,当 Stream.Read 或 Stream.ReadAsync 被调用时具有长度为 N 的缓冲区的受影响流类型之一,操作在以下时间完成:
Starting in .NET 6, when Stream.Read or Stream.ReadAsync is called on one of the affected stream types with a buffer of length N, the operation completes when:
已经从流中读取了至少一个字节,或者底层它们包装的流从对其读取的调用中返回 0,表示不再数据可用.
At least one byte has been read from the stream, or The underlying stream they wrap returns 0 from a call to its read, indicating no more data is available.
在您的情况下,您会因为 Decrypt
方法中的此代码而受到影响:
In your case you are affected because of this code in Decrypt
method:
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
var plainTextBytes = new byte[cipherTextBytes.Length];
var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
memoryStream.Close();
cryptoStream.Close();
return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
}
您不检查 Read
实际读取了多少字节以及它是否全部读取.您可以在以前版本的 .NET 中避免这种情况,因为如上所述 CryptoStream
行为与其他流不同,并且因为您的缓冲区长度足以容纳所有数据.但是,情况不再如此,您需要像检查其他流一样检查它.甚至更好 - 只需使用 CopyTo
:
You do not check how much bytes Read
actually read and whether it read them all. You could get away with this in previous versions of .NET because as mentioned CryptoStream
behaviour was different from other streams, and because your buffer length is enough to hold all data. However, this is no longer the case and you need to check it as you would do for other streams. Or even better - just use CopyTo
:
using (var plainTextStream = new MemoryStream())
{
cryptoStream.CopyTo(plainTextStream);
var plainTextBytes = plainTextStream.ToArray();
return Encoding.UTF8.GetString(plainTextBytes, 0, plainTextBytes.Length);
}
或者甚至更好,因为您解密了 UTF8 文本:
Or even better as another answer suggests, since you decrypt UTF8 text:
using (var plainTextReader = new StreamReader(cryptoStream))
{
return plainTextReader.ReadToEnd();
}
这篇关于问题更新到 .Net 6 - 加密字符串的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!