我一直在使用以下代码来加密我的应用程序中的一些密码。原始代码列于此处Encrypting & Decrypting a String in C#。我已经更新了它以满足 .NET6 的要求。
它与哈希算法 SHA1 一起按预期工作,但 .NET 8 的更新需要更改为另一种算法,例如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);
}
}
}
上面的代码会产生以下输出:
短文本按预期工作
string plainText = "abc";
Console.WriteLine(plainText);
string cypherText = plainText.Encrypt();
Console.WriteLine(cypherText);
plainText = cypherText.Decrypt();
Console.WriteLine(plainText);
abc
rsWFdjY34byN8xxMt/pJxXc4s0Nvi/HwZEW9g0KFhAS+qqi+qlTlbHg73WN49HNOxSB4jXPqqev7MucKwyLvUepR
abc
较长的文本被正确解密但被截断
plainText = "abcdefghijklmnopqrstuvwxyz";
Console.WriteLine(plainText);
cypherText = plainText.Encrypt();
Console.WriteLine(cypherText);
plainText = cypherText.Decrypt();
Console.WriteLine(plainText);
abcdefghijklmnopqrstuvwxyz
xoTpfRH4GIMrB4KrrjWxAy872n3dCAvGZteJCb1W+xfX3jIa9ZFFn94lUhhxwQWaJzh1lAi/B44ZXrYCQK67u3z4BZIelPxEBHfuFeEXKsIJiA==
abcdefghijklmnop
我的期望是没有截断的相同纯文本。 据我所知,当使用已弃用的 SHA1 时,上述代码可以按照全文预期工作。
解决方案:
正如 Topaco 在评论中提到的,CryptoStream 在读取时会提前停止。
将解密的最后部分更改为使用 StreamReader 和 ReadToEnd() 获取完整文本。
using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
{
using (MemoryStream memoryStream = new MemoryStream(cipherTextBytes))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
using (StreamReader plainTextReader = new StreamReader(cryptoStream))
{
plainText = plainTextReader.ReadToEnd();
memoryStream.Close();
cryptoStream.Close();
}
return plainText;
}
}
}
使用此解密可以按预期进行。
正如 Topaco 在评论中提到的,在阅读时,
CryptoStream
会提前停止。
将解密的最后部分更改为使用
StreamReader
和 ReadToEnd()
获取完整文本。
using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
{
using (MemoryStream memoryStream = new MemoryStream(cipherTextBytes))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
using (StreamReader plainTextReader = new StreamReader(cryptoStream))
{
plainText = plainTextReader.ReadToEnd();
memoryStream.Close();
cryptoStream.Close();
}
return plainText;
}
}
}
使用此解密可以按预期进行。