Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 1.0.10 #27

Merged
merged 2 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"DateTime": "2018-01-01T12:34:56.789",
"IntArray": [ 4, 5, 6 ],
"Nullable": {
"DateTime": "Protect:{2016-10-01T20:34:56.789Z}",
"Double": "Protect:{123.456}",
"DoubleArray": [ "2.71", "Protect:{3.14}", "6.67", "Protect:{%customSubPurpose%}:{3.14}" ],
"Int": "Protect:{98765}",
"Bool": "Protect:{true}"
"DateTime": "Protect:{2016-10-01T20:34:56.789Z}", // protecting datetime
"Double": "Protect:{123.456}", // protecting float
"DoubleArray": [ "2.71", "Protect:{3.14}", "6.67", "Protect:{%customSubPurpose%}:{3.14}" ], // example of array element encryption and per configuration key subpurpose
"Int": "Protect:{98765}", // protecting integer
"Bool": "Protect:{true}" //protecting boolean
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"DateTime": "2016-01-01T12:34:56.789",
"IntArray": [ 1, 2 ],
"PlainTextJsonSpecialCharacters": "\\!*+?|{[()^$.#\u0022",
"EncryptedJsonSpecialCharacters": "Protect:{\\!*+?|{[()^$.#\u0022}",
"EncryptedJsonSpecialCharacters": "Protect:{\\!*+?|{[()^$.#\u0022}", // protecting encoded characters
"Nullable": {
"Int": null,
"Double": null,
"Double": null, // sample null value
"Bool": null,
"DateTime": null,
"DoubleArray": [ 3.14, 2.71 ]
"DoubleArray": [ 3.14, 2.71 ]
},
"ConnectionStrings": {
"PlainTextConnectionString": "Data Source=localhost\\SQLEXPRESS; Initial Catalog=DB name; User ID=sa; Password=pass1234; MultipleActiveResultSets=True;",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<EncryptedXmlSecretKey>OtherProtect:{Xml Secret Key_\&gt;!*+?|{[()^$.#}</EncryptedXmlSecretKey>
<EncryptedXmlSecretKey>OtherProtect:{Xml Secret Key_\&gt;!*+?|{[()^$.#}</EncryptedXmlSecretKey> <!--Protecting encoded characters-->
<PlainTextXmlSecretKey>Xml Secret Key_\&gt;!*+?|{[()^$.#</PlainTextXmlSecretKey>
<TransientFaultHandlingOptions>
<Enabled>true</Enabled>
<AutoRetryDelay>OtherProtect:{00:00:07}</AutoRetryDelay>
<AutoRetryDelaySubPurpose>OtherProtect:{sUbPuRpOsE}:{00:00:07}</AutoRetryDelaySubPurpose>
<AutoRetryDelaySubPurpose>OtherProtect:{sUbPuRpOsE}:{00:00:07}</AutoRetryDelaySubPurpose> <!-- example of per configuration key subpurpose -->
</TransientFaultHandlingOptions>
<Logging>
<LogLevel>
<Default>Information</Default>
<Microsoft>OtherProtect:{Warning}</Microsoft>
<Microsoft>OtherProtect:{Warning}</Microsoft> <!--protecting string-->
</LogLevel>
</Logging>
<EmptyElement></EmptyElement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static class ConfigurationBuilderExtensions
/// This list gets processed in first-in first-out order (FIFO) and stops as soon as a matching is found. By default three types of custom processors are supported:<br/>
/// - One for JSON files (<see cref="JsonFileProtectOption"/> and <see cref="JsonFileProtectProcessor"/>)<br/>
/// - One for XML files (<see cref="XmlFileProtectOption"/> and <see cref="XmlFileProtectProcessor"/>)<br/>
/// - One for raw files (<see cref="RawFileProtectOption"/> and <see cref="RawFileProtectProcessor"/>)<br/>
/// - One for RAW files (<see cref="RawFileProtectOption"/> and <see cref="RawFileProtectProcessor"/>)<br/>
/// This list has a public getter so you can add any additional decoding function you want or replace an existing one for your needs.
/// </summary>
public static List<FilesProtectOptions> ProtectFilesOptions { get; private set; } = new List<FilesProtectOptions>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>disable</ImplicitUsings>

<PublishSingleFile>true</PublishSingleFile>
<Version>1.0.9</Version>
<Version>1.0.10</Version>
<Authors>Federico Di Marco &lt;[email protected]&gt;</Authors>
<Description>Fededim.Extensions.Configuration.Protected is an improved ConfigurationBuilder which allows partial or full encryption of configuration values stored inside any possible ConfigurationSource and fully integrated in the ASP.NET Core architecture. Basically, it implements a custom ConfigurationBuilder and a custom ConfigurationProvider defining a custom tokenization tag which whenever found decrypts the enclosed encrypted data using ASP.NET Core Data Protection API.</Description>
<Copyright>$([System.DateTime]::UtcNow.Year)</Copyright>
Expand Down Expand Up @@ -56,6 +56,10 @@

v1.0.9
- No changes, just a rebuild due to a misalignment with symbols.

v1.0.10
- Improvement: Allow the specification of JsonSerializationOptions for JsonFileProtectProcessor to tweak its settings (comments inside JSON files are now skipped by default)
- Improvement: Allow the specification of LoadOptions and SaveOptions for XmlFileProtectProcessor to tweak its settings
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
69 changes: 56 additions & 13 deletions Fededim.Extensions.Configuration.Protected/ProtectFileProcessors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Linq;
using System.Collections.Generic;

namespace Fededim.Extensions.Configuration.Protected
{
/// <summary>
/// This is the interface which must be implemented by a custom FileProtectProcessor. It contains a single method <see cref="ProtectFile"/> used to read, encrypt and return the encrypted file as string.
/// This is the interface which must be implemented by a custom FileProtectProcessor. It contains a single method <see cref="ProtectFile"/> used to decode, encrypt and re-encode the input file and return it as string.
/// </summary>
public interface IFileProtectProcessor
{
/// <summary>
/// This method actually implements a custom FileProtectProcessor which must read, encrypt and return the encrypted file as string.
/// This method actually implements a custom FileProtectProcessor which must decode, encrypt and re-encode the input file and return it as string.
/// </summary>
/// <param name="rawFileText">The is the raw input file as a string</param>
/// <param name="protectRegex">This is the configured protected regex which must be matched in file values in order to choose whether to encrypt or not the data.</param>
/// <param name="protectFunction">This is the protect function taking the plaintext data as input and producing encrypted base64 data as output</param>
/// <returns>the encrypted file as a string</returns>
/// <returns>the encrypted re-encoded file as a string</returns>
String ProtectFile(String rawFileText, Regex protectRegex, Func<String, String> protectFunction);
}

Expand All @@ -40,7 +39,7 @@ public class FilesProtectOptions
public Regex FilenameRegex { get; private set; }

/// <summary>
/// Specifies the FileProtectProcessor class implementing the <see cref="IFileProtectProcessor"/> interface used to read, encrypt and return the encrypted file as string.
/// Specifies the FileProtectProcessor class implementing the <see cref="IFileProtectProcessor"/> interface used to decode, encrypt and re-encode the input file and return it as string.
/// </summary>
public IFileProtectProcessor FileProtectProcessor { get; private set; }

Expand All @@ -65,7 +64,7 @@ public class RawFileProtectProcessor : IFileProtectProcessor
/// <param name="rawFileText">The is the raw input file as a string</param>
/// <param name="protectRegex">This is the configured protected regex which must be matched in file values in order to choose whether to encrypt or not the data.</param>
/// <param name="protectFunction">This is the protect function taking the plaintext data as input and producing encrypted base64 data as output</param>
/// <returns>the encrypted file as a string</returns>
/// <returns>the encrypted re-encoded file as a string</returns>
public String ProtectFile(String rawFileText, Regex protectRegex, Func<String, String> ProtectFunction)
{
if (protectRegex.IsMatch(rawFileText))
Expand All @@ -82,17 +81,40 @@ public String ProtectFile(String rawFileText, Regex protectRegex, Func<String, S
/// </summary>
public class JsonFileProtectProcessor : IFileProtectProcessor
{
protected JsonSerializerOptions JsonSerializerOptions { get; set; }
protected JsonNodeOptions JsonNodeOptions { get; set; }
protected JsonDocumentOptions JsonDocumentOptions { get; set; }


/// <summary>
/// JsonFileProtectProcessor constructor accepting a JsonSerializerOptions.
/// JSON files in contrast to XML files must not have comments according to the standard, but this constraint can be relaxed by setting JsonCommentHandling property in the <see cref="jsonSerializerOptions"/>
/// Since NET Core 3.1 JsonCommentHandling.Allow option always raises an exception, so we set by default to allow comments but to skip them (JsonCommentHandling.Skip).
/// Moreover JsonSerializer generates always strict JSON files, so it won't output any comments in the encrypted re-encoded file.
/// By default we set ReadCommentHandling = JsonCommentHandling.Skip and WriteIndented = true
/// </summary>
/// <param name="jsonSerializerOptions">a custom JsonSerializerOptions if you want to override the default one</param>
public JsonFileProtectProcessor(JsonSerializerOptions jsonSerializerOptions = null)
{
jsonSerializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip, WriteIndented = true };

JsonSerializerOptions = jsonSerializerOptions;
JsonNodeOptions = new JsonNodeOptions { PropertyNameCaseInsensitive = jsonSerializerOptions.PropertyNameCaseInsensitive };
JsonDocumentOptions = new JsonDocumentOptions { CommentHandling = jsonSerializerOptions.ReadCommentHandling, AllowTrailingCommas = jsonSerializerOptions.AllowTrailingCommas, MaxDepth = jsonSerializerOptions.MaxDepth };
}


/// <summary>
/// Please see <see cref="IFileProtectProcessor.ProtectFile"/> interface
/// </summary>
/// <param name="rawFileText">The is the raw input file as a string</param>
/// <param name="protectRegex">This is the configured protected regex which must be matched in file values in order to choose whether to encrypt or not the data.</param>
/// <param name="protectFunction">This is the protect function taking the plaintext data as input and producing encrypted base64 data as output</param>
/// <returns>the encrypted file as a string</returns>
/// <returns>the encrypted re-encoded file as a string</returns>
public String ProtectFile(String rawFileText, Regex protectRegex, Func<String, String> protectFunction)
{
// Loads the JSON file
var document = JsonNode.Parse(rawFileText);
var document = JsonNode.Parse(rawFileText, JsonNodeOptions, JsonDocumentOptions);

// extract and encrypts all string node values
// extraction must be done first because if you change any value while enumerating the collection it raises InvalidOperationException
Expand All @@ -119,10 +141,10 @@ public String ProtectFile(String rawFileText, Regex protectRegex, Func<String, S
}

// returns back the encrypted json file
return document.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
return document.ToJsonString(JsonSerializerOptions);
}



/// <summary>
/// Helper method which extracts all string nodes from the JSON document. It implements a recursive DFS of the JSON parsed tree.
Expand Down Expand Up @@ -165,24 +187,45 @@ protected List<JsonNode> ExtractAllStringNodes(JsonNode node, Regex protectRegex
/// </summary>
public class XmlFileProtectProcessor : IFileProtectProcessor
{
protected LoadOptions LoadOptions { get; set; }
protected SaveOptions? SaveOptions { get; set; }

/// <summary>
/// XmlFileProtectProcessor constructor accepting a LoadOptions and/or SaveOptions.
/// By default we set LoadOptions.None and SaveOptions are instead taken from XML annotations (e.g. the first parent node with such SaveOptions annotation,
/// for XML annotations you can see GetSaveOptionsFromAnnotations method inside XLinq.cs <see href="https://github.com/microsoft/referencesource/blob/master/System.Xml.Linq/System/Xml/Linq/XLinq.cs#L1303-L1318"/> )
/// </summary>
/// <param name="loadOptions">a custom LoadOptions if you want to override the default one</param>
/// <param name="saveOptions">a custom SaveOptions if you want to override the default one</param>

public XmlFileProtectProcessor(LoadOptions loadOptions = LoadOptions.None, SaveOptions? saveOptions = null)
{
LoadOptions = loadOptions;
SaveOptions = saveOptions;
}

/// <summary>
/// Please see <see cref="IFileProtectProcessor.ProtectFile"/> interface
/// </summary>
/// <param name="rawFileText">The is the raw input file as a string</param>
/// <param name="protectRegex">This is the configured protected regex which must be matched in file values in order to choose whether to encrypt or not the data.</param>
/// <param name="protectFunction">This is the protect function taking the plaintext data as input and producing encrypted base64 data as output</param>
/// <returns>the encrypted file as a string</returns>
/// <returns>the encrypted re-encoded file as a string</returns>
public String ProtectFile(String rawFileText, Regex protectRegex, Func<String, String> protectFunction)
{
// Loads the XML File
var document = XDocument.Parse(rawFileText);
var document = XDocument.Parse(rawFileText,LoadOptions);

ProtectXmlNodes(document.Root, protectRegex, protectFunction);

// returns back the encrypted xml file
using (var xmlBytes = new MemoryStream())
{
document.Save(xmlBytes);
if (SaveOptions.HasValue)
document.Save(xmlBytes, SaveOptions.Value); // use the SaveOptions specified in constructor
else
document.Save(xmlBytes); // use the SaveOptions from XML Annotations

return Encoding.UTF8.GetString(xmlBytes.ToArray());
}
}
Expand Down
5 changes: 5 additions & 0 deletions Fededim.Extensions.Configuration.Protected/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Fededim.Extensions.Configuration.Protected is an improved ConfigurationBuilder w
- Supports almost any NET framework (net6.0, netstandard2.0 and net462)
- Pluggable into any project with almost no changes to original NET / NET Core.
- Supports automatic re-decryption on configuration reload if underlying IConfigurationProvider supports it
- Supports per configuration value derived encryption subkey (called "subpurposes" in Data Protection API)

# How to Use

Expand Down Expand Up @@ -204,6 +205,10 @@ v1.0.8
v1.0.9
- No changes, just a rebuild due to a misalignment with symbols.

v1.0.10
- Improvement: Allow the specification of JsonSerializationOptions for JsonFileProtectProcessor to tweak its settings (comments inside JSON files are now skipped by default)
- Improvement: Allow the specification of LoadOptions and SaveOptions for XmlFileProtectProcessor to tweak its settings

# Detailed guide

You can find a [detailed article on CodeProject](https://www.codeproject.com/Articles/5374311/Fededim-Extensions-Configuration-Protected-the-ult) explaning the origin, how to use it and the main point of the implementation.
Expand Down