Add DPAPI support to the DataProtection library.
This commit is contained in:
parent
4bc8d93777
commit
7aa23bfc05
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Microsoft.AspNet.Security.DataProtection.Util;
|
using Microsoft.AspNet.Security.DataProtection.Util;
|
||||||
|
|
@ -172,6 +173,34 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
return checked((int) retVal);
|
return checked((int) retVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper function to take a key, apply a purpose, and generate a new subkey ("entropy") for DPAPI-specific scenarios
|
||||||
|
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
|
// helper function that's similar to RNGCryptoServiceProvider, but works directly with pointers
|
||||||
public static void GenRandom(byte* buffer, int bufferBytes)
|
public static void GenRandom(byte* buffer, int bufferBytes)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,10 +11,11 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
private const int MASTER_KEY_REQUIRED_LENGTH = 512/8;
|
private const int MASTER_KEY_REQUIRED_LENGTH = 512/8;
|
||||||
|
|
||||||
private static readonly byte[] MASTER_SUBKEY_GENERATOR = GetMasterSubkeyGenerator();
|
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];
|
byte[] retVal = new byte[sizeof (Guid)*2];
|
||||||
fixed (byte* pRetVal = retVal)
|
fixed (byte* pRetVal = retVal)
|
||||||
|
|
@ -31,7 +32,15 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static IDataProtectionProvider CreateNew()
|
public static IDataProtectionProvider CreateNew()
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +57,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new IDataProtectorFactory with the provided master key.
|
/// Creates a new IDataProtectionProvider with the provided master key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IDataProtectionProvider CreateFromKey(byte[] masterKey)
|
public static IDataProtectionProvider CreateFromKey(byte[] masterKey)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// 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
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
// the code is regenerated.
|
// the code is regenerated.
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources {
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources {
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
get {
|
get {
|
||||||
if (object.ReferenceEquals(resourceMan, null)) {
|
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;
|
resourceMan = temp;
|
||||||
}
|
}
|
||||||
return resourceMan;
|
return resourceMan;
|
||||||
|
|
@ -78,5 +77,14 @@ namespace Microsoft.AspNet.Security.DataProtection.Resources {
|
||||||
return ResourceManager.GetString("DataProtectorImpl_BadEncryptedData", resourceCulture);
|
return ResourceManager.GetString("DataProtectorImpl_BadEncryptedData", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Couldn't protect data. Perhaps the user profile isn't loaded?.
|
||||||
|
/// </summary>
|
||||||
|
internal static string DpapiDataProtectorImpl_ProfileNotLoaded {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DpapiDataProtectorImpl_ProfileNotLoaded", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,4 +123,7 @@
|
||||||
<data name="DataProtectorImpl_BadEncryptedData" xml:space="preserve">
|
<data name="DataProtectorImpl_BadEncryptedData" xml:space="preserve">
|
||||||
<value>The data to decrypt is invalid.</value>
|
<value>The data to decrypt is invalid.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DpapiDataProtectorImpl_ProfileNotLoaded" xml:space="preserve">
|
||||||
|
<value>Couldn't protect data. Perhaps the user profile isn't loaded?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
internal static unsafe class UnsafeNativeMethods
|
internal static unsafe class UnsafeNativeMethods
|
||||||
{
|
{
|
||||||
private const string BCRYPT_LIB = "bcrypt.dll";
|
private const string BCRYPT_LIB = "bcrypt.dll";
|
||||||
|
private const string CRYPT32_LIB = "crypt32.dll";
|
||||||
private const string KERNEL32_LIB = "kernel32.dll";
|
private const string KERNEL32_LIB = "kernel32.dll";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -148,6 +149,32 @@ namespace Microsoft.AspNet.Security.DataProtection
|
||||||
[In] uint cbInput,
|
[In] uint cbInput,
|
||||||
[In] uint dwFlags);
|
[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
|
* KERNEL32.DLL
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue