diff --git a/DataProtection.sln b/DataProtection.sln index c4bc85e46d..c08ab6a1ce 100644 --- a/DataProtection.sln +++ b/DataProtection.sln @@ -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}" diff --git a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs index fdd2f4881c..57e740f04b 100644 --- a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs +++ b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/KeyDerivationPrf.cs @@ -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 { /// diff --git a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj index 14940b2c46..70205f1754 100644 --- a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj +++ b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj @@ -2,7 +2,7 @@ ASP.NET Core utilities for key derivation. - netstandard2.0 + netstandard2.0;netcoreapp2.0 true true aspnetcore;dataprotection diff --git a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs new file mode 100644 index 0000000000..2aaf445dda --- /dev/null +++ b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/NetCorePbkdf2Provider.cs @@ -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 +{ + /// + /// Implements Pbkdf2 using . + /// + 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 diff --git a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs index dbe5a4120d..f7c99c4bcb 100644 --- a/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs +++ b/src/Microsoft.AspNetCore.Cryptography.KeyDerivation/PBKDF2/Pbkdf2Util.cs @@ -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 } } } diff --git a/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs b/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs index 2ded2300ab..6c78225a92 100644 --- a/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs +++ b/test/Microsoft.AspNetCore.Cryptography.KeyDerivation.Test/Pbkdf2Tests.cs @@ -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(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(salt, expectedDerivedKeyBase64); + } + + [Fact] + public void RunTest_WithLongPassword_NetCore() + { + // salt longer than 8 bytes + var salt = Encoding.UTF8.GetBytes("abcdefghijkl"); + RunTest_WithLongPassword_Impl(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() 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(salt, expectedDerivedKeyBase64); + } + + private static void RunTest_WithLongPassword_Impl(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;