Skip to content

Instantly share code, notes, and snippets.

@winterlimelight
Last active February 16, 2018 17:41
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save winterlimelight/62c49847622c11c237794fbb00aa217e to your computer and use it in GitHub Desktop.
Save winterlimelight/62c49847622c11c237794fbb00aa217e to your computer and use it in GitHub Desktop.
appsettings encrypted provider
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Configuration;
namespace Settings
{
public static class CustomConfigurationExtensions
{
public static IConfigurationBuilder AddEncryptedAndJsonFiles(this IConfigurationBuilder builder, string fileName, string basePath, bool optional, bool reloadOnChange = false)
{
string jsonFilePath = builder.GetFileProvider().GetFileInfo(fileName).PhysicalPath;
var encryptedConfiguration = new EncryptedConfigurationSource(jsonFilePath, basePath);
encryptedConfiguration.UpdateStoredSettings();
return builder
.AddJsonFile(fileName, optional, reloadOnChange)
.Add(encryptedConfiguration);
}
}
public class EncryptedConfigurationProvider : ConfigurationProvider
{
EncryptedConfigurationSource _source;
public EncryptedConfigurationProvider(EncryptedConfigurationSource source)
{
_source = source;
}
public override void Load()
{
if (!File.Exists(_source.JsonFilePath))
return;
var jsonRoot = JObject.Parse(File.ReadAllText(_source.JsonFilePath));
string keyPath = _source.GetEncryptionKeyPath(jsonRoot);
if (String.IsNullOrEmpty(keyPath))
return; // no encryption is to be done on this file
Aes aes = _source.GetEncryptionAlgorithm(keyPath);
if(!File.Exists(_source.EncryptedFilePath))
throw new Exception("Encryption file not found at given path.");
JObject encJsonRoot = _source.GetEncryptedContents(File.ReadAllBytes(_source.EncryptedFilePath), aes);
foreach (JToken item in new JsonInOrderIterator(encJsonRoot))
{
if (item.Parent.Type != JTokenType.Property)
continue;
var prop = item.Parent as JProperty;
Data[prop.Name] = prop.Value.ToString();
}
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
namespace Settings
{
public class EncryptedConfigurationSource : IConfigurationSource
{
public string JsonFilePath { get; }
public string EncryptedFilePath { get; }
private string _settingsBasePath;
public EncryptedConfigurationSource(string jsonFilePath, string settingsBasePath)
{
_settingsBasePath = settingsBasePath;
JsonFilePath = jsonFilePath;
EncryptedFilePath = Regex.Replace(JsonFilePath, Regex.Escape(".json") + "$", ".enc");
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EncryptedConfigurationProvider(this);
}
public void UpdateStoredSettings()
{
if (!File.Exists(JsonFilePath))
return;
var jsonRoot = JObject.Parse(File.ReadAllText(JsonFilePath));
string keyPath = GetEncryptionKeyPath(jsonRoot);
if (String.IsNullOrEmpty(keyPath))
return; // no encryption is to be done on this file
Aes aes = GetEncryptionAlgorithm(keyPath);
// Get/Create encrypted file
var fiEncrypted = new FileInfo(EncryptedFilePath);
JObject settingsJson = new JObject();
if (fiEncrypted.Exists)
settingsJson = GetEncryptedContents(File.ReadAllBytes(fiEncrypted.FullName), aes);
// Add new properties to file
List<JProperty> sensitiveProps = GetSensitiveProperties(jsonRoot);
foreach (var prop in sensitiveProps)
{
var key = prop.Path.Replace("SENSITIVE_", "").Replace(".", ":");
settingsJson[key] = prop.Value; //overwrite existing
}
// Encrypt changes
using (MemoryStream msEncrypt = new MemoryStream())
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt, System.Text.Encoding.UTF8))
swEncrypt.Write(settingsJson.ToString());
File.WriteAllBytes(EncryptedFilePath, msEncrypt.ToArray());
}
// Remove sensitive properties from plaintext settings file.
foreach (var prop in sensitiveProps)
prop.Remove();
File.WriteAllText(JsonFilePath, jsonRoot.ToString());
}
internal string GetEncryptionKeyPath(JObject jsonRoot)
{
var path = jsonRoot["EncryptionKeyPath"];
if(path == null) return null;
var fiEncKey = new FileInfo(Path.Combine(_settingsBasePath, path.ToString()));
if(!fiEncKey.Exists)
throw new Exception("EncryptionKeyPath was specified but cannot be found");
return fiEncKey.FullName;
}
internal JObject GetEncryptedContents(byte[] encrypted, Aes aes)
{
using (MemoryStream msDecrypt = new MemoryStream(encrypted))
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, aes.CreateDecryptor(), CryptoStreamMode.Read))
using (StreamReader srDecrypt = new StreamReader(csDecrypt, System.Text.Encoding.UTF8))
{
string plaintext = srDecrypt.ReadToEnd();
return JObject.Parse(plaintext);
}
}
internal Aes GetEncryptionAlgorithm(string keyPath)
{
if (!File.Exists(keyPath))
throw new InvalidDataException("EncryptionKeyPath key cannot be found");
byte[] data = File.ReadAllBytes(keyPath);
if (data.Length != 48)
throw new InvalidDataException("EncryptionKeyPath key does not contain valid key and IV. Must be 48 bytes length.");
var aes = Aes.Create();
byte[]key = new byte[32];
Array.Copy(data, key, 32);
aes.Key = key;
byte [] iv = new byte[16];
Array.Copy(data, 32, iv, 0, 16);
aes.IV = iv;
return aes;
}
private List<JProperty> GetSensitiveProperties(JObject jsonRoot)
{
var sensitiveProps = new List<JProperty>();
foreach (JToken item in new JsonInOrderIterator(jsonRoot))
{
if (item.Parent.Type != JTokenType.Property)
continue; // we're only looking for "SENSITIVE_x":"y" so parent must be a property.
var prop = item.Parent as JProperty;
if (prop.Name.StartsWith("SENSITIVE_"))
sensitiveProps.Add(prop);
}
return sensitiveProps;
}
}
}
using System.Collections;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Settings
{
public class JsonInOrderIterator : IEnumerable
{
private readonly JObject _root;
public JsonInOrderIterator(JObject root)
{
_root = root;
}
public System.Collections.IEnumerator GetEnumerator()
{
foreach (var item in DoObject(_root))
yield return item;
}
private System.Collections.IEnumerable DoObject(JObject obj)
{
foreach (JProperty prop in obj.Properties())
foreach(var item in DoProperty(prop))
yield return item;
}
private System.Collections.IEnumerable DoArray(JArray ary)
{
foreach (JToken value in ary.Values())
{
if (value.Type == JTokenType.Property)
foreach(var item in DoProperty(value as JProperty))
yield return item;
else
yield return value;
}
}
private System.Collections.IEnumerable DoProperty(JProperty prop)
{
var value = prop.Value;
if (value.Type == JTokenType.Object)
foreach (var res in DoObject(value as JObject))
yield return res;
else if (value.Type == JTokenType.Array)
foreach (var res in DoArray(value as JArray))
yield return res;
else
yield return value;
}
}
}
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using Xunit;
using Newtonsoft.Json.Linq;
using Settings;
namespace Settings.Tests
{
public class SettingsTests
{
[Fact]
public void EncryptSettingsTest()
{
string basePath = Path.GetTempFileName();
string settingsPath = basePath + ".json";
string encPath = basePath + ".enc";
string keyPath = basePath + ".key";
// create key file
var aes = Aes.Create();
aes.GenerateKey();
aes.GenerateIV();
var keyBytes = new byte[48];
Array.Copy(aes.Key, keyBytes, 32);
Array.Copy(aes.IV, 0, keyBytes, 32, 16);
File.WriteAllBytes(keyPath, keyBytes);
// create settings file
File.WriteAllText(settingsPath, @"{
""EncryptionKeyPath"":""./" + Path.GetFileName(keyPath) + @""",
""a"":""b"",
""SENSITIVE_c"":""d"",
""e"":{
""SENSITIVE_f"": ""g""
},
""h"":[
""i"", { ""SENSITIVE_j"": ""k"" }
]
}");
var fiSettings = new FileInfo(settingsPath);
var builder = new ConfigurationBuilder();
builder.SetBasePath(fiSettings.DirectoryName);
try
{
Func<JObject,IEnumerable<JToken>> sensitiveProps = (node) => {
return node.Descendants().Where(t => t.Type == JTokenType.Property && ((JProperty)t).Name.StartsWith("SENSITIVE_"));
};
var root = JObject.Parse(File.ReadAllText(fiSettings.FullName));
Assert.Equal(3, sensitiveProps(root).Count());
var configSrc = new EncryptedConfigurationSource(fiSettings.FullName, fiSettings.DirectoryName);
configSrc.UpdateStoredSettings();
Assert.True(File.Exists(encPath));
root = JObject.Parse(File.ReadAllText(fiSettings.FullName));
Assert.Equal(4, root.Count); // SENSITIVE_c will be removed, reduce root tokens from 5 to 4
Assert.Equal(0, sensitiveProps(root).Count());
builder.AddJsonFile(fiSettings.Name, false, false);
builder.Add(configSrc);
var configuration = builder.Build();
Assert.Equal("b", configuration["a"]); // from json
Assert.Equal("d", configuration["c"]); // from enc
Assert.Equal("g", configuration["e:f"]); // from enc
Assert.Equal("k", configuration["h[1]:j"]); // from enc
Assert.Null(configuration["SENSITIVE_c:d"]); // removed from json
Assert.Null(configuration["h[0]:j"]); // never existed
// Add to main settings file and repeat the process
root.Last.AddAfterSelf(new JProperty("SENSITIVE_l", "m"));
root.Last.AddAfterSelf(new JProperty("SENSITIVE_c", "dd"));
root.Last.AddAfterSelf(new JProperty("n", "o"));
File.WriteAllText(fiSettings.FullName, root.ToString());
root = JObject.Parse(File.ReadAllText(fiSettings.FullName));
Assert.Equal(2, sensitiveProps(root).Count());
builder = new ConfigurationBuilder();
builder.SetBasePath(fiSettings.DirectoryName);
builder.AddEncryptedAndJsonFiles(fiSettings.Name, fiSettings.DirectoryName, false, false); //use the extension method this time
configuration = builder.Build();
root = JObject.Parse(File.ReadAllText(fiSettings.FullName));
Assert.Equal(5, root.Count); // n:o has been added
Assert.Equal(0, sensitiveProps(root).Count());
Assert.Equal("b", configuration["a"]); // from json
Assert.Equal("dd", configuration["c"]); // from enc (updated)
Assert.Equal("g", configuration["e:f"]); // from enc (1st time)
Assert.Equal("k", configuration["h[1]:j"]); // from enc (1st time)
Assert.Equal("m", configuration["l"]); // from enc
Assert.Equal("o", configuration["n"]); // from json
Assert.Null(configuration["SENSITIVE_l:m"]); // removed from json
}
finally
{
if (File.Exists(settingsPath)) File.Delete(settingsPath);
if (File.Exists(encPath)) File.Delete(encPath);
if (File.Exists(keyPath)) File.Delete(keyPath);
File.Delete(basePath); //GetTempFileName() actually creates a file
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment