diff --git a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs index 3c28aaceec..8d1cfc4884 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/BCryptUtil.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Security.Cryptography; using Microsoft.AspNet.Security.DataProtection.Util; @@ -172,6 +173,34 @@ namespace Microsoft.AspNet.Security.DataProtection return checked((int) retVal); } + // helper function to take a key, apply a purpose, and generate a new subkey ("entropy") for DPAPI-specific scenarios + public static byte[] GenerateDpapiSubkey(byte[] previousKey, string purpose) + { + Debug.Assert(previousKey != null); + purpose = purpose ?? String.Empty; // cannot be null + + // create the HMAC object + BCryptHashHandle hashHandle; + fixed (byte* pPreviousKey = previousKey) + { + hashHandle = CreateHash(Algorithms.HMACSHA256AlgorithmHandle, pPreviousKey, previousKey.Length); + } + + // hash the purpose string, treating it as UTF-16LE + using (hashHandle) + { + byte[] retVal = new byte[256 / 8]; // fixed length output since we're hardcoded to HMACSHA256 + fixed (byte* pRetVal = retVal) + { + fixed (char* pPurpose = purpose) + { + HashData(hashHandle, (byte*)pPurpose, checked(purpose.Length * sizeof(char)), pRetVal, retVal.Length); + return retVal; + } + } + } + } + // helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers public static void GenRandom(byte* buffer, int bufferBytes) { diff --git a/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs new file mode 100644 index 0000000000..55bfc7ea8e --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DATA_BLOB.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNet.Security.DataProtection +{ + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa381414(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct DATA_BLOB + { + 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 e90ce87080..91d48649d0 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionProvider.cs @@ -11,10 +11,11 @@ namespace Microsoft.AspNet.Security.DataProtection private 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 byte[] GetMasterSubkeyGenerator() + private static byte[] GetMasterSubkeyGenerator(bool isDpapi = false) { - TypeInfo typeInfo = typeof (DataProtectionProvider).GetTypeInfo(); + TypeInfo typeInfo = ((isDpapi) ? typeof(DpapiDataProtectionProviderImpl) : typeof(DataProtectionProvider)).GetTypeInfo(); byte[] retVal = new byte[sizeof (Guid)*2]; fixed (byte* pRetVal = retVal) @@ -31,7 +32,15 @@ namespace Microsoft.AspNet.Security.DataProtection } /// - /// Creates a new IDataProtectorFactory with a randomly-generated master key. + /// Creates a new IDataProtectionProvider backed by DPAPI. + /// + public static IDataProtectionProvider CreateFromDpapi() + { + return new DpapiDataProtectionProviderImpl(MASTER_DPAPI_ENTROPY); + } + + /// + /// Creates a new IDataProtectionProvider with a randomly-generated master key. /// public static IDataProtectionProvider CreateNew() { @@ -48,7 +57,7 @@ namespace Microsoft.AspNet.Security.DataProtection } /// - /// Creates a new IDataProtectorFactory with the provided master key. + /// Creates a new IDataProtectionProvider with the provided master key. /// public static IDataProtectionProvider CreateFromKey(byte[] masterKey) { diff --git a/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs new file mode 100644 index 0000000000..fa37a07bae --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectionProviderImpl.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics; + +namespace Microsoft.AspNet.Security.DataProtection +{ + internal sealed class DpapiDataProtectionProviderImpl : IDataProtectionProvider + { + private readonly byte[] _entropy; + + public DpapiDataProtectionProviderImpl(byte[] entropy) + { + Debug.Assert(entropy != null); + _entropy = entropy; + } + + public IDataProtector CreateProtector(string purpose) + { + return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose)); + } + + public void Dispose() + { + // 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 new file mode 100644 index 0000000000..5678e3a2f3 --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/DpapiDataProtectorImpl.cs @@ -0,0 +1,143 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +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 DpapiDataProtectorImpl : IDataProtector + { + // from dpapi.h + private const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1; + + // Used as the 'purposes' parameter to DPAPI operations + private readonly byte[] _entropy; + + public DpapiDataProtectorImpl(byte[] entropy) + { + Debug.Assert(entropy != null); + _entropy = entropy; + } + + private static CryptographicException CreateGenericCryptographicException(bool isErrorDueToProfileNotLoaded = false) + { + string message = (isErrorDueToProfileNotLoaded) ? Res.DpapiDataProtectorImpl_ProfileNotLoaded : Res.DataProtectorImpl_BadEncryptedData; + return new CryptographicException(message); + } + + public IDataProtector CreateSubProtector(string purpose) + { + return new DpapiDataProtectorImpl(BCryptUtil.GenerateDpapiSubkey(_entropy, purpose)); + } + + public void Dispose() + { + // no-op; no unmanaged resources to dispose + } + + public byte[] Protect(byte[] unprotectedData) + { + if (unprotectedData == null) + { + throw new ArgumentNullException("unprotectedData"); + } + + DATA_BLOB dataOut = default(DATA_BLOB); + +#if NET45 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + bool success; + fixed (byte* pUnprotectedData = unprotectedData) + { + 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); + } + } + + // Did a failure occur? + if (!success) + { + int errorCode = Marshal.GetLastWin32Error(); + bool isErrorDueToProfileNotLoaded = ((errorCode & 0xffff) == 2 /* ERROR_FILE_NOT_FOUND */); + throw CreateGenericCryptographicException(isErrorDueToProfileNotLoaded); + } + + // OOMs may be marked as success but won't return a valid pointer + if (dataOut.pbData == null) + { + throw new OutOfMemoryException(); + } + + return BufferUtil.ToManagedByteArray(dataOut.pbData, dataOut.cbData); + } + finally + { + // per MSDN, we need to use LocalFree (implemented by Marshal.FreeHGlobal) to clean up CAPI-allocated memory + if (dataOut.pbData != null) + { + Marshal.FreeHGlobal((IntPtr)dataOut.pbData); + } + } + } + + public byte[] Unprotect(byte[] protectedData) + { + if (protectedData == null) + { + throw new ArgumentNullException("protectedData"); + } + + DATA_BLOB dataOut = default(DATA_BLOB); + +#if NET45 + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { + bool success; + fixed (byte* pProtectedData = protectedData) + { + 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); + } + } + + // Did a failure occur? + if (!success) + { + throw CreateGenericCryptographicException(); + } + + // OOMs may be marked as success but won't return a valid pointer + if (dataOut.pbData == null) + { + throw new OutOfMemoryException(); + } + + return BufferUtil.ToManagedByteArray(dataOut.pbData, dataOut.cbData); + } + finally + { + // per MSDN, we need to use LocalFree (implemented by Marshal.FreeHGlobal) to clean up CAPI-allocated memory + if (dataOut.pbData != null) + { + Marshal.FreeHGlobal((IntPtr)dataOut.pbData); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs index d62ce5ee40..45215fb506 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34003 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,7 +12,6 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources { using System; using System.Reflection; - /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources { 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); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.DataProtection.Res", typeof(Res).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; @@ -78,5 +77,14 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources { return ResourceManager.GetString("DataProtectorImpl_BadEncryptedData", resourceCulture); } } + + /// + /// Looks up a localized string similar to Couldn't protect data. Perhaps the user profile isn't loaded?. + /// + internal static string DpapiDataProtectorImpl_ProfileNotLoaded { + get { + return ResourceManager.GetString("DpapiDataProtectorImpl_ProfileNotLoaded", resourceCulture); + } + } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx index f28f1d7003..d195f18d48 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx +++ b/src/Microsoft.AspNet.Security.DataProtection/Resources/Res.resx @@ -123,4 +123,7 @@ The data to decrypt is invalid. + + Couldn't protect data. Perhaps the user profile isn't loaded? + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs index c34dd599d0..5e3cea28ec 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/UnsafeNativeMethods.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Security.DataProtection internal static unsafe class UnsafeNativeMethods { private const string BCRYPT_LIB = "bcrypt.dll"; + private const string CRYPT32_LIB = "crypt32.dll"; private const string KERNEL32_LIB = "kernel32.dll"; /* @@ -148,6 +149,32 @@ namespace Microsoft.AspNet.Security.DataProtection [In] uint cbInput, [In] uint dwFlags); + /* + * CRYPT32.DLL + */ + + [DllImport(CRYPT32_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)] + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261(v=vs.85).aspx + internal static extern bool CryptProtectData( + [In] DATA_BLOB* pDataIn, + [In] IntPtr szDataDescr, + [In] DATA_BLOB* pOptionalEntropy, + [In] IntPtr pvReserved, + [In] IntPtr pPromptStruct, + [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/aa380882(v=vs.85).aspx + internal static extern bool CryptUnprotectData( + [In] DATA_BLOB* pDataIn, + [In] IntPtr ppszDataDescr, + [In] DATA_BLOB* pOptionalEntropy, + [In] IntPtr pvReserved, + [In] IntPtr pPromptStruct, + [In] uint dwFlags, + [Out] out DATA_BLOB pDataOut); + /* * KERNEL32.DLL */