From 1959aa9e7fd01407a639c654ef38d7d36d9d961b Mon Sep 17 00:00:00 2001 From: GrabYourPitchforks Date: Tue, 15 Apr 2014 18:03:41 -0700 Subject: [PATCH] Merge from internal DataProtection repo. --- .../Algorithms.cs | 27 +-- .../BCRYPT_KEY_DATA_BLOB_HEADER.cs | 2 +- .../BCryptAlgorithmFlags.cs | 2 +- .../BCryptAlgorithmHandle.cs | 2 +- .../BCryptBuffer.cs | 2 +- .../BCryptBufferDesc.cs | 2 +- .../BCryptEncryptFlags.cs | 2 +- .../BCryptGenRandomFlags.cs | 2 +- .../BCryptHashHandle.cs | 2 +- .../BCryptKeyDerivationBufferType.cs | 2 +- .../BCryptKeyHandle.cs | 2 +- .../BCryptUtil.cs | 147 +++++++------- .../Constants.cs | 2 +- .../CryptographicException.cs | 15 -- .../DATA_BLOB.cs | 3 +- .../DataProtectionProvider.cs | 58 +++--- .../DataProtectionProviderImpl.cs | 18 +- .../DataProtectorImpl.cs | 68 ++++--- .../DpapiDataProtectionProviderImpl.cs | 8 +- .../DpapiDataProtectorImpl.cs | 30 ++- .../IDataProtectionProvider.cs | 5 +- .../IDataProtector.cs | 2 +- ...osoft.AspNet.Security.DataProtection.kproj | 7 +- .../PBKDF2.cs | 65 ------ .../SP800_108Helper.cs | 190 ++++++++++++++++++ .../SafeLibraryHandle.cs | 121 +++++++++++ ...ssUnmanagedCodeSecurityAttribute - Copy.cs | 10 + .../UnsafeNativeMethods.cs | 37 ++-- .../Util/BufferUtil.cs | 78 ++++--- .../Util/ByteArrayExtensions.cs | 23 +++ .../Util/MemoryUtil.cs | 20 ++ .../project.json | 5 +- 32 files changed, 647 insertions(+), 312 deletions(-) delete mode 100644 src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs delete mode 100644 src/Microsoft.AspNet.Security.DataProtection/PBKDF2.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs create mode 100644 src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs diff --git a/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs b/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs index 0af410cea5..a2e3589613 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs @@ -3,12 +3,11 @@ using System.Security.Cryptography; namespace Microsoft.AspNet.Security.DataProtection { - internal static unsafe class Algorithms + internal unsafe static class Algorithms { public static readonly BCryptAlgorithmHandle AESAlgorithmHandle = CreateAESAlgorithmHandle(); public static readonly BCryptAlgorithmHandle HMACSHA256AlgorithmHandle = CreateHMACSHA256AlgorithmHandle(); public static readonly BCryptAlgorithmHandle HMACSHA512AlgorithmHandle = CreateHMACSHA512AlgorithmHandle(); - public static readonly BCryptAlgorithmHandle SP800108AlgorithmHandle = CreateSP800108AlgorithmHandle(); private static BCryptAlgorithmHandle CreateAESAlgorithmHandle() { @@ -23,7 +22,7 @@ namespace Microsoft.AspNet.Security.DataProtection // change it to use CBC chaining; it already uses PKCS7 padding by default fixed (char* pCbcMode = Constants.BCRYPT_CHAIN_MODE_CBC) { - status = UnsafeNativeMethods.BCryptSetProperty(algHandle, Constants.BCRYPT_CHAINING_MODE, (IntPtr) pCbcMode, (uint) ((Constants.BCRYPT_CHAIN_MODE_CBC.Length + 1 /* trailing null */)*sizeof (char)), dwFlags: 0); + status = UnsafeNativeMethods.BCryptSetProperty(algHandle, Constants.BCRYPT_CHAINING_MODE, (IntPtr)pCbcMode, (uint)((Constants.BCRYPT_CHAIN_MODE_CBC.Length + 1 /* trailing null */) * sizeof(char)), dwFlags: 0); } if (status != 0) { @@ -32,11 +31,11 @@ namespace Microsoft.AspNet.Security.DataProtection return algHandle; } - - internal static BCryptAlgorithmHandle CreateGenericHMACHandleFromPrimitiveProvider(string algorithmName) + private static BCryptAlgorithmHandle CreateHMACSHA256AlgorithmHandle() { + // create the HMACSHA-256 instance BCryptAlgorithmHandle algHandle; - int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, algorithmName, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: BCryptAlgorithmFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG); + int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_SHA256_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: BCryptAlgorithmFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG); if (status != 0 || algHandle == null || algHandle.IsInvalid) { throw new CryptographicException(status); @@ -45,23 +44,11 @@ namespace Microsoft.AspNet.Security.DataProtection return algHandle; } - private static BCryptAlgorithmHandle CreateHMACSHA256AlgorithmHandle() - { - // create the HMACSHA-256 instance - return CreateGenericHMACHandleFromPrimitiveProvider(Constants.BCRYPT_SHA256_ALGORITHM); - } - private static BCryptAlgorithmHandle CreateHMACSHA512AlgorithmHandle() { // create the HMACSHA-512 instance - return CreateGenericHMACHandleFromPrimitiveProvider(Constants.BCRYPT_SHA512_ALGORITHM); - } - - private static BCryptAlgorithmHandle CreateSP800108AlgorithmHandle() - { - // create the SP800-108 instance BCryptAlgorithmHandle algHandle; - int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_SP800108_CTR_HMAC_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: 0); + int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_SHA512_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: BCryptAlgorithmFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG); if (status != 0 || algHandle == null || algHandle.IsInvalid) { throw new CryptographicException(status); @@ -70,4 +57,4 @@ namespace Microsoft.AspNet.Security.DataProtection return algHandle; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs b/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs index 3bc50731df..ffb5f32308 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs @@ -23,4 +23,4 @@ namespace Microsoft.AspNet.Security.DataProtection pHeader.dwVersion = BCRYPT_KEY_DATA_BLOB_VERSION1; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs index 25bbe91cfa..a4c2e5b927 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs @@ -10,4 +10,4 @@ namespace Microsoft.AspNet.Security.DataProtection BCRYPT_CAPI_AES_FLAG = 0x00000010, BCRYPT_HASH_REUSABLE_FLAG = 0x00000020, } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs index f5a54fa9f8..0824fb3aab 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs @@ -17,4 +17,4 @@ namespace Microsoft.AspNet.Security.DataProtection return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs index 0a73118bbb..20f3305c18 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs @@ -12,4 +12,4 @@ namespace Microsoft.AspNet.Security.DataProtection public BCryptKeyDerivationBufferType BufferType; // Buffer type public IntPtr pvBuffer; // Pointer to buffer } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs index 32eed76657..4ed446cb39 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs @@ -20,4 +20,4 @@ namespace Microsoft.AspNet.Security.DataProtection bufferDesc.ulVersion = BCRYPTBUFFER_VERSION; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs index dfc3d86d71..491e2ab9bd 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs @@ -8,4 +8,4 @@ namespace Microsoft.AspNet.Security.DataProtection { BCRYPT_BLOCK_PADDING = 0x00000001, } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs index 8fdce726b2..6c17410e5d 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs @@ -9,4 +9,4 @@ namespace Microsoft.AspNet.Security.DataProtection BCRYPT_RNG_USE_ENTROPY_IN_BUFFER = 0x00000001, BCRYPT_USE_SYSTEM_PREFERRED_RNG = 0x00000002, } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs index ab59352f9d..c20e2e785f 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs @@ -17,4 +17,4 @@ namespace Microsoft.AspNet.Security.DataProtection return (UnsafeNativeMethods.BCryptDestroyHash(handle) == 0); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs index 143c811529..11fe12af50 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs @@ -23,4 +23,4 @@ namespace Microsoft.AspNet.Security.DataProtection KDF_SALT = 0xF, KDF_ITERATION_COUNT = 0x10, } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs index 16ab238e5a..491b807dd0 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs @@ -17,4 +17,4 @@ namespace Microsoft.AspNet.Security.DataProtection return (UnsafeNativeMethods.BCryptDestroyKey(handle) == 0); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs index 8d1cfc4884..0ec7d478f1 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs @@ -1,13 +1,21 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; +using System.Text; using Microsoft.AspNet.Security.DataProtection.Util; namespace Microsoft.AspNet.Security.DataProtection { - internal static unsafe class BCryptUtil + internal unsafe static class BCryptUtil { + // from dpapi.h + const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; + const uint CRYPTPROTECTMEMORY_SAME_PROCESS = 0x00; + + private static readonly UTF8Encoding _secureUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + // constant-time buffer comparison [MethodImpl(MethodImplOptions.NoOptimization)] public static bool BuffersAreEqualSecure(byte* p1, byte* p2, uint count) @@ -23,22 +31,22 @@ namespace Microsoft.AspNet.Security.DataProtection [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CheckOverflowUnderflow(int input) { - var unused = checked((uint) input); + var unused = checked((uint)input); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CheckOverflowUnderflow(uint input) { - var unused = checked((int) input); + var unused = checked((int)input); } - // helper function to wrap BCryptCreateHash - public static BCryptHashHandle CreateHash(BCryptAlgorithmHandle algorithmHandle, byte* key, int keyLengthInBytes) + // helper function to wrap BCryptCreateHash, passing in a key used for HMAC + public static BCryptHashHandle CreateHMACHandle(BCryptAlgorithmHandle algorithmHandle, byte* key, int keyLengthInBytes) { CheckOverflowUnderflow(keyLengthInBytes); BCryptHashHandle retVal; - int status = UnsafeNativeMethods.BCryptCreateHash(algorithmHandle, out retVal, IntPtr.Zero, 0, key, (uint) keyLengthInBytes, dwFlags: 0); + int status = UnsafeNativeMethods.BCryptCreateHash(algorithmHandle, out retVal, IntPtr.Zero, 0, key, (uint)keyLengthInBytes, dwFlags: 0); if (status != 0 || retVal == null || retVal.IsInvalid) { throw new CryptographicException(status); @@ -61,24 +69,24 @@ namespace Microsoft.AspNet.Security.DataProtection throw new InvalidOperationException(); } byte* pDuplicatedIV = stackalloc byte[ivLength]; - BufferUtil.BlockCopy(from: (IntPtr) iv, to: (IntPtr) pDuplicatedIV, byteCount: ivLength); + BufferUtil.BlockCopy(from: iv, to: pDuplicatedIV, byteCount: ivLength); uint retVal; - int status = UnsafeNativeMethods.BCryptDecrypt(keyHandle, input, (uint) inputLength, IntPtr.Zero, pDuplicatedIV, (uint) ivLength, output, (uint) outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + int status = UnsafeNativeMethods.BCryptDecrypt(keyHandle, input, (uint)inputLength, IntPtr.Zero, pDuplicatedIV, (uint)ivLength, output, (uint)outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); if (status != 0) { throw new CryptographicException(status); } - return checked((int) retVal); + return checked((int)retVal); } // helper function to wrap BCryptKeyDerivation using SP800-108-CTR-HMAC-SHA512 - public static void DeriveKeysSP800108(BCryptAlgorithmHandle kdfAlgorithmHandle, BCryptKeyHandle keyHandle, string purpose, BCryptAlgorithmHandle encryptionAlgorithmHandle, out BCryptKeyHandle encryptionKeyHandle, BCryptAlgorithmHandle hashAlgorithmHandle, out BCryptHashHandle hmacHandle, out BCryptKeyHandle kdfKeyHandle) + public static void DeriveKeysSP800108(byte[] protectedKdk, string purpose, BCryptAlgorithmHandle encryptionAlgorithmHandle, out BCryptKeyHandle encryptionKeyHandle, BCryptAlgorithmHandle hashAlgorithmHandle, out BCryptHashHandle hmacHandle, out byte[] kdfSubkey) { - const int ENCRYPTION_KEY_SIZE_IN_BYTES = 256/8; - const int HMAC_KEY_SIZE_IN_BYTES = 256/8; - const int KDF_SUBKEY_SIZE_IN_BYTES = 512/8; + const int ENCRYPTION_KEY_SIZE_IN_BYTES = 256 / 8; + const int HMAC_KEY_SIZE_IN_BYTES = 256 / 8; + const int KDF_SUBKEY_SIZE_IN_BYTES = 512 / 8; const int TOTAL_NUM_BYTES_TO_DERIVE = ENCRYPTION_KEY_SIZE_IN_BYTES + HMAC_KEY_SIZE_IN_BYTES + KDF_SUBKEY_SIZE_IN_BYTES; // keep our buffers on the stack while we're generating key material @@ -87,50 +95,27 @@ namespace Microsoft.AspNet.Security.DataProtection byte* pNewHmacKey = &pNewEncryptionKey[ENCRYPTION_KEY_SIZE_IN_BYTES]; byte* pNewKdfSubkey = &pNewHmacKey[HMAC_KEY_SIZE_IN_BYTES]; - try + protectedKdk = (byte[])protectedKdk.Clone(); // CryptUnprotectMemory mutates its input, so we preserve the original + fixed (byte* pKdk = protectedKdk) { - fixed (char* pszPrfAlgorithmName = Constants.BCRYPT_SHA512_ALGORITHM) + try { - // Create a buffer to hold the hash algorithm name, currently hardcoded to HMACSHA512 - uint numBuffers = 1; - BCryptBuffer* pBCryptBuffers = stackalloc BCryptBuffer[2]; - pBCryptBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM; - pBCryptBuffers[0].pvBuffer = (IntPtr) pszPrfAlgorithmName; - pBCryptBuffers[0].cbBuffer = (uint) ((Constants.BCRYPT_SHA512_ALGORITHM.Length + 1)*sizeof (char)); // per http://msdn.microsoft.com/en-us/library/windows/desktop/aa375368(v=vs.85).aspx, need to include terminating null - fixed (char* pszPurpose = (String.IsNullOrEmpty(purpose) ? (string) null : purpose)) - { - // Create a buffer to hold the purpose string if it is specified (we'll treat it as UTF-16LE) - if (pszPurpose != null) - { - numBuffers = 2; - pBCryptBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_LABEL; - pBCryptBuffers[1].pvBuffer = (IntPtr) pszPurpose; - pBCryptBuffers[1].cbBuffer = checked((uint) (purpose.Length*sizeof (char))); - } + // Since the KDK is pinned, the GC won't move around the array containing the plaintext key before we + // have the opportunity to clear its contents. + UnprotectMemoryWithinThisProcess(pKdk, (uint)protectedKdk.Length); - // .. and the header .. - BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); - BCryptBufferDesc.Initialize(ref bufferDesc); - bufferDesc.cBuffers = numBuffers; - bufferDesc.pBuffers = pBCryptBuffers; + byte[] purposeBytes = (!String.IsNullOrEmpty(purpose)) ? _secureUtf8Encoding.GetBytes(purpose) : null; + SP800_108Helper.DeriveKeys(pKdk, protectedKdk.Length, purposeBytes, pBuffer, TOTAL_NUM_BYTES_TO_DERIVE); - uint numBytesDerived; - int status = UnsafeNativeMethods.BCryptKeyDerivation(keyHandle, &bufferDesc, pBuffer, TOTAL_NUM_BYTES_TO_DERIVE, out numBytesDerived, dwFlags: 0); - if (status != 0 || numBytesDerived != TOTAL_NUM_BYTES_TO_DERIVE) - { - throw new CryptographicException(status); - } - } + // Split into AES, HMAC, and KDF subkeys + encryptionKeyHandle = ImportKey(encryptionAlgorithmHandle, pNewEncryptionKey, ENCRYPTION_KEY_SIZE_IN_BYTES); + hmacHandle = CreateHMACHandle(hashAlgorithmHandle, pNewHmacKey, HMAC_KEY_SIZE_IN_BYTES); + kdfSubkey = BufferUtil.ToProtectedManagedByteArray(pNewKdfSubkey, KDF_SUBKEY_SIZE_IN_BYTES); + } + finally + { + BufferUtil.SecureZeroMemory(pKdk, protectedKdk.Length); } - - // At this point, we have all the bytes we need. - encryptionKeyHandle = ImportKey(encryptionAlgorithmHandle, pNewEncryptionKey, ENCRYPTION_KEY_SIZE_IN_BYTES); - hmacHandle = CreateHash(hashAlgorithmHandle, pNewHmacKey, HMAC_KEY_SIZE_IN_BYTES); - kdfKeyHandle = ImportKey(kdfAlgorithmHandle, pNewKdfSubkey, KDF_SUBKEY_SIZE_IN_BYTES); - } - finally - { - BufferUtil.ZeroMemory(pBuffer, TOTAL_NUM_BYTES_TO_DERIVE); } } @@ -161,16 +146,16 @@ namespace Microsoft.AspNet.Security.DataProtection throw new InvalidOperationException(); } byte* pDuplicatedIV = stackalloc byte[ivLength]; - BufferUtil.BlockCopy(from: (IntPtr) iv, to: (IntPtr) pDuplicatedIV, byteCount: ivLength); + BufferUtil.BlockCopy(from: iv, to: pDuplicatedIV, byteCount: ivLength); uint retVal; - int status = UnsafeNativeMethods.BCryptEncrypt(keyHandle, input, (uint) inputLength, IntPtr.Zero, pDuplicatedIV, (uint) ivLength, output, (uint) outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); + int status = UnsafeNativeMethods.BCryptEncrypt(keyHandle, input, (uint)inputLength, IntPtr.Zero, pDuplicatedIV, (uint)ivLength, output, (uint)outputLength, out retVal, BCryptEncryptFlags.BCRYPT_BLOCK_PADDING); if (status != 0) { throw new CryptographicException(status); } - return checked((int) retVal); + return checked((int)retVal); } // helper function to take a key, apply a purpose, and generate a new subkey ("entropy") for DPAPI-specific scenarios @@ -183,7 +168,7 @@ namespace Microsoft.AspNet.Security.DataProtection BCryptHashHandle hashHandle; fixed (byte* pPreviousKey = previousKey) { - hashHandle = CreateHash(Algorithms.HMACSHA256AlgorithmHandle, pPreviousKey, previousKey.Length); + hashHandle = CreateHMACHandle(Algorithms.HMACSHA256AlgorithmHandle, pPreviousKey, previousKey.Length); } // hash the purpose string, treating it as UTF-16LE @@ -206,7 +191,7 @@ namespace Microsoft.AspNet.Security.DataProtection { CheckOverflowUnderflow(bufferBytes); - int status = UnsafeNativeMethods.BCryptGenRandom(IntPtr.Zero, buffer, (uint) bufferBytes, BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG); + int status = UnsafeNativeMethods.BCryptGenRandom(IntPtr.Zero, buffer, (uint)bufferBytes, BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG); if (status != 0) { throw new CryptographicException(status); @@ -219,13 +204,13 @@ namespace Microsoft.AspNet.Security.DataProtection CheckOverflowUnderflow(inputBytes); CheckOverflowUnderflow(outputBytes); - int status = UnsafeNativeMethods.BCryptHashData(hashHandle, input, (uint) inputBytes, dwFlags: 0); + int status = UnsafeNativeMethods.BCryptHashData(hashHandle, input, (uint)inputBytes, dwFlags: 0); if (status != 0) { throw new CryptographicException(status); } - status = UnsafeNativeMethods.BCryptFinishHash(hashHandle, output, (uint) outputBytes, dwFlags: 0); + status = UnsafeNativeMethods.BCryptFinishHash(hashHandle, output, (uint)outputBytes, dwFlags: 0); if (status != 0) { throw new CryptographicException(status); @@ -238,7 +223,7 @@ namespace Microsoft.AspNet.Security.DataProtection CheckOverflowUnderflow(keyBytes); byte[] heapAllocatedKeyDataBlob = null; - int numBytesRequiredForKeyDataBlob = checked(keyBytes + sizeof (BCRYPT_KEY_DATA_BLOB_HEADER)); + int numBytesRequiredForKeyDataBlob = checked(keyBytes + sizeof(BCRYPT_KEY_DATA_BLOB_HEADER)); if (numBytesRequiredForKeyDataBlob > Constants.MAX_STACKALLOC_BYTES) { heapAllocatedKeyDataBlob = new byte[numBytesRequiredForKeyDataBlob]; // allocate on heap if we cannot allocate on stack @@ -248,28 +233,28 @@ namespace Microsoft.AspNet.Security.DataProtection BCryptKeyHandle retVal; fixed (byte* pHeapAllocatedKeyDataBlob = heapAllocatedKeyDataBlob) { - // The header is first - BCRYPT_KEY_DATA_BLOB_HEADER* pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*) pHeapAllocatedKeyDataBlob; + // The header is first; if it wasn't heap-allocated we can stack-allocate now + BCRYPT_KEY_DATA_BLOB_HEADER* pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*)pHeapAllocatedKeyDataBlob; if (pKeyDataBlobHeader == null) { byte* temp = stackalloc byte[numBytesRequiredForKeyDataBlob]; // won't be released until frame pops - pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*) temp; + pKeyDataBlobHeader = (BCRYPT_KEY_DATA_BLOB_HEADER*)temp; } BCRYPT_KEY_DATA_BLOB_HEADER.Initialize(ref *pKeyDataBlobHeader); - pKeyDataBlobHeader->cbKeyData = (uint) keyBytes; + pKeyDataBlobHeader->cbKeyData = (uint)keyBytes; // the raw material immediately follows the header - byte* pKeyDataRawMaterial = (byte*) (&pKeyDataBlobHeader[1]); + byte* pKeyDataRawMaterial = (byte*)(&pKeyDataBlobHeader[1]); try { - BufferUtil.BlockCopy(from: (IntPtr) key, to: (IntPtr) pKeyDataRawMaterial, byteCount: keyBytes); - status = UnsafeNativeMethods.BCryptImportKey(algHandle, IntPtr.Zero, Constants.BCRYPT_KEY_DATA_BLOB, out retVal, IntPtr.Zero, 0, (byte*) pKeyDataBlobHeader, (uint) numBytesRequiredForKeyDataBlob, dwFlags: 0); + BufferUtil.BlockCopy(from: key, to: pKeyDataRawMaterial, byteCount: keyBytes); + status = UnsafeNativeMethods.BCryptImportKey(algHandle, IntPtr.Zero, Constants.BCRYPT_KEY_DATA_BLOB, out retVal, IntPtr.Zero, 0, (byte*)pKeyDataBlobHeader, (uint)numBytesRequiredForKeyDataBlob, dwFlags: 0); } finally { // zero out the key we just copied - BufferUtil.ZeroMemory(pKeyDataRawMaterial, keyBytes); + BufferUtil.SecureZeroMemory(pKeyDataRawMaterial, keyBytes); } } @@ -279,5 +264,29 @@ namespace Microsoft.AspNet.Security.DataProtection } return retVal; } + + internal static void ProtectMemoryWithinThisProcess(byte* pBuffer, uint bufferLength) + { + Debug.Assert(pBuffer != null); + Debug.Assert(bufferLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "Input buffer size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE."); + + bool success = UnsafeNativeMethods.CryptProtectMemory(pBuffer, bufferLength, CRYPTPROTECTMEMORY_SAME_PROCESS); + if (!success) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + } + + internal static void UnprotectMemoryWithinThisProcess(byte* pBuffer, uint bufferLength) + { + Debug.Assert(pBuffer != null); + Debug.Assert(bufferLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0, "Input buffer size must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE."); + + bool success = UnsafeNativeMethods.CryptUnprotectMemory(pBuffer, bufferLength, CRYPTPROTECTMEMORY_SAME_PROCESS); + if (!success) + { + throw new CryptographicException(Marshal.GetLastWin32Error()); + } + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Constants.cs b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs index 059590c7e5..1cba9e94bc 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Constants.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs @@ -82,4 +82,4 @@ namespace Microsoft.AspNet.Security.DataProtection internal const string BCRYPT_CHAIN_MODE_CCM = "ChainingModeCCM"; internal const string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM"; } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs b/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs deleted file mode 100644 index 19e72c0c2f..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -#if !NET45 -namespace System.Security.Cryptography { - internal sealed class CryptographicException : Exception { - internal CryptographicException(string message) - : base(message) { - - } - - internal CryptographicException(int unused) { - } - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs index 55bfc7ea8e..73a4cb5263 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Microsoft.AspNet.Security.DataProtection @@ -11,4 +10,4 @@ namespace Microsoft.AspNet.Security.DataProtection public uint cbData; public byte* pbData; } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs index fc011d8c0a..2babfa5008 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs @@ -1,41 +1,38 @@ using System; using System.Globalization; -using System.Reflection; +using System.Text; +using Microsoft.AspNet.Security.DataProtection; using Microsoft.AspNet.Security.DataProtection.Util; namespace Microsoft.AspNet.Security.DataProtection { - public static unsafe class DataProtectionProvider + /// + /// Provides methods for creating IDataProtectionProvider instances. + /// + public unsafe static class DataProtectionProvider { - private const int MASTER_KEY_REQUIRED_LENGTH = 512/8; + const int MASTER_KEY_REQUIRED_LENGTH = 512 / 8; - private static readonly byte[] MASTER_SUBKEY_GENERATOR = GetMasterSubkeyGenerator(); - private static readonly byte[] MASTER_DPAPI_ENTROPY = GetMasterSubkeyGenerator(isDpapi: true); + private static readonly byte[] MASTER_SUBKEY_GENERATOR = Encoding.ASCII.GetBytes("Microsoft.AspNet.Security.DataProtection"); - private static byte[] GetMasterSubkeyGenerator(bool isDpapi = false) + /// + /// Creates a new IDataProtectionProvider backed by DPAPI, where the protected + /// payload can only be decrypted by the current user. + /// + public static IDataProtectionProvider CreateFromDpapi() { - TypeInfo typeInfo = ((isDpapi) ? typeof(DpapiDataProtectionProviderImpl) : typeof(DataProtectionProvider)).GetTypeInfo(); - - byte[] retVal = new byte[sizeof (Guid)*2]; - fixed (byte* pRetVal = retVal) - { - Guid* guids = (Guid*) pRetVal; - guids[0] = typeInfo.GUID; -#if NET45 - guids[1] = typeInfo.Module.ModuleVersionId; -#else - guids[1] = default(Guid); -#endif - } - return retVal; + return CreateFromDpapi(protectToLocalMachine: false); } /// /// Creates a new IDataProtectionProvider backed by DPAPI. /// - public static IDataProtectionProvider CreateFromDpapi() + /// True if protected payloads can be decrypted by any user + /// on the local machine, false if protected payloads should only be able to decrypted by the + /// current user account. + public static IDataProtectionProvider CreateFromDpapi(bool protectToLocalMachine) { - return new DpapiDataProtectionProviderImpl(MASTER_DPAPI_ENTROPY); + return new DpapiDataProtectionProviderImpl(MASTER_SUBKEY_GENERATOR, protectToLocalMachine); } /// @@ -51,7 +48,7 @@ namespace Microsoft.AspNet.Security.DataProtection } finally { - BufferUtil.ZeroMemory(masterKey, MASTER_KEY_REQUIRED_LENGTH); + BufferUtil.SecureZeroMemory(masterKey, MASTER_KEY_REQUIRED_LENGTH); } } @@ -82,17 +79,20 @@ namespace Microsoft.AspNet.Security.DataProtection byte* masterSubkey = stackalloc byte[MASTER_KEY_REQUIRED_LENGTH]; try { - using (var hashHandle = BCryptUtil.CreateHash(Algorithms.HMACSHA512AlgorithmHandle, masterKey, masterKeyLengthInBytes)) + using (var hashHandle = BCryptUtil.CreateHMACHandle(Algorithms.HMACSHA512AlgorithmHandle, masterKey, masterKeyLengthInBytes)) { - BCryptUtil.HashData(hashHandle, masterKey, masterKeyLengthInBytes, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + fixed (byte* pMasterSubkeyGenerator = MASTER_SUBKEY_GENERATOR) + { + BCryptUtil.HashData(hashHandle, pMasterSubkeyGenerator, MASTER_SUBKEY_GENERATOR.Length, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + } } - BCryptKeyHandle kdfSubkeyHandle = BCryptUtil.ImportKey(Algorithms.SP800108AlgorithmHandle, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); - return new DataProtectionProviderImpl(kdfSubkeyHandle); + byte[] protectedKdk = BufferUtil.ToProtectedManagedByteArray(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + return new DataProtectionProviderImpl(protectedKdk); } finally { - BufferUtil.ZeroMemory(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + BufferUtil.SecureZeroMemory(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs index 2165b21fed..97f0b7f895 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs @@ -2,28 +2,28 @@ namespace Microsoft.AspNet.Security.DataProtection { - internal sealed unsafe class DataProtectionProviderImpl : IDataProtectionProvider + internal unsafe sealed class DataProtectionProviderImpl : IDataProtectionProvider { - private readonly BCryptKeyHandle _kdfSubkeyHandle; + private readonly byte[] _protectedKdk; - public DataProtectionProviderImpl(BCryptKeyHandle kdfSubkeyHandle) + public DataProtectionProviderImpl(byte[] protectedKdk) { - _kdfSubkeyHandle = kdfSubkeyHandle; + _protectedKdk = protectedKdk; } public IDataProtector CreateProtector(string purpose) { BCryptKeyHandle newAesKeyHandle; BCryptHashHandle newHmacHashHandle; - BCryptKeyHandle newKdfSubkeyHandle; + byte[] newProtectedKdfSubkey; - BCryptUtil.DeriveKeysSP800108(Algorithms.SP800108AlgorithmHandle, _kdfSubkeyHandle, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newKdfSubkeyHandle); - return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newKdfSubkeyHandle); + BCryptUtil.DeriveKeysSP800108(_protectedKdk, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newProtectedKdfSubkey); + return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newProtectedKdfSubkey); } public void Dispose() { - _kdfSubkeyHandle.Dispose(); + // no-op: we hold no protected resources } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs index dec7c4c485..111e3118b8 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs @@ -5,29 +5,37 @@ using Microsoft.AspNet.Security.DataProtection.Util; namespace Microsoft.AspNet.Security.DataProtection { - internal sealed unsafe class DataProtectorImpl : IDataProtector + internal unsafe sealed class DataProtectorImpl : IDataProtector { - private const int AES_BLOCK_LENGTH_IN_BYTES = 128/8; - private const int MAC_LENGTH_IN_BYTES = 256/8; + private const int AES_BLOCK_LENGTH_IN_BYTES = 128 / 8; + private const int AES_IV_LENGTH_IN_BYTES = AES_BLOCK_LENGTH_IN_BYTES; + private const int MAC_LENGTH_IN_BYTES = 256 / 8; private readonly BCryptKeyHandle _aesKeyHandle; private readonly BCryptHashHandle _hmacHashHandle; - private readonly BCryptKeyHandle _kdfSubkeyHandle; + private readonly byte[] _protectedKdk; - public DataProtectorImpl(BCryptKeyHandle aesKeyHandle, BCryptHashHandle hmacHashHandle, BCryptKeyHandle kdfSubkeyHandle) + public DataProtectorImpl(BCryptKeyHandle aesKeyHandle, BCryptHashHandle hmacHashHandle, byte[] protectedKdk) { _aesKeyHandle = aesKeyHandle; _hmacHashHandle = hmacHashHandle; - _kdfSubkeyHandle = kdfSubkeyHandle; + _protectedKdk = protectedKdk; } - private static int CalculateTotalProtectedDataSize(int unprotectedDataSize) + private static int CalculateTotalProtectedDataSize(int unprotectedDataSizeInBytes) { - Debug.Assert(unprotectedDataSize >= 0); + Debug.Assert(unprotectedDataSizeInBytes >= 0); - // Calculates - int numFullBlocks = unprotectedDataSize/AES_BLOCK_LENGTH_IN_BYTES; - return checked(AES_BLOCK_LENGTH_IN_BYTES /* IV */+ (numFullBlocks + 1)*AES_BLOCK_LENGTH_IN_BYTES /* ciphertext w/ padding */+ MAC_LENGTH_IN_BYTES /* HMAC */); + checked + { + // Padding always rounds the block count up, never down. + // If the input size is already a multiple of the block length, a block is added. + int numBlocks = 1 + unprotectedDataSizeInBytes / AES_BLOCK_LENGTH_IN_BYTES; + return + AES_IV_LENGTH_IN_BYTES /* IV */ + + numBlocks * AES_BLOCK_LENGTH_IN_BYTES /* ciphertext with padding */ + + MAC_LENGTH_IN_BYTES /* MAC */; + } } private static CryptographicException CreateGenericCryptographicException() @@ -39,17 +47,16 @@ namespace Microsoft.AspNet.Security.DataProtection { BCryptKeyHandle newAesKeyHandle; BCryptHashHandle newHmacHashHandle; - BCryptKeyHandle newKdfSubkeyHandle; + byte[] newProtectedKdfSubkey; - BCryptUtil.DeriveKeysSP800108(Algorithms.SP800108AlgorithmHandle, _kdfSubkeyHandle, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newKdfSubkeyHandle); - return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newKdfSubkeyHandle); + BCryptUtil.DeriveKeysSP800108(_protectedKdk, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newProtectedKdfSubkey); + return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newProtectedKdfSubkey); } public void Dispose() { _aesKeyHandle.Dispose(); _hmacHashHandle.Dispose(); - _kdfSubkeyHandle.Dispose(); } public byte[] Protect(byte[] unprotectedData) @@ -66,14 +73,14 @@ namespace Microsoft.AspNet.Security.DataProtection { // first, generate a random IV for CBC mode encryption byte* pIV = pProtectedData; - BCryptUtil.GenRandom(pIV, AES_BLOCK_LENGTH_IN_BYTES); + BCryptUtil.GenRandom(pIV, AES_IV_LENGTH_IN_BYTES); // then, encrypt the plaintext contents - byte* pCiphertext = &pIV[AES_BLOCK_LENGTH_IN_BYTES]; - int expectedCiphertextLength = protectedData.Length - AES_BLOCK_LENGTH_IN_BYTES - MAC_LENGTH_IN_BYTES; - fixed (byte* pPlaintext = unprotectedData) + byte* pCiphertext = &pIV[AES_IV_LENGTH_IN_BYTES]; + int expectedCiphertextLength = protectedData.Length - AES_IV_LENGTH_IN_BYTES - MAC_LENGTH_IN_BYTES; + fixed (byte* pPlaintext = unprotectedData.AsFixed()) { - int actualCiphertextLength = BCryptUtil.EncryptWithPadding(_aesKeyHandle, pPlaintext, unprotectedData.Length, pIV, AES_BLOCK_LENGTH_IN_BYTES, pCiphertext, expectedCiphertextLength); + int actualCiphertextLength = BCryptUtil.EncryptWithPadding(_aesKeyHandle, pPlaintext, unprotectedData.Length, pIV, AES_IV_LENGTH_IN_BYTES, pCiphertext, expectedCiphertextLength); if (actualCiphertextLength != expectedCiphertextLength) { throw new InvalidOperationException("Unexpected error while encrypting data."); @@ -86,7 +93,7 @@ namespace Microsoft.AspNet.Security.DataProtection { // Use a cloned hash handle since IDataProtector instances could be singletons, but BCryptHashHandle instances contain // state hence aren't thread-safe. Our own perf testing shows that duplicating existing hash handles is very fast. - BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_BLOCK_LENGTH_IN_BYTES + expectedCiphertextLength, pMac, MAC_LENGTH_IN_BYTES); + BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_IV_LENGTH_IN_BYTES + expectedCiphertextLength, pMac, MAC_LENGTH_IN_BYTES); } } @@ -125,7 +132,7 @@ namespace Microsoft.AspNet.Security.DataProtection Debug.Assert(protectedData != null); // is the protected data even long enough to be valid? - if (protectedData.Length < AES_BLOCK_LENGTH_IN_BYTES /* IV */+ AES_BLOCK_LENGTH_IN_BYTES /* min ciphertext size = 1 block */+ MAC_LENGTH_IN_BYTES) + if (protectedData.Length < AES_IV_LENGTH_IN_BYTES /* IV */ + AES_BLOCK_LENGTH_IN_BYTES /* min ciphertext size = 1 block */ + MAC_LENGTH_IN_BYTES) { return null; } @@ -134,8 +141,8 @@ namespace Microsoft.AspNet.Security.DataProtection { // calculate pointer offsets byte* pIV = pProtectedData; - byte* pCiphertext = &pProtectedData[AES_BLOCK_LENGTH_IN_BYTES]; - int ciphertextLength = protectedData.Length - AES_BLOCK_LENGTH_IN_BYTES /* IV */- MAC_LENGTH_IN_BYTES /* MAC */; + byte* pCiphertext = &pProtectedData[AES_IV_LENGTH_IN_BYTES]; + int ciphertextLength = protectedData.Length - AES_IV_LENGTH_IN_BYTES /* IV */ - MAC_LENGTH_IN_BYTES /* MAC */; byte* pSuppliedMac = &pCiphertext[ciphertextLength]; // first, ensure that the MAC is valid @@ -143,7 +150,7 @@ namespace Microsoft.AspNet.Security.DataProtection using (var clonedHashHandle = BCryptUtil.DuplicateHash(_hmacHashHandle)) { // see comments in Protect(byte[]) for why we duplicate the hash - BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_BLOCK_LENGTH_IN_BYTES + ciphertextLength, pCalculatedMac, MAC_LENGTH_IN_BYTES); + BCryptUtil.HashData(clonedHashHandle, pProtectedData, AES_IV_LENGTH_IN_BYTES + ciphertextLength, pCalculatedMac, MAC_LENGTH_IN_BYTES); } if (!BCryptUtil.BuffersAreEqualSecure(pSuppliedMac, pCalculatedMac, MAC_LENGTH_IN_BYTES)) { @@ -168,18 +175,13 @@ namespace Microsoft.AspNet.Security.DataProtection pPlaintextBuffer = temp; } - int actualPlaintextLength = BCryptUtil.DecryptWithPadding(_aesKeyHandle, pCiphertext, ciphertextLength, pIV, AES_BLOCK_LENGTH_IN_BYTES, pPlaintextBuffer, plaintextBufferLength); + int actualPlaintextLength = BCryptUtil.DecryptWithPadding(_aesKeyHandle, pCiphertext, ciphertextLength, pIV, AES_IV_LENGTH_IN_BYTES, pPlaintextBuffer, plaintextBufferLength); Debug.Assert(actualPlaintextLength >= 0 && actualPlaintextLength < ciphertextLength); // truncate the return value to accomodate the plaintext size perfectly - byte[] retVal = new byte[actualPlaintextLength]; - fixed (byte* pRetVal = retVal) - { - BufferUtil.BlockCopy(from: (IntPtr) pPlaintextBuffer, to: (IntPtr) pRetVal, byteCount: actualPlaintextLength); - } - return retVal; + return BufferUtil.ToManagedByteArray(pPlaintextBuffer, actualPlaintextLength); } } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs index fa37a07bae..679a5b094a 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs @@ -6,16 +6,18 @@ namespace Microsoft.AspNet.Security.DataProtection internal sealed class DpapiDataProtectionProviderImpl : IDataProtectionProvider { private readonly byte[] _entropy; + private readonly bool _protectToLocalMachine; - public DpapiDataProtectionProviderImpl(byte[] entropy) + public DpapiDataProtectionProviderImpl(byte[] entropy, bool protectToLocalMachine) { Debug.Assert(entropy != null); _entropy = entropy; + _protectToLocalMachine = protectToLocalMachine; } public IDataProtector CreateProtector(string purpose) { - return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose)); + return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose), _protectToLocalMachine); } public void Dispose() @@ -23,4 +25,4 @@ namespace Microsoft.AspNet.Security.DataProtection // no-op; no unmanaged resources to dispose } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs index 13e5319e1a..66be4205a2 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs @@ -10,15 +10,19 @@ namespace Microsoft.AspNet.Security.DataProtection internal unsafe sealed class DpapiDataProtectorImpl : IDataProtector { // from dpapi.h + private const uint CRYPTPROTECT_LOCAL_MACHINE = 0x4; private const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1; // Used as the 'purposes' parameter to DPAPI operations private readonly byte[] _entropy; - public DpapiDataProtectorImpl(byte[] entropy) + private readonly bool _protectToLocalMachine; + + public DpapiDataProtectorImpl(byte[] entropy, bool protectToLocalMachine) { Debug.Assert(entropy != null); _entropy = entropy; + _protectToLocalMachine = protectToLocalMachine; } private static CryptographicException CreateGenericCryptographicException(bool isErrorDueToProfileNotLoaded = false) @@ -29,7 +33,7 @@ namespace Microsoft.AspNet.Security.DataProtection public IDataProtector CreateSubProtector(string purpose) { - return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose)); + return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose), _protectToLocalMachine); } public void Dispose() @@ -37,6 +41,18 @@ namespace Microsoft.AspNet.Security.DataProtection // no-op; no unmanaged resources to dispose } + private uint GetCryptProtectUnprotectFlags() + { + if (_protectToLocalMachine) + { + return CRYPTPROTECT_LOCAL_MACHINE | CRYPTPROTECT_UI_FORBIDDEN; + } + else + { + return CRYPTPROTECT_UI_FORBIDDEN; + } + } + public byte[] Protect(byte[] unprotectedData) { if (unprotectedData == null) @@ -52,14 +68,14 @@ namespace Microsoft.AspNet.Security.DataProtection try { bool success; - fixed (byte* pUnprotectedData = unprotectedData) + fixed (byte* pUnprotectedData = unprotectedData.AsFixed()) { fixed (byte* pEntropy = _entropy) { // no need for checked arithmetic here DATA_BLOB dataIn = new DATA_BLOB() { cbData = (uint)unprotectedData.Length, pbData = pUnprotectedData }; DATA_BLOB optionalEntropy = new DATA_BLOB() { cbData = (uint)_entropy.Length, pbData = pEntropy }; - success = UnsafeNativeMethods.CryptProtectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, CRYPTPROTECT_UI_FORBIDDEN, out dataOut); + success = UnsafeNativeMethods.CryptProtectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, GetCryptProtectUnprotectFlags(), out dataOut); } } @@ -104,14 +120,14 @@ namespace Microsoft.AspNet.Security.DataProtection try { bool success; - fixed (byte* pProtectedData = protectedData) + fixed (byte* pProtectedData = protectedData.AsFixed()) { fixed (byte* pEntropy = _entropy) { // no need for checked arithmetic here DATA_BLOB dataIn = new DATA_BLOB() { cbData = (uint)protectedData.Length, pbData = pProtectedData }; DATA_BLOB optionalEntropy = new DATA_BLOB() { cbData = (uint)_entropy.Length, pbData = pEntropy }; - success = UnsafeNativeMethods.CryptUnprotectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, CRYPTPROTECT_UI_FORBIDDEN, out dataOut); + success = UnsafeNativeMethods.CryptUnprotectData(&dataIn, IntPtr.Zero, &optionalEntropy, IntPtr.Zero, IntPtr.Zero, GetCryptProtectUnprotectFlags(), out dataOut); } } @@ -139,4 +155,4 @@ namespace Microsoft.AspNet.Security.DataProtection } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs index 99bc6e3285..eb529127b6 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs @@ -2,6 +2,9 @@ namespace Microsoft.AspNet.Security.DataProtection { + /// + /// A factory that can provide IDataProtector instances. + /// public interface IDataProtectionProvider : IDisposable { /// @@ -11,4 +14,4 @@ namespace Microsoft.AspNet.Security.DataProtection /// An IDataProtector. IDataProtector CreateProtector(string purpose); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs index 5577a3cbdb..f1f41dd81e 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs @@ -32,4 +32,4 @@ namespace Microsoft.AspNet.Security.DataProtection /// Throws CryptographicException if the protectedData parameter has been tampered with. byte[] Unprotect(byte[] protectedData); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Microsoft.AspNet.Security.DataProtection.kproj b/src/Microsoft.AspNet.Security.DataProtection/Microsoft.AspNet.Security.DataProtection.kproj index d68788f92f..9b57d1f1b4 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Microsoft.AspNet.Security.DataProtection.kproj +++ b/src/Microsoft.AspNet.Security.DataProtection/Microsoft.AspNet.Security.DataProtection.kproj @@ -23,6 +23,8 @@ + + @@ -34,7 +36,6 @@ - @@ -44,11 +45,13 @@ - + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2.cs b/src/Microsoft.AspNet.Security.DataProtection/PBKDF2.cs deleted file mode 100644 index c8824b37c1..0000000000 --- a/src/Microsoft.AspNet.Security.DataProtection/PBKDF2.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Security.Cryptography; - -namespace Microsoft.AspNet.Security.DataProtection -{ - /// - /// Helper class to derive keys from low-entropy passwords using the PBKDF2 algorithm. - /// - public static class PBKDF2 - { - /// - /// Derives a key from a low-entropy password. - /// - /// The name of the PRF to use for key derivation. - /// The low-entropy password from which to generate a key. - /// The salt used to randomize the key derivation. - /// The number of iterations to perform. - /// The desired byte length of the derived key. - /// A key derived from the provided password. - /// For compatibility with the Rfc2898DeriveBytes class, specify "SHA1" for the algorithmName parameter. - public unsafe static byte[] DeriveKey(string algorithmName, byte[] password, byte[] salt, ulong iterationCount, uint numBytesToDerive) - { - if (String.IsNullOrEmpty(algorithmName)) - { - throw new ArgumentException(Res.Common_NullOrEmpty, "algorithmName"); - } - if (password == null || password.Length == 0) - { - throw new ArgumentException(Res.Common_NullOrEmpty, "password"); - } - if (salt == null || salt.Length == 0) - { - throw new ArgumentException(Res.Common_NullOrEmpty, "salt"); - } - if (iterationCount <= 0) - { - throw new ArgumentOutOfRangeException("iterationCount"); - } - - byte[] derivedKey = new byte[numBytesToDerive]; - int status; - - using (BCryptAlgorithmHandle algHandle = Algorithms.CreateGenericHMACHandleFromPrimitiveProvider(algorithmName)) - { - fixed (byte* pPassword = password) - fixed (byte* pSalt = salt) - fixed (byte* pDerivedKey = derivedKey) - { - status = UnsafeNativeMethods.BCryptDeriveKeyPBKDF2( - algHandle, pPassword, (uint)password.Length, pSalt, (uint)salt.Length, iterationCount, - pDerivedKey, numBytesToDerive, dwFlags: 0); - } - } - - if (status == 0 /* STATUS_SUCCESS */) - { - return derivedKey; - } - else - { - throw new CryptographicException(status); - } - } - } -} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs b/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs new file mode 100644 index 0000000000..f08127f864 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SP800_108Helper.cs @@ -0,0 +1,190 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Util; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection +{ + /// + /// Provides an implementation of the SP800-108-CTR-HMACSHA512 key derivation function. + /// This class assumes at least Windows 7 / Server 2008 R2. + /// + /// + /// More info at http://csrc.nist.gov/publications/nistpubs/800-108/sp800-108.pdf, Sec. 5.1. + /// + internal unsafe static class SP800_108Helper + { + private const string BCRYPT_LIB = "bcrypt.dll"; + + [SuppressUnmanagedCodeSecurity] + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/hh448506(v=vs.85).aspx + private delegate int BCryptKeyDerivation( + [In] BCryptKeyHandle hKey, + [In] BCryptBufferDesc* pParameterList, + [In] byte* pbDerivedKey, + [In] uint cbDerivedKey, + [Out] out uint pcbResult, + [In] uint dwFlags); + + private static readonly BCryptAlgorithmHandle SP800108AlgorithmHandle; + private delegate void DeriveKeysDelegate(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength); + private static DeriveKeysDelegate _thunk = CreateThunk(out SP800108AlgorithmHandle); + + private static BCryptAlgorithmHandle CreateSP800108AlgorithmHandle() + { + // create the SP800-108 instance + BCryptAlgorithmHandle algHandle; + int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_SP800108_CTR_HMAC_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: 0); + if (status != 0 || algHandle == null || algHandle.IsInvalid) + { + throw new CryptographicException(status); + } + + return algHandle; + } + + private static DeriveKeysDelegate CreateThunk(out BCryptAlgorithmHandle sp800108AlgorithmHandle) + { + SafeLibraryHandle bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB); + var win8Thunk = bcryptLibHandle.GetProcAddress("BCryptKeyDerivation", throwIfNotFound: false); + if (win8Thunk != null) + { + // Permanently reference bcrypt.dll for the lifetime of the AppDomain. + // When the AD goes away the SafeLibraryHandle will automatically be released. + GCHandle.Alloc(bcryptLibHandle); + sp800108AlgorithmHandle = CreateSP800108AlgorithmHandle(); + return win8Thunk.DeriveKeysWin8; + } + else + { + sp800108AlgorithmHandle = null; + return DeriveKeysWin7; + } + } + + /// + /// Performs a key derivation using SP800-108-CTR-HMACSHA512. + /// + /// Pointer to the key derivation key. + /// Length (in bytes) of the key derivation key. + /// Purpose to attach to the generated subkey. Corresponds to the 'Label' parameter + /// in the KDF. May be null. + /// Pointer to a buffer which will receive the subkey. + /// Length (in bytes) of the output buffer. + public static void DeriveKeys(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) + { + _thunk(pKdk, kdkByteLength, purpose, pOutputBuffer, outputBufferByteLength); + } + + // Wraps our own SP800-108 implementation around bcrypt.dll primitives. + private static void DeriveKeysWin7(byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) + { + const int TEMP_RESULT_OUTPUT_BYTES = 512 / 8; // hardcoded to HMACSHA512 + + // NOTE: pOutputBuffer and outputBufferByteLength are modified as data is copied from temporary buffers + // to the final output buffer. + + // used to hold the output of the HMACSHA512 routine + byte* pTempResultBuffer = stackalloc byte[TEMP_RESULT_OUTPUT_BYTES]; + int purposeLength = (purpose != null) ? purpose.Length : 0; + + // this will be zero-inited + byte[] dataToBeHashed = new byte[checked( + sizeof(int) /* [i] */ + + purposeLength /* Label */ + + 1 /* 0x00 */ + + 0 /* Context */ + + sizeof(int) /* [L] */)]; + + fixed (byte* pDataToBeHashed = dataToBeHashed) + { + // Step 1: copy purpose into Label part of data to be hashed + if (purposeLength > 0) + { + fixed (byte* pPurpose = purpose) + { + BufferUtil.BlockCopy(from: pPurpose, to: &pDataToBeHashed[sizeof(int)], byteCount: purposeLength); + } + } + + // Step 2: copy [L] into last part of data to be hashed, big-endian + uint numBitsToGenerate = checked(outputBufferByteLength * 8); + MemoryUtil.UnalignedWriteBigEndian(&pDataToBeHashed[dataToBeHashed.Length - sizeof(int)], numBitsToGenerate); + + // Step 3: iterate until all desired bytes have been generated + for (int i = 1; outputBufferByteLength > 0; i++) + { + // Step 3a: Copy [i] into the first part of data to be hashed, big-endian + MemoryUtil.UnalignedWriteBigEndian(pDataToBeHashed, (uint)i); + + // Step 3b: Hash. Win7 doesn't allow reusing hash algorithm objects after the final hash + // has been computed, so we need to create a new instance of the hash object for each + // iteration. We don't bother with this optimization on Win8 since we call BCryptKeyDerivation + // instead when on that OS. + using (var hashHandle = BCryptUtil.CreateHMACHandle(Algorithms.HMACSHA512AlgorithmHandle, pKdk, kdkByteLength)) + { + BCryptUtil.HashData(hashHandle, pDataToBeHashed, dataToBeHashed.Length, pTempResultBuffer, TEMP_RESULT_OUTPUT_BYTES); + } + + // Step 3c: Copy bytes from the temporary buffer to the output buffer. + uint numBytesToCopy = Math.Min(outputBufferByteLength, (uint)TEMP_RESULT_OUTPUT_BYTES); + BufferUtil.BlockCopy(from: pTempResultBuffer, to: pOutputBuffer, byteCount: numBytesToCopy); + pOutputBuffer += numBytesToCopy; + outputBufferByteLength -= numBytesToCopy; + } + } + } + + // Calls into the Win8 implementation (bcrypt.dll) for the SP800-108 KDF + private static void DeriveKeysWin8(this BCryptKeyDerivation fnKeyDerivation, byte* pKdk, int kdkByteLength, byte[] purpose, byte* pOutputBuffer, uint outputBufferByteLength) + { + // Create a buffer to hold the hash algorithm name + fixed (char* pszPrfAlgorithmName = Constants.BCRYPT_SHA512_ALGORITHM) + { + BCryptBuffer* pBCryptBuffers = stackalloc BCryptBuffer[2]; + + // The first buffer should contain the PRF algorithm name (hardcoded to HMACSHA512). + // Per http://msdn.microsoft.com/en-us/library/aa375368(v=vs.85).aspx, cbBuffer must include the terminating null char. + pBCryptBuffers[0].BufferType = BCryptKeyDerivationBufferType.KDF_HASH_ALGORITHM; + pBCryptBuffers[0].pvBuffer = (IntPtr)pszPrfAlgorithmName; + pBCryptBuffers[0].cbBuffer = (uint)((Constants.BCRYPT_SHA512_ALGORITHM.Length + 1) * sizeof(char)); + uint numBuffers = 1; + + fixed (byte* pPurpose = ((purpose != null && purpose.Length != 0) ? purpose : null)) + { + if (pPurpose != null) + { + // The second buffer will hold the purpose bytes if they're specified. + pBCryptBuffers[1].BufferType = BCryptKeyDerivationBufferType.KDF_LABEL; + pBCryptBuffers[1].pvBuffer = (IntPtr)pPurpose; + pBCryptBuffers[1].cbBuffer = (uint)purpose.Length; + numBuffers = 2; + } + + // Add the header + BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); + BCryptBufferDesc.Initialize(ref bufferDesc); + bufferDesc.cBuffers = numBuffers; + bufferDesc.pBuffers = pBCryptBuffers; + + // Finally, perform the calculation and validate that the actual number of bytes derived matches + // the number that the caller requested. + uint numBytesDerived; + int status; + using (BCryptKeyHandle kdkHandle = BCryptUtil.ImportKey(SP800108AlgorithmHandle, pKdk, kdkByteLength)) + { + status = fnKeyDerivation(kdkHandle, &bufferDesc, pOutputBuffer, outputBufferByteLength, out numBytesDerived, dwFlags: 0); + } + if (status != 0 || numBytesDerived != outputBufferByteLength) + { + throw new CryptographicException(status); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs new file mode 100644 index 0000000000..7d1a131676 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeLibraryHandle.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; + +#if NET45 +using System.Runtime.ConstrainedExecution; +#endif + +namespace Microsoft.Win32.SafeHandles +{ + /// + /// Represents a handle to a Windows module (DLL). + /// + internal sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + // Called by P/Invoke when returning SafeHandles + private SafeLibraryHandle() + : base(ownsHandle: true) { } + + /// + /// Gets a delegate pointing to a given export from this library. + /// + public TDelegate GetProcAddress(string lpProcName, bool throwIfNotFound = true) where TDelegate : class + { + Debug.Assert(typeof(TDelegate).GetTypeInfo().IsSubclassOf(typeof(Delegate)), "TDelegate must be a delegate type!"); + + IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName); + if (pfnProc == IntPtr.Zero) + { + if (throwIfNotFound) + { + UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); + } + else + { + return null; + } + } + + return (TDelegate)(object)Marshal.GetDelegateForFunctionPointer(pfnProc, typeof(TDelegate)); + } + + /// + /// Forbids this library from being unloaded. The library will remain loaded until process termination, + /// regardless of how many times FreeLibrary is called. + /// + public void ForbidUnload() + { + // from winbase.h + const uint GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x00000004U; + const uint GET_MODULE_HANDLE_EX_FLAG_PIN = 0x00000001U; + + IntPtr unused; + bool retVal = UnsafeNativeMethods.GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, this, out unused); + if (!retVal) + { + UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); + } + } + + /// + /// Opens a library. If 'filename' is not a fully-qualified path, the default search path is used. + /// + public static SafeLibraryHandle Open(string filename) + { + SafeLibraryHandle handle = UnsafeNativeMethods.LoadLibrary(filename); + if (handle == null || handle.IsInvalid) + { + UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); + } + return handle; + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + return UnsafeNativeMethods.FreeLibrary(handle); + } + + [SuppressUnmanagedCodeSecurity] + private static class UnsafeNativeMethods + { + private const string KERNEL32_LIB = "kernel32.dll"; + + // http://msdn.microsoft.com/en-us/library/ms683152(v=vs.85).aspx + [return: MarshalAs(UnmanagedType.Bool)] +#if NET45 + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] +#endif + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] + internal static extern bool FreeLibrary(IntPtr hModule); + + // http://msdn.microsoft.com/en-us/library/ms683200(v=vs.85).aspx + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + internal static extern bool GetModuleHandleEx( + [In] uint dwFlags, + [In] SafeLibraryHandle lpModuleName, // can point to a location within the module if GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS is set + [Out] out IntPtr phModule); + + // http://msdn.microsoft.com/en-us/library/ms683212(v=vs.85).aspx + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern IntPtr GetProcAddress( + [In] SafeLibraryHandle hModule, + [In, MarshalAs(UnmanagedType.LPStr)] string lpProcName); + + // http://msdn.microsoft.com/en-us/library/ms684175(v=vs.85).aspx + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern SafeLibraryHandle LoadLibrary( + [In, MarshalAs(UnmanagedType.LPWStr)]string lpFileName); + + internal static void ThrowExceptionForLastWin32Error() + { + int hr = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR(hr); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs b/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs new file mode 100644 index 0000000000..18ed3b1c92 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SuppressUnmanagedCodeSecurityAttribute - Copy.cs @@ -0,0 +1,10 @@ +using System; +using System.Runtime.InteropServices; + +#if !NET45 +namespace System.Security +{ + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class SuppressUnmanagedCodeSecurityAttribute : Attribute { } +} +#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs index 611d1d4084..88e8eb961a 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs @@ -1,17 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Security; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNet.Security.DataProtection { -#if NET45 [SuppressUnmanagedCodeSecurity] -#endif - internal static unsafe class UnsafeNativeMethods + internal unsafe static class UnsafeNativeMethods { private const string BCRYPT_LIB = "bcrypt.dll"; private const string CRYPT32_LIB = "crypt32.dll"; @@ -52,19 +46,6 @@ namespace Microsoft.AspNet.Security.DataProtection [Out] out uint pcbResult, [In] BCryptEncryptFlags dwFlags); - [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] - // http://msdn.microsoft.com/en-us/library/windows/desktop/dd433795(v=vs.85).aspx - internal static extern int BCryptDeriveKeyPBKDF2( - [In] BCryptAlgorithmHandle hPrf, - [In] byte* pbPassword, - [In] uint cbPassword, - [In] byte* pbSalt, - [In] uint cbSalt, - [In] ulong cIterations, - [In] byte* pbDerivedKey, - [In] uint cbDerivedKey, - [In] uint dwFlags); - [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375399(v=vs.85).aspx internal static extern int BCryptDestroyHash( @@ -177,6 +158,13 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint dwFlags, [Out] out DATA_BLOB pDataOut); + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380262(v=vs.85).aspx + internal static extern bool CryptProtectMemory( + [In] byte* pData, + [In] uint cbData, + [In] uint dwFlags); + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380882(v=vs.85).aspx internal static extern bool CryptUnprotectData( @@ -188,6 +176,13 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint dwFlags, [Out] out DATA_BLOB pDataOut); + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380890(v=vs.85).aspx + internal static extern bool CryptUnprotectMemory( + [In] byte* pData, + [In] uint cbData, + [In] uint dwFlags); + /* * KERNEL32.DLL */ @@ -197,4 +192,4 @@ namespace Microsoft.AspNet.Security.DataProtection [In] IntPtr Destination, [In] UIntPtr /* SIZE_T */ Length); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs index e982d35f9e..443ead3f93 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs @@ -1,40 +1,67 @@ using System; using System.Runtime.CompilerServices; +using System.Security.Cryptography; namespace Microsoft.AspNet.Security.DataProtection.Util { - internal static unsafe class BufferUtil + internal unsafe static class BufferUtil { private static readonly byte[] _emptyArray = new byte[0]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BlockCopy(IntPtr from, IntPtr to, int byteCount) + public static void BlockCopy(void* from, void* to, int byteCount) { - BlockCopy(from, to, checked((uint) byteCount)); // will be checked before invoking the delegate + BlockCopy(from, to, checked((uint)byteCount)); // will be checked before invoking the delegate } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void BlockCopy(IntPtr from, IntPtr to, uint byteCount) + public static void BlockCopy(void* from, void* to, uint byteCount) { - BlockCopySlow((byte*) from, (byte*) to, byteCount); + if (byteCount != 0) + { +#if NET45 + BlockCopySlow((byte*)from, (byte*)to, byteCount); +#else + Buffer.MemoryCopy(source: from, destination: to, destinationSizeInBytes: byteCount, sourceBytesToCopy: byteCount); +#endif + } } +#if NET45 [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void BlockCopySlow(byte* from, byte* to, uint byteCount) + public static void BlockCopySlow(byte* from, byte* to, uint byteCount) { - // slow, but works while (byteCount-- != 0) { *(to++) = *(from++); } } +#endif + + /// + /// Securely clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SecureZeroMemory(byte* buffer, int byteCount) + { + SecureZeroMemory(buffer, checked((uint)byteCount)); + } + + /// + /// Securely clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SecureZeroMemory(byte* buffer, uint byteCount) + { + UnsafeNativeMethods.RtlZeroMemory((IntPtr)buffer, (UIntPtr)byteCount); + } /// /// Creates a new managed byte[] from unmanaged memory. /// public static byte[] ToManagedByteArray(byte* ptr, int byteCount) { - return ToManagedByteArray(ptr, checked((uint) byteCount)); + return ToManagedByteArray(ptr, checked((uint)byteCount)); } /// @@ -51,28 +78,33 @@ namespace Microsoft.AspNet.Security.DataProtection.Util byte[] bytes = new byte[byteCount]; fixed (byte* pBytes = bytes) { - BlockCopy(from: (IntPtr) ptr, to: (IntPtr) pBytes, byteCount: byteCount); + BlockCopy(from: ptr, to: pBytes, byteCount: byteCount); } return bytes; } } /// - /// Clears a memory buffer. + /// Creates a new managed byte[] from unmanaged memory. The returned value will be protected + /// by CryptProtectMemory. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ZeroMemory(byte* buffer, int byteCount) + public static byte[] ToProtectedManagedByteArray(byte* ptr, int byteCount) { - ZeroMemory(buffer, checked((uint) byteCount)); - } - - /// - /// Clears a memory buffer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ZeroMemory(byte* buffer, uint byteCount) - { - UnsafeNativeMethods.RtlZeroMemory((IntPtr) buffer, (UIntPtr) byteCount); // don't require 'checked': uint -> UIntPtr always guaranteed to succeed + byte[] bytes = new byte[byteCount]; + fixed (byte* pBytes = bytes) + { + try + { + BlockCopy(from: ptr, to: pBytes, byteCount: byteCount); + BCryptUtil.ProtectMemoryWithinThisProcess(pBytes, (uint)byteCount); + } + catch + { + SecureZeroMemory(pBytes, byteCount); + throw; + } + } + return bytes; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs new file mode 100644 index 0000000000..f3bfe2b5fe --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Util/ByteArrayExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics; + +namespace Microsoft.AspNet.Security.DataProtection.Util +{ + /// + /// Defines helper methods for working with fixed expression blocks. + /// + internal static class ByteArrayExtensions + { + private static readonly byte[] _dummyBuffer = new byte[1]; + + // Since the 'fixed' keyword turns a zero-length array into a pointer, we need + // to make sure we're always providing a buffer of length >= 1 so that the + // p/invoke methods we pass the pointers to don't see a null pointer. Callers + // are still responsible for passing a proper length to the p/invoke routines. + public static byte[] AsFixed(this byte[] buffer) + { + Debug.Assert(buffer != null); + return (buffer.Length != 0) ? buffer : _dummyBuffer; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs new file mode 100644 index 0000000000..0d2b727507 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Util/MemoryUtil.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Security.DataProtection.Util +{ + internal unsafe static class MemoryUtil + { + /// + /// Writes an Int32 to a potentially unaligned memory address, big-endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnalignedWriteBigEndian(byte* address, uint value) + { + *(address++) = (byte)(value >> 24); + *(address++) = (byte)(value >> 16); + *(address++) = (byte)(value >> 8); + *(address) = (byte)value; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/project.json b/src/Microsoft.AspNet.Security.DataProtection/project.json index e1784c0b4e..ef495793cb 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/project.json +++ b/src/Microsoft.AspNet.Security.DataProtection/project.json @@ -11,7 +11,10 @@ "System.Reflection": "4.0.10.0", "System.Resources.ResourceManager": "4.0.0.0", "System.Runtime": "4.0.20.0", - "System.Runtime.InteropServices": "4.0.20.0" + "System.Runtime.Extensions": "4.0.10.0", + "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Cryptography": "4.0.0.0", + "System.Text.Encoding.Extensions": "4.0.10.0" } } },