C# AES加密内存使用量

3

我有一个关于RSA和AES混合加密算法内存使用的问题。我编写了一个简单的控制台程序(.NET Core和C# 8.0 beta),生成随机证书并加密/解密文件,执行时间似乎可以接受。

1000次迭代的用时如下:

  • 230 KB文件需要约2毫秒
  • 28 MB文件需要约150毫秒
  • 92 MB文件需要约500毫秒

问题似乎在于内存使用。对于230 KB的文件,程序使用了大约20 MB的内存。对于28 MB的文件,程序使用了约490 MB的内存。而92 MB的文件则跃升至2 GB,使用了近1.8 GB的内存。

这些数字属于"正常"的使用范围,还是我的代码有问题?

下面是我的AES加密实现:

static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
{
    // Salt not modified for sample
    byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
    using MemoryStream ms = new MemoryStream();
    using RijndaelManaged AES = new RijndaelManaged();
    AES.KeySize = 256;
    AES.BlockSize = 128;

    Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
    AES.Key = key.GetBytes(AES.KeySize / 8);
    AES.IV = key.GetBytes(AES.BlockSize / 8);

    AES.Mode = CipherMode.CBC;
    using ICryptoTransform csTf = AES.CreateEncryptor();
    using CryptoStream cs = new CryptoStream(ms, csTf, CryptoStreamMode.Write);
    cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
    cs.Close();
    return ms.ToArray();
}

static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
{
    // Salt not modified for sample
    byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
    using MemoryStream ms = new MemoryStream();
    using RijndaelManaged AES = new RijndaelManaged();
    AES.KeySize = 256;
    AES.BlockSize = 128;

    Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
    AES.Key = key.GetBytes(AES.KeySize / 8);
    AES.IV = key.GetBytes(AES.BlockSize / 8);

    AES.Mode = CipherMode.CBC;

    using ICryptoTransform csTf = AES.CreateDecryptor();
    using CryptoStream cs = new CryptoStream(ms, csTf, CryptoStreamMode.Write);
    cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
    cs.Close();
    return ms.ToArray();
}

static string EncryptString(string text, string password)
{
    byte[] baEncrypted = new byte[GetSaltLength() + Encoding.UTF8.GetByteCount(text)];

    Array.Copy(GetRandomBytes(), 0, baEncrypted, 0, GetSaltLength());
    Array.Copy(Encoding.UTF8.GetBytes(text), 0, baEncrypted, GetSaltLength(), Encoding.UTF8.GetByteCount(text));

    return Convert.ToBase64String(AES_Encrypt(baEncrypted, SHA256Managed.Create().ComputeHash(Encoding.UTF8.GetBytes(password))));
}

static string DecryptString(string text, string password)
{
    byte[] baDecrypted = AES_Decrypt(Convert.FromBase64String(text), SHA256Managed.Create().ComputeHash(Encoding.UTF8.GetBytes(password)));

    byte[] baResult = new byte[baDecrypted.Length - GetSaltLength()];

    Array.Copy(baDecrypted, GetSaltLength(), baResult, 0, baResult.Length);

    return Encoding.UTF8.GetString(baResult);
}

static byte[] GetRandomBytes()
{
    byte[] ba = new byte[GetSaltLength()];
    RNGCryptoServiceProvider.Create().GetBytes(ba);
    return ba;
}

static int GetSaltLength()
{
    return 8;
}

调用方法和迭代调用

static void Main(string[] args)
{
    CertificateRequest certificateRequest = new CertificateRequest("cn=random_cert", RSA.Create(4096), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

    X509Certificate2 certificate = certificateRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(2));

    String data = File.ReadAllText(@"PATH TO FILE");

    Int64 AESenc, RSAenc, AESdec, RSAdec;

    List<Int64> aesEncTime = new List<Int64>();
    List<Int64> aesDecTime = new List<Int64>();
    List<Int64> rsaEncTime = new List<Int64>();
    List<Int64> rsaDecTime = new List<Int64>();

    for (int i = 0; i < 1000; i++)
    {
        encryptData(ref certificate, ref data, out AESenc, out RSAenc, out AESdec, out RSAdec);
        aesEncTime.Add(AESenc);
        aesDecTime.Add(AESdec);
        rsaEncTime.Add(RSAenc);
        rsaDecTime.Add(RSAdec);

        Console.Clear();

        Console.WriteLine($"data.Length:\t{data.Length:n0} b");
        Console.WriteLine($"UTF8 Bytes:\t{Encoding.UTF8.GetByteCount(data):n0} b");

        Console.WriteLine($"Loop:\t\t{i + 1}");

        Console.WriteLine("---------------------------------------------------------");
        Console.WriteLine($"|AES Enc|Avg: {aesEncTime.Average():0000.00} ms|Max: {aesEncTime.Max():0000.00} ms|Min: {aesEncTime.Min():0000.00} ms|");
        Console.WriteLine("|-------|---------------|---------------|---------------|");
        Console.WriteLine($"|AES Dec|Avg: {aesDecTime.Average():0000.00} ms|Max: {aesDecTime.Max():0000.00} ms|Min: {aesDecTime.Min():0000.00} ms|");
        Console.WriteLine("|-------|---------------|---------------|---------------|");
        Console.WriteLine($"|RSA Enc|Avg: {rsaEncTime.Average():0000.00} ms|Max: {rsaEncTime.Max():0000.00} ms|Min: {rsaEncTime.Min():0000.00} ms|");
        Console.WriteLine("|-------|---------------|---------------|---------------|");
        Console.WriteLine($"|RSA Dec|Avg: {rsaDecTime.Average():0000.00} ms|Max: {rsaDecTime.Max():0000.00} ms|Min: {rsaDecTime.Min():0000.00} ms|");
        Console.WriteLine("---------------------------------------------------------");
        // Moving GC.Collect outside of the for-loop increases the memory usage
        GC.Collect();
    }

    Console.ReadKey();
}

static void encryptData(ref X509Certificate2 certificate, ref String data, out Int64 AESenc, out Int64 RSAenc, out Int64 AESdec, out Int64 RSAdec)
{
    Stopwatch stopwatch = new Stopwatch();

    String hash = getSha256(ref data);

    stopwatch.Start();

    String encryptedData = EncryptString(data, hash);

    stopwatch.Stop();

    AESenc = stopwatch.ElapsedMilliseconds;

    stopwatch.Restart();

    String encryptedKey = Convert.ToBase64String(certificate.GetRSAPublicKey().Encrypt(Encoding.UTF8.GetBytes(hash), RSAEncryptionPadding.Pkcs1));

    stopwatch.Stop();

    RSAenc = stopwatch.ElapsedMilliseconds;

    stopwatch.Restart();

    String decryptedKey = Encoding.UTF8.GetString(certificate.GetRSAPrivateKey().Decrypt(Convert.FromBase64String(encryptedKey), RSAEncryptionPadding.Pkcs1));

    stopwatch.Stop();

    RSAdec = stopwatch.ElapsedMilliseconds;

    stopwatch.Restart();

    String decryptedData = DecryptString(encryptedData, decryptedKey);

    stopwatch.Stop();

    encryptedData = null;
    decryptedData = null;

    AESdec = stopwatch.ElapsedMilliseconds;
}

static String getSha256(ref String value)
{
    String hash = String.Empty;
    Byte[] data = Encoding.UTF8.GetBytes(value);
    using SHA256Managed sHA256Managed = new SHA256Managed();
    Byte[] hashData = sHA256Managed.ComputeHash(data);
    foreach (Byte item in hashData)
    {
        hash += $"{item:x2}";
    }
    return hash;
}

代码可以在没有任何外部资源(除了要加密的文件)的情况下执行。

泄漏是由代码引起的。hash += $"{item:x2}"; 是一个字符串泄漏。每个字符串操作都会创建一个新的临时字符串。应改用 StringBuilder。将项目添加到列表中也会浪费内存。列表将数据存储在缓冲区中。当缓冲区耗尽时,它们会分配一个具有两倍大小的新缓冲区,然后将数据复制过去。在构造函数中指定 capacity 参数以只创建足够大的缓冲区一次。MemoryStreams 也是如此。它们只是在缓冲区上包装的流。 - Panagiotis Kanavos
1
关于基准测试,请使用BencharkDotNet而不是Stopwatch和手动检查内存使用情况。它会多次运行每个案例以计算有意义的平均值,并收集内存使用情况、分配和垃圾回收统计信息。这将告诉您这些MB是什么。 - Panagiotis Kanavos
1
关于 C# 8 标签,这并不是真正的 C# 8 问题。但是您可以利用几个新的 .NET Core 特性(例如 Span<> 和缓冲池)来改进代码。您可以使用 ReadOnlySpan<char> 来代替操作字符串和生成新的临时字符串。您也可以从 MemoryPool 中“租用”一个足够大的缓冲区,并在使用完成后将其归还,而不必在每次运行时创建新的 byte[] 缓冲区。 - Panagiotis Kanavos
1
当您运行测试时,Visual Studio的诊断工具窗口可以显示CPU和内存使用情况。我怀疑您会看到一个不断增长的线,因为对象被分配了。 - Panagiotis Kanavos
另一个可能性是,在调试期间,调试器 会在内存中固定对象,即使您以发布配置进行构建。使用BencharkDotNet并在调试器外运行测试以获得有意义的数字。 - Panagiotis Kanavos
显示剩余5条评论
1个回答

0

您可以通过从FileStream读取数据,然后使用CryptoStream将其流式传输到固定大小的缓冲区中,在文件中创建密文,并将其放入一个FileStream(而不是MemoryStream)以进行输出。

要解密,请在用于读取的FileStream前创建一个CryptoStream,然后将缓冲区中的数据写入用于写入的FileStream

如果您有明文或密文的字节数组,或者正在使用MemoryStream,则您正在错误地进行操作。


PS:盐值不应该是一个静态值,而且1000次迭代对于大多数密码来说远远不够。 - Maarten Bodewes
如果我正在使用MemoryStream,为什么我的操作是错误的?如果我不想在文件系统中加密或解密未加密数据(如果我们谈论加密和隐私,将数据转储到临时文件仍然是一个坏主意),那么我应该把它放在哪里? - JoeJoe87577
你在问题中提到了加密/解密文件。因此,我认为输入和输出都是来自文件?在这种情况下,混淆在哪里?如果您链接两个流,您首先加密数据,然后按部就班地将其存储在文件中。您不会先将其存储在文件中,然后再进行加密。因此,我不认为存在安全问题。 - Maarten Bodewes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接