aspnet/DataProtection

Move KeyDerivation to use the built in Rfc2989 SHA2* support in Core 2.0

blowdart opened this issue · 7 comments

https://docs.microsoft.com/dotnet/api/system.security.cryptography.rfc2898derivebytes now supports SHA256, SHA384 and SHA512. We could remove the code from data protection and just use the framework support.

Looks like a good opportunity to delete code (a favorite pastime). Before we do, however, we should ensure we don't regress behavior by checking our test coverage.

Use caution that you don't cause a perf regression when you do this. The code as currently written determines which platform it is running on and calls into the fastest implementation for the platform. This is especially important when we're talking about password hashing, which defaults to 10,000 iterations. You probably don't want to take the CPU cost of 10,000 p/invoke calls in a tight loop if you can get away with just one. (Of course, this all depends on how the Rfc2898DeriveBytes class is written.)

@GrabYourPitchforks do you think we should do this the other way around, and move the data protection version to core?

@blowdart that would be ideal since you could bring the perf gains to the core class. It looks like Rfc2898DeriveBytes has some custom caching logic for results so you'd have to make sure that you don't break the scenario where somebody calls GetBytes multiple times.

Atleast on my Windows machine, the Dataprotection version is still ~25% faster than the pbkdf2 version from .NET Core:

BenchmarkDotNet=v0.10.9, OS=Windows 10 Redstone 1 (10.0.14393)
Processor=Intel Xeon CPU E5-1620 0 3.60GHz, ProcessorCount=8
Frequency=3507177 Hz, Resolution=285.1296 ns, Timer=TSC
.NET Core SDK=2.0.2
  [Host]     : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT

Method Iterations Mean Error StdDev
Rfc2898DeriveBytesSHA512 1000 1.273 ms 0.0034 ms 0.0032 ms
KeyDerivationPBKDF2SHA512 1000 1.019 ms 0.0025 ms 0.0024 ms
Rfc2898DeriveBytesSHA512 10000 12.652 ms 0.0160 ms 0.0142 ms
KeyDerivationPBKDF2SHA512 10000 10.143 ms 0.0361 ms 0.0301 ms
Rfc2898DeriveBytesSHA512 100000 128.595 ms 0.8824 ms 0.8254 ms
KeyDerivationPBKDF2SHA512 100000 102.741 ms 0.9762 ms 0.9131 ms

Apparently there are quite a few performance improvements in there, last time I checked against the vanilla implementation (modified to support SHA512) on netfx, the dataprotection version was pretty much twice as fast.

As for source:

using System.Security.Cryptography;
using System.Text;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;

namespace PBKDF2Benchmark
{
    public class HashBenchmark
    {
        [Params(1000, 10000, 100000)]
        public int Iterations { get; set; }
        private static readonly byte[] _password = Encoding.UTF8.GetBytes("Hello World");
        private static readonly byte[] _salt = GenerateSalt();

        private static byte[] GenerateSalt()
        {
            using (var random = new RNGCryptoServiceProvider())
            {
                var result = new byte[64];
                random.GetBytes(result);
                return result;
            }
        }

        [Benchmark]
        public byte[] Rfc2898DeriveBytesSHA512()
        {
            var rfc = new Rfc2898DeriveBytes(_password, _salt, Iterations, HashAlgorithmName.SHA512);
            return rfc.GetBytes(64);
        }
        [Benchmark]
        public byte[] KeyDerivationPBKDF2SHA512()
        {
            var key = KeyDerivation.Pbkdf2("Hello World", _salt, KeyDerivationPrf.HMACSHA512, Iterations, 512 / 8);
            return key;
        }
    }
}

Thanks for the numbers. I'll take a look at these more closely before we change implementations.

This issue was moved to dotnet/aspnetcore#2508