Add DPAPI support to the DataProtection library.

This commit is contained in:
GrabYourPitchforks 2014-02-13 17:42:04 -08:00
parent 4bc8d93777
commit 7aa23bfc05
8 changed files with 266 additions and 7 deletions

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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
}
/// <summary>
/// Creates a new IDataProtectorFactory with a randomly-generated master key.
/// Creates a new IDataProtectionProvider backed by DPAPI.
/// </summary>
public static IDataProtectionProvider CreateFromDpapi()
{
return new DpapiDataProtectionProviderImpl(MASTER_DPAPI_ENTROPY);
}
/// <summary>
/// Creates a new IDataProtectionProvider with a randomly-generated master key.
/// </summary>
public static IDataProtectionProvider CreateNew()
{
@ -48,7 +57,7 @@ namespace Microsoft.AspNet.Security.DataProtection
}
/// <summary>
/// Creates a new IDataProtectorFactory with the provided master key.
/// Creates a new IDataProtectionProvider with the provided master key.
/// </summary>
public static IDataProtectionProvider CreateFromKey(byte[] masterKey)
{

View File

@ -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
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
@ -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);
}
}
/// <summary>
/// Looks up a localized string similar to Couldn&apos;t protect data. Perhaps the user profile isn&apos;t loaded?.
/// </summary>
internal static string DpapiDataProtectorImpl_ProfileNotLoaded {
get {
return ResourceManager.GetString("DpapiDataProtectorImpl_ProfileNotLoaded", resourceCulture);
}
}
}
}

View File

@ -123,4 +123,7 @@
<data name="DataProtectorImpl_BadEncryptedData" xml:space="preserve">
<value>The data to decrypt is invalid.</value>
</data>
<data name="DpapiDataProtectorImpl_ProfileNotLoaded" xml:space="preserve">
<value>Couldn't protect data. Perhaps the user profile isn't loaded?</value>
</data>
</root>

View File

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