File: KeyManagement\XmlKeyManagerTests.cs
Web Access
Project: src\src\DataProtection\DataProtection\test\Microsoft.AspNetCore.DataProtection.Tests\Microsoft.AspNetCore.DataProtection.Tests.csproj (Microsoft.AspNetCore.DataProtection.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.Internal;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
 
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
 
public class XmlKeyManagerTests
{
    private static readonly XElement serializedDescriptor = XElement.Parse(@"
            <theElement>
              <secret enc:requiresEncryption='true' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
                <![CDATA[This is a secret value.]]>
              </secret>
            </theElement>");
 
    [Fact]
    public void Ctor_WithoutEncryptorOrRepository_UsesFallback()
    {
        // Arrange
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = null,
            XmlEncryptor = null
        });
 
        // Act
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        // Assert
        Assert.NotNull(keyManager.KeyRepository);
 
        if (OSVersionUtil.IsWindows())
        {
            Assert.NotNull(keyManager.KeyEncryptor);
        }
    }
 
    [Fact]
    public void Ctor_WithEncryptorButNoRepository_UsesFallback()
    {
        // Arrange
        var expectedXmlEncryptor = new Mock<IXmlEncryptor>().Object;
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = null,
            XmlEncryptor = expectedXmlEncryptor
        });
 
        // Act
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        // Assert
        Assert.NotNull(keyManager.KeyRepository);
 
        Assert.Same(expectedXmlEncryptor, keyManager.KeyEncryptor);
    }
 
    [Fact]
    public void CreateNewKey_Internal_NoEscrowOrEncryption()
    {
        // Constants
        var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
        var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
        var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
        var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
 
        // Arrange
        XElement elementStoredInRepository = null;
        string friendlyNameStoredInRepository = null;
        var expectedAuthenticatedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
        var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
        mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
        var expectedDescriptor = mockDescriptor.Object;
        var testEncryptorFactory = new TestEncryptorFactory(expectedDescriptor, expectedAuthenticatedEncryptor);
        var mockConfiguration = new Mock<AlgorithmConfiguration>();
        mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(expectedDescriptor);
        var mockXmlRepository = new Mock<IXmlRepository>();
        mockXmlRepository
            .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
            .Callback<XElement, string>((el, friendlyName) =>
            {
                elementStoredInRepository = el;
                friendlyNameStoredInRepository = friendlyName;
            });
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = mockConfiguration.Object,
            XmlRepository = mockXmlRepository.Object,
            XmlEncryptor = null
        });
        options.Value.AuthenticatedEncryptorFactories.Add(testEncryptorFactory);
 
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        // Act & assert
 
        // The cancellation token should not already be fired
        var firstCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.False(firstCancellationToken.IsCancellationRequested);
 
        // After the call to CreateNewKey, the first CT should be fired,
        // and we should've gotten a new CT.
        var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
            keyId: keyId,
            creationDate: creationDate,
            activationDate: activationDate,
            expirationDate: expirationDate);
        var secondCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.True(firstCancellationToken.IsCancellationRequested);
        Assert.False(secondCancellationToken.IsCancellationRequested);
 
        // Does the IKey have the properties we requested?
        Assert.Equal(keyId, newKey.KeyId);
        Assert.Equal(creationDate, newKey.CreationDate);
        Assert.Equal(activationDate, newKey.ActivationDate);
        Assert.Equal(expirationDate, newKey.ExpirationDate);
        Assert.Same(expectedDescriptor, newKey.Descriptor);
        Assert.False(newKey.IsRevoked);
        Assert.Same(expectedAuthenticatedEncryptor, testEncryptorFactory.CreateEncryptorInstance(newKey));
 
        // Finally, was the correct element stored in the repository?
        string expectedXml = string.Format(
            CultureInfo.InvariantCulture,
            @"
                <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
                  {1}
                  {2}
                  {3}
                  <descriptor deserializerType='{0}'>
                    <theElement>
                      <secret enc:requiresEncryption='true'>
                        <![CDATA[This is a secret value.]]>
                      </secret>
                    </theElement>
                  </descriptor>
                </key>",
            typeof(MyDeserializer).AssemblyQualifiedName,
            new XElement("creationDate", creationDate),
            new XElement("activationDate", activationDate),
            new XElement("expirationDate", expirationDate));
        XmlAssert.Equal(expectedXml, elementStoredInRepository);
        Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
    }
 
    [Fact]
    public void CreateNewKey_Internal_WithEscrowAndEncryption()
    {
        // Constants
        var creationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
        var activationDate = new DateTimeOffset(2014, 02, 01, 0, 0, 0, TimeSpan.Zero);
        var expirationDate = new DateTimeOffset(2014, 03, 01, 0, 0, 0, TimeSpan.Zero);
        var keyId = new Guid("3d6d01fd-c0e7-44ae-82dd-013b996b4093");
 
        // Arrange
        XElement elementStoredInEscrow = null;
        Guid? keyIdStoredInEscrow = null;
        XElement elementStoredInRepository = null;
        string friendlyNameStoredInRepository = null;
        var expectedAuthenticatedEncryptor = new Mock<IAuthenticatedEncryptor>().Object;
        var mockDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>();
        mockDescriptor.Setup(o => o.ExportToXml()).Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(MyDeserializer)));
        var expectedDescriptor = mockDescriptor.Object;
        var testEncryptorFactory = new TestEncryptorFactory(expectedDescriptor, expectedAuthenticatedEncryptor);
        var mockConfiguration = new Mock<AlgorithmConfiguration>();
        mockConfiguration.Setup(o => o.CreateNewDescriptor()).Returns(expectedDescriptor);
        var mockXmlRepository = new Mock<IXmlRepository>();
        mockXmlRepository
            .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
            .Callback<XElement, string>((el, friendlyName) =>
            {
                elementStoredInRepository = el;
                friendlyNameStoredInRepository = friendlyName;
            });
        var mockKeyEscrow = new Mock<IKeyEscrowSink>();
        mockKeyEscrow
            .Setup(o => o.Store(It.IsAny<Guid>(), It.IsAny<XElement>()))
            .Callback<Guid, XElement>((innerKeyId, el) =>
            {
                keyIdStoredInEscrow = innerKeyId;
                elementStoredInEscrow = el;
            });
 
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = mockConfiguration.Object,
            XmlRepository = mockXmlRepository.Object,
            XmlEncryptor = new NullXmlEncryptor()
        });
        options.Value.AuthenticatedEncryptorFactories.Add(testEncryptorFactory);
        options.Value.KeyEscrowSinks.Add(mockKeyEscrow.Object);
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        // Act & assert
 
        // The cancellation token should not already be fired
        var firstCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.False(firstCancellationToken.IsCancellationRequested);
 
        // After the call to CreateNewKey, the first CT should be fired,
        // and we should've gotten a new CT.
        var newKey = ((IInternalXmlKeyManager)keyManager).CreateNewKey(
            keyId: keyId,
            creationDate: creationDate,
            activationDate: activationDate,
            expirationDate: expirationDate);
        var secondCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.True(firstCancellationToken.IsCancellationRequested);
        Assert.False(secondCancellationToken.IsCancellationRequested);
 
        // Does the IKey have the properties we requested?
        Assert.Equal(keyId, newKey.KeyId);
        Assert.Equal(creationDate, newKey.CreationDate);
        Assert.Equal(activationDate, newKey.ActivationDate);
        Assert.Equal(expirationDate, newKey.ExpirationDate);
        Assert.Same(expectedDescriptor, newKey.Descriptor);
        Assert.False(newKey.IsRevoked);
        Assert.Same(expectedAuthenticatedEncryptor, testEncryptorFactory.CreateEncryptorInstance(newKey));
 
        // Was the correct element stored in escrow?
        // This should not have gone through the encryptor.
        string expectedEscrowXml = string.Format(
          CultureInfo.InvariantCulture,
            @"
                <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
                  {1}
                  {2}
                  {3}
                  <descriptor deserializerType='{0}'>
                    <theElement>
                      <secret enc:requiresEncryption='true'>
                        <![CDATA[This is a secret value.]]>
                      </secret>
                    </theElement>
                  </descriptor>
                </key>",
            typeof(MyDeserializer).AssemblyQualifiedName,
            new XElement("creationDate", creationDate),
            new XElement("activationDate", activationDate),
            new XElement("expirationDate", expirationDate));
        XmlAssert.Equal(expectedEscrowXml, elementStoredInEscrow);
        Assert.Equal(keyId, keyIdStoredInEscrow.Value);
 
        // Finally, was the correct element stored in the repository?
        // This should have gone through the encryptor (which we set to be the null encryptor in this test)
        string expectedRepositoryXml = String.Format(
          CultureInfo.InvariantCulture,
          @"
                <key id='3d6d01fd-c0e7-44ae-82dd-013b996b4093' version='1' xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
                  {2}
                  {3}
                  {4}
                  <descriptor deserializerType='{0}'>
                    <theElement>
                      <enc:encryptedSecret decryptorType='{1}'>
                        <unencryptedKey>
                          <secret enc:requiresEncryption='true'>
                            <![CDATA[This is a secret value.]]>
                          </secret>
                        </unencryptedKey>
                      </enc:encryptedSecret>
                    </theElement>
                  </descriptor>
                </key>",
            typeof(MyDeserializer).AssemblyQualifiedName,
            typeof(NullXmlDecryptor).AssemblyQualifiedName,
            new XElement("creationDate", creationDate),
            new XElement("activationDate", activationDate),
            new XElement("expirationDate", expirationDate));
        XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
        Assert.Equal("key-3d6d01fd-c0e7-44ae-82dd-013b996b4093", friendlyNameStoredInRepository);
    }
 
    [Fact]
    public void CreateNewKey_CallsInternalManager()
    {
        // Arrange
        DateTimeOffset minCreationDate = DateTimeOffset.UtcNow;
        DateTimeOffset? actualCreationDate = null;
        DateTimeOffset activationDate = minCreationDate + TimeSpan.FromDays(7);
        DateTimeOffset expirationDate = activationDate.AddMonths(1);
        var mockInternalKeyManager = new Mock<IInternalXmlKeyManager>();
        mockInternalKeyManager
            .Setup(o => o.CreateNewKey(It.IsAny<Guid>(), It.IsAny<DateTimeOffset>(), activationDate, expirationDate))
            .Callback<Guid, DateTimeOffset, DateTimeOffset, DateTimeOffset>((innerKeyId, innerCreationDate, innerActivationDate, innerExpirationDate) =>
            {
                actualCreationDate = innerCreationDate;
            });
 
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = new Mock<IXmlRepository>().Object,
            XmlEncryptor = null
        });
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance, mockInternalKeyManager.Object);
 
        // Act
        keyManager.CreateNewKey(activationDate, expirationDate);
 
        // Assert
        Assert.InRange(actualCreationDate.Value, minCreationDate, DateTimeOffset.UtcNow);
    }
 
    [Fact]
    public void GetAllKeys_Empty()
    {
        // Arrange
        const string xml = @"<root />";
        var activator = new Mock<IActivator>().Object;
 
        // Act
        var keys = RunGetAllKeysCore(xml, activator);
 
        // Assert
        Assert.Empty(keys);
    }
 
    [Fact]
    public void GetAllKeys_IgnoresUnknownElements()
    {
        // Arrange
        const string xml = @"
                <root>
                  <key id='62a72ad9-42d7-4e97-b3fa-05bad5d53d33' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>2015-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='deserializer-A'>
                      <elementA />
                    </descriptor>
                  </key>
                  <unknown>
                    <![CDATA[Unknown elements are ignored.]]>
                  </unknown>
                  <key id='041be4c0-52d7-48b4-8d32-f8c0ff315459' version='1'>
                    <creationDate>2015-04-01T00:00:00Z</creationDate>
                    <activationDate>2015-05-01T00:00:00Z</activationDate>
                    <expirationDate>2015-06-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='deserializer-B'>
                      <elementB />
                    </descriptor>
                  </key>
                </root>";
 
        var descriptorA = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
        var descriptorB = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
        var mockActivator = new Mock<IActivator>();
        mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("deserializer-A", "<elementA />", descriptorA);
        mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("deserializer-B", "<elementB />", descriptorB);
 
        // Act
        var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
 
        // Assert
        Assert.Equal(2, keys.Length);
        Assert.Equal(new Guid("62a72ad9-42d7-4e97-b3fa-05bad5d53d33"), keys[0].KeyId);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-01-01T00:00:00Z"), keys[0].CreationDate);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-02-01T00:00:00Z"), keys[0].ActivationDate);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-03-01T00:00:00Z"), keys[0].ExpirationDate);
        Assert.False(keys[0].IsRevoked);
        Assert.Same(descriptorA, keys[0].Descriptor);
        Assert.Equal(new Guid("041be4c0-52d7-48b4-8d32-f8c0ff315459"), keys[1].KeyId);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-04-01T00:00:00Z"), keys[1].CreationDate);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-05-01T00:00:00Z"), keys[1].ActivationDate);
        Assert.Equal(XmlConvert.ToDateTimeOffset("2015-06-01T00:00:00Z"), keys[1].ExpirationDate);
        Assert.False(keys[1].IsRevoked);
        Assert.Same(descriptorB, keys[1].Descriptor);
    }
 
    [Fact]
    public void GetAllKeys_UnderstandsRevocations()
    {
        // Arrange
        const string xml = @"
                <root>
                  <key id='67f9cdea-83ba-41ed-b160-2b1d0ea30251' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>2015-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='theDeserializer'>
                      <node />
                    </descriptor>
                  </key>
                  <key id='0cf83742-d175-42a8-94b5-1ec049b354c3' version='1'>
                    <creationDate>2016-01-01T00:00:00Z</creationDate>
                    <activationDate>2016-02-01T00:00:00Z</activationDate>
                    <expirationDate>2016-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='theDeserializer'>
                      <node />
                    </descriptor>
                  </key>
                  <key id='21580ac4-c83a-493c-bde6-29a1cc97ca0f' version='1'>
                    <creationDate>2017-01-01T00:00:00Z</creationDate>
                    <activationDate>2017-02-01T00:00:00Z</activationDate>
                    <expirationDate>2017-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='theDeserializer'>
                      <node />
                    </descriptor>
                  </key>
                  <key id='6bd14f12-0bb8-4822-91d7-04b360de0497' version='1'>
                    <creationDate>2018-01-01T00:00:00Z</creationDate>
                    <activationDate>2018-02-01T00:00:00Z</activationDate>
                    <expirationDate>2018-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='theDeserializer'>
                      <node />
                    </descriptor>
                  </key>
                  <revocation version='1'>
                    <!-- The below will revoke no keys. -->
                    <revocationDate>2014-01-01T00:00:00Z</revocationDate>
                    <key id='*' />
                  </revocation>
                  <revocation version='1'>
                    <!-- The below will revoke the first two keys. -->
                    <revocationDate>2017-01-01T00:00:00Z</revocationDate>
                    <key id='*' />
                  </revocation>
                  <revocation version='1'>
                    <!-- The below will revoke only the last key. -->
                    <revocationDate>2020-01-01T00:00:00Z</revocationDate>
                    <key id='6bd14f12-0bb8-4822-91d7-04b360de0497' />
                  </revocation>
                </root>";
 
        var mockActivator = new Mock<IActivator>();
        mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("theDeserializer", "<node />", new Mock<IAuthenticatedEncryptorDescriptor>().Object);
 
        // Act
        var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
 
        // Assert
        Assert.Equal(4, keys.Length);
        Assert.Equal(new Guid("67f9cdea-83ba-41ed-b160-2b1d0ea30251"), keys[0].KeyId);
        Assert.True(keys[0].IsRevoked);
        Assert.Equal(new Guid("0cf83742-d175-42a8-94b5-1ec049b354c3"), keys[1].KeyId);
        Assert.True(keys[1].IsRevoked);
        Assert.Equal(new Guid("21580ac4-c83a-493c-bde6-29a1cc97ca0f"), keys[2].KeyId);
        Assert.False(keys[2].IsRevoked);
        Assert.Equal(new Guid("6bd14f12-0bb8-4822-91d7-04b360de0497"), keys[3].KeyId);
        Assert.True(keys[3].IsRevoked);
    }
 
    [Fact]
    public void GetAllKeys_PerformsDecryption()
    {
        // Arrange
        const string xml = @"
                <root xmlns:enc='http://schemas.asp.net/2015/03/dataProtection'>
                  <key id='09712588-ba68-438a-a5ee-fe842b3453b2' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>2015-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='theDeserializer'>
                      <enc:encryptedSecret decryptorType='theDecryptor'>
                        <node xmlns='private' />
                      </enc:encryptedSecret>
                    </descriptor>
                  </key>
                </root>";
 
        var expectedDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
        var mockActivator = new Mock<IActivator>();
        mockActivator.ReturnDecryptedElementGivenDecryptorTypeNameAndInput("theDecryptor", "<node xmlns='private' />", "<decryptedNode />");
        mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("theDeserializer", "<decryptedNode />", expectedDescriptor);
 
        // Act
        var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
 
        // Assert
        Assert.Single(keys);
        Assert.Equal(new Guid("09712588-ba68-438a-a5ee-fe842b3453b2"), keys[0].KeyId);
        Assert.Same(expectedDescriptor, keys[0].Descriptor);
    }
 
    [Fact]
    public void GetAllKeys_SwallowsKeyDeserializationErrors()
    {
        // Arrange
        const string xml = @"
                <root>
                  <!-- The below key will throw an exception when deserializing. -->
                  <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>NOT A VALID DATE</expirationDate>
                    <descriptor deserializerType='badDeserializer'>
                      <node />
                    </descriptor>
                  </key>
                  <!-- The below key will deserialize properly. -->
                  <key id='49c0cda9-0232-4d8c-a541-de20cc5a73d6' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>2015-03-01T00:00:00Z</expirationDate>
                    <descriptor deserializerType='goodDeserializer'>
                      <node xmlns='private' />
                    </descriptor>
                  </key>
                </root>";
 
        var expectedDescriptor = new Mock<IAuthenticatedEncryptorDescriptor>().Object;
        var mockActivator = new Mock<IActivator>();
        mockActivator.ReturnDescriptorGivenDeserializerTypeNameAndInput("goodDeserializer", "<node xmlns='private' />", expectedDescriptor);
 
        // Act
        var keys = RunGetAllKeysCore(xml, mockActivator.Object).ToArray();
 
        // Assert
        Assert.Single(keys);
        Assert.Equal(new Guid("49c0cda9-0232-4d8c-a541-de20cc5a73d6"), keys[0].KeyId);
        Assert.Same(expectedDescriptor, keys[0].Descriptor);
    }
 
    [Fact]
    public void GetAllKeys_WithKeyDeserializationError_LogLevelDebug_DoesNotWriteSensitiveInformation()
    {
        // Arrange
        const string xml = @"
                <root>
                  <!-- The below key will throw an exception when deserializing. -->
                  <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>NOT A VALID DATE</expirationDate>
                    <!-- Secret information: 1A2B3C4D -->
                  </key>
                </root>";
 
        var loggerFactory = new StringLoggerFactory(LogLevel.Debug);
 
        // Act
        RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
 
        // Assert
        Assert.False(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should not have been logged.");
    }
 
    [Fact]
    public void GetAllKeys_WithKeyDeserializationError_LogLevelTrace_WritesSensitiveInformation()
    {
        // Arrange
        const string xml = @"
                <root>
                  <!-- The below key will throw an exception when deserializing. -->
                  <key id='78cd498e-9375-4e55-ac0d-d79527ecd09d' version='1'>
                    <creationDate>2015-01-01T00:00:00Z</creationDate>
                    <activationDate>2015-02-01T00:00:00Z</activationDate>
                    <expirationDate>NOT A VALID DATE</expirationDate>
                    <!-- Secret information: 1A2B3C4D -->
                  </key>
                </root>";
 
        var loggerFactory = new StringLoggerFactory(LogLevel.Trace);
 
        // Act
        RunGetAllKeysCore(xml, new Mock<IActivator>().Object, loggerFactory).ToArray();
 
        // Assert
        Assert.True(loggerFactory.ToString().Contains("1A2B3C4D"), "The secret '1A2B3C4D' should have been logged.");
    }
 
    [Fact]
    public void GetAllKeys_SurfacesRevocationDeserializationErrors()
    {
        // Arrange
        const string xml = @"
                <root>
                  <revocation version='1'>
                    <revocationDate>2015-01-01T00:00:00Z</revocationDate>
                    <key id='{invalid}' />
                  </revocation>
                </root>";
 
        // Act & assert
        // Bad GUID will lead to FormatException
        Assert.Throws<FormatException>(() => RunGetAllKeysCore(xml, new Mock<IActivator>().Object));
    }
 
    private static IReadOnlyCollection<IKey> RunGetAllKeysCore(string xml, IActivator activator, ILoggerFactory loggerFactory = null)
    {
        // Arrange
        var mockXmlRepository = new Mock<IXmlRepository>();
        mockXmlRepository.Setup(o => o.GetAllElements()).Returns(XElement.Parse(xml).Elements().ToArray());
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = mockXmlRepository.Object,
            XmlEncryptor = null
        });
        var keyManager = new XmlKeyManager(options, activator, loggerFactory ?? NullLoggerFactory.Instance);
 
        // Act
        return keyManager.GetAllKeys();
    }
 
    [Fact]
    public void RevokeAllKeys()
    {
        // Arrange
        XElement elementStoredInRepository = null;
        string friendlyNameStoredInRepository = null;
        var mockXmlRepository = new Mock<IXmlRepository>();
        mockXmlRepository
            .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
            .Callback<XElement, string>((el, friendlyName) =>
            {
                elementStoredInRepository = el;
                friendlyNameStoredInRepository = friendlyName;
            });
 
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = mockXmlRepository.Object,
            XmlEncryptor = null
        });
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        var revocationDate = XmlConvert.ToDateTimeOffset("2015-03-01T19:13:19.7573854-08:00");
 
        // Act & assert
 
        // The cancellation token should not already be fired
        var firstCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.False(firstCancellationToken.IsCancellationRequested);
 
        // After the call to RevokeAllKeys, the first CT should be fired,
        // and we should've gotten a new CT.
        keyManager.RevokeAllKeys(revocationDate, "Here's some reason text.");
        var secondCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.True(firstCancellationToken.IsCancellationRequested);
        Assert.False(secondCancellationToken.IsCancellationRequested);
 
        // Was the correct element stored in the repository?
        const string expectedRepositoryXml = @"
                <revocation version='1'>
                  <revocationDate>2015-03-01T19:13:19.7573854-08:00</revocationDate>
                  <!--All keys created before the revocation date are revoked.-->
                  <key id='*' />
                  <reason>Here's some reason text.</reason>
                </revocation>";
        XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
        Assert.Equal("revocation-20150302T0313197573854Z", friendlyNameStoredInRepository);
    }
 
    [Fact]
    public void RevokeSingleKey_Internal()
    {
        // Arrange - mocks
        XElement elementStoredInRepository = null;
        string friendlyNameStoredInRepository = null;
        var mockXmlRepository = new Mock<IXmlRepository>();
        mockXmlRepository
            .Setup(o => o.StoreElement(It.IsAny<XElement>(), It.IsAny<string>()))
            .Callback<XElement, string>((el, friendlyName) =>
            {
                elementStoredInRepository = el;
                friendlyNameStoredInRepository = friendlyName;
            });
 
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = mockXmlRepository.Object,
            XmlEncryptor = null
        });
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
 
        var revocationDate = new DateTimeOffset(2014, 01, 01, 0, 0, 0, TimeSpan.Zero);
 
        // Act & assert
 
        // The cancellation token should not already be fired
        var firstCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.False(firstCancellationToken.IsCancellationRequested);
 
        // After the call to RevokeKey, the first CT should be fired,
        // and we should've gotten a new CT.
        ((IInternalXmlKeyManager)keyManager).RevokeSingleKey(
            keyId: new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932"),
            revocationDate: revocationDate,
            reason: "Here's some reason text.");
        var secondCancellationToken = keyManager.GetCacheExpirationToken();
        Assert.True(firstCancellationToken.IsCancellationRequested);
        Assert.False(secondCancellationToken.IsCancellationRequested);
 
        // Was the correct element stored in the repository?
        var expectedRepositoryXml = string.Format(
          CultureInfo.InvariantCulture,
          @"
                <revocation version='1'>
                  {0}
                  <key id='a11f35fc-1fed-4bd4-b727-056a63b70932' />
                  <reason>Here's some reason text.</reason>
                </revocation>",
            new XElement("revocationDate", revocationDate));
        XmlAssert.Equal(expectedRepositoryXml, elementStoredInRepository);
        Assert.Equal("revocation-a11f35fc-1fed-4bd4-b727-056a63b70932", friendlyNameStoredInRepository);
    }
 
    [Fact]
    public void RevokeKey_CallsInternalManager()
    {
        // Arrange
        var keyToRevoke = new Guid("a11f35fc-1fed-4bd4-b727-056a63b70932");
        DateTimeOffset minRevocationDate = DateTimeOffset.UtcNow;
        DateTimeOffset? actualRevocationDate = null;
        var mockInternalKeyManager = new Mock<IInternalXmlKeyManager>();
        mockInternalKeyManager
            .Setup(o => o.RevokeSingleKey(keyToRevoke, It.IsAny<DateTimeOffset>(), "Here's some reason text."))
            .Callback<Guid, DateTimeOffset, string>((innerKeyId, innerRevocationDate, innerReason) =>
            {
                actualRevocationDate = innerRevocationDate;
            });
 
        var options = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>().Object,
            XmlRepository = new Mock<IXmlRepository>().Object,
            XmlEncryptor = null
        });
        var keyManager = new XmlKeyManager(options, SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance, mockInternalKeyManager.Object);
 
        // Act
        keyManager.RevokeKey(keyToRevoke, "Here's some reason text.");
 
        // Assert
        Assert.InRange(actualRevocationDate.Value, minRevocationDate, DateTimeOffset.UtcNow);
    }
 
    [Fact]
    public void CreatedKeyReused()
    {
        // Arrange
 
        // The only descriptor we'll use
        var descriptor = new Mock<IAuthenticatedEncryptorDescriptor>(MockBehavior.Strict);
        descriptor
            .Setup(o => o.ExportToXml())
            // Shouldn't be an interface, but we control the activator and will return a mock
            .Returns(new XmlSerializedDescriptorInfo(serializedDescriptor, typeof(IAuthenticatedEncryptorDescriptorDeserializer)));
 
        // The factory always returns the only descriptor
        var authenticatedEncryptorConfiguration = new Mock<AlgorithmConfiguration>(MockBehavior.Strict);
        authenticatedEncryptorConfiguration
            .Setup(o => o.CreateNewDescriptor())
            .Returns(descriptor.Object);
 
        // Any xml element deserializes to the only descriptor
        var descriptorDeserializer = new Mock<IAuthenticatedEncryptorDescriptorDeserializer>(MockBehavior.Strict);
        descriptorDeserializer
            .Setup(o => o.ImportFromXml(It.IsAny<XElement>()))
            .Returns(descriptor.Object);
 
        // Keep track of how many times a key needs to be decrypted
        var decryptionCount = 0;
        var decryptor = new Mock<IXmlDecryptor>(MockBehavior.Strict);
        decryptor
            .Setup(o => o.Decrypt(It.IsAny<XElement>()))
            .Returns<XElement>(element =>
            {
                decryptionCount++;
                return element;
            });
 
        // We need a simple encryptor for the newly created key
        var encryptor = new Mock<IXmlEncryptor>(MockBehavior.Strict);
        encryptor
            .Setup(o => o.Encrypt(It.IsAny<XElement>()))
            // Shouldn't be an interface, but we control the activator and will return a mock
            .Returns<XElement>(element => new EncryptedXmlInfo(element, typeof(IXmlDecryptor)));
 
        // We control deserialization by hooking activation
        var activator = new Mock<IActivator>(MockBehavior.Strict);
        activator
            .Setup(o => o.CreateInstance(typeof(IXmlDecryptor), It.IsAny<string>()))
            .Returns(decryptor.Object);
        activator
            .Setup(o => o.CreateInstance(typeof(IAuthenticatedEncryptorDescriptorDeserializer), It.IsAny<string>()))
            .Returns(descriptorDeserializer.Object);
 
        var keyManagementOptions = Options.Create(new KeyManagementOptions()
        {
            AuthenticatedEncryptorConfiguration = authenticatedEncryptorConfiguration.Object,
            XmlRepository = new EphemeralXmlRepository(NullLoggerFactory.Instance), // A realistic repository is fine
            XmlEncryptor = encryptor.Object,
        });
 
        var keyManager = new XmlKeyManager(keyManagementOptions, activator.Object);
 
        var creationDate = DateTimeOffset.UtcNow;
        var activationDate = creationDate.AddDays(2);
        var expirationDate = creationDate.AddDays(90);
 
        var createdKey = keyManager.CreateNewKey(activationDate, expirationDate);
 
        // Force decryption, if encrypted.  The real call would be CreateEncryptor(), but that would access Descriptor under the hood
        _ = createdKey.Descriptor;
        Assert.Equal(0, decryptionCount);
 
        // Act
        var fetchedKeys = keyManager.GetAllKeys();
 
        // Assert
        var fetchedKey = Assert.Single(fetchedKeys);
        Assert.NotSame(createdKey, fetchedKey); // It's mutable, so we don't want to reuse the same instance
        Assert.Equal(createdKey.KeyId, fetchedKey.KeyId); // But it should be equivalent
 
        // Nothing up to this point should have required decryption
        Assert.Equal(0, decryptionCount);
 
        // Force decryption, if encrypted.  The real call would be CreateEncryptor(), but that would access Descriptor under the hood
        _ = fetchedKey.Descriptor;
        Assert.Equal(0, decryptionCount);
 
        var fetchedKeys2 = keyManager.GetAllKeys();
 
        var fetchedKey2 = Assert.Single(fetchedKeys2);
        Assert.NotSame(createdKey, fetchedKey2); // It's mutable, so we don't want to reuse the same instance
        Assert.NotSame(fetchedKey, fetchedKey2);
        Assert.Equal(createdKey.KeyId, fetchedKey2.KeyId); // But it should be equivalent
 
        // Force decryption, if encrypted.  The real call would be CreateEncryptor(), but that would access Descriptor under the hood
        _ = fetchedKey2.Descriptor;
        Assert.Equal(0, decryptionCount);
    }
 
    [Fact]
    public void NovelFetchedKeyRequiresDecryption()
    {
        // Arrange
        var descriptorDeserializer = new Mock<IAuthenticatedEncryptorDescriptorDeserializer>(MockBehavior.Strict);
        descriptorDeserializer
            .Setup(o => o.ImportFromXml(It.IsAny<XElement>()))
            .Returns(new Mock<IAuthenticatedEncryptorDescriptor>(MockBehavior.Strict).Object);
 
        // Keep track of how many times a key needs to be decrypted
        var decryptionCount = 0;
        var decryptor = new Mock<IXmlDecryptor>(MockBehavior.Strict);
        decryptor
            .Setup(o => o.Decrypt(It.IsAny<XElement>()))
            .Returns<XElement>(element =>
            {
                decryptionCount++;
                return element;
            });
 
        var encryptor = new Mock<IXmlEncryptor>(MockBehavior.Strict);
 
        // We control deserialization by hooking activation
        var activator = new Mock<IActivator>(MockBehavior.Strict);
        activator
            .Setup(o => o.CreateInstance(typeof(IXmlDecryptor), It.IsAny<string>()))
            .Returns(decryptor.Object);
        activator
            .Setup(o => o.CreateInstance(typeof(IAuthenticatedEncryptorDescriptorDeserializer), It.IsAny<string>()))
            .Returns(descriptorDeserializer.Object);
 
        var keyElement = XElement.Parse(@"
<key id=""3d6d01fd-c0e7-44ae-82dd-013b996b4093"">
    <creationDate>2015-01-01T00:00:00Z</creationDate>
    <activationDate>2015-01-02T00:00:00Z</activationDate>
    <expirationDate>2015-03-01T00:00:00Z</expirationDate>
    <descriptor deserializerType=""SomeAQN1"">
        <descriptor>
        <encryption algorithm=""AES_256_CBC"" />
        <validation algorithm=""HMACSHA256"" />
        <encryptedSecret decryptorType=""SomeAQN2"" xmlns=""http://schemas.asp.net/2015/03/dataProtection"">
            <encryptedKey xmlns="""">
                <value>KeyAsBase64</value>
            </encryptedKey>
        </encryptedSecret>
        </descriptor>
    </descriptor>
</key>
");
 
        var respository = new Mock<IXmlRepository>();
        respository
            .Setup(o => o.GetAllElements())
            .Returns([keyElement]);
 
        var keyManagementOptions = Options.Create(new KeyManagementOptions()
        {
            XmlRepository = respository.Object,
            XmlEncryptor = encryptor.Object,
        });
 
        var keyManager = new XmlKeyManager(keyManagementOptions, activator.Object);
 
        // Act
        var fetchedKeys = keyManager.GetAllKeys();
 
        // Assert
        var fetchedKey = Assert.Single(fetchedKeys);
 
        // Decryption is lazy
        Assert.Equal(0, decryptionCount);
 
        // Force decryption.  The real call would be CreateEncryptor(), but that would access Descriptor under the hood
        _ = fetchedKey.Descriptor;
        Assert.Equal(1, decryptionCount);
 
        var fetchedKeys2 = keyManager.GetAllKeys();
 
        var fetchedKey2 = Assert.Single(fetchedKeys2);
        Assert.NotSame(fetchedKey, fetchedKey2); // It's mutable, so we don't want to reuse the same instance
        Assert.Equal(fetchedKey.KeyId, fetchedKey2.KeyId); // But it should be equivalent
 
        // Force decryption, if encrypted.  The real call would be CreateEncryptor(), but that would access Descriptor under the hood
        _ = fetchedKey2.Descriptor;
        Assert.Equal(1, decryptionCount); // Still 1 (i.e. no change)
    }
 
    [Fact]
    public void DeleteKeys()
    {
        var repository = new EphemeralXmlRepository(NullLoggerFactory.Instance);
 
        var keyManager = new XmlKeyManager(
            Options.Create(new KeyManagementOptions()
            {
                AuthenticatedEncryptorConfiguration = new AuthenticatedEncryptorConfiguration(),
                XmlRepository = repository,
                XmlEncryptor = null
            }),
            SimpleActivator.DefaultWithoutServices,
            NullLoggerFactory.Instance);
 
        var activationTime = DateTimeOffset.UtcNow.AddHours(1);
 
        var key1 = keyManager.CreateNewKey(activationTime, activationTime.AddMinutes(10));
        keyManager.RevokeAllKeys(DateTimeOffset.UtcNow, "Revoking all keys"); // This should revoke key1
        var key2 = keyManager.CreateNewKey(activationTime, activationTime.AddMinutes(10));
        keyManager.RevokeAllKeys(DateTimeOffset.UtcNow, "Revoking all keys"); // This should revoke key1 and key2
        var key3 = keyManager.CreateNewKey(activationTime, activationTime.AddMinutes(10));
        var key4 = keyManager.CreateNewKey(activationTime, activationTime.AddMinutes(10));
 
        keyManager.RevokeKey(key2.KeyId); // Revoked by time, but also individually
        keyManager.RevokeKey(key3.KeyId);
        keyManager.RevokeKey(key3.KeyId); // Nothing prevents us from revoking the same key twice
 
        Assert.Equal(9, repository.GetAllElements().Count); // 4 keys, 2 time-revocations, 3 guid-revocations
 
        // The keys are stale now, but we only care about the IDs
 
        var keyDictWithRevocations = keyManager.GetAllKeys().ToDictionary(k => k.KeyId);
        Assert.Equal(4, keyDictWithRevocations.Count);
        Assert.True(keyDictWithRevocations[key1.KeyId].IsRevoked);
        Assert.True(keyDictWithRevocations[key2.KeyId].IsRevoked);
        Assert.True(keyDictWithRevocations[key3.KeyId].IsRevoked);
        Assert.False(keyDictWithRevocations[key4.KeyId].IsRevoked);
 
        Assert.True(keyManager.DeleteKeys(key => key.KeyId == key1.KeyId || key.KeyId == key3.KeyId));
 
        Assert.Equal(4, repository.GetAllElements().Count); // 2 keys, 1 time-revocation, 1 guid-revocations
 
        var keyDictWithDeletions = keyManager.GetAllKeys().ToDictionary(k => k.KeyId);
        Assert.Equal(2, keyDictWithDeletions.Count);
        Assert.DoesNotContain(key1.KeyId, keyDictWithDeletions.Keys);
        Assert.True(keyDictWithRevocations[key2.KeyId].IsRevoked);
        Assert.DoesNotContain(key3.KeyId, keyDictWithDeletions.Keys);
        Assert.False(keyDictWithRevocations[key4.KeyId].IsRevoked);
    }
 
    [Fact]
    public void CanDeleteKey()
    {
        var withDeletion = new XmlKeyManager(Options.Create(new KeyManagementOptions()
        {
            XmlRepository = XmlRepositoryWithDeletion.Instance,
            XmlEncryptor = null
        }), SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
        Assert.True(withDeletion.CanDeleteKeys);
 
        var withoutDeletion = new XmlKeyManager(Options.Create(new KeyManagementOptions()
        {
            XmlRepository = XmlRepositoryWithoutDeletion.Instance,
            XmlEncryptor = null
        }), SimpleActivator.DefaultWithoutServices, NullLoggerFactory.Instance);
        Assert.False(withoutDeletion.CanDeleteKeys);
        Assert.Throws<NotSupportedException>(() => withoutDeletion.DeleteKeys(_ => false));
    }
 
    private sealed class XmlRepositoryWithoutDeletion : IXmlRepository
    {
        public static readonly IXmlRepository Instance = new XmlRepositoryWithoutDeletion();
 
        private XmlRepositoryWithoutDeletion() { }
 
        IReadOnlyCollection<XElement> IXmlRepository.GetAllElements() => [];
        void IXmlRepository.StoreElement(XElement element, string friendlyName) => throw new InvalidOperationException();
    }
 
    private sealed class XmlRepositoryWithDeletion : IDeletableXmlRepository
    {
        public static readonly IDeletableXmlRepository Instance = new XmlRepositoryWithDeletion();
 
        private XmlRepositoryWithDeletion() { }
 
        IReadOnlyCollection<XElement> IXmlRepository.GetAllElements() => [];
        void IXmlRepository.StoreElement(XElement element, string friendlyName) => throw new InvalidOperationException();
        bool IDeletableXmlRepository.DeleteElements(Action<IReadOnlyCollection<IDeletableElement>> chooseElements) => throw new InvalidOperationException();
    }
 
    private class MyDeserializer : IAuthenticatedEncryptorDescriptorDeserializer
    {
        public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element)
        {
            throw new NotImplementedException();
        }
    }
 
    private class TestEncryptorFactory : IAuthenticatedEncryptorFactory
    {
        private IAuthenticatedEncryptorDescriptor _associatedDescriptor;
        private IAuthenticatedEncryptor _expectedEncryptor;
 
        public TestEncryptorFactory(IAuthenticatedEncryptorDescriptor associatedDescriptor = null, IAuthenticatedEncryptor expectedEncryptor = null)
        {
            _associatedDescriptor = associatedDescriptor;
            _expectedEncryptor = expectedEncryptor;
        }
 
        public IAuthenticatedEncryptor CreateEncryptorInstance(IKey key)
        {
            if (_associatedDescriptor != null && _associatedDescriptor != key.Descriptor)
            {
                return null;
            }
 
            return _expectedEncryptor ?? new Mock<IAuthenticatedEncryptor>().Object;
        }
    }
}