From 4588b1c898ae3fc9892a4dbaf85b5dd533a430c6 Mon Sep 17 00:00:00 2001 From: Levi B Date: Wed, 20 Aug 2014 22:47:47 -0700 Subject: [PATCH] Add preliminary PBKDF2 routines to unblock identity work. Win7 and Win8 optimizations will be committed as part of the larger DataProtection overhaul. --- .../CryptoUtil.cs | 35 ++++++ .../KeyDerivation.cs | 38 +++++++ .../KeyDerivationPrf.cs | 28 +++++ .../PBKDF2/IPbkdf2Provider.cs | 15 +++ .../PBKDF2/ManagedPbkdf2Provider.cs | 102 ++++++++++++++++++ .../PBKDF2/Pbkdf2Util.cs | 25 +++++ .../project.json | 1 + 7 files changed, 244 insertions(+) create mode 100644 src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs diff --git a/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs new file mode 100644 index 0000000000..29fabca02e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/CryptoUtil.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + internal unsafe static class CryptoUtil + { + // This isn't a typical Debug.Assert; the check is always performed, even in retail builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool condition, string message) + { + if (!condition) + { + Fail(message); + } + } + + // This isn't a typical Debug.Fail; an error always occurs, even in retail builds. + // This method doesn't return, but since the CLR doesn't allow specifying a 'never' + // return type, we mimic it by specifying our return type as Exception. That way + // callers can write 'throw Fail(...);' to make the C# compiler happy, as the + // throw keyword is implicitly of type O. + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception Fail(string message) + { + Debug.Fail(message); + throw new CryptographicException("Assertion failed: " + message); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs new file mode 100644 index 0000000000..548e0e7f65 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivation.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Security.DataProtection.Cng.PBKDF2; + +namespace Microsoft.AspNet.Security.DataProtection.Cng +{ + public static class KeyDerivation + { + public static byte[] Pbkdf2(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested) + { + // parameter checking + if (password == null) + { + throw new ArgumentNullException("password"); + } + if (salt == null) + { + throw new ArgumentNullException("salt"); + } + if (prf < KeyDerivationPrf.Sha1 || prf > KeyDerivationPrf.Sha512) + { + throw new ArgumentOutOfRangeException("prf"); + } + if (iterationCount <= 0) + { + throw new ArgumentOutOfRangeException("iterationCount"); + } + if (numBytesRequested <= 0) + { + throw new ArgumentOutOfRangeException("numBytesRequested"); + } + + return Pbkdf2Util.Pbkdf2Provider.DeriveKey(password, salt, prf, iterationCount, numBytesRequested); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs new file mode 100644 index 0000000000..600383eb7a --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/KeyDerivationPrf.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Security.DataProtection.Cng +{ + /// + /// Specifies the PRF which should be used for the key derivation algorithm. + /// + public enum KeyDerivationPrf + { + /// + /// SHA-1 (FIPS PUB 180-4) + /// + Sha1, + + /// + /// SHA-256 (FIPS PUB 180-4) + /// + Sha256, + + /// + /// SHA-512 (FIPS PUB 180-4) + /// + Sha512, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs new file mode 100644 index 0000000000..a9e499b80e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/IPbkdf2Provider.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Security.DataProtection.Cng.PBKDF2 +{ + /// + /// Internal interface used for abstracting away the PBKDF2 implementation since the implementation is OS-specific. + /// + internal interface IPbkdf2Provider + { + byte[] DeriveKey(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs new file mode 100644 index 0000000000..3fc75f67fd --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/ManagedPbkdf2Provider.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 +{ + /// + /// A PBKDF2 provider which utilizes the managed hash algorithm classes as PRFs. + /// This isn't the preferred provider since the implementation is slow, but it is provided as a fallback. + /// + internal sealed class ManagedPbkdf2Provider : IPbkdf2Provider + { + 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); + + // PBKDF2 is defined in NIST SP800-132, Sec. 5.3. + // http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf + + byte[] retVal = new byte[numBytesRequested]; + int numBytesWritten = 0; + int numBytesRemaining = numBytesRequested; + + // For each block index, U_0 := Salt || block_index + byte[] saltWithBlockIndex = new byte[checked(salt.Length + sizeof(uint))]; + Buffer.BlockCopy(salt, 0, saltWithBlockIndex, 0, salt.Length); + + using (var hashAlgorithm = PrfToManagedHmacAlgorithm(prf, password)) + { + for (uint blockIndex = 1; numBytesRemaining > 0; blockIndex++) + { + // write the block index out as big-endian + saltWithBlockIndex[saltWithBlockIndex.Length - 4] = (byte)(blockIndex >> 24); + saltWithBlockIndex[saltWithBlockIndex.Length - 3] = (byte)(blockIndex >> 16); + saltWithBlockIndex[saltWithBlockIndex.Length - 2] = (byte)(blockIndex >> 8); + saltWithBlockIndex[saltWithBlockIndex.Length - 1] = (byte)blockIndex; + + // U_1 = PRF(U_0) = PRF(Salt || block_index) + // T_blockIndex = U_1 + byte[] U_iter = hashAlgorithm.ComputeHash(saltWithBlockIndex); // this is U_1 + byte[] T_blockIndex = U_iter; + + for (int iter = 1; iter < iterationCount; iter++) + { + U_iter = hashAlgorithm.ComputeHash(U_iter); + XorBuffers(src: U_iter, dest: T_blockIndex); + // At this point, the 'U_iter' variable actually contains U_{iter+1} (due to indexing differences). + } + + // At this point, we're done iterating on this block, so copy the transformed block into retVal. + int numBytesToCopy = Math.Min(numBytesRemaining, T_blockIndex.Length); + Buffer.BlockCopy(T_blockIndex, 0, retVal, numBytesWritten, numBytesToCopy); + numBytesWritten += numBytesToCopy; + numBytesRemaining -= numBytesToCopy; + } + } + + // retVal := T_1 || T_2 || ... || T_n, where T_n may be truncated to meet the desired output length + return retVal; + } + + private static KeyedHashAlgorithm PrfToManagedHmacAlgorithm(KeyDerivationPrf prf, string password) + { + byte[] passwordBytes = Pbkdf2Util.SecureUtf8Encoding.GetBytes(password); + try + { + switch (prf) + { + case KeyDerivationPrf.Sha1: + return new HMACSHA1(passwordBytes); + case KeyDerivationPrf.Sha256: + return new HMACSHA256(passwordBytes); + case KeyDerivationPrf.Sha512: + return new HMACSHA512(passwordBytes); + default: + throw CryptoUtil.Fail("Unrecognized PRF."); + } + } + finally + { + // The HMAC ctor makes a duplicate of this key; we clear original buffer to limit exposure to the GC. + Array.Clear(passwordBytes, 0, passwordBytes.Length); + } + } + + private static void XorBuffers(byte[] src, byte[] dest) + { + // Note: dest buffer is mutated. + Debug.Assert(src.Length == dest.Length); + for (int i = 0; i < src.Length; i++) + { + dest[i] ^= src[i]; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs new file mode 100644 index 0000000000..1af12b4bdc --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2/Pbkdf2Util.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; + +namespace Microsoft.AspNet.Security.DataProtection.Cng.PBKDF2 +{ + /// + /// Internal base class used for abstracting away the PBKDF2 implementation since the implementation is OS-specific. + /// + internal static class Pbkdf2Util + { + public static readonly IPbkdf2Provider Pbkdf2Provider = GetPbkdf2Provider(); + public static readonly UTF8Encoding SecureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); + + private static IPbkdf2Provider GetPbkdf2Provider() + { + // In priority order, our three implementations are Win8, Win7, and "other". + + // TODO: Provide Win7 & Win8 implementations when the new DataProtection stack is fully copied over. + return new ManagedPbkdf2Provider(); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/project.json b/src/Microsoft.AspNet.Security.DataProtection/project.json index 66ba19caba..b2e6788017 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/project.json +++ b/src/Microsoft.AspNet.Security.DataProtection/project.json @@ -18,6 +18,7 @@ "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.InteropServices": "4.0.20.0", "System.Security.Cryptography.Encryption": "4.0.0.0", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0", "System.Text.Encoding.Extensions": "4.0.10.0" } }