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