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); + } + } +}