Make EphemeralDataProtectionProvider and ProtectedMemoryBlob work on non-Windows platforms.
This commit is contained in:
parent
7d5a29a9fd
commit
c3b76d14a3
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue