diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs
index 2765421512..c9f6b4f8d0 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngCbcAuthenticatedEncryptorConfigurationOptions.cs
@@ -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.
///
- public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions
+ public sealed class CngCbcAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
///
/// 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);
+ }
}
}
diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs
index 33ad8e8eb5..271a43eefe 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/CngGcmAuthenticatedEncryptorConfigurationOptions.cs
@@ -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.
///
- public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions
+ public sealed class CngGcmAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
///
/// 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);
+ }
}
}
diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs
new file mode 100644
index 0000000000..7b26d09838
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/IInternalConfigurationOptions.cs
@@ -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);
+ }
+}
diff --git a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs
index 0a9036886c..9ebed10e96 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/AuthenticatedEncryption/ManagedAuthenticatedEncryptorConfigurationOptions.cs
@@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
/// Options for configuring an authenticated encryption mechanism which uses
/// managed SymmetricAlgorithm and KeyedHashAlgorithm implementations.
///
- public sealed class ManagedAuthenticatedEncryptorConfigurationOptions
+ public sealed class ManagedAuthenticatedEncryptorConfigurationOptions : IInternalConfigurationOptions
{
///
/// The type of the algorithm to use for symmetric encryption.
@@ -102,6 +102,11 @@ namespace Microsoft.AspNet.Security.DataProtection.AuthenticatedEncryption
return ((IActivator)Activator.CreateInstance(typeof(AlgorithmActivator<>).MakeGenericType(ValidationAlgorithmType))).Creator;
}
+ IAuthenticatedEncryptor IInternalConfigurationOptions.CreateAuthenticatedEncryptor(ISecret secret)
+ {
+ return CreateAuthenticatedEncryptor(secret);
+ }
+
private interface IActivator
{
Func Creator { get; }
diff --git a/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs b/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs
index 15e7ef1fbb..a13c20c727 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/EphemeralDataProtectionProvider.cs
@@ -28,12 +28,12 @@ namespace Microsoft.AspNet.Security.DataProtection
if (OSVersionUtil.IsBCryptOnWin7OrLaterAvailable())
{
// Fastest implementation: AES-GCM
- keyringProvider = new CngEphemeralKeyRing();
+ keyringProvider = new EphemeralKeyRing();
}
else
{
// Slowest implementation: managed CBC + HMAC
- keyringProvider = new ManagedEphemeralKeyRing();
+ keyringProvider = new EphemeralKeyRing();
}
_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 : IKeyRing, IKeyRingProvider
+ where T : IInternalConfigurationOptions, new()
{
- public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new CngGcmAuthenticatedEncryptorConfigurationFactory(new DefaultOptionsAccessor()).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()).CreateNewConfiguration().CreateEncryptorInstance();
+ public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor { get; } = new T().CreateAuthenticatedEncryptor(ProtectedMemoryBlob.Random(NUM_BYTES_IN_KDK));
public Guid DefaultKeyId { get; } = default(Guid);
diff --git a/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs b/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs
index ce3b6dae7f..f89af3bfdd 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/ProtectedMemoryBlob.cs
@@ -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 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(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 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);
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
index bc2265436b..7e2027e78a 100644
--- a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/CbcAuthenticatedEncryptorTests.cs
@@ -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()
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
index 5663edd0e1..b1eae351d7 100644
--- a/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/Cng/GcmAuthenticatedEncryptorTests.cs
@@ -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()
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/EphemeralDataProtectionProviderTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
new file mode 100644
index 0000000000..b580ebf97a
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/EphemeralDataProtectionProviderTests.cs
@@ -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(() =>
+ {
+ 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(() =>
+ {
+ 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);
+ }
+ }
+}