Switch default implementation of Pbkdf2 on Linux/macOS to Rfc2898DeriveBytes (#301)

Requires adding .NET Core 2.0 target framework to the package because only SHA1 is supported in .NET Standard 2.0
This commit is contained in:
Nate McMaster 2018-03-06 09:31:03 -08:00 committed by GitHub
parent 0ec64af67c
commit e552b5861a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 8 deletions

View File

@ -5,13 +5,30 @@ MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{60336AB3-948D-4D15-A5FB-F32A2B91E814}"
ProjectSection(SolutionItems) = preProject
test\CreateTestCert.ps1 = test\CreateTestCert.ps1
test\Directory.Build.props = test\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5A3A5DE3-49AD-431C-971D-B01B62D94AE2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E1D86B1B-41D8-43C9-97FD-C2BF65C414E2}"
ProjectSection(SolutionItems) = preProject
.appveyor.yml = .appveyor.yml
.gitattributes = .gitattributes
.gitignore = .gitignore
.travis.yml = .travis.yml
CONTRIBUTING.md = CONTRIBUTING.md
build\dependencies.props = build\dependencies.props
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
korebuild.json = korebuild.json
LICENSE.txt = LICENSE.txt
NuGet.config = NuGet.config
NuGetPackageVerifier.json = NuGetPackageVerifier.json
Provision-AutoGenKeys.ps1 = Provision-AutoGenKeys.ps1
README.md = README.md
version.props = version.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection", "src\Microsoft.AspNetCore.DataProtection\Microsoft.AspNetCore.DataProtection.csproj", "{1E570CD4-6F12-44F4-961E-005EE2002BC2}"

View File

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
{
/// <summary>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Description>ASP.NET Core utilities for key derivation.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;netcoreapp2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;dataprotection</PackageTags>

View File

@ -0,0 +1,71 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if NETCOREAPP2_0
// Rfc2898DeriveBytes in .NET Standard 2.0 only supports SHA1
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
{
/// <summary>
/// Implements Pbkdf2 using <see cref="Rfc2898DeriveBytes"/>.
/// </summary>
internal sealed class NetCorePbkdf2Provider : IPbkdf2Provider
{
private static readonly ManagedPbkdf2Provider _fallbackProvider = new ManagedPbkdf2Provider();
public byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
{
Debug.Assert(password != null);
Debug.Assert(salt != null);
Debug.Assert(iterationCount > 0);
Debug.Assert(numBytesRequested > 0);
if (salt.Length < 8)
{
// Rfc2898DeriveBytes enforces the 8 byte recommendation.
// To maintain compatibility, we call into ManagedPbkdf2Provider for salts shorter than 8 bytes
// because we can't use Rfc2898DeriveBytes with this salt.
return _fallbackProvider.DeriveKey(password, salt, prf, iterationCount, numBytesRequested);
}
else
{
return DeriveKeyImpl(password, salt, prf, iterationCount, numBytesRequested);
}
}
private static byte[] DeriveKeyImpl(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested)
{
HashAlgorithmName algorithmName;
switch (prf)
{
case KeyDerivationPrf.HMACSHA1:
algorithmName = HashAlgorithmName.SHA1;
break;
case KeyDerivationPrf.HMACSHA256:
algorithmName = HashAlgorithmName.SHA256;
break;
case KeyDerivationPrf.HMACSHA512:
algorithmName = HashAlgorithmName.SHA512;
break;
default:
throw new ArgumentOutOfRangeException();
}
var passwordBytes = Encoding.UTF8.GetBytes(password);
using (var rfc = new Rfc2898DeriveBytes(passwordBytes, salt, iterationCount, algorithmName))
{
return rfc.GetBytes(numBytesRequested);
}
}
}
}
#elif NETSTANDARD2_0
#else
#error Update target frameworks
#endif

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Cryptography.Cng;
namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
@ -20,15 +19,28 @@ namespace Microsoft.AspNetCore.Cryptography.KeyDerivation.PBKDF2
{
// fastest implementation
return new Win8Pbkdf2Provider();
} else if (OSVersionUtil.IsWindows())
}
else if (OSVersionUtil.IsWindows())
{
// acceptable implementation
return new Win7Pbkdf2Provider();
} else
}
#if NETCOREAPP2_0
else
{
// fastest implementation on .NET Core for Linux/macOS.
// Not supported on .NET Framework
return new NetCorePbkdf2Provider();
}
#elif NETSTANDARD2_0
else
{
// slowest implementation
return new ManagedPbkdf2Provider();
}
#else
#error Update target frameworks
#endif
}
}
}

View File

@ -12,6 +12,58 @@ namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
{
public class Pbkdf2Tests
{
#if NET461
#elif NETCOREAPP2_0 || NETCOREAPP2_1
// The 'numBytesRequested' parameters below are chosen to exercise code paths where
// this value straddles the digest length of the PRF. We only use 5 iterations so
// that our unit tests are fast.
// This provider is only available in .NET Core because .NET Standard only supports HMACSHA1
[Theory]
[InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
[InlineData("my-password", KeyDerivationPrf.HMACSHA512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
public void RunTest_Normal_NetCore(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
{
// Arrange
byte[] salt = new byte[256];
for (int i = 0; i < salt.Length; i++)
{
salt[i] = (byte)i;
}
// Act & assert
TestProvider<NetCorePbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
}
[Fact]
public void RunTest_WithLongPassword_NetCore_FallbackToManaged()
{
// salt is less than 8 bytes
byte[] salt = Encoding.UTF8.GetBytes("salt");
const string expectedDerivedKeyBase64 = "Sc+V/c3fiZq5Z5qH3iavAiojTsW97FAp2eBNmCQAwCNzA8hfhFFYyQLIMK65qPnBFHOHXQPwAxNQNhaEAH9hzfiaNBSRJpF9V4rpl02d5ZpI6cZbsQFF7TJW7XJzQVpYoPDgJlg0xVmYLhn1E9qMtUVUuXsBjOOdd7K1M+ZI00c=";
RunTest_WithLongPassword_Impl<NetCorePbkdf2Provider>(salt, expectedDerivedKeyBase64);
}
[Fact]
public void RunTest_WithLongPassword_NetCore()
{
// salt longer than 8 bytes
var salt = Encoding.UTF8.GetBytes("abcdefghijkl");
RunTest_WithLongPassword_Impl<NetCorePbkdf2Provider>(salt, "NGJtFzYUaaSxu+3ZsMeZO5d/qPJDUYW4caLkFlaY0cLSYdh1PN4+nHUVp4pUUubJWu3UeXNMnHKNDfnn8GMfnDVrAGTv1lldszsvUJ0JQ6p4+daQEYBc//Tj/ejuB3luwW0IinyE7U/ViOQKbfi5pCZFMQ0FFx9I+eXRlyT+I74=");
}
#else
#error Update target framework
#endif
// The 'numBytesRequested' parameters below are chosen to exercise code paths where
// this value straddles the digest length of the PRF. We only use 5 iterations so
// that our unit tests are fast.
@ -115,10 +167,16 @@ namespace Microsoft.AspNetCore.Cryptography.KeyDerivation
private static void RunTest_WithLongPassword_Impl<TProvider>()
where TProvider : IPbkdf2Provider, new()
{
// Arrange
string password = new String('x', 50000); // 50,000 char password
byte[] salt = Encoding.UTF8.GetBytes("salt");
const string expectedDerivedKeyBase64 = "Sc+V/c3fiZq5Z5qH3iavAiojTsW97FAp2eBNmCQAwCNzA8hfhFFYyQLIMK65qPnBFHOHXQPwAxNQNhaEAH9hzfiaNBSRJpF9V4rpl02d5ZpI6cZbsQFF7TJW7XJzQVpYoPDgJlg0xVmYLhn1E9qMtUVUuXsBjOOdd7K1M+ZI00c=";
RunTest_WithLongPassword_Impl<TProvider>(salt, expectedDerivedKeyBase64);
}
private static void RunTest_WithLongPassword_Impl<TProvider>(byte[] salt, string expectedDerivedKeyBase64)
where TProvider : IPbkdf2Provider, new()
{
// Arrange
string password = new String('x', 50000); // 50,000 char password
const KeyDerivationPrf prf = KeyDerivationPrf.HMACSHA256;
const int iterationCount = 5;
const int numBytesRequested = 128;