diff --git a/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs b/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs new file mode 100644 index 0000000000..850a7415f7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Algorithms.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Security.DataProtection { + + 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() { + // create the AES instance + BCryptAlgorithmHandle algHandle; + int status = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, Constants.BCRYPT_AES_ALGORITHM, Constants.MS_PRIMITIVE_PROVIDER, dwFlags: 0); + if (status != 0 || algHandle == null || algHandle.IsInvalid) { + throw new CryptographicException(status); + } + + // 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); + } + if (status != 0) { + throw new CryptographicException(status); + } + + return algHandle; + } + private static BCryptAlgorithmHandle CreateHMACSHA256AlgorithmHandle() { + // create the HMACSHA-256 instance + BCryptAlgorithmHandle algHandle; + 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); + } + + return algHandle; + } + + private static BCryptAlgorithmHandle CreateHMACSHA512AlgorithmHandle() { + // create the HMACSHA-512 instance + BCryptAlgorithmHandle algHandle; + 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); + } + + return algHandle; + } + + 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; + } + + } +} 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 new file mode 100644 index 0000000000..25fbecc1d2 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCRYPT_KEY_DATA_BLOB_HEADER.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection { + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375524(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal struct BCRYPT_KEY_DATA_BLOB_HEADER { + // from bcrypt.h + private const uint BCRYPT_KEY_DATA_BLOB_MAGIC = 0x4d42444b; //Key Data Blob Magic (KDBM) + private const uint BCRYPT_KEY_DATA_BLOB_VERSION1 = 0x1; + + public uint dwMagic; + public uint dwVersion; + public uint cbKeyData; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Initialize(ref BCRYPT_KEY_DATA_BLOB_HEADER pHeader) { + pHeader.dwMagic = BCRYPT_KEY_DATA_BLOB_MAGIC; + pHeader.dwVersion = BCRYPT_KEY_DATA_BLOB_VERSION1; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs new file mode 100644 index 0000000000..aa091e9c25 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmFlags.cs @@ -0,0 +1,11 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + // from bcrypt.h + [Flags] + internal enum BCryptAlgorithmFlags { + BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008, + BCRYPT_CAPI_AES_FLAG = 0x00000010, + BCRYPT_HASH_REUSABLE_FLAG = 0x00000020, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs new file mode 100644 index 0000000000..6346215a9a --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptAlgorithmHandle.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection { + internal sealed class BCryptAlgorithmHandle : SafeHandleZeroOrMinusOneIsInvalid { + // Called by P/Invoke when returning SafeHandles + private BCryptAlgorithmHandle() + : base(ownsHandle: true) { + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() { + return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs new file mode 100644 index 0000000000..3ffbc5156f --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptBuffer.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection { + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375368(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal struct BCryptBuffer { + public uint cbBuffer; // Length of buffer, in bytes + public BCryptKeyDerivationBufferType BufferType; // Buffer type + public IntPtr pvBuffer; // Pointer to buffer + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs new file mode 100644 index 0000000000..0e5336f496 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptBufferDesc.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection { + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375370(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct BCryptBufferDesc { + private const int BCRYPTBUFFER_VERSION = 0; + + public uint ulVersion; // Version number + public uint cBuffers; // Number of buffers + public BCryptBuffer* pBuffers; // Pointer to array of buffers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Initialize(ref BCryptBufferDesc bufferDesc) { + bufferDesc.ulVersion = BCRYPTBUFFER_VERSION; + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs new file mode 100644 index 0000000000..55a2af8300 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptEncryptFlags.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + // from bcrypt.h + [Flags] + internal enum BCryptEncryptFlags { + BCRYPT_BLOCK_PADDING = 0x00000001, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs new file mode 100644 index 0000000000..250d7e0ee6 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptGenRandomFlags.cs @@ -0,0 +1,10 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + // from bcrypt.h + [Flags] + internal enum BCryptGenRandomFlags { + BCRYPT_RNG_USE_ENTROPY_IN_BUFFER = 0x00000001, + BCRYPT_USE_SYSTEM_PREFERRED_RNG = 0x00000002, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs new file mode 100644 index 0000000000..317a5f4bf1 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptHashHandle.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection { + internal sealed class BCryptHashHandle : SafeHandleZeroOrMinusOneIsInvalid { + // Called by P/Invoke when returning SafeHandles + private BCryptHashHandle() + : base(ownsHandle: true) { + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() { + return (UnsafeNativeMethods.BCryptDestroyHash(handle) == 0); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs new file mode 100644 index 0000000000..0fb2e84347 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyDerivationBufferType.cs @@ -0,0 +1,24 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + // from bcrypt.h + internal enum BCryptKeyDerivationBufferType { + KDF_HASH_ALGORITHM = 0x0, + KDF_SECRET_PREPEND = 0x1, + KDF_SECRET_APPEND = 0x2, + KDF_HMAC_KEY = 0x3, + KDF_TLS_PRF_LABEL = 0x4, + KDF_TLS_PRF_SEED = 0x5, + KDF_SECRET_HANDLE = 0x6, + KDF_TLS_PRF_PROTOCOL = 0x7, + KDF_ALGORITHMID = 0x8, + KDF_PARTYUINFO = 0x9, + KDF_PARTYVINFO = 0xA, + KDF_SUPPPUBINFO = 0xB, + KDF_SUPPPRIVINFO = 0xC, + KDF_LABEL = 0xD, + KDF_CONTEXT = 0xE, + KDF_SALT = 0xF, + KDF_ITERATION_COUNT = 0x10, + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs new file mode 100644 index 0000000000..cd6e48fc88 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptKeyHandle.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNet.Security.DataProtection { + internal sealed class BCryptKeyHandle : SafeHandleZeroOrMinusOneIsInvalid { + // Called by P/Invoke when returning SafeHandles + private BCryptKeyHandle() + : base(ownsHandle: true) { + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() { + return (UnsafeNativeMethods.BCryptDestroyKey(handle) == 0); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs new file mode 100644 index 0000000000..4ba8237436 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs @@ -0,0 +1,221 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Util; + +namespace Microsoft.AspNet.Security.DataProtection { + internal unsafe static class BCryptUtil { + + // constant-time buffer comparison + [MethodImpl(MethodImplOptions.NoOptimization)] + public static bool BuffersAreEqualSecure(byte* p1, byte* p2, uint count) { + bool retVal = true; + while (count-- > 0) { + retVal &= (*(p1++) == *(p2++)); + } + return retVal; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CheckOverflowUnderflow(int input) { + var unused = checked((uint)input); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CheckOverflowUnderflow(uint input) { + var unused = checked((int)input); + } + + // helper function to wrap BCryptCreateHash + public static BCryptHashHandle CreateHash(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); + if (status != 0 || retVal == null || retVal.IsInvalid) { + throw new CryptographicException(status); + } + + return retVal; + } + + // helper function to wrap BCryptEncrypt; returns number of bytes written to 'output' + // assumes the output buffer is large enough to hold the ciphertext + any necessary padding + public static int DecryptWithPadding(BCryptKeyHandle keyHandle, byte* input, int inputLength, byte* iv, int ivLength, byte* output, int outputLength) { + CheckOverflowUnderflow(inputLength); + CheckOverflowUnderflow(ivLength); + CheckOverflowUnderflow(outputLength); + + // BCryptEncrypt destroys the 'iv' parameter, so we need to pass a duplicate instead of the original + if (ivLength > Constants.MAX_STACKALLOC_BYTES) { + throw new InvalidOperationException(); + } + byte* pDuplicatedIV = stackalloc byte[ivLength]; + BufferUtil.BlockCopy(from: (IntPtr)iv, to: (IntPtr)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); + if (status != 0) { + throw new CryptographicException(status); + } + + 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) { + 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 + byte* pBuffer = stackalloc byte[TOTAL_NUM_BYTES_TO_DERIVE]; // will be freed with frame pops + byte* pNewEncryptionKey = pBuffer; + byte* pNewHmacKey = &pNewEncryptionKey[ENCRYPTION_KEY_SIZE_IN_BYTES]; + byte* pNewKdfSubkey = &pNewHmacKey[HMAC_KEY_SIZE_IN_BYTES]; + + try { + fixed (char* pszPrfAlgorithmName = Constants.BCRYPT_SHA512_ALGORITHM) { + // 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))); + } + + // .. and the header .. + BCryptBufferDesc bufferDesc = default(BCryptBufferDesc); + BCryptBufferDesc.Initialize(ref bufferDesc); + bufferDesc.cBuffers = numBuffers; + bufferDesc.pBuffers = pBCryptBuffers; + + 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); + } + } + } + + // 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); + } + } + + // helper function to wrap BCryptDuplicateHash + public static BCryptHashHandle DuplicateHash(BCryptHashHandle hashHandle) { + BCryptHashHandle retVal; + int status = UnsafeNativeMethods.BCryptDuplicateHash(hashHandle, out retVal, IntPtr.Zero, 0, dwFlags: 0); + if (status != 0 || retVal == null || retVal.IsInvalid) { + throw new CryptographicException(status); + } + + return retVal; + } + + // helper function to wrap BCryptEncrypt; returns number of bytes written to 'output' + // assumes the output buffer is large enough to hold the ciphertext + any necessary padding + public static int EncryptWithPadding(BCryptKeyHandle keyHandle, byte* input, int inputLength, byte* iv, int ivLength, byte* output, int outputLength) { + CheckOverflowUnderflow(inputLength); + CheckOverflowUnderflow(ivLength); + CheckOverflowUnderflow(outputLength); + + // BCryptEncrypt destroys the 'iv' parameter, so we need to pass a duplicate instead of the original + if (ivLength > Constants.MAX_STACKALLOC_BYTES) { + throw new InvalidOperationException(); + } + byte* pDuplicatedIV = stackalloc byte[ivLength]; + BufferUtil.BlockCopy(from: (IntPtr)iv, to: (IntPtr)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); + if (status != 0) { + throw new CryptographicException(status); + } + + return checked((int)retVal); + } + + // helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers + public static void GenRandom(byte* buffer, int bufferBytes) { + CheckOverflowUnderflow(bufferBytes); + + int status = UnsafeNativeMethods.BCryptGenRandom(IntPtr.Zero, buffer, (uint)bufferBytes, BCryptGenRandomFlags.BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != 0) { + throw new CryptographicException(status); + } + } + + // helper function that wraps BCryptHashData / BCryptFinishHash + public static void HashData(BCryptHashHandle hashHandle, byte* input, int inputBytes, byte* output, int outputBytes) { + CheckOverflowUnderflow(inputBytes); + CheckOverflowUnderflow(outputBytes); + + 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); + if (status != 0) { + throw new CryptographicException(status); + } + } + + // helper function that wraps BCryptImportKey with a key data blob + public static BCryptKeyHandle ImportKey(BCryptAlgorithmHandle algHandle, byte* key, int keyBytes) { + CheckOverflowUnderflow(keyBytes); + + byte[] heapAllocatedKeyDataBlob = null; + 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 + } + + int status; + BCryptKeyHandle retVal; + fixed (byte* pHeapAllocatedKeyDataBlob = heapAllocatedKeyDataBlob) { + // The header is first + 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; + } + BCRYPT_KEY_DATA_BLOB_HEADER.Initialize(ref *pKeyDataBlobHeader); + pKeyDataBlobHeader->cbKeyData = (uint)keyBytes; + + // the raw material immediately follows the header + 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); + } + finally { + // zero out the key we just copied + BufferUtil.ZeroMemory(pKeyDataRawMaterial, keyBytes); + } + } + + if (status != 0 || retVal == null || retVal.IsInvalid) { + throw new CryptographicException(status); + } + return retVal; + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Constants.cs b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs new file mode 100644 index 0000000000..c6ca8cbb7c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Constants.cs @@ -0,0 +1,83 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + // from bcrypt.h + internal static class Constants { + internal const int MAX_STACKALLOC_BYTES = 256; // greatest number of bytes that we'll ever allow to stackalloc in a single frame + + // BCrypt(Import/Export)Key BLOB types + internal const string BCRYPT_OPAQUE_KEY_BLOB = "OpaqueKeyBlob"; + internal const string BCRYPT_KEY_DATA_BLOB = "KeyDataBlob"; + internal const string BCRYPT_AES_WRAP_KEY_BLOB = "Rfc3565KeyWrapBlob"; + + // Microsoft built-in providers. + internal const string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider"; + internal const string MS_PLATFORM_CRYPTO_PROVIDER = "Microsoft Platform Crypto Provider"; + + // Common algorithm identifiers. + internal const string BCRYPT_RSA_ALGORITHM = "RSA"; + internal const string BCRYPT_RSA_SIGN_ALGORITHM = "RSA_SIGN"; + internal const string BCRYPT_DH_ALGORITHM = "DH"; + internal const string BCRYPT_DSA_ALGORITHM = "DSA"; + internal const string BCRYPT_RC2_ALGORITHM = "RC2"; + internal const string BCRYPT_RC4_ALGORITHM = "RC4"; + internal const string BCRYPT_AES_ALGORITHM = "AES"; + internal const string BCRYPT_DES_ALGORITHM = "DES"; + internal const string BCRYPT_DESX_ALGORITHM = "DESX"; + internal const string BCRYPT_3DES_ALGORITHM = "3DES"; + internal const string BCRYPT_3DES_112_ALGORITHM = "3DES_112"; + internal const string BCRYPT_MD2_ALGORITHM = "MD2"; + internal const string BCRYPT_MD4_ALGORITHM = "MD4"; + internal const string BCRYPT_MD5_ALGORITHM = "MD5"; + internal const string BCRYPT_SHA1_ALGORITHM = "SHA1"; + internal const string BCRYPT_SHA256_ALGORITHM = "SHA256"; + internal const string BCRYPT_SHA384_ALGORITHM = "SHA384"; + internal const string BCRYPT_SHA512_ALGORITHM = "SHA512"; + internal const string BCRYPT_AES_GMAC_ALGORITHM = "AES-GMAC"; + internal const string BCRYPT_AES_CMAC_ALGORITHM = "AES-CMAC"; + internal const string BCRYPT_ECDSA_P256_ALGORITHM = "ECDSA_P256"; + internal const string BCRYPT_ECDSA_P384_ALGORITHM = "ECDSA_P384"; + internal const string BCRYPT_ECDSA_P521_ALGORITHM = "ECDSA_P521"; + internal const string BCRYPT_ECDH_P256_ALGORITHM = "ECDH_P256"; + internal const string BCRYPT_ECDH_P384_ALGORITHM = "ECDH_P384"; + internal const string BCRYPT_ECDH_P521_ALGORITHM = "ECDH_P521"; + internal const string BCRYPT_RNG_ALGORITHM = "RNG"; + internal const string BCRYPT_RNG_FIPS186_DSA_ALGORITHM = "FIPS186DSARNG"; + internal const string BCRYPT_RNG_DUAL_EC_ALGORITHM = "DUALECRNG"; + internal const string BCRYPT_SP800108_CTR_HMAC_ALGORITHM = "SP800_108_CTR_HMAC"; + internal const string BCRYPT_SP80056A_CONCAT_ALGORITHM = "SP800_56A_CONCAT"; + internal const string BCRYPT_PBKDF2_ALGORITHM = "PBKDF2"; + internal const string BCRYPT_CAPI_KDF_ALGORITHM = "CAPI_KDF"; + + // BCryptGetProperty strings + internal const string BCRYPT_OBJECT_LENGTH = "ObjectLength"; + internal const string BCRYPT_ALGORITHM_NAME = "AlgorithmName"; + internal const string BCRYPT_PROVIDER_HANDLE = "ProviderHandle"; + internal const string BCRYPT_CHAINING_MODE = "ChainingMode"; + internal const string BCRYPT_BLOCK_LENGTH = "BlockLength"; + internal const string BCRYPT_KEY_LENGTH = "KeyLength"; + internal const string BCRYPT_KEY_OBJECT_LENGTH = "KeyObjectLength"; + internal const string BCRYPT_KEY_STRENGTH = "KeyStrength"; + internal const string BCRYPT_KEY_LENGTHS = "KeyLengths"; + internal const string BCRYPT_BLOCK_SIZE_LIST = "BlockSizeList"; + internal const string BCRYPT_EFFECTIVE_KEY_LENGTH = "EffectiveKeyLength"; + internal const string BCRYPT_HASH_LENGTH = "HashDigestLength"; + internal const string BCRYPT_HASH_OID_LIST = "HashOIDList"; + internal const string BCRYPT_PADDING_SCHEMES = "PaddingSchemes"; + internal const string BCRYPT_SIGNATURE_LENGTH = "SignatureLength"; + internal const string BCRYPT_HASH_BLOCK_LENGTH = "HashBlockLength"; + internal const string BCRYPT_AUTH_TAG_LENGTH = "AuthTagLength"; + internal const string BCRYPT_PRIMITIVE_TYPE = "PrimitiveType"; + internal const string BCRYPT_IS_KEYED_HASH = "IsKeyedHash"; + internal const string BCRYPT_IS_REUSABLE_HASH = "IsReusableHash"; + internal const string BCRYPT_MESSAGE_BLOCK_LENGTH = "MessageBlockLength"; + + // Property Strings + internal const string BCRYPT_CHAIN_MODE_NA = "ChainingModeN/A"; + internal const string BCRYPT_CHAIN_MODE_CBC = "ChainingModeCBC"; + internal const string BCRYPT_CHAIN_MODE_ECB = "ChainingModeECB"; + internal const string BCRYPT_CHAIN_MODE_CFB = "ChainingModeCFB"; + internal const string BCRYPT_CHAIN_MODE_CCM = "ChainingModeCCM"; + internal const string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM"; + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs b/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs new file mode 100644 index 0000000000..1737c90f38 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/CryptographicException.cs @@ -0,0 +1,15 @@ +using System; + +#if !NET45 +namespace System.Security.Cryptography { + internal sealed class CryptographicException : Exception { + internal CryptographicException(string message) + : base(message) { + + } + + internal CryptographicException(int unused) { + } + } +} +#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs new file mode 100644 index 0000000000..75320aee41 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs @@ -0,0 +1,77 @@ +using System; +using System.Globalization; +using System.Reflection; +using Microsoft.AspNet.Security.DataProtection.Resources; +using Microsoft.AspNet.Security.DataProtection.Util; + +namespace Microsoft.AspNet.Security.DataProtection { + public unsafe static class DataProtectionProvider { + + const int MASTER_KEY_REQUIRED_LENGTH = 512 / 8; + + private static readonly byte[] MASTER_SUBKEY_GENERATOR = GetMasterSubkeyGenerator(); + + private static byte[] GetMasterSubkeyGenerator() { + TypeInfo typeInfo = 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; + } + + /// + /// Creates a new IDataProtectorFactory with a randomly-generated master key. + /// + public static IDataProtectionProvider CreateNew() { + byte* masterKey = stackalloc byte[MASTER_KEY_REQUIRED_LENGTH]; + try { + BCryptUtil.GenRandom(masterKey, MASTER_KEY_REQUIRED_LENGTH); + return CreateImpl(masterKey, MASTER_KEY_REQUIRED_LENGTH); + } + finally { + BufferUtil.ZeroMemory(masterKey, MASTER_KEY_REQUIRED_LENGTH); + } + } + + /// + /// Creates a new IDataProtectorFactory with the provided master key. + /// + public static IDataProtectionProvider CreateFromKey(byte[] masterKey) { + if (masterKey == null) { + throw new ArgumentNullException("masterKey"); + } + if (masterKey.Length < MASTER_KEY_REQUIRED_LENGTH) { + string errorMessage = String.Format(CultureInfo.CurrentCulture, Res.DataProtectorFactory_MasterKeyTooShort, MASTER_KEY_REQUIRED_LENGTH); + throw new ArgumentOutOfRangeException("masterKey", errorMessage); + } + + fixed (byte* pMasterKey = masterKey) { + return CreateImpl(pMasterKey, masterKey.Length); + } + } + + private static DataProtectionProviderImpl CreateImpl(byte* masterKey, int masterKeyLengthInBytes) { + // We don't use the master key directly. We derive a master subkey via HMAC_{master_key}(MASTER_SUBKEY_GENERATOR). + byte* masterSubkey = stackalloc byte[MASTER_KEY_REQUIRED_LENGTH]; + try { + using (var hashHandle = BCryptUtil.CreateHash(Algorithms.HMACSHA512AlgorithmHandle, masterKey, masterKeyLengthInBytes)) { + BCryptUtil.HashData(hashHandle, masterKey, masterKeyLengthInBytes, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + } + BCryptKeyHandle kdfSubkeyHandle = BCryptUtil.ImportKey(Algorithms.SP800108AlgorithmHandle, masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + return new DataProtectionProviderImpl(kdfSubkeyHandle); + } + finally { + BufferUtil.ZeroMemory(masterSubkey, MASTER_KEY_REQUIRED_LENGTH); + } + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs new file mode 100644 index 0000000000..f78cde3fc6 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProviderImpl.cs @@ -0,0 +1,26 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + internal unsafe sealed class DataProtectionProviderImpl : IDataProtectionProvider { + + private readonly BCryptKeyHandle _kdfSubkeyHandle; + + public DataProtectionProviderImpl(BCryptKeyHandle kdfSubkeyHandle) { + _kdfSubkeyHandle = kdfSubkeyHandle; + } + + public IDataProtector CreateProtector(string purpose) { + BCryptKeyHandle newAesKeyHandle; + BCryptHashHandle newHmacHashHandle; + BCryptKeyHandle newKdfSubkeyHandle; + + BCryptUtil.DeriveKeysSP800108(Algorithms.SP800108AlgorithmHandle, _kdfSubkeyHandle, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newKdfSubkeyHandle); + return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newKdfSubkeyHandle); + } + + public void Dispose() { + _kdfSubkeyHandle.Dispose(); + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs new file mode 100644 index 0000000000..1aeacc31b8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectorImpl.cs @@ -0,0 +1,160 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography; +using Microsoft.AspNet.Security.DataProtection.Resources; +using Microsoft.AspNet.Security.DataProtection.Util; + +namespace Microsoft.AspNet.Security.DataProtection { + 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 readonly BCryptKeyHandle _aesKeyHandle; + private readonly BCryptHashHandle _hmacHashHandle; + private readonly BCryptKeyHandle _kdfSubkeyHandle; + + public DataProtectorImpl(BCryptKeyHandle aesKeyHandle, BCryptHashHandle hmacHashHandle, BCryptKeyHandle kdfSubkeyHandle) { + _aesKeyHandle = aesKeyHandle; + _hmacHashHandle = hmacHashHandle; + _kdfSubkeyHandle = kdfSubkeyHandle; + } + + private static int CalculateTotalProtectedDataSize(int unprotectedDataSize) { + Debug.Assert(unprotectedDataSize >= 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 */); + } + + private static CryptographicException CreateGenericCryptographicException() { + return new CryptographicException(Res.DataProtectorImpl_BadEncryptedData); + } + + public IDataProtector CreateSubProtector(string purpose) { + BCryptKeyHandle newAesKeyHandle; + BCryptHashHandle newHmacHashHandle; + BCryptKeyHandle newKdfSubkeyHandle; + + BCryptUtil.DeriveKeysSP800108(Algorithms.SP800108AlgorithmHandle, _kdfSubkeyHandle, purpose, Algorithms.AESAlgorithmHandle, out newAesKeyHandle, Algorithms.HMACSHA256AlgorithmHandle, out newHmacHashHandle, out newKdfSubkeyHandle); + return new DataProtectorImpl(newAesKeyHandle, newHmacHashHandle, newKdfSubkeyHandle); + } + + public void Dispose() { + _aesKeyHandle.Dispose(); + _hmacHashHandle.Dispose(); + _kdfSubkeyHandle.Dispose(); + } + + public byte[] Protect(byte[] unprotectedData) { + if (unprotectedData == null) { + throw new ArgumentNullException("unprotectedData"); + } + + // When this method finishes, protectedData will contain { IV || ciphertext || HMAC(IV || ciphertext) } + byte[] protectedData = new byte[CalculateTotalProtectedDataSize(unprotectedData.Length)]; + + fixed (byte* pProtectedData = protectedData) { + // first, generate a random IV for CBC mode encryption + byte* pIV = pProtectedData; + BCryptUtil.GenRandom(pIV, AES_BLOCK_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) { + int actualCiphertextLength = BCryptUtil.EncryptWithPadding(_aesKeyHandle, pPlaintext, unprotectedData.Length, pIV, AES_BLOCK_LENGTH_IN_BYTES, pCiphertext, expectedCiphertextLength); + if (actualCiphertextLength != expectedCiphertextLength) { + throw new InvalidOperationException("Unexpected error while encrypting data."); + } + } + + // finally, calculate an HMAC over { IV || ciphertext } + byte* pMac = &pCiphertext[expectedCiphertextLength]; + using (var clonedHashHandle = BCryptUtil.DuplicateHash(_hmacHashHandle)) { + // 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); + } + } + + return protectedData; + } + + public byte[] Unprotect(byte[] protectedData) { + if (protectedData == null) { + throw new ArgumentNullException("protectedData"); + } + + byte[] retVal = null; + try { + retVal = UnprotectImpl(protectedData); + } + catch { + // swallow all exceptions; we'll homogenize + } + + if (retVal != null) { + return retVal; + } + else { + throw CreateGenericCryptographicException(); + } + } + + private byte[] UnprotectImpl(byte[] protectedData) { + 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) { + return null; + } + + fixed (byte* pProtectedData = protectedData) { + // 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* pSuppliedMac = &pCiphertext[ciphertextLength]; + + // first, ensure that the MAC is valid + byte* pCalculatedMac = stackalloc byte[MAC_LENGTH_IN_BYTES]; + 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); + } + if (!BCryptUtil.BuffersAreEqualSecure(pSuppliedMac, pCalculatedMac, MAC_LENGTH_IN_BYTES)) { + return null; // MAC check failed + } + + // next, perform the actual decryption + // we don't know the actual plaintext length, but we know it must be strictly less than the ciphertext length + int plaintextBufferLength = ciphertextLength; + byte[] heapAllocatedPlaintext = null; + if (ciphertextLength > Constants.MAX_STACKALLOC_BYTES) { + heapAllocatedPlaintext = new byte[plaintextBufferLength]; + } + + fixed (byte* pHeapAllocatedPlaintext = heapAllocatedPlaintext) { + byte* pPlaintextBuffer = pHeapAllocatedPlaintext; + if (pPlaintextBuffer == null) { + byte* temp = stackalloc byte[plaintextBufferLength]; // will be released when frame pops + pPlaintextBuffer = temp; + } + + int actualPlaintextLength = BCryptUtil.DecryptWithPadding(_aesKeyHandle, pCiphertext, ciphertextLength, pIV, AES_BLOCK_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; + } + } + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs new file mode 100644 index 0000000000..520359930e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtectionProvider.cs @@ -0,0 +1,12 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + public interface IDataProtectionProvider : IDisposable { + /// + /// Given a purpose, returns a new IDataProtector that has unique cryptographic keys tied to this purpose. + /// + /// The consumer of the IDataProtector. + /// An IDataProtector. + IDataProtector CreateProtector(string purpose); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs new file mode 100644 index 0000000000..c932d4522c --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/IDataProtector.cs @@ -0,0 +1,33 @@ +using System; + +namespace Microsoft.AspNet.Security.DataProtection { + /// + /// Represents an object that can perform cryptographic operations. + /// + public interface IDataProtector : IDisposable { + /// + /// Given a subpurpose, returns a new IDataProtector that has unique cryptographic keys tied both the purpose + /// that was used to create this IDataProtector instance and the purpose that is provided as a parameter + /// to this method. + /// + /// The sub-consumer of the IDataProtector. + /// An IDataProtector. + IDataProtector CreateSubProtector(string purpose); + + /// + /// Cryptographically protects some input data. + /// + /// The data to be protected. + /// An array containing cryptographically protected data. + /// To retrieve the original data, call Unprotect on the protected data. + byte[] Protect(byte[] unprotectedData); + + /// + /// Retrieves the original data that was protected by a call to Protect. + /// + /// The protected data to be decrypted. + /// The original data. + /// Throws CryptographicException if the protectedData parameter has been tampered with. + byte[] Unprotect(byte[] protectedData); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..38a4928ff0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.AspNet.Security.DataProtection")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Microsoft.AspNet.Security.DataProtection")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("130d9afa-6535-42bf-ba70-610b677d5acf")] + +[assembly: AssemblyCompany("Microsoft Corporation")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: NeutralResourcesLanguage("en-US")] + +// for OOB servicing +[assembly: AssemblyMetadata("Serviceable", "True")] diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs new file mode 100644 index 0000000000..d62ce5ee40 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.DataProtection.Resources { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Res { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Res() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.DataProtection.Res.resources", typeof(Res).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The master key is too short. It must be at least {0} bytes in length.. + /// + internal static string DataProtectorFactory_MasterKeyTooShort { + get { + return ResourceManager.GetString("DataProtectorFactory_MasterKeyTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The data to decrypt is invalid.. + /// + internal static string DataProtectorImpl_BadEncryptedData { + get { + return ResourceManager.GetString("DataProtectorImpl_BadEncryptedData", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx new file mode 100644 index 0000000000..f28f1d7003 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The master key is too short. It must be at least {0} bytes in length. + + + The data to decrypt is invalid. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs b/src/Microsoft.AspNet.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs new file mode 100644 index 0000000000..244f118df0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/SafeHandleZeroOrMinusOneIsInvalid.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.InteropServices; + +#if !NET45 +namespace Microsoft.Win32.SafeHandles { + internal abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle { + // Called by P/Invoke when returning SafeHandles + protected SafeHandleZeroOrMinusOneIsInvalid(bool ownsHandle) + : base(IntPtr.Zero, ownsHandle) { + } + + public override bool IsInvalid { + get { + return (handle == IntPtr.Zero || handle == (IntPtr)(-1)); + } + } + } +} +#endif diff --git a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs new file mode 100644 index 0000000000..34689a9917 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs @@ -0,0 +1,159 @@ +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 unsafe static class UnsafeNativeMethods { + + private const string BCRYPT_LIB = "bcrypt.dll"; + private const string KERNEL32_LIB = "kernel32.dll"; + + /* + * BCRYPT.DLL + */ + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375377(v=vs.85).aspx + internal static extern int BCryptCloseAlgorithmProvider( + [In] IntPtr hAlgorithm, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375383(v=vs.85).aspx + internal static extern int BCryptCreateHash( + [In] BCryptAlgorithmHandle hAlgorithm, + [Out] out BCryptHashHandle phHash, + [In] IntPtr pbHashObject, + [In] uint cbHashObject, + [In] byte* pbSecret, + [In] uint cbSecret, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375391(v=vs.85).aspx + internal static extern int BCryptDecrypt( + [In] BCryptKeyHandle hKey, + [In] byte* pbInput, + [In] uint cbInput, + [In] IntPtr pPaddingInfo, + [In] byte* pbIV, + [In] uint cbIV, + [In] byte* pbOutput, + [In] uint cbOutput, + [Out] out uint pcbResult, + [In] BCryptEncryptFlags 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( + [In] IntPtr hHash); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375404(v=vs.85).aspx + internal static extern int BCryptDestroyKey( + [In] IntPtr hKey); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375413(v=vs.85).aspx + internal static extern int BCryptDuplicateHash( + [In] BCryptHashHandle hHash, + [Out] out BCryptHashHandle phNewHash, + [In] IntPtr pbHashObject, + [In] uint cbHashObject, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375421(v=vs.85).aspx + internal static extern int BCryptEncrypt( + [In] BCryptKeyHandle hKey, + [In] byte* pbInput, + [In] uint cbInput, + [In] IntPtr pPaddingInfo, + [In] byte* pbIV, + [In] uint cbIV, + [In] byte* pbOutput, + [In] uint cbOutput, + [Out] out uint pcbResult, + [In] BCryptEncryptFlags dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375443(v=vs.85).aspx + internal static extern int BCryptFinishHash( + [In] BCryptHashHandle hHash, + [In] byte* pbOutput, + [In] uint cbOutput, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375458(v=vs.85).aspx + internal static extern int BCryptGenRandom( + [In] IntPtr hAlgorithm, + [In] byte* pbBuffer, + [In] uint cbBuffer, + [In] BCryptGenRandomFlags dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375468(v=vs.85).aspx + internal static extern int BCryptHashData( + [In] BCryptHashHandle hHash, + [In] byte* pbInput, + [In] uint cbInput, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375475(v=vs.85).aspx + internal static extern int BCryptImportKey( + [In] BCryptAlgorithmHandle hAlgorithm, + [In] IntPtr hImportKey, // unused + [In, MarshalAs(UnmanagedType.LPWStr)] string pszBlobType, + [Out] out BCryptKeyHandle phKey, + [In] IntPtr pbKeyObject, // unused + [In] uint cbKeyObject, + [In] byte* pbInput, + [In] uint cbInput, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/hh448506(v=vs.85).aspx + internal static extern int BCryptKeyDerivation( + [In] BCryptKeyHandle hKey, + [In] BCryptBufferDesc* pParameterList, + [In] byte* pbDerivedKey, + [In] uint cbDerivedKey, + [Out] out uint pcbResult, + [In] uint dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375479(v=vs.85).aspx + internal static extern int BCryptOpenAlgorithmProvider( + [Out] out BCryptAlgorithmHandle phAlgorithm, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszAlgId, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszImplementation, + [In] BCryptAlgorithmFlags dwFlags); + + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375504(v=vs.85).aspx + internal static extern int BCryptSetProperty( + [In] SafeHandle hObject, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszProperty, + [In] IntPtr pbInput, + [In] uint cbInput, + [In] uint dwFlags); + + /* + * KERNEL32.DLL + */ + + [DllImport(KERNEL32_LIB, CallingConvention = CallingConvention.Winapi)] + internal static extern void RtlZeroMemory( + [In] IntPtr Destination, + [In] UIntPtr /* SIZE_T */ Length); + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs new file mode 100644 index 0000000000..36a781aa17 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Util/BufferUtil.cs @@ -0,0 +1,66 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Security.DataProtection.Util { + 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) { + 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) { + BlockCopySlow((byte*)from, (byte*)to, byteCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BlockCopySlow(byte* from, byte* to, uint byteCount) { + // slow, but works + while (byteCount-- != 0) { + *(to++) = *(from++); + } + } + + /// + /// Creates a new managed byte[] from unmanaged memory. + /// + public static byte[] ToManagedByteArray(byte* ptr, int byteCount) { + return ToManagedByteArray(ptr, checked((uint)byteCount)); + } + + /// + /// Creates a new managed byte[] from unmanaged memory. + /// + public static byte[] ToManagedByteArray(byte* ptr, uint byteCount) { + if (byteCount == 0) { + return _emptyArray; // degenerate case + } + else { + byte[] bytes = new byte[byteCount]; + fixed (byte* pBytes = bytes) { + BlockCopy(from: (IntPtr)ptr, to: (IntPtr)pBytes, byteCount: byteCount); + } + return bytes; + } + } + + /// + /// Clears a memory buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroMemory(byte* buffer, 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 + } + + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/project.json b/src/Microsoft.AspNet.Security.DataProtection/project.json new file mode 100644 index 0000000000..ea163de588 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/project.json @@ -0,0 +1,10 @@ +{ + "version": "0.1-alpha-*", + "configurations": { + "net45" : {}, + "k10" : {} + }, + "compilationOptions": { + "allowUnsafe": true + } +} \ No newline at end of file