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
*/