【问题标题】:Encrypted configuration in ASP.NET CoreASP.NET Core 中的加密配置
【发布时间】:2016-07-03 22:13:45
【问题描述】:

随着web.config 的消失,在使用 ASP.NET Core 构建的 Web 应用程序的配置中存储敏感信息(密码、令牌)的首选方式是什么?

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

【问题讨论】:

    标签: asp.net-mvc asp.net-core asp.net-core-mvc .net-core


    【解决方案1】:

    用户密码看起来是存储密码的好解决方案,通常是应用程序密码,至少在开发过程中。

    检查official Microsoft documentation。您还可以查看this 其他 SO 问题。

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

    如果您想将加密的appsettings.json 投入生产,您可以通过构建custom configuration provider 来实现。

    例如:

    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();
        }
    }
    

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

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

    然后你就可以激活它了:

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

    【讨论】:

    • 我也有同样的问题。您可以根据您的评论完成 UnencryptMyConfiguration 吗?(例如加载文件并逐个密钥解密)
    • 我在下面发布了一个适合我的解决方案,它更简单,更适合我的需求。
    • 为了那些对 ASP.NET Core 非常陌生的人的利益,我最终如何访问Startup.ConfigureServices() 中的连接字符串值以便将其传递给UseSqlServerStorage()?
    【解决方案2】:

    我同意@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);
            }
        }
    }
    

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

    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"));
    }
    

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

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

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

    【讨论】:

    • 我正在利用这种技术。我从控制器获得了一个加密值,但是当受保护部分尝试取消保护时,我收到一个加密异常,指出有效负载无效。哎呀,按回车太快了。是否需要对加密部分进行特殊处理才能将其与未保护部分相关联?
    • 看起来我遇到了命名问题。当我将两个 DataProtectors 的命名都更改为“Test”时,它起作用了。关于我的部分名称的某些内容不起作用,但至少我发现了问题。
    • 我可以加密连接字符串,但如何在应用程序启动期间再次解密?
    • 默认每90天重新生成一个新密钥,这样配置文件中的密文如果不更新就无法再次解密。此外,ASP.NET Core 在不同机器上运行时会生成不同的密钥来加密数据,因此如果您的应用程序是分布式的,它就不能像这样使用。
    • 我也很好奇,PersistKeysToFileSystem指定路径中存储的secret是什么形式的?如果它们是加密的,那么如何加密?
    【解决方案3】:

    我不想编写自定义提供程序——工作量太大。我只是想利用 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;
        }
    }
    

    【讨论】:

    • 当然帮助了我。非常感谢。
    • 哪种加密方式会更好??
    • Azure KeyVault (.NET Core 1.1) 和 DataProtection (.NET Core 1.0) 都存在很长时间,数据保护可以追溯到 1.0 的 rc 版本
    • @Tseng 如果攻击者拥有您的服务器,则以纯文本形式保存您的机密或使用数据保护或 Azure Key Vault 没有区别,因为他始终可以对您的应用程序进行内存转储以检索数据,一旦你的应用被解密。
    • @VladRadu:无处声称它可以保护攻击者对服务器具有完全访问权限的位置(即可以运行代码和管理员/根权限)。然而,许多攻击包括不受保护的路径遍历攻击,即允许一个人读取配置文件或下载应用程序二进制文件,但不执行代码。在这种情况下,Azure VaultKey 确实可以保护您的数据。更不用说在 VaultKey 中更容易管理。一项更改会影响所有应用程序和实例,其中加密 appsettings 中的值需要您重新编译/重新部署您的应用程序并首先对其值进行编码
    【解决方案4】:

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

    执行以下步骤:

    1. Json 文件已加载
    2. 确定与给定正则表达式匹配的 JSON 部分是否已加密(或未加密)。这是通过 JSON 部分的 base-64 解码来完成的,并验证它是否以预期的前缀 !ENC!)
    3. 开头
    4. 如果未加密,则首先使用 DPAPI 加密 JSON 部分,然后添加前缀 !ENC! 并编码为 base-64
    5. 用 Json 文件中的加密(base-64)值覆盖未加密的 JSON 部分

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

    此解决方案包含以下类:

    1. ProtectedJsonConfigurationProvider 类(= 自定义 JsonConfigurationProvider)
    2. ProtectedJsonConfigurationSource 类(= 自定义 JsonConfigurationSource)
    3. IConfigurationBuilder 上的 AddProtectedJsonFile() 扩展方法,以便简单地添加受保护的配置

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

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

    加载后变成(有点)如下

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

    并假设基于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; }
    }
    

    示例代码下方:

    //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
    

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

    最后是魔法发生的类:)

    /// <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);
        }
    }
    

    【讨论】:

      【解决方案5】:
      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);
          });
      }
      

      这个方法是对的

      【讨论】:

      • 它是否符合 FIPS 标准?
      • 你能分享它的工作副本吗?由于某种原因,我无法使其工作。
      【解决方案6】:

      只是一些澄清,以帮助避免问题。当您加密一个值时,它使用该部分作为“目的”(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"
        }
      }
      

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

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

      【讨论】:

      • 而且您可能不想通过 http 发送要在 URL 中加密的文本
      猜你喜欢
      • 2018-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-14
      • 1970-01-01
      • 1970-01-01
      • 2019-06-18
      相关资源
      最近更新 更多