File: KeyManagement\Key.cs
Web Access
Project: src\src\DataProtection\DataProtection\src\Microsoft.AspNetCore.DataProtection.csproj (Microsoft.AspNetCore.DataProtection)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
 
namespace Microsoft.AspNetCore.DataProtection.KeyManagement;
 
/// <summary>
/// The basic implementation of <see cref="IKey"/>.
/// </summary>
internal sealed class Key : IKey
{
    private IAuthenticatedEncryptorDescriptor? _descriptor;
 
    // If descriptor is available at construction time, these will remain null forever
    private readonly object? _descriptorLock; // Protects _descriptor and _descriptorException
    private readonly Func<IAuthenticatedEncryptorDescriptor>? _descriptorFactory; // May not be used
    private Exception? _descriptorException;
 
    private readonly IEnumerable<IAuthenticatedEncryptorFactory> _encryptorFactories;
 
    private IAuthenticatedEncryptor? _encryptor;
 
    /// <summary>
    /// The basic implementation of <see cref="IKey"/>, where the <see cref="IAuthenticatedEncryptorDescriptor"/>
    /// has already been created.
    /// </summary>
    public Key(
        Guid keyId,
        DateTimeOffset creationDate,
        DateTimeOffset activationDate,
        DateTimeOffset expirationDate,
        IAuthenticatedEncryptorDescriptor descriptor,
        IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories)
        : this(keyId,
              creationDate,
              activationDate,
              expirationDate,
              encryptorFactories,
              descriptor,
              descriptorFactory: null,
              descriptorException: null)
    {
    }
 
    /// <summary>
    /// The basic implementation of <see cref="IKey"/>, where the incoming XML element
    /// hasn't yet been fully processed.
    /// </summary>
    public Key(
        Guid keyId,
        DateTimeOffset creationDate,
        DateTimeOffset activationDate,
        DateTimeOffset expirationDate,
        IInternalXmlKeyManager keyManager,
        XElement keyElement,
        IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories)
        : this(keyId,
              creationDate,
              activationDate,
              expirationDate,
              encryptorFactories,
              descriptor: null,
              descriptorFactory: GetLazyDescriptorDelegate(keyManager, keyElement),
              descriptorException: null)
    {
    }
 
    // internal for testing
    internal Key(
        Guid keyId,
        DateTimeOffset creationDate,
        DateTimeOffset activationDate,
        DateTimeOffset expirationDate,
        IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories,
        Func<IAuthenticatedEncryptorDescriptor>? descriptorFactory)
        : this(keyId,
              creationDate,
              activationDate,
              expirationDate,
              encryptorFactories,
              descriptor: null,
              descriptorFactory: descriptorFactory,
              descriptorException: null)
    {
    }
 
    private Key(
        Guid keyId,
        DateTimeOffset creationDate,
        DateTimeOffset activationDate,
        DateTimeOffset expirationDate,
        IEnumerable<IAuthenticatedEncryptorFactory> encryptorFactories,
        IAuthenticatedEncryptorDescriptor? descriptor,
        Func<IAuthenticatedEncryptorDescriptor>? descriptorFactory,
        Exception? descriptorException)
    {
        KeyId = keyId;
        CreationDate = creationDate;
        ActivationDate = activationDate;
        ExpirationDate = expirationDate;
        _encryptorFactories = encryptorFactories;
        _descriptor = descriptor;
        _descriptorFactory = descriptorFactory;
        _descriptorException = descriptorException;
        _descriptorLock = descriptor is null ? new() : null;
    }
 
    public DateTimeOffset ActivationDate { get; }
 
    public DateTimeOffset CreationDate { get; }
 
    public DateTimeOffset ExpirationDate { get; }
 
    public bool IsRevoked { get; private set; }
 
    public Guid KeyId { get; }
 
    public IAuthenticatedEncryptorDescriptor Descriptor
    {
        get
        {
            // We could check for _descriptorException here, but there's no reason to optimize that case
            // (i.e. by avoiding taking the lock)
 
            if (_descriptor is not null) // Can only go from null to non-null, so losing a race here doesn't matter
            {
                Debug.Assert(_descriptorException is null); // Mutually exclusive with _descriptor
                return _descriptor;
            }
 
            lock (_descriptorLock!)
            {
                if (_descriptorException is not null)
                {
                    throw _descriptorException;
                }
 
                if (_descriptor is not null)
                {
                    return _descriptor;
                }
 
                Debug.Assert(_descriptorFactory is not null, "Key constructed without either descriptor or descriptor factory");
 
                try
                {
                    _descriptor = _descriptorFactory();
                    return _descriptor;
                }
                catch (Exception ex)
                {
                    _descriptorException = ex;
                    throw;
                }
            }
        }
    }
 
    internal void ResetDescriptor()
    {
        if (_descriptor is not null)
        {
            Debug.Fail("ResetDescriptor called with descriptor available");
            Debug.Assert(_descriptorException is null); // Mutually exclusive with _descriptor
            return;
        }
 
        lock (_descriptorLock!)
        {
            _descriptor = null;
            _descriptorException = null;
        }
    }
 
    public IAuthenticatedEncryptor? CreateEncryptor()
    {
        if (_encryptor == null)
        {
            foreach (var factory in _encryptorFactories)
            {
                var encryptor = factory.CreateEncryptorInstance(this);
                if (encryptor != null)
                {
                    _encryptor = encryptor;
                    break;
                }
            }
        }
 
        return _encryptor;
    }
 
    internal void SetRevoked()
    {
        IsRevoked = true;
    }
 
    internal Key Clone()
    {
        // Note that we don't reuse _descriptorLock
        return new Key(
            keyId: KeyId,
            creationDate: CreationDate,
            activationDate: ActivationDate,
            expirationDate: ExpirationDate,
            encryptorFactories: _encryptorFactories,
            descriptor: _descriptor,
            descriptorFactory: _descriptorFactory,
            descriptorException: _descriptorException)
        {
            IsRevoked = IsRevoked,
        };
    }
 
    private static Func<IAuthenticatedEncryptorDescriptor> GetLazyDescriptorDelegate(IInternalXmlKeyManager keyManager, XElement keyElement)
    {
        // The <key> 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 GetLazyDescriptorDelegate;
        }
        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!;
        }
 
        IAuthenticatedEncryptorDescriptor GetLazyDescriptorDelegate()
        {
            return keyManager.DeserializeDescriptorFromKeyElement(encryptedKeyElement.ToXElement());
        }
    }
}