diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs index 5ad6d238f8..9ab241650c 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/CacheableKeyRing.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement private readonly CancellationToken _expirationToken; internal CacheableKeyRing(CancellationToken expirationToken, DateTimeOffset expirationTime, IKey defaultKey, IEnumerable allKeys) - : this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey.KeyId, allKeys)) + : this(expirationToken, expirationTime, keyRing: new KeyRing(defaultKey, allKeys)) { } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs index 42cc7c2741..f714319fe4 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolution.cs @@ -10,6 +10,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement /// /// The default key, may be null if no key is a good default candidate. /// + /// + /// If this property is non-null, its method will succeed + /// so is appropriate for use with deferred keys. + /// public IKey DefaultKey; /// @@ -17,6 +21,10 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement /// honor the property. This property may /// be null if there is no viable fallback key. /// + /// + /// If this property is non-null, its method will succeed + /// so is appropriate for use with deferred keys. + /// public IKey FallbackKey; /// diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs index 795751fa50..6698105d41 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DefaultKeyResolver.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNet.Cryptography; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.DataProtection.KeyManagement @@ -43,11 +45,21 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement _logger = services.GetLogger(); } - public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable allKeys) + private bool CanCreateAuthenticatedEncryptor(IKey key) { - DefaultKeyResolution retVal = default(DefaultKeyResolution); - retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey); - return retVal; + try + { + var encryptorInstance = key.CreateEncryptorInstance() ?? CryptoUtil.Fail("CreateEncryptorInstance returned null."); + return true; + } + catch (Exception ex) + { + if (_logger.IsWarningLevelEnabled()) + { + _logger.LogWarningF(ex, $"Key {key.KeyId:B} is ineligible to be the default key because its {nameof(IKey.CreateEncryptorInstance)} method failed."); + } + return false; + } } private IKey FindDefaultKey(DateTimeOffset now, IEnumerable allKeys, out IKey fallbackKey, out bool callerShouldGenerateNewKey) @@ -66,11 +78,11 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement } // if the key has been revoked or is expired, it is no longer a candidate - if (preferredDefaultKey.IsExpired(now) || preferredDefaultKey.IsRevoked) + if (preferredDefaultKey.IsRevoked || preferredDefaultKey.IsExpired(now) || !CanCreateAuthenticatedEncryptor(preferredDefaultKey)) { if (_logger.IsVerboseLevelEnabled()) { - _logger.LogVerboseF($"Key {preferredDefaultKey.KeyId:B} is no longer under consideration as default key because it is expired or revoked."); + _logger.LogVerboseF($"Key {preferredDefaultKey.KeyId:B} is no longer under consideration as default key because it is expired, revoked, or cannot be deciphered."); } preferredDefaultKey = null; } @@ -112,7 +124,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement select key).Concat(from key in allKeys orderby key.CreationDate ascending select key) - where !key.IsRevoked + where !key.IsRevoked && CanCreateAuthenticatedEncryptor(key) select key).FirstOrDefault(); if (_logger.IsVerboseLevelEnabled()) @@ -123,5 +135,12 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement callerShouldGenerateNewKey = true; return null; } + + public DefaultKeyResolution ResolveDefaultKeyPolicy(DateTimeOffset now, IEnumerable allKeys) + { + DefaultKeyResolution retVal = default(DefaultKeyResolution); + retVal.DefaultKey = FindDefaultKey(now, allKeys, out retVal.FallbackKey, out retVal.ShouldGenerateNewKey); + return retVal; + } } } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/DeferredKey.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/DeferredKey.cs new file mode 100644 index 0000000000..d75c7d84eb --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/DeferredKey.cs @@ -0,0 +1,41 @@ +// 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.Xml.Linq; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Microsoft.AspNet.DataProtection.XmlEncryption; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + /// + /// The basic implementation of , where the incoming XML element + /// hasn't yet been fully processed. + /// + internal sealed class DeferredKey : KeyBase + { + public DeferredKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IInternalXmlKeyManager keyManager, XElement keyElement) + : base(keyId, creationDate, activationDate, expirationDate, new Lazy(GetLazyEncryptorDelegate(keyManager, keyElement))) + { + } + + private static Func GetLazyEncryptorDelegate(IInternalXmlKeyManager keyManager, XElement keyElement) + { + // The element will be held around in memory for a potentially lengthy period + // of time. Since it might contain sensitive information, we should protect it. + var encryptedKeyElement = keyElement.ToSecret(); + + try + { + return () => keyManager.DeserializeDescriptorFromKeyElement(encryptedKeyElement.ToXElement()).CreateEncryptorInstance(); + } + finally + { + // It's important that the lambda above doesn't capture 'descriptorElement'. Clearing the reference here + // helps us detect if we've done this by causing a null ref at runtime. + keyElement = null; + } + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs index 7c2cd20685..1170c14ceb 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/IInternalXmlKeyManager.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Xml.Linq; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; namespace Microsoft.AspNet.DataProtection.KeyManagement { @@ -9,6 +11,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement internal interface IInternalXmlKeyManager { IKey CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate); + + IAuthenticatedEncryptorDescriptor DeserializeDescriptorFromKeyElement(XElement keyElement); + void RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason); } } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs index d436b18498..5d8c41e25d 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/Key.cs @@ -8,40 +8,14 @@ using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel namespace Microsoft.AspNet.DataProtection.KeyManagement { /// - /// The basic implementation of . + /// The basic implementation of , where the + /// has already been created. /// - internal sealed class Key : IKey + internal sealed class Key : KeyBase { - private readonly IAuthenticatedEncryptorDescriptor _descriptor; - public Key(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, IAuthenticatedEncryptorDescriptor descriptor) + : base(keyId, creationDate, activationDate, expirationDate, new Lazy(descriptor.CreateEncryptorInstance)) { - KeyId = keyId; - CreationDate = creationDate; - ActivationDate = activationDate; - ExpirationDate = expirationDate; - - _descriptor = descriptor; - } - - public DateTimeOffset ActivationDate { get; } - - public DateTimeOffset CreationDate { get; } - - public DateTimeOffset ExpirationDate { get; } - - public bool IsRevoked { get; private set; } - - public Guid KeyId { get; } - - public IAuthenticatedEncryptor CreateEncryptorInstance() - { - return _descriptor.CreateEncryptorInstance(); - } - - internal void SetRevoked() - { - IsRevoked = true; } } } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyBase.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyBase.cs new file mode 100644 index 0000000000..aab41a279c --- /dev/null +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyBase.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.AspNet.DataProtection.AuthenticatedEncryption; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + /// + /// The basic implementation of . + /// + internal abstract class KeyBase : IKey + { + private readonly Lazy _lazyEncryptor; + + public KeyBase(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate, Lazy lazyEncryptor) + { + KeyId = keyId; + CreationDate = creationDate; + ActivationDate = activationDate; + ExpirationDate = expirationDate; + _lazyEncryptor = lazyEncryptor; + } + + public DateTimeOffset ActivationDate { get; } + + public DateTimeOffset CreationDate { get; } + + public DateTimeOffset ExpirationDate { get; } + + public bool IsRevoked { get; private set; } + + public Guid KeyId { get; } + + public IAuthenticatedEncryptor CreateEncryptorInstance() + { + return _lazyEncryptor.Value; + } + + internal void SetRevoked() + { + IsRevoked = true; + } + } +} diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs index 38d8b20099..9aacfb9ed6 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRing.cs @@ -16,16 +16,24 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement private readonly KeyHolder _defaultKeyHolder; private readonly Dictionary _keyIdToKeyHolderMap; - public KeyRing(Guid defaultKeyId, IEnumerable keys) + public KeyRing(IKey defaultKey, IEnumerable allKeys) { _keyIdToKeyHolderMap = new Dictionary(); - foreach (IKey key in keys) + foreach (IKey key in allKeys) { _keyIdToKeyHolderMap.Add(key.KeyId, new KeyHolder(key)); } - DefaultKeyId = defaultKeyId; - _defaultKeyHolder = _keyIdToKeyHolderMap[defaultKeyId]; + // It's possible under some circumstances that the default key won't be part of 'allKeys', + // such as if the key manager is forced to use the key it just generated even if such key + // wasn't in the underlying repository. In this case, we just add it now. + if (!_keyIdToKeyHolderMap.ContainsKey(defaultKey.KeyId)) + { + _keyIdToKeyHolderMap.Add(defaultKey.KeyId, new KeyHolder(defaultKey)); + } + + DefaultKeyId = defaultKey.KeyId; + _defaultKeyHolder = _keyIdToKeyHolderMap[DefaultKeyId]; } public IAuthenticatedEncryptor DefaultAuthenticatedEncryptor diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs index 9f12454e40..f5ffaadb53 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/KeyRingProvider.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement ?? new DefaultKeyResolver(_keyManagementOptions.KeyPropagationWindow, _keyManagementOptions.MaxServerClockSkew, services); } - private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, bool allowRecursiveCalls = false) + private CacheableKeyRing CreateCacheableKeyRingCore(DateTimeOffset now, IKey keyJustAdded) { // Refresh the list of all keys var cacheExpirationToken = _keyManager.GetCacheExpirationToken(); @@ -50,21 +50,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement _logger.LogVerbose("Policy resolution states that a new key should be added to the key ring."); } - // At this point, we know we need to generate a new key. - - // This should only occur if a call to CreateNewKey immediately followed by a call to - // GetAllKeys returned 'you need to add a key to the key ring'. This should never happen - // in practice unless there's corruption in the backing store. Regardless, we can't recurse - // forever, so we have to bail now. - if (!allowRecursiveCalls) + // We shouldn't call CreateKey more than once, else we risk stack diving. This code path shouldn't + // get hit unless there was an ineligible key with an activation date slightly later than the one we + // just added. If this does happen, then we'll just use whatever key we can instead of creating + // new keys endlessly, eventually falling back to the one we just added if all else fails. + if (keyJustAdded != null) { - if (_logger.IsErrorLevelEnabled()) - { - _logger.LogError("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey."); - } - throw CryptoUtil.Fail("Policy resolution states that a new key should be added to the key ring, even after a call to CreateNewKey."); + var keyToUse = defaultKeyPolicy.DefaultKey ?? defaultKeyPolicy.FallbackKey ?? keyJustAdded; + return CreateCacheableKeyRingCoreStep2(now, cacheExpirationToken, keyToUse, allKeys); } + // At this point, we know we need to generate a new key. + // We have been asked to generate a new key, but auto-generation of keys has been disabled. // We need to use the fallback key or fail. if (!_keyManagementOptions.AutoGenerateKeys) @@ -92,16 +89,16 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement { // The case where there's no default key is the easiest scenario, since it // means that we need to create a new key with immediate activation. - _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime); - return CreateCacheableKeyRingCore(now); // recursively call + var newKey = _keyManager.CreateNewKey(activationDate: now, expirationDate: now + _keyManagementOptions.NewKeyLifetime); + return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call } else { // If there is a default key, then the new key we generate should become active upon // expiration of the default key. The new key lifetime is measured from the creation // date (now), not the activation date. - _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime); - return CreateCacheableKeyRingCore(now); // recursively call + var newKey = _keyManager.CreateNewKey(activationDate: defaultKeyPolicy.DefaultKey.ExpirationDate, expirationDate: now + _keyManagementOptions.NewKeyLifetime); + return CreateCacheableKeyRingCore(now, keyJustAdded: newKey); // recursively call } } @@ -109,6 +106,9 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement { Debug.Assert(defaultKey != null); + // Invariant: our caller ensures that CreateEncryptorInstance succeeded at least once + Debug.Assert(defaultKey.CreateEncryptorInstance() != null); + if (_logger.IsVerboseLevelEnabled()) { _logger.LogVerboseF($"Using key {defaultKey.KeyId:B} as the default key."); @@ -186,7 +186,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement CacheableKeyRing ICacheableKeyRingProvider.GetCacheableKeyRing(DateTimeOffset now) { // the entry point allows one recursive call - return CreateCacheableKeyRingCore(now, allowRecursiveCalls: true); + return CreateCacheableKeyRingCore(now, keyJustAdded: null); } } } diff --git a/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs b/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs index baabe020ae..2465348513 100644 --- a/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs +++ b/src/Microsoft.AspNet.DataProtection/KeyManagement/XmlKeyManager.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement var allElements = KeyRepository.GetAllElements(); // We aggregate all the information we read into three buckets - Dictionary keyIdToKeyMap = new Dictionary(); + Dictionary keyIdToKeyMap = new Dictionary(); HashSet revokedKeyIds = null; DateTimeOffset? mostRecentMassRevocationDate = null; @@ -132,7 +132,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement { // ProcessKeyElement can return null in the case of failure, and if this happens we'll move on. // Still need to throw if we see duplicate keys with the same id. - Key key = ProcessKeyElement(element); + KeyBase key = ProcessKeyElement(element); if (key != null) { if (keyIdToKeyMap.ContainsKey(key.KeyId)) @@ -179,7 +179,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement { foreach (Guid revokedKeyId in revokedKeyIds) { - Key key; + KeyBase key; keyIdToKeyMap.TryGetValue(revokedKeyId, out key); if (key != null) { @@ -224,60 +224,36 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement return Interlocked.CompareExchange(ref _cacheExpirationTokenSource, null, null).Token; } - private Key ProcessKeyElement(XElement keyElement) + private KeyBase ProcessKeyElement(XElement keyElement) { Debug.Assert(keyElement.Name == KeyElementName); try { - // Read metadata + // Read metadata and prepare the key for deferred instantiation Guid keyId = (Guid)keyElement.Attribute(IdAttributeName); DateTimeOffset creationDate = (DateTimeOffset)keyElement.Element(CreationDateElementName); DateTimeOffset activationDate = (DateTimeOffset)keyElement.Element(ActivationDateElementName); DateTimeOffset expirationDate = (DateTimeOffset)keyElement.Element(ExpirationDateElementName); - // Figure out who will be deserializing this - XElement descriptorElement = keyElement.Element(DescriptorElementName); - string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName); - - // Decrypt the descriptor element and pass it to the descriptor for consumption - XElement unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator); - var deserializerInstance = _activator.CreateInstance(descriptorDeserializerTypeName); - var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer); - - // Finally, create the Key instance if (_logger.IsVerboseLevelEnabled()) { _logger.LogVerboseF($"Found key {keyId:B}."); } - return new Key( + + return new DeferredKey( keyId: keyId, creationDate: creationDate, activationDate: activationDate, expirationDate: expirationDate, - descriptor: descriptorInstance); + keyManager: _internalKeyManager, + keyElement: keyElement); } catch (Exception ex) { - // We only write the exception out to the 'debug' log since it could contain sensitive - // information and we don't want to leak it. - if (_logger.IsDebugLevelEnabled()) - { - if (_logger.IsWarningLevelEnabled()) - { - _logger.LogWarningF($"An exception of type '{ex.GetType().FullName}' occurred while processing the key element '{keyElement.WithoutChildNodes()}', so the key will not be included in the keyring. Full details of the exception will be written to the 'Debug' log."); - } - _logger.LogDebugF(ex, $"An exception occurred while processing the key element '{keyElement}'."); - } - else - { - if (_logger.IsWarningLevelEnabled()) - { - _logger.LogWarningF($"An exception of type '{ex.GetType().FullName}' occurred while processing the key element '{keyElement.WithoutChildNodes()}', so the key will not be included in the keyring. To prevent accidental disclosure of sensitive information the full exception details are not being logged. To enable logging full exception details, enable 'Debug' level logging for this provider."); - } - } + WriteKeyDeserializationErrorToLog(ex, keyElement); - // If an error occurs, we just skip this key. + // Don't include this key in the key ring return null; } } @@ -369,6 +345,26 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Interlocked.Exchange(ref _cacheExpirationTokenSource, new CancellationTokenSource())?.Cancel(); } + private void WriteKeyDeserializationErrorToLog(Exception error, XElement keyElement) + { + // Ideally we'd suppress the error since it might contain sensitive information, but it would be too difficult for + // an administrator to diagnose the issue if we hide this information. Instead we'll log the error to the error + // log and the raw element to the debug log. This works for our out-of-box XML decryptors since they don't + // include sensitive information in the exception message. + + if (_logger.IsErrorLevelEnabled()) + { + // write sanitized element + _logger.LogErrorF(error, $"An exception occurred while processing the key element '{keyElement.WithoutChildNodes()}'."); + } + + if (_logger.IsDebugLevelEnabled()) + { + // write full element + _logger.LogDebugF(error, $"An exception occurred while processing the key element '{keyElement}'."); + } + } + IKey IInternalXmlKeyManager.CreateNewKey(Guid keyId, DateTimeOffset creationDate, DateTimeOffset activationDate, DateTimeOffset expirationDate) { // @@ -440,6 +436,28 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement descriptor: newDescriptor); } + IAuthenticatedEncryptorDescriptor IInternalXmlKeyManager.DeserializeDescriptorFromKeyElement(XElement keyElement) + { + try + { + // Figure out who will be deserializing this + XElement descriptorElement = keyElement.Element(DescriptorElementName); + string descriptorDeserializerTypeName = (string)descriptorElement.Attribute(DeserializerTypeAttributeName); + + // Decrypt the descriptor element and pass it to the descriptor for consumption + XElement unencryptedInputToDeserializer = descriptorElement.Elements().Single().DecryptElement(_activator); + var deserializerInstance = _activator.CreateInstance(descriptorDeserializerTypeName); + var descriptorInstance = deserializerInstance.ImportFromXml(unencryptedInputToDeserializer); + + return descriptorInstance ?? CryptoUtil.Fail("ImportFromXml returned null."); + } + catch (Exception ex) + { + WriteKeyDeserializationErrorToLog(ex, keyElement); + throw; + } + } + void IInternalXmlKeyManager.RevokeSingleKey(Guid keyId, DateTimeOffset revocationDate, string reason) { // diff --git a/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs b/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs index e97bc112e9..941a0bea66 100644 --- a/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs +++ b/src/Microsoft.AspNet.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs @@ -123,8 +123,8 @@ namespace Microsoft.AspNet.DataProtection.XmlEncryption } /// - /// Converts an to a so that it can be run through - /// the DPAPI routines. + /// Converts an to a so that it can be kept in memory + /// securely or run through the DPAPI routines. /// public static Secret ToSecret(this XElement element) { @@ -163,7 +163,7 @@ namespace Microsoft.AspNet.DataProtection.XmlEncryption } /// - /// Converts a provided by the DPAPI routines back into an . + /// Converts a back into an . /// public static XElement ToXElement(this Secret secret) { diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs index 9f755c2816..1dd54cac67 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DefaultKeyResolverTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; using Moq; using Xunit; @@ -105,7 +106,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement } [Fact] - public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_ReturnsNull() + public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfRevocation_ReturnsNull() { // Arrange var resolver = CreateDefaultKeyResolver(); @@ -120,6 +121,22 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Assert.True(resolution.ShouldGenerateNewKey); } + [Fact] + public void ResolveDefaultKeyPolicy_MostRecentKeyIsInvalid_BecauseOfFailureToDecipher_ReturnsNull() + { + // Arrange + var resolver = CreateDefaultKeyResolver(); + var key1 = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z"); + var key2 = CreateKey("2015-03-02 00:00:00Z", "2016-03-01 00:00:00Z", createEncryptorInstanceThrows: true); + + // Act + var resolution = resolver.ResolveDefaultKeyPolicy("2015-04-01 00:00:00Z", key1, key2); + + // Assert + Assert.Null(resolution.DefaultKey); + Assert.True(resolution.ShouldGenerateNewKey); + } + [Fact] public void ResolveDefaultKeyPolicy_FutureKeyIsValidAndWithinClockSkew_ReturnsFutureKey() { @@ -168,7 +185,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement } [Fact] - public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow() + public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresRevokedKeys() { // Arrange var resolver = CreateDefaultKeyResolver(); @@ -185,6 +202,24 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Assert.True(resolution.ShouldGenerateNewKey); } + [Fact] + public void ResolveDefaultKeyPolicy_FallbackKey_SelectsLatestBeforePriorPropagationWindow_IgnoresFailures() + { + // Arrange + var resolver = CreateDefaultKeyResolver(); + var key1 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-01 00:00:00Z"); + var key2 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-02 00:00:00Z"); + var key3 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-03 00:00:00Z", createEncryptorInstanceThrows: true); + var key4 = CreateKey("2010-01-01 00:00:00Z", "2010-01-01 00:00:00Z", creationDate: "2000-01-04 00:00:00Z"); + + // Act + var resolution = resolver.ResolveDefaultKeyPolicy("2000-01-05 00:00:00Z", key1, key2, key3, key4); + + // Assert + Assert.Same(key2, resolution.FallbackKey); + Assert.True(resolution.ShouldGenerateNewKey); + } + [Fact] public void ResolveDefaultKeyPolicy_FallbackKey_NoNonRevokedKeysBeforePriorPropagationWindow_SelectsEarliestNonRevokedKey() { @@ -210,7 +245,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement services: null); } - private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false) + private static IKey CreateKey(string activationDate, string expirationDate, string creationDate = null, bool isRevoked = false, bool createEncryptorInstanceThrows = false) { var mockKey = new Mock(); mockKey.Setup(o => o.KeyId).Returns(Guid.NewGuid()); @@ -218,6 +253,14 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.IsRevoked).Returns(isRevoked); + if (createEncryptorInstanceThrows) + { + mockKey.Setup(o => o.CreateEncryptorInstance()).Throws(new Exception("This method fails.")); + } + else + { + mockKey.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock().Object); + } return mockKey.Object; } } diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DeferredKeyTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DeferredKeyTests.cs new file mode 100644 index 0000000000..a28aef0dd8 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/DeferredKeyTests.cs @@ -0,0 +1,95 @@ +// 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.Xml.Linq; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Microsoft.AspNet.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + public class DeferredKeyTests + { + [Fact] + public void Ctor_Properties() + { + // Arrange + var keyId = Guid.NewGuid(); + var creationDate = DateTimeOffset.Now; + var activationDate = creationDate.AddDays(2); + var expirationDate = creationDate.AddDays(90); + + // Act + var key = new DeferredKey(keyId, creationDate, activationDate, expirationDate, new Mock().Object, XElement.Parse(@"")); + + // Assert + Assert.Equal(keyId, key.KeyId); + Assert.Equal(creationDate, key.CreationDate); + Assert.Equal(activationDate, key.ActivationDate); + Assert.Equal(expirationDate, key.ExpirationDate); + } + + [Fact] + public void SetRevoked_Respected() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var key = new DeferredKey(Guid.Empty, now, now, now, new Mock().Object, XElement.Parse(@"")); + + // Act & assert + Assert.False(key.IsRevoked); + key.SetRevoked(); + Assert.True(key.IsRevoked); + } + + [Fact] + public void CreateEncryptorInstance_Success() + { + // Arrange + var expectedEncryptor = new Mock().Object; + var mockDescriptor = new Mock(); + mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expectedEncryptor); + var mockKeyManager = new Mock(); + mockKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny())) + .Returns(element => + { + XmlAssert.Equal(@"", element); + return mockDescriptor.Object; + }); + + var now = DateTimeOffset.UtcNow; + var key = new DeferredKey(Guid.Empty, now, now, now, mockKeyManager.Object, XElement.Parse(@"")); + + // Act + var actual = key.CreateEncryptorInstance(); + + // Assert + Assert.Same(expectedEncryptor, actual); + } + + [Fact] + public void CreateEncryptorInstance_CachesFailures() + { + // Arrange + int numTimesCalled = 0; + var mockKeyManager = new Mock(); + mockKeyManager.Setup(o => o.DeserializeDescriptorFromKeyElement(It.IsAny())) + .Returns(element => + { + numTimesCalled++; + throw new Exception("How exceptional."); + }); + + var now = DateTimeOffset.UtcNow; + var key = new DeferredKey(Guid.Empty, now, now, now, mockKeyManager.Object, XElement.Parse(@"")); + + // Act & assert + ExceptionAssert.Throws(() => key.CreateEncryptorInstance(), "How exceptional."); + ExceptionAssert.Throws(() => key.CreateEncryptorInstance(), "How exceptional."); + Assert.Equal(1, numTimesCalled); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs index 6bd46fc6c6..8f934fe96b 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingBasedDataProtectorTests.cs @@ -203,7 +203,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement // the keyring has only one key Key key = new Key(Guid.Empty, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); - var keyRing = new KeyRing(Guid.Empty, new[] { key }); + var keyRing = new KeyRing(key, new[] { key }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); @@ -233,7 +233,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement // the keyring has only one key Key key = new Key(keyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); key.SetRevoked(); - var keyRing = new KeyRing(keyId, new[] { key }); + var keyRing = new KeyRing(key, new[] { key }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); @@ -272,7 +272,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); defaultKey.SetRevoked(); - var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey }); + var keyRing = new KeyRing(defaultKey, new[] { defaultKey }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); @@ -318,7 +318,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(mockEncryptor.Object); Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); - var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey }); + var keyRing = new KeyRing(defaultKey, new[] { defaultKey }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); @@ -368,7 +368,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Key defaultKey = new Key(defaultKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new Mock().Object); Key embeddedKey = new Key(embeddedKeyId, DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, mockDescriptor.Object); - var keyRing = new KeyRing(defaultKeyId, new[] { defaultKey, embeddedKey }); + var keyRing = new KeyRing(defaultKey, new[] { defaultKey, embeddedKey }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); @@ -399,7 +399,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement // Arrange byte[] plaintext = new byte[] { 0x10, 0x20, 0x30, 0x40, 0x50 }; Key key = new Key(Guid.NewGuid(), DateTimeOffset.Now, DateTimeOffset.Now, DateTimeOffset.Now, new AuthenticatedEncryptorConfiguration(new AuthenticatedEncryptionOptions()).CreateNewDescriptor()); - var keyRing = new KeyRing(key.KeyId, new[] { key }); + var keyRing = new KeyRing(key, new[] { key }); var mockKeyRingProvider = new Mock(); mockKeyRingProvider.Setup(o => o.GetCurrentKeyRing()).Returns(keyRing); diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs index ee896e917f..747dee772b 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingProviderTests.cs @@ -6,10 +6,13 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption; using Microsoft.Framework.DependencyInjection; using Moq; using Xunit; +using static System.FormattableString; + namespace Microsoft.AspNet.DataProtection.KeyManagement { public class KeyRingProviderTests @@ -110,7 +113,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token }, getAllKeysReturnValues: new[] { allKeys1, allKeys2 }, createNewKeyCallbacks: new[] { - Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90)) + Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey()) }, resolveDefaultKeyPolicyReturnValues: new[] { @@ -140,6 +143,54 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence); } + [Fact] + public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_CreatesNewKeyWithImmediateActivation_StillNoDefaultKey_ReturnsNewlyCreatedKey() + { + // Arrange + var callSequence = new List(); + var expirationCts1 = new CancellationTokenSource(); + var expirationCts2 = new CancellationTokenSource(); + + var now = StringToDateTime("2015-03-01 00:00:00Z"); + var allKeys = new IKey[0]; + + var newlyCreatedKey = CreateKey("2015-03-01 00:00:00Z", "2016-03-01 00:00:00Z"); + + var keyRingProvider = SetupCreateCacheableKeyRingTestAndCreateKeyManager( + callSequence: callSequence, + getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token }, + getAllKeysReturnValues: new[] { allKeys, allKeys }, + createNewKeyCallbacks: new[] { + Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), newlyCreatedKey) + }, + resolveDefaultKeyPolicyReturnValues: new[] + { + Tuple.Create((DateTimeOffset)now, (IEnumerable)allKeys, new DefaultKeyResolution() + { + DefaultKey = null, + ShouldGenerateNewKey = true + }), + Tuple.Create((DateTimeOffset)now, (IEnumerable)allKeys, new DefaultKeyResolution() + { + DefaultKey = null, + ShouldGenerateNewKey = true + }) + }); + + // Act + var cacheableKeyRing = keyRingProvider.GetCacheableKeyRing(now); + + // Assert + Assert.Equal(newlyCreatedKey.KeyId, cacheableKeyRing.KeyRing.DefaultKeyId); + AssertWithinJitterRange(cacheableKeyRing.ExpirationTimeUtc, now); + Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now)); + expirationCts1.Cancel(); + Assert.True(CacheableKeyRing.IsValid(cacheableKeyRing, now)); + expirationCts2.Cancel(); + Assert.False(CacheableKeyRing.IsValid(cacheableKeyRing, now)); + Assert.Equal(new[] { "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy", "CreateNewKey", "GetCacheExpirationToken", "GetAllKeys", "ResolveDefaultKeyPolicy" }, callSequence); + } + [Fact] public void CreateCacheableKeyRing_GenerationRequired_NoDefaultKey_KeyGenerationDisabled_Fails() { @@ -154,7 +205,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement getCacheExpirationTokenReturnValues: new[] { CancellationToken.None }, getAllKeysReturnValues: new[] { allKeys }, createNewKeyCallbacks: new[] { - Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90)) + Tuple.Create((DateTimeOffset)now, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey()) }, resolveDefaultKeyPolicyReturnValues: new[] { @@ -194,7 +245,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement getCacheExpirationTokenReturnValues: new[] { expirationCts1.Token, expirationCts2.Token }, getAllKeysReturnValues: new[] { allKeys1, allKeys2 }, createNewKeyCallbacks: new[] { - Tuple.Create(key1.ExpirationDate, (DateTimeOffset)now + TimeSpan.FromDays(90)) + Tuple.Create(key1.ExpirationDate, (DateTimeOffset)now + TimeSpan.FromDays(90), CreateKey()) }, resolveDefaultKeyPolicyReturnValues: new[] { @@ -304,7 +355,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement IList callSequence, IEnumerable getCacheExpirationTokenReturnValues, IEnumerable> getAllKeysReturnValues, - IEnumerable> createNewKeyCallbacks, + IEnumerable> createNewKeyCallbacks, IEnumerable, DefaultKeyResolution>> resolveDefaultKeyPolicyReturnValues, KeyManagementOptions keyManagementOptions = null) { @@ -337,21 +388,21 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement createNewKeyCallbacksEnumerator.MoveNext(); Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item1, activationDate); Assert.Equal(createNewKeyCallbacksEnumerator.Current.Item2, expirationDate); - return null; // nobody uses this return value - }); + return createNewKeyCallbacksEnumerator.Current.Item3; + }); } var resolveDefaultKeyPolicyReturnValuesEnumerator = resolveDefaultKeyPolicyReturnValues.GetEnumerator(); var mockDefaultKeyResolver = new Mock(MockBehavior.Strict); mockDefaultKeyResolver.Setup(o => o.ResolveDefaultKeyPolicy(It.IsAny(), It.IsAny>())) - .Returns>((now, allKeys) => - { - callSequence.Add("ResolveDefaultKeyPolicy"); - resolveDefaultKeyPolicyReturnValuesEnumerator.MoveNext(); - Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item1, now); - Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item2, allKeys); - return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3; - }); + .Returns>((now, allKeys) => + { + callSequence.Add("ResolveDefaultKeyPolicy"); + resolveDefaultKeyPolicyReturnValuesEnumerator.MoveNext(); + Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item1, now); + Assert.Equal(resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item2, allKeys); + return resolveDefaultKeyPolicyReturnValuesEnumerator.Current.Item3; + }); return CreateKeyRingProvider(mockKeyManager.Object, mockDefaultKeyResolver.Object, keyManagementOptions); } @@ -495,6 +546,12 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement return DateTimeOffset.ParseExact(input, "u", CultureInfo.InvariantCulture).UtcDateTime; } + private static IKey CreateKey() + { + var now = DateTimeOffset.Now; + return CreateKey(Invariant($"{now:u}"), Invariant($"{now.AddDays(90):u}")); + } + private static IKey CreateKey(string activationDate, string expirationDate, bool isRevoked = false) { var mockKey = new Mock(); @@ -502,6 +559,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement mockKey.Setup(o => o.ActivationDate).Returns(DateTimeOffset.ParseExact(activationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.ExpirationDate).Returns(DateTimeOffset.ParseExact(expirationDate, "u", CultureInfo.InvariantCulture)); mockKey.Setup(o => o.IsRevoked).Returns(isRevoked); + mockKey.Setup(o => o.CreateEncryptorInstance()).Returns(new Mock().Object); return mockKey.Object; } } diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs index aa192fc4d6..904a8afe86 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyRingTests.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement var key2 = new MyKey(); // Act - var keyRing = new KeyRing(key1.KeyId, new[] { key1, key2 }); + var keyRing = new KeyRing(key1, new[] { key1, key2 }); // Assert Assert.Equal(0, key1.NumTimesCreateEncryptorInstanceCalled); @@ -38,12 +38,29 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement var key2 = new MyKey(); // Act - var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); + var keyRing = new KeyRing(key2, new[] { key1, key2 }); // Assert Assert.Equal(key2.KeyId, keyRing.DefaultKeyId); } + [Fact] + public void DefaultKeyIdAndEncryptor_IfDefaultKeyNotPresentInAllKeys() + { + // Arrange + var key1 = new MyKey(); + var key2 = new MyKey(); + var key3 = new MyKey(expectedEncryptorInstance: new Mock().Object); + + // Act + var keyRing = new KeyRing(key3, new[] { key1, key2 }); + + // Assert + bool unused; + Assert.Equal(key3.KeyId, keyRing.DefaultKeyId); + Assert.Equal(key3.CreateEncryptorInstance(), keyRing.GetAuthenticatedEncryptorByKeyId(key3.KeyId, out unused)); + } + [Fact] public void GetAuthenticatedEncryptorByKeyId_DefersInstantiation_AndReturnsRevocationInfo() { @@ -55,7 +72,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement var key2 = new MyKey(expectedEncryptorInstance: expectedEncryptorInstance2); // Act - var keyRing = new KeyRing(key2.KeyId, new[] { key1, key2 }); + var keyRing = new KeyRing(key2, new[] { key1, key2 }); // Assert bool isRevoked; diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyTests.cs new file mode 100644 index 0000000000..88c9795eb9 --- /dev/null +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/KeyTests.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 Microsoft.AspNet.DataProtection.AuthenticatedEncryption; +using Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.DataProtection.KeyManagement +{ + public class KeyTests + { + [Fact] + public void Ctor_Properties() + { + // Arrange + var keyId = Guid.NewGuid(); + var creationDate = DateTimeOffset.Now; + var activationDate = creationDate.AddDays(2); + var expirationDate = creationDate.AddDays(90); + + // Act + var key = new Key(keyId, creationDate, activationDate, expirationDate, new Mock().Object); + + // Assert + Assert.Equal(keyId, key.KeyId); + Assert.Equal(creationDate, key.CreationDate); + Assert.Equal(activationDate, key.ActivationDate); + Assert.Equal(expirationDate, key.ExpirationDate); + } + + [Fact] + public void SetRevoked_Respected() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var key = new Key(Guid.Empty, now, now, now, new Mock().Object); + + // Act & assert + Assert.False(key.IsRevoked); + key.SetRevoked(); + Assert.True(key.IsRevoked); + } + + [Fact] + public void CreateEncryptorInstance() + { + // Arrange + var expected = new Mock().Object; + var mockDescriptor = new Mock(); + mockDescriptor.Setup(o => o.CreateEncryptorInstance()).Returns(expected); + + var now = DateTimeOffset.UtcNow; + var key = new Key(Guid.Empty, now, now, now, mockDescriptor.Object); + + // Act + var actual = key.CreateEncryptorInstance(); + + // Assert + Assert.Same(expected, actual); + } + } +} diff --git a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs index 388b5bc67e..1fa9079564 100644 --- a/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs +++ b/test/Microsoft.AspNet.DataProtection.Test/KeyManagement/XmlKeyManagerTests.cs @@ -473,7 +473,7 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement 2015-01-01T00:00:00Z 2015-02-01T00:00:00Z - 2015-03-01T00:00:00Z + NOT A VALID DATE @@ -492,7 +492,6 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement var expectedEncryptor = new Mock().Object; var mockActivator = new Mock(); mockActivator.ReturnAuthenticatedEncryptorGivenDeserializerTypeNameAndInput("goodDeserializer", "", expectedEncryptor); - mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("How exceptional!")); // Act var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray(); @@ -513,26 +512,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement 2015-01-01T00:00:00Z 2015-02-01T00:00:00Z - 2015-03-01T00:00:00Z - - - - - + NOT A VALID DATE + "; - var mockActivator = new Mock(); - mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W")); - var loggerFactory = new StringLoggerFactory(LogLevel.Verbose); // Act - RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); + RunGetAllKeysCore(xml, new Mock().Object, loggerFactory).ToArray(); // Assert Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged."); - Assert.False(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '1A2B3C4D' should not have been logged."); } [Fact] @@ -545,26 +536,18 @@ namespace Microsoft.AspNet.DataProtection.KeyManagement 2015-01-01T00:00:00Z 2015-02-01T00:00:00Z - 2015-03-01T00:00:00Z - - - - - + NOT A VALID DATE + "; - var mockActivator = new Mock(); - mockActivator.Setup(o => o.CreateInstance(It.IsAny(), "badDeserializer")).Throws(new Exception("Secret information: 9Z8Y7X6W")); - var loggerFactory = new StringLoggerFactory(LogLevel.Debug); // Act - RunGetAllKeysCore(xml, mockActivator.Object, loggerFactory).ToArray(); + RunGetAllKeysCore(xml, new Mock().Object, loggerFactory).ToArray(); // Assert Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged."); - Assert.True(loggerFactory.ToString().Contains("9Z8Y7X6W"), "The secret '9Z8Y7X6W' should have been logged."); } [Fact]