Shamshiel
Shamshiel

Reputation: 2211

Custom JsonConfigurationProvider - ASP.NET Core uses wrong implementation

I tried to implement a custom JsonConfigurationProvider called 'CryptographyConfigProvider' that decrypts JSON from a stream if it is encrypted.

I inherited from the JsonConfigurationProvider and implemented a custom Load-method.

I use it like this in the Program.cs:

  var configuration = new ConfigurationBuilder()
                .AddEncryptedJsonFile($"appsettings.{enviromentValue}.json", optional: true, reloadOnChange: false)
                .Build();

This call executes my custom implementation. (see below)

But after this ASP.NET Core tries to access the appsettings file again here:

 webHostBuilder.Build().Run();

The exception indicates that the normal JsonConfigurationProvider is called and not my inherited class CryptographyConfigProvider.

System.FormatException: Could not parse the JSON file. Error on line number '0': 'EncryptedString'. 
---> Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: S. Path '', line 0, position 0.
at Newtonsoft.Json.JsonTextReader.ParseValue()
at Newtonsoft.Json.Linq.JObject.Load(JsonReader reader, JsonLoadSettings settings)
at Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser.Parse(Stream input)
at Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider.Load(Stream stream)
--- End of inner exception stack trace ---
at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)
at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load()
at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)
at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
at Main(String[] args) in Program.cs

Has someone an idea why ASP.NET Core is using the normal JsonConfigurationProvider?

Here is my implementation:

public static class DecryptionConfigProviderExtension
{
    public static IConfigurationBuilder AddEncryptedJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
    {
        return builder.AddJsonFile(s =>
        {
            s.FileProvider = null;
            s.Path = path;
            s.Optional = optional;
            s.ReloadOnChange = reloadOnChange;
            s.ResolveFileProvider();
        });
    }

    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<CryptographyConfigurationSource> configureSource) => builder.Add(configureSource);
}

public class CryptographyConfigurationSource : JsonConfigurationSource, IConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new CryptographyConfigProvider(this);
    }
}

public class CryptographyConfigProvider : JsonConfigurationProvider
{
    private const string EncryptionKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456";

    private AesCryptography _aesCryptography;

    public CryptographyConfigProvider(CryptographyConfigurationSource cryptographyConfigurationSource) : base(cryptographyConfigurationSource)
    {
        _aesCryptography = new AesCryptography();
    }

    public override void Load(Stream stream)
    {
        Data = UnencryptConfiguration(stream);
    }

    private IDictionary<string, string> UnencryptConfiguration(Stream stream)
    {
        var reader = new StreamReader(stream);
        var text = reader.ReadToEnd();
        var jsonString = DecryptIfEncrypted(text);

        using (MemoryStream jsonStream = new MemoryStream())
        {
            var parser = new JsonConfigurationFileParser();
            StreamWriter writer = new StreamWriter(jsonStream);
            writer.Write(jsonString);
            writer.Flush();
            jsonStream.Position = 0;
            return parser.Parse(jsonStream);
        };
    }

    private string DecryptIfEncrypted(string text)
    {
        var jsonString = string.Empty;

        try
        {
            jsonString = _aesCryptography.DecryptString(text, EncryptionKey);
        }
        catch
        {
            jsonString = text;
        }

        return jsonString;
    }
}

Upvotes: 9

Views: 8082

Answers (2)

Dan Wilson
Dan Wilson

Reputation: 4047

As of .NET Core 2.0, appsettings.{env.EnvironmentName}.json is loaded automatically for you. If you have encrypted it, then the framework will probably have an issue parsing it.

.ConfigureAppConfiguration((hostingContext, config) =>
{
    ...

    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

    ...

MetaPackages/src/Microsoft.AspNetCore/WebHost.cs

I would try to name your file something else.

An alternative solution that my team recently implemented was to move secrets to app.config and use protected configuration to encrypt it. A custom configuration provider reads the application settings (e.g. Azure:ApiKey) and supplies them to the Core framework.

Upvotes: 5

Scott Roberts
Scott Roberts

Reputation: 341

Having to create custom providers and use old-school XML config files to handle encrypted settings is crazy. This should be handled by the framework, IMO.

In the mean time, my answer to this question is a pretty simple and straight-forward way to encrypt values in your settings files. It uses the existing JSON provider, preferred .Net Core encryption techniques, and is DI friendly.

Hope it helps!

Upvotes: 1

Related Questions