asp.net-mvc ASP.NET Core 中的加密配置

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/36062670/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-07 23:32:15  来源:igfitidea点击:

Encrypted configuration in ASP.NET Core

asp.net-mvcasp.net-coreasp.net-core-mvc.net-core

提问by Ovi

With web.configgoing away, what is the preferred way to store sensitive info (passwords, tokens) in the configurations of a web app built using ASP.NET Core?

随着web.config去了,什么是存储敏感信息(密码,令牌)使用ASP.NET核心内建一个web应用程序的配置首选的方法是什么?

Is there a way to automatically get encrypted configuration sections in appsetttings.json?

有没有办法自动获取加密的配置部分appsetttings.json

采纳答案by Luca Ghersi

User secrets looks like a good solution for storing passwords, and, generally, application secrets, at least during development.

用户机密看起来是一个很好的存储密码的解决方案,而且通常是应用程序机密,至少在开发过程中是如此

Check this articleor this. You can also check thisother SO question.

检查这篇文章这个。您还可以检查这个其他 SO 问题。

This is just a way to "hide" you secrets during development process and to avoid disclosing them into the source tree; the Secret Manager tool does not encrypt the stored secrets and should not be treated as a trusted store.

这只是在开发过程中“隐藏”你的秘密并避免将它们泄露到源代码树中的一种方式;Secret Manager 工具不会加密存储的机密,不应将其视为受信任的存储。

If you want to bring an encrypted appsettings.json to production, there is no limit about that. You can build your custom configuration provider. Check this.

如果您想将加密的 appsettings.json 带入生产环境,则没有任何限制。您可以构建自定义配置提供程序。检查这个

For example:

例如:

public class CustomConfigProvider : ConfigurationProvider, IConfigurationSource
{
    public CustomConfigProvider()
    {
    }

    public override void Load()
    {
        Data = UnencryptMyConfiguration();
    }

    private IDictionary<string, string> UnencryptMyConfiguration()
    {
        // do whatever you need to do here, for example load the file and unencrypt key by key
        //Like:
       var configValues = new Dictionary<string, string>
       {
            {"key1", "unencryptedValue1"},
            {"key2", "unencryptedValue2"}
       };
       return configValues;
    }

    private IDictionary<string, string> CreateAndSaveDefaultValues(IDictionary<string, string> defaultDictionary)
    {
        var configValues = new Dictionary<string, string>
        {
            {"key1", "encryptedValue1"},
            {"key2", "encryptedValue2"}
        };
        return configValues;                
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
       return new CustomConfigProvider();
    }
}

Define a static class for your extension method:

为您的扩展方法定义一个静态类:

public static class CustomConfigProviderExtensions
{              
        public static IConfigurationBuilder AddEncryptedProvider(this IConfigurationBuilder builder)
        {
            return builder.Add(new CustomConfigProvider());
        }
}

And then you can activate it:

然后你可以激活它:

// Set up configuration sources.
var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEncryptedProvider()
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

回答by CoderSteve

I didn't want to write a custom provider – way too much work. I just wanted to tap into JsonConfigurationProvider, so I figured out a way that works for me, hope it helps someone.

我不想编写自定义提供程序 - 太多工作了。我只是想利用 JsonConfigurationProvider,所以我想出了一种对我有用的方法,希望对某人有所帮助。

public class JsonConfigurationProvider2 : JsonConfigurationProvider
{
    public JsonConfigurationProvider2(JsonConfigurationSource2 source) : base(source)
    {
    }

    public override void Load(Stream stream)
    {
        // Let the base class do the heavy lifting.
        base.Load(stream);

        // Do decryption here, you can tap into the Data property like so:

         Data["abc:password"] = MyEncryptionLibrary.Decrypt(Data["abc:password"]);

        // But you have to make your own MyEncryptionLibrary, not included here
    }
}

public class JsonConfigurationSource2 : JsonConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new JsonConfigurationProvider2(this);
    }
}

public static class JsonConfigurationExtensions2
{
    public static IConfigurationBuilder AddJsonFile2(this IConfigurationBuilder builder, string path, bool optional,
        bool reloadOnChange)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }
        if (string.IsNullOrEmpty(path))
        {
            throw new ArgumentException("File path must be a non-empty string.");
        }

        var source = new JsonConfigurationSource2
        {
            FileProvider = null,
            Path = path,
            Optional = optional,
            ReloadOnChange = reloadOnChange
        };

        source.ResolveFileProvider();
        builder.Add(source);
        return builder;
    }
}

回答by Scott Roberts

I agree with @CoderSteve that writing a whole new provider is too much work. It also doesn't build on the existing standard JSON architecture. Here is a solution that I come up with the builds on top of the standard JSON architecture, uses the preferred .Net Core encryption libraries, and is very DI friendly.

我同意@CoderSteve 的观点,即编写一个全新的提供程序工作量太大。它也不是建立在现有的标准 JSON 架构上。这是我在标准 JSON 架构之上构建的解决方案,使用首选的 .Net Core 加密库,并且非常适合 DI。

public static class IServiceCollectionExtensions
{
    public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services)
    {
        services
            .AddDataProtection()
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\keys"))
            .ProtectKeysWithDpapi();

        return services;
    }

    public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
    {
        return services.AddSingleton(provider =>
        {
            var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
            section = new ProtectedConfigurationSection(dataProtectionProvider, section);

            var options = section.Get<TOptions>();
            return Options.Create(options);
        });
    }

    private class ProtectedConfigurationSection : IConfigurationSection
    {
        private readonly IDataProtectionProvider _dataProtectionProvider;
        private readonly IConfigurationSection _section;
        private readonly Lazy<IDataProtector> _protector;

        public ProtectedConfigurationSection(
            IDataProtectionProvider dataProtectionProvider,
            IConfigurationSection section)
        {
            _dataProtectionProvider = dataProtectionProvider;
            _section = section;

            _protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
        }

        public IConfigurationSection GetSection(string key)
        {
            return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
        }

        public IEnumerable<IConfigurationSection> GetChildren()
        {
            return _section.GetChildren()
                .Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
        }

        public IChangeToken GetReloadToken()
        {
            return _section.GetReloadToken();
        }

        public string this[string key]
        {
            get => GetProtectedValue(_section[key]);
            set => _section[key] = _protector.Value.Protect(value);
        }

        public string Key => _section.Key;
        public string Path => _section.Path;

        public string Value
        {
            get => GetProtectedValue(_section.Value);
            set => _section.Value = _protector.Value.Protect(value);
        }

        private string GetProtectedValue(string value)
        {
            if (value == null)
                return null;

            return _protector.Value.Unprotect(value);
        }
    }
}

Wire up your protected config sections like this:

像这样连接受保护的配置部分:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // Configure normal config settings
    services.Configure<MySettings>(Configuration.GetSection("MySettings"));

    // Configure protected config settings
    services.AddProtectedConfiguration();
    services.ConfigureProtected<MyProtectedSettings>(Configuration.GetSection("MyProtectedSettings"));
}

You can easily create encrypted values for your config files using a controller like this:

您可以使用这样的控制器轻松地为您的配置文件创建加密值:

[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
    var protector = _dataProtectionProvider.CreateProtector(section);
    return protector.Protect(value);
}

Usage: http://localhost/cryptography/encrypt?section=SectionName:KeyName&value=PlainTextValue

用法: http://localhost/cryptography/encrypt?section=SectionName:KeyName&value=PlainTextValue

回答by Roel van Megen

I managed to create a custom JSON configuration provider which uses DPAPI to encrypt and decrypt secrets. It basically uses simple regular expressions that you can define to specify what parts of the JSON needs to be encrypted.

我设法创建了一个自定义 JSON 配置提供程序,它使用 DPAPI 来加密和解密机密。它基本上使用简单的正则表达式,您可以定义这些表达式来指定 JSON 的哪些部分需要加密。

The following steps are performed:

执行以下步骤:

  1. Json file is loaded
  2. Determine whether the JSON parts that match the given regular expressions are already encrypted (or not). This is done by base-64 decoding of the JSON part and verify whether it starts with the expected prefix !ENC!)
  3. If not encrypted, then encrypt the JSON part by first using DPAPI and secondly add the prefix !ENC!and encode to base-64
  4. Overwrite the unencrypted JSON parts with the encrypted (base-64) values in the Json file
  1. 加载了 Json 文件
  2. 确定与给定正则表达式匹配的 JSON 部分是否已加密(或未加密)。这是通过对 JSON 部分进行 base-64 解码并验证它是否以预期的前缀!ENC!开头来完成的)
  3. 如果未加密,则首先使用 DPAPI 加密 JSON 部分,然后添加前缀!ENC! 并编码为 base-64
  4. 使用 Json 文件中的加密 (base-64) 值覆盖未加密的 JSON 部分

Note that the base-64 does not bring better security, but only hides the prefix !ENC!for cosmetic reasons. This is just a matter of taste of course ;)

请注意,base-64 并没有带来更好的安全性,而只是隐藏了前缀!ENC! 出于美容的原因。这当然只是一个品味问题;)

This solution consists of the following classes:

此解决方案由以下类组成:

  1. ProtectedJsonConfigurationProviderclass (= custom JsonConfigurationProvider)
  2. ProtectedJsonConfigurationSourceclass (= custom JsonConfigurationSource)
  3. AddProtectedJsonFile()extension method on the IConfigurationBuilderin order to simple add the protected configuration
  1. ProtectedJsonConfigurationProvider类(= 自定义 JsonConfigurationProvider)
  2. ProtectedJsonConfigurationSource类(= 自定义 JsonConfigurationSource)
  3. IConfigurationBuilder上的AddProtectedJsonFile()扩展方法,以便简单地添加受保护的配置

Assuming the following initial authentication.jsonfile:

假设以下初始authentication.json文件:

{
    "authentication": {
        "credentials": [
            {
                user: "john",
                password: "just a password"
            },
            {
                user: "jane",
                password: "just a password"
            }
        ]
    }
}

Which becomes (sort of) the following after loading

加载后变成(某种)以下内容

{
    "authentication": {
        "credentials": [
            {
                "user": "john",
                "password": "IUVOQyEBAAAA0Iyd3wEV0R=="
            },
            {
                "user": "jane",
                "password": "IUVOQyEBAAAA0Iyd3wEV0R=="
            }
        ]
    }
}

And assuming the following configuration class based on the json format

并假设以下基于json格式的配置类

public class AuthenticationConfiguration
{
    [JsonProperty("credentials")]
    public Collection<CredentialConfiguration> Credentials { get; set; }
}

public class CredentialConfiguration
{
    [JsonProperty("user")]
    public string User { get; set; }
    [JsonProperty("password")]
    public string Password { get; set; }
}

Below the sample code:

示例代码下方:

//Note that the regular expression will cause the authentication.credentials.password path to be encrypted.
//Also note that the byte[] contains the entropy to increase security
var configurationBuilder = new ConfigurationBuilder()
    .AddProtectedJsonFile("authentication.json", true, new byte[] { 9, 4, 5, 6, 2, 8, 1 },
        new Regex("authentication:credentials:[0-9]*:password"));

var configuration = configurationBuilder.Build();
var authenticationConfiguration = configuration.GetSection("authentication").Get<AuthenticationConfiguration>();

//Get the decrypted password from the encrypted JSON file.
//Note that the ProtectedJsonConfigurationProvider.TryGet() method is called (I didn't expect that :D!)
var password = authenticationConfiguration.Credentials.First().Password

Install the Microsoft.Extensions.Configuration.Binder package in order to get the configuration.GetSection("authentication").Get<T>()implementation

安装 Microsoft.Extensions.Configuration.Binder 包以获取 configuration.GetSection("authentication")。Get<T>()实现

And finally the classes in which the magic happens :)

最后是魔法发生的课程:)

/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
public class ProtectedJsonConfigurationSource : JsonConfigurationSource
{
    /// <summary>Gets the byte array to increse protection</summary>
    internal byte[] Entropy { get; private set; }

    /// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
    /// <param name="entropy">Byte array to increase protection</param>
    /// <exception cref="ArgumentNullException"/>
    public ProtectedJsonConfigurationSource(byte[] entropy)
    {
        this.Entropy = entropy ?? throw new ArgumentNullException(Localization.EntropyNotSpecifiedError);
    }

    /// <summary>Builds the configuration provider</summary>
    /// <param name="builder">Builder to build in</param>
    /// <returns>Returns the configuration provider</returns>
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new ProtectedJsonConfigurationProvider(this);
    }

    /// <summary>Gets or sets the protection scope of the configuration provider. Default value is <see cref="DataProtectionScope.CurrentUser"/></summary>
    public DataProtectionScope Scope { get; set; }
    /// <summary>Gets or sets the regular expressions that must match the keys to encrypt</summary>
    public IEnumerable<Regex> EncryptedKeyExpressions { get; set; }
}

/// <summary>Represents a provider that protects a JSON configuration file</summary>
public partial class ProtectedJsonConfigurationProvider : JsonConfigurationProvider
{
    private readonly ProtectedJsonConfigurationSource protectedSource;
    private readonly HashSet<string> encryptedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    private static readonly byte[] encryptedPrefixBytes = Encoding.UTF8.GetBytes("!ENC!");

    /// <summary>Checks whether the given text is encrypted</summary>
    /// <param name="text">Text to check</param>
    /// <returns>Returns true in case the text is encrypted</returns>
    private bool isEncrypted(string text)
    {
        if (text == null) { return false; }

        //Decode the data in order to verify whether the decoded data starts with the expected prefix
        byte[] decodedBytes;
        try { decodedBytes = Convert.FromBase64String(text); }
        catch (FormatException) { return false; }

        return decodedBytes.Length >= encryptedPrefixBytes.Length
            && decodedBytes.AsSpan(0, encryptedPrefixBytes.Length).SequenceEqual(encryptedPrefixBytes);
    }

    /// <summary>Converts the given key to the JSON token path equivalent</summary>
    /// <param name="key">Key to convert</param>
    /// <returns>Returns the JSON token path equivalent</returns>
    private string convertToTokenPath(string key)
    {
        var jsonStringBuilder = new StringBuilder();

        //Split the key by ':'
        var keyParts = key.Split(':');
        for (var keyPartIndex = 0; keyPartIndex < keyParts.Length; keyPartIndex++)
        {
            var keyPart = keyParts[keyPartIndex];

            if (keyPart.All(char.IsDigit)) { jsonStringBuilder.Append('[').Append(keyPart).Append(']'); }
            else if (keyPartIndex > 0) { jsonStringBuilder.Append('.').Append(keyPart); }
            else { jsonStringBuilder.Append(keyPart); }
        }

        return jsonStringBuilder.ToString();
    }

    /// <summary>Writes the given encrypted key/values to the JSON oconfiguration file</summary>
    /// <param name="encryptedKeyValues">Encrypted key/values to write</param>
    private void writeValues(IDictionary<string, string> encryptedKeyValues)
    {
        try
        {
            if (encryptedKeyValues == null || encryptedKeyValues.Count == 0) { return; }

            using (var stream = new FileStream(this.protectedSource.Path, FileMode.Open, FileAccess.ReadWrite))
            {
                JObject json;

                using (var streamReader = new StreamReader(stream, Encoding.UTF8, true, 4096, true))
                {
                    using (var jsonTextReader = new JsonTextReader(streamReader))
                    {
                        json = JObject.Load(jsonTextReader);

                        foreach (var encryptedKeyValue in encryptedKeyValues)
                        {
                            var tokenPath = this.convertToTokenPath(encryptedKeyValue.Key);
                            var value = json.SelectToken(tokenPath) as JValue;
                            if (value.Value != null) { value.Value = encryptedKeyValue.Value; }
                        }
                    }
                }

                stream.Seek(0, SeekOrigin.Begin);
                using (var streamWriter = new StreamWriter(stream))
                {
                    using (var jsonTextWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented })
                    {
                        json.WriteTo(jsonTextWriter);
                    }
                }
            }
        }
        catch (Exception exception)
        {
            throw new Exception(string.Format(Localization.ProtectedJsonConfigurationWriteEncryptedValues, this.protectedSource.Path), exception);
        }
    }

    /// <summary>Represents a provider that protects a JSON configuration file</summary>
    /// <param name="source">Settings of the source</param>
    /// <see cref="ArgumentNullException"/>
    public ProtectedJsonConfigurationProvider(ProtectedJsonConfigurationSource source) : base(source)
    {
        this.protectedSource = source as ProtectedJsonConfigurationSource;
    }

    /// <summary>Loads the JSON data from the given <see cref="Stream"/></summary>
    /// <param name="stream"><see cref="Stream"/> to load</param>
    public override void Load(Stream stream)
    {
        //Call the base method first to ensure the data to be available
        base.Load(stream);

        var expressions = protectedSource.EncryptedKeyExpressions;
        if (expressions != null)
        {
            //Dictionary that contains the keys (and their encrypted value) that must be written to the JSON file
            var encryptedKeyValuesToWrite = new Dictionary<string, string>();

            //Iterate through the data in order to verify whether the keys that require to be encrypted, as indeed encrypted.
            //Copy the keys to a new string array in order to avoid a collection modified exception
            var keys = new string[this.Data.Keys.Count];
            this.Data.Keys.CopyTo(keys, 0);

            foreach (var key in keys)
            {
                //Iterate through each expression in order to check whether the current key must be encrypted and is encrypted.
                //If not then encrypt the value and overwrite the key
                var value = this.Data[key];
                if (!string.IsNullOrEmpty(value) && expressions.Any(e => e.IsMatch(key)))
                {
                    this.encryptedKeys.Add(key);

                    //Verify whether the value is encrypted
                    if (!this.isEncrypted(value))
                    {
                        var protectedValue = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), protectedSource.Entropy, protectedSource.Scope);
                        var protectedValueWithPrefix = new List<byte>(encryptedPrefixBytes);
                        protectedValueWithPrefix.AddRange(protectedValue);

                        //Convert the protected value to a base-64 string in order to mask the prefix (for cosmetic purposes)
                        //and overwrite the key with the encrypted value
                        var protectedBase64Value = Convert.ToBase64String(protectedValueWithPrefix.ToArray());
                        encryptedKeyValuesToWrite.Add(key, protectedBase64Value);
                        this.Data[key] = protectedBase64Value;
                    }
                }
            }

            //Write the encrypted key/values to the JSON configuration file
            this.writeValues(encryptedKeyValuesToWrite);
        }
    }

    /// <summary>Attempts to get the value of the given key</summary>
    /// <param name="key">Key to get</param>
    /// <param name="value">Value of the key</param>
    /// <returns>Returns true in case the key has been found</returns>
    public override bool TryGet(string key, out string value)
    {
        if (!base.TryGet(key, out value)) { return false; }
        else if (!this.encryptedKeys.Contains(key)) { return true; }

        //Key is encrypted and must therefore be decrypted in order to return.
        //Note that the decoded base-64 bytes contains the encrypted prefix which must be excluded when unprotection
        var protectedValueWithPrefix = Convert.FromBase64String(value);
        var protectedValue = new byte[protectedValueWithPrefix.Length - encryptedPrefixBytes.Length];
        Buffer.BlockCopy(protectedValueWithPrefix, encryptedPrefixBytes.Length, protectedValue, 0, protectedValue.Length);

        var unprotectedValue = ProtectedData.Unprotect(protectedValue, this.protectedSource.Entropy, this.protectedSource.Scope);
        value = Encoding.UTF8.GetString(unprotectedValue);
        return true;
    }

/// <summary>Provides extensions concerning <see cref="ProtectedJsonConfigurationProvider"/></summary>
public static class ProtectedJsonConfigurationProviderExtensions
{
    /// <summary>Adds a protected JSON file</summary>
    /// <param name="configurationBuilder"><see cref="IConfigurationBuilder"/> in which to apply the JSON file</param>
    /// <param name="path">Path to the JSON file</param>
    /// <param name="optional">Specifies whether the JSON file is optional</param>
    /// <param name="entropy">Byte array to increase protection</param>
    /// <returns>Returns the <see cref="IConfigurationBuilder"/></returns>
    /// <exception cref="ArgumentNullException"/>
    public static IConfigurationBuilder AddProtectedJsonFile(this IConfigurationBuilder configurationBuilder, string path, bool optional, byte[] entropy, params Regex[] encryptedKeyExpressions)
    {
        var source = new ProtectedJsonConfigurationSource(entropy)
        {
            Path = path,
            Optional = optional,
            EncryptedKeyExpressions = encryptedKeyExpressions
        };

        return configurationBuilder.Add(source);
    }
}

回答by Andrei Kutishchev

public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
    return services.AddSingleton(provider =>
    {
        var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
        var protectedSection = new ProtectedConfigurationSection(dataProtectionProvider, section);

        var options = protectedSection.Get<TOptions>();
        return Options.Create(options);
    });
}

This method is correct

这个方法是正确的

回答by IslandMan

Just a few clarifications to help avoid problems. When you encrypt a value, it's using the section as 'Purpose' (https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-2.2) When you get a 'Payload not valid' or something similar, it's likely that the purpose you used to encrypt it, differs from the purpose use to decrypt it. So, let's say I have a first level section in my appsettings.json named 'SecureSettings' and within it a connection string:

只需进行一些说明即可帮助避免出现问题。当您加密一个值时,它使用该部分作为“目的”(https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore -2.2) 当您收到“有效负载无效”或类似信息时,您用于加密它的目的可能与用于解密它的目的不同。因此,假设我的 appsettings.json 中有一个名为“SecureSettings”的第一级部分,其中有一个连接字符串:

{
"SecureSettings": 
  {
    "ConnectionString":"MyClearTextConnectionString"
  }
}

To encrypt the value, I'd call: http://localhost/cryptography/encrypt?section=SecureSettings:ConnectionString&value=MyClearTextConnectionString

要加密该值,我会调用:http://localhost/cryptography/encrypt?section=SecureSettings:ConnectionString&value=MyClearTextConnectionString

You may not want to keep an Encrypt controller in the app itself btw.

顺便说一句,您可能不想在应用程序本身中保留加密控制器。