File: Internal\KeyManagementOptionsPostSetup.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.IO;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.DataProtection.Internal;
 
/// <summary>
/// Performs additional <see cref="KeyManagementOptions" /> configuration, after the user's configuration has been applied.
/// </summary>
/// <remarks>
/// In practice, this type is used to set key management to readonly mode if an environment variable is set and the user
/// has not explicitly configured data protection.
/// </remarks>
internal sealed class KeyManagementOptionsPostSetup : IPostConfigureOptions<KeyManagementOptions>
{
    /// <remarks>
    /// Settable as `ReadOnlyDataProtectionKeyDirectory`, `DOTNET_ReadOnlyDataProtectionKeyDirectory`,
    /// or `ASPNETCORE_ReadOnlyDataProtectionKeyDirectory`, in descending order of precedence.
    /// </remarks>
    internal const string ReadOnlyDataProtectionKeyDirectoryKey = "ReadOnlyDataProtectionKeyDirectory";
 
    private readonly string? _keyDirectoryPath;
    private readonly ILoggerFactory? _loggerFactory; // Null iff _keyDirectoryPath is null
    private readonly ILogger<KeyManagementOptionsPostSetup>? _logger; // Null iff _keyDirectoryPath is null
 
    public KeyManagementOptionsPostSetup()
    {
        // If there's no IConfiguration, there's no _keyDirectoryPath and this type will do nothing.
        // This is mostly a convenience for tests since ASP.NET Core apps will have an IConfiguration.
    }
 
    public KeyManagementOptionsPostSetup(IConfiguration configuration, ILoggerFactory loggerFactory)
    {
        var dirPath = configuration[ReadOnlyDataProtectionKeyDirectoryKey];
        if (string.IsNullOrEmpty(dirPath))
        {
            return;
        }
 
        _keyDirectoryPath = dirPath;
        _loggerFactory = loggerFactory;
        _logger = loggerFactory.CreateLogger<KeyManagementOptionsPostSetup>();
    }
 
    void IPostConfigureOptions<KeyManagementOptions>.PostConfigure(string? name, KeyManagementOptions options)
    {
        if (_keyDirectoryPath is null)
        {
            // There's no logger, so we couldn't log if we wanted to
            return;
        }
 
        var logger = _logger!;
 
        if (name != Options.DefaultName)
        {
            logger.IgnoringReadOnlyConfigurationForNonDefaultOptions(ReadOnlyDataProtectionKeyDirectoryKey, name);
            return;
        }
 
        // If Data Protection has not been configured, then set it up according to the environment variable
        if (options is { XmlRepository: null, XmlEncryptor: null })
        {
            var keyDirectory = new DirectoryInfo(_keyDirectoryPath);
 
            logger.UsingReadOnlyKeyConfiguration(keyDirectory.FullName);
 
            options.AutoGenerateKeys = false;
            options.XmlEncryptor = InvalidEncryptor.Instance;
            options.XmlRepository = new ReadOnlyFileSystemXmlRepository(keyDirectory, _loggerFactory!);
        }
        else if (options.XmlRepository is not null)
        {
            logger.NotUsingReadOnlyKeyConfigurationBecauseOfRepository();
        }
        else
        {
            logger.NotUsingReadOnlyKeyConfigurationBecauseOfEncryptor();
        }
    }
 
    private sealed class InvalidEncryptor : IXmlEncryptor
    {
        public static readonly IXmlEncryptor Instance = new InvalidEncryptor();
 
        private InvalidEncryptor()
        {
        }
 
        EncryptedXmlInfo IXmlEncryptor.Encrypt(XElement plaintextElement)
        {
            throw new InvalidOperationException("Keys access is set up as read-only, so nothing should be encrypting");
        }
    }
 
    private sealed class ReadOnlyFileSystemXmlRepository : FileSystemXmlRepository
    {
        public ReadOnlyFileSystemXmlRepository(DirectoryInfo directory, ILoggerFactory loggerFactory)
            : base(directory, loggerFactory)
        {
        }
 
        public override void StoreElement(XElement element, string friendlyName)
        {
            throw new InvalidOperationException("Keys access is set up as read-only, so nothing should be storing keys");
        }
    }
}