Make EphemeralDataProtectionProvider and ProtectedMemoryBlob work on non-Windows platforms.

This commit is contained in:
Levi B 2014-10-14 17:15:00 -07:00
parent 7d5a29a9fd
commit c3b76d14a3
9 changed files with 136 additions and 37 deletions

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
/// Options for configuring an authenticated encryption mechanism which uses /// Options for configuring an authenticated encryption mechanism which uses
/// Windows CNG algorithms in CBC encryption + HMAC validation modes. /// Windows CNG algorithms in CBC encryption + HMAC validation modes.
/// </summary> /// </summary>
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{ {
/// <summary> /// <summary>
/// The name of the algorithm to use for symmetric encryption. /// The name of the algorithm to use for symmetric encryption.
@ -178,5 +178,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
} }
return value; return value;
} }
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
{
return CreateAuthenticatedEncryptor(secret);
}
} }
} }

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
/// Options for configuring an authenticated encryption mechanism which uses /// Options for configuring an authenticated encryption mechanism which uses
/// Windows CNG encryption algorithms in Galois/Counter Mode. /// Windows CNG encryption algorithms in Galois/Counter Mode.
/// </summary> /// </summary>
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{ {
/// <summary> /// <summary>
/// The name of the algorithm to use for symmetric encryption. /// The name of the algorithm to use for symmetric encryption.
@ -120,5 +120,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
} }
return value; return value;
} }
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
{
return CreateAuthenticatedEncryptor(secret);
}
} }
} }

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
{
internal interface IInternalConfigurationOptions
{
IAuthenticatedEncryptor CreateAuthenticatedEncryptor(ISecret secret);
}
}

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
/// Options for configuring an authenticated encryption mechanism which uses /// Options for configuring an authenticated encryption mechanism which uses
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations. /// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
/// </summary> /// </summary>
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions public sealed class ManagedAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{ {
/// <summary> /// <summary>
/// The type of the algorithm to use for symmetric encryption. /// The type of the algorithm to use for symmetric encryption.
@ -102,6 +102,11 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
return ((IActivator<KeyedHashAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator; return ((IActivator<KeyedHashAlgorithm>)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
} }
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
{
return CreateAuthenticatedEncryptor(secret);
}
private interface IActivator<out T> private interface IActivator<out T>
{ {
Func<T> Creator { get; } Func<T> Creator { get; }

View File

@ -28,12 +28,12 @@ namespace Microsoft.AspNet.Security.DataProtection
if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable()) if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{ {
// Fastest implementation: AES-GCM // Fastest implementation: AES-GCM
keyringProvider = new CngEphemeralKeyRing(); keyringProvider = new EphemeralKeyRing<CngGcmAuthenticatedEncryptorConfigurationOptions>();
} }
else else
{ {
// Slowest implementation: managed CBC + HMAC // Slowest implementation: managed CBC + HMAC
keyringProvider = new ManagedEphemeralKeyRing(); keyringProvider = new EphemeralKeyRing<ManagedAuthenticatedEncryptorConfigurationOptions>();
} }
_dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider); _dataProtectionProvider = new KeyRingBasedDataProtectionProvider(keyringProvider);
@ -55,29 +55,13 @@ namespace Microsoft.AspNet.Security.DataProtection
} }
} }
// A special key ring that only understands one key id and which uses CNG. private sealed class EphemeralKeyRing<T> : IKeyRing, IKeyRingProvider
private sealed class CngEphemeralKeyRing : IKeyRing, IKeyRingProvider where T : IInternalConfigurationOptions, new()
{ {
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new CngGcmAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor<CngGcmAuthenticatedEncryptorConfigurationOptions>()).CreateNewConfiguration().CreateEncryptorInstance(); // Currently hardcoded to a 512-bit KDK.
private const int NUM_BYTES_IN_KDK = 512 / 8;
public Guid DefaultKeyId { get; } = default(Guid); public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().CreateAuthenticatedEncryptor(ProtectedMemoryBlob.Random(NUM_BYTES_IN_KDK));
public IAuthenticatedEncryptor GetAuthenticatedEncryptorByKeyId(Guid keyId, out bool isRevoked)
{
isRevoked = false;
return (keyId == default(Guid)) ? DefaultAuthenticatedEncryptor : null;
}
public IKeyRing GetCurrentKeyRing()
{
return this;
}
}
// A special key ring that only understands one key id and which uses managed CBC + HMAC.
private sealed class ManagedEphemeralKeyRing : IKeyRing, IKeyRingProvider
{
public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new ManagedAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor<ManagedAuthenticatedEncryptorConfigurationOptions>()).CreateNewConfiguration().CreateEncryptorInstance();
public Guid DefaultKeyId { get; } = default(Guid); public Guid DefaultKeyId { get; } = default(Guid);

View File

@ -3,8 +3,8 @@
using System; using System;
using Microsoft.AspNet.Security.DataProtection.Cng; using Microsoft.AspNet.Security.DataProtection.Cng;
using Microsoft.AspNet.Security.DataProtection.Managed;
using Microsoft.AspNet.Security.DataProtection.SafeHandles; using Microsoft.AspNet.Security.DataProtection.SafeHandles;
using Microsoft.Win32.SafeHandles;
namespace Microsoft.AspNet.Security.DataProtection namespace Microsoft.AspNet.Security.DataProtection
{ {
@ -13,14 +13,14 @@ namespace Microsoft.AspNet.Security.DataProtection
// from wincrypt.h // from wincrypt.h
private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16; private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16;
private readonly SecureLocalAllocHandle _encryptedMemoryHandle; private readonly SecureLocalAllocHandle _localAllocHandle;
private readonly uint _plaintextLength; private readonly uint _plaintextLength;
public ProtectedMemoryBlob(ArraySegment<byte> plaintext) public ProtectedMemoryBlob(ArraySegment<byte> plaintext)
{ {
plaintext.Validate(); plaintext.Validate();
_encryptedMemoryHandle = Protect(plaintext); _localAllocHandle = Protect(plaintext);
_plaintextLength = (uint)plaintext.Count; _plaintextLength = (uint)plaintext.Count;
} }
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Security.DataProtection
throw new ArgumentOutOfRangeException("plaintextLength"); throw new ArgumentOutOfRangeException("plaintextLength");
} }
_encryptedMemoryHandle = Protect(plaintext, (uint)plaintextLength); _localAllocHandle = Protect(plaintext, (uint)plaintextLength);
_plaintextLength = (uint)plaintextLength; _plaintextLength = (uint)plaintextLength;
} }
@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Security.DataProtection
if (other != null) if (other != null)
{ {
// Fast-track: simple deep copy scenario. // Fast-track: simple deep copy scenario.
this._encryptedMemoryHandle = other._encryptedMemoryHandle.Duplicate(); this._localAllocHandle = other._localAllocHandle.Duplicate();
this._plaintextLength = other._plaintextLength; this._plaintextLength = other._plaintextLength;
} }
else else
@ -68,7 +68,7 @@ namespace Microsoft.AspNet.Security.DataProtection
try try
{ {
secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer)); secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer));
_encryptedMemoryHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length); _localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
_plaintextLength = (uint)tempPlaintextBuffer.Length; _plaintextLength = (uint)tempPlaintextBuffer.Length;
} }
finally finally
@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Security.DataProtection
public void Dispose() public void Dispose()
{ {
_encryptedMemoryHandle.Dispose(); _localAllocHandle.Dispose();
} }
private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext) private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext)
@ -102,6 +102,16 @@ namespace Microsoft.AspNet.Security.DataProtection
private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext) private static SecureLocalAllocHandle Protect(byte* pbPlaintext, uint cbPlaintext)
{ {
// If we're not running on a platform that supports CryptProtectMemory,
// shove the plaintext directly into a LocalAlloc handle. Ideally we'd
// mark this memory page as non-pageable, but this is fraught with peril.
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{
SecureLocalAllocHandle handle = SecureLocalAllocHandle.Allocate((IntPtr)checked((int)cbPlaintext));
UnsafeBufferUtil.BlockCopy(from: pbPlaintext, to: handle, byteCount: cbPlaintext);
return handle;
}
// We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE. // We need to make sure we're a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE.
uint numTotalBytesToAllocate = cbPlaintext; uint numTotalBytesToAllocate = cbPlaintext;
uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE); uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE);
@ -135,6 +145,12 @@ namespace Microsoft.AspNet.Security.DataProtection
} }
else else
{ {
// Don't use CNG if we're not on Windows.
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{
return new ProtectedMemoryBlob(ManagedGenRandomImpl.Instance.GenRandom(numBytes));
}
byte[] bytes = new byte[numBytes]; byte[] bytes = new byte[numBytes];
fixed (byte* pbBytes = bytes) fixed (byte* pbBytes = bytes)
{ {
@ -153,18 +169,26 @@ namespace Microsoft.AspNet.Security.DataProtection
private void UnprotectInto(byte* pbBuffer) private void UnprotectInto(byte* pbBuffer)
{ {
// If we're not running on a platform that supports CryptProtectMemory,
// the handle contains plaintext bytes.
if (!OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
return;
}
if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0) if (_plaintextLength % CRYPTPROTECTMEMORY_BLOCK_SIZE == 0)
{ {
// Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there. // Case 1: Secret length is an exact multiple of the block size. Copy directly to the buffer and decrypt there.
// We go through this code path even for empty plaintexts since we still want SafeHandle dispose semantics. // We go through this code path even for empty plaintexts since we still want SafeHandle dispose semantics.
UnsafeBufferUtil.BlockCopy(from: _encryptedMemoryHandle, to: pbBuffer, byteCount: _plaintextLength); UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength); MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
} }
else else
{ {
// Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and // Case 2: Secret length is not a multiple of the block size. We'll need to duplicate the data and
// perform the decryption in the duplicate buffer, then copy the plaintext data over. // perform the decryption in the duplicate buffer, then copy the plaintext data over.
using (var duplicateHandle = _encryptedMemoryHandle.Duplicate()) using (var duplicateHandle = _localAllocHandle.Duplicate())
{ {
MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length)); MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length));
UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength); UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength);

View File

@ -10,7 +10,7 @@ using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
{ {
public unsafe class CbcAuthenticatedEncryptorTests public class CbcAuthenticatedEncryptorTests
{ {
[Fact] [Fact]
public void Encrypt_Decrypt_RoundTrips() public void Encrypt_Decrypt_RoundTrips()

View File

@ -10,7 +10,7 @@ using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
{ {
public unsafe class GcmAuthenticatedEncryptorTests public class GcmAuthenticatedEncryptorTests
{ {
[Fact] [Fact]
public void Encrypt_Decrypt_RoundTrips() public void Encrypt_Decrypt_RoundTrips()

View File

@ -0,0 +1,64 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Cryptography;
using System.Text;
using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test
{
public class EphemeralDataProtectionProviderTests
{
[Fact]
public void DifferentProvider_SamePurpose_DoesNotRoundTripData()
{
// Arrange
var dataProtector1 = new EphemeralDataProtectionProvider().CreateProtector("purpose");
var dataProtector2 = new EphemeralDataProtectionProvider().CreateProtector("purpose");
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
// Act & assert
// Each instance of the EphemeralDataProtectionProvider has its own unique KDK, so payloads can't be shared.
byte[] protectedBytes = dataProtector1.Protect(bytes);
Assert.ThrowsAny<CryptographicException>(() =>
{
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
});
}
[Fact]
public void SingleProvider_DifferentPurpose_DoesNotRoundTripData()
{
// Arrange
var dataProtectionProvider = new EphemeralDataProtectionProvider();
var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
var dataProtector2 = dataProtectionProvider.CreateProtector("different purpose");
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
// Act & assert
byte[] protectedBytes = dataProtector1.Protect(bytes);
Assert.ThrowsAny<CryptographicException>(() =>
{
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
});
}
[Fact]
public void SingleProvider_SamePurpose_RoundTripsData()
{
// Arrange
var dataProtectionProvider = new EphemeralDataProtectionProvider();
var dataProtector1 = dataProtectionProvider.CreateProtector("purpose");
var dataProtector2 = dataProtectionProvider.CreateProtector("purpose"); // should be equivalent to the previous instance
byte[] bytes = Encoding.UTF8.GetBytes("Hello there!");
// Act
byte[] protectedBytes = dataProtector1.Protect(bytes);
byte[] unprotectedBytes = dataProtector2.Unprotect(protectedBytes);
// Assert
Assert.Equal(bytes, unprotectedBytes);
}
}
}