Add DPAPI support to the DataProtection library.
This commit is contained in:
parent
4bc8d93777
commit
7aa23bfc05
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// 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'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">
|
||||
<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>
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue