Last active
February 16, 2018 17:41
-
-
Save winterlimelight/62c49847622c11c237794fbb00aa217e to your computer and use it in GitHub Desktop.
appsettings encrypted provider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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