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
/// Windows CNG algorithms in CBC encryption + HMAC validation modes.
/// </summary>
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions
public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
/// <summary>
/// The name of the algorithm to use for symmetric encryption.
@ -178,5 +178,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
}
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
/// Windows CNG encryption algorithms in Galois/Counter Mode.
/// </summary>
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions
public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
/// <summary>
/// The name of the algorithm to use for symmetric encryption.
@ -120,5 +120,10 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
}
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
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
/// </summary>
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions
public sealed class ManagedAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
/// <summary>
/// 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;
}
IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
{
return CreateAuthenticatedEncryptor(secret);
}
private interface IActivator<out T>
{
Func<T> Creator { get; }

View File

@ -28,12 +28,12 @@ namespace Microsoft.AspNet.Security.DataProtection
if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{
// Fastest implementation: AES-GCM
keyringProvider = new CngEphemeralKeyRing();
keyringProvider = new EphemeralKeyRing<CngGcmAuthenticatedEncryptorConfigurationOptions>();
}
else
{
// Slowest implementation: managed CBC + HMAC
keyringProvider = new ManagedEphemeralKeyRing();
keyringProvider = new EphemeralKeyRing<ManagedAuthenticatedEncryptorConfigurationOptions>();
}
_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 CngEphemeralKeyRing : IKeyRing, IKeyRingProvider
private sealed class EphemeralKeyRing<T> : 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 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 IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().CreateAuthenticatedEncryptor(ProtectedMemoryBlob.Random(NUM_BYTES_IN_KDK));
public Guid DefaultKeyId { get; } = default(Guid);

View File

@ -3,8 +3,8 @@
using System;
using Microsoft.AspNet.Security.DataProtection.Cng;
using Microsoft.AspNet.Security.DataProtection.Managed;
using Microsoft.AspNet.Security.DataProtection.SafeHandles;
using Microsoft.Win32.SafeHandles;
namespace Microsoft.AspNet.Security.DataProtection
{
@ -13,14 +13,14 @@ namespace Microsoft.AspNet.Security.DataProtection
// from wincrypt.h
private const uint CRYPTPROTECTMEMORY_BLOCK_SIZE = 16;
private readonly SecureLocalAllocHandle _encryptedMemoryHandle;
private readonly SecureLocalAllocHandle _localAllocHandle;
private readonly uint _plaintextLength;
public ProtectedMemoryBlob(ArraySegment<byte> plaintext)
{
plaintext.Validate();
_encryptedMemoryHandle = Protect(plaintext);
_localAllocHandle = Protect(plaintext);
_plaintextLength = (uint)plaintext.Count;
}
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Security.DataProtection
throw new ArgumentOutOfRangeException("plaintextLength");
}
_encryptedMemoryHandle = Protect(plaintext, (uint)plaintextLength);
_localAllocHandle = Protect(plaintext, (uint)plaintextLength);
_plaintextLength = (uint)plaintextLength;
}
@ -55,7 +55,7 @@ namespace Microsoft.AspNet.Security.DataProtection
if (other != null)
{
// Fast-track: simple deep copy scenario.
this._encryptedMemoryHandle = other._encryptedMemoryHandle.Duplicate();
this._localAllocHandle = other._localAllocHandle.Duplicate();
this._plaintextLength = other._plaintextLength;
}
else
@ -68,7 +68,7 @@ namespace Microsoft.AspNet.Security.DataProtection
try
{
secret.WriteSecretIntoBuffer(new ArraySegment<byte>(tempPlaintextBuffer));
_encryptedMemoryHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
_localAllocHandle = Protect(pbTempPlaintextBuffer, (uint)tempPlaintextBuffer.Length);
_plaintextLength = (uint)tempPlaintextBuffer.Length;
}
finally
@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Security.DataProtection
public void Dispose()
{
_encryptedMemoryHandle.Dispose();
_localAllocHandle.Dispose();
}
private static SecureLocalAllocHandle Protect(ArraySegment<byte> plaintext)
@ -102,6 +102,16 @@ namespace Microsoft.AspNet.Security.DataProtection
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.
uint numTotalBytesToAllocate = cbPlaintext;
uint numBytesPaddingRequired = CRYPTPROTECTMEMORY_BLOCK_SIZE - (numTotalBytesToAllocate % CRYPTPROTECTMEMORY_BLOCK_SIZE);
@ -135,6 +145,12 @@ namespace Microsoft.AspNet.Security.DataProtection
}
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];
fixed (byte* pbBytes = bytes)
{
@ -153,18 +169,26 @@ namespace Microsoft.AspNet.Security.DataProtection
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)
{
// 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.
UnsafeBufferUtil.BlockCopy(from: _encryptedMemoryHandle, to: pbBuffer, byteCount: _plaintextLength);
UnsafeBufferUtil.BlockCopy(from: _localAllocHandle, to: pbBuffer, byteCount: _plaintextLength);
MemoryProtection.CryptUnprotectMemory(pbBuffer, _plaintextLength);
}
else
{
// 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.
using (var duplicateHandle = _encryptedMemoryHandle.Duplicate())
using (var duplicateHandle = _localAllocHandle.Duplicate())
{
MemoryProtection.CryptUnprotectMemory(duplicateHandle, checked((uint)duplicateHandle.Length));
UnsafeBufferUtil.BlockCopy(from: duplicateHandle, to: pbBuffer, byteCount: _plaintextLength);

View File

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

View File

@ -10,7 +10,7 @@ using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
{
public unsafe class GcmAuthenticatedEncryptorTests
public class GcmAuthenticatedEncryptorTests
{
[Fact]
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);
}
}
}