diff --git a/src/App/App.csproj b/src/App/App.csproj
index e0272736..334ea3a7 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -18,7 +18,7 @@
true
true
- true
+ true
true
diff --git a/src/App/Program.cs b/src/App/Program.cs
index a844887d..fc546d75 100644
--- a/src/App/Program.cs
+++ b/src/App/Program.cs
@@ -90,27 +90,13 @@ private static void AddOperatingSpecificConfigurationFolders([NotNull] this ICon
{
if (cfg == null) throw new ArgumentNullException(nameof(cfg));
- const string appSpecificFolder = "financial-app";
const string configFileName = "config";
const string iniFileExt = "ini";
const string jsonFileExt = "json";
- string MakeWin32FilePath(string extension)
+ string MakeFilePath(string extension)
{
- return Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
- appSpecificFolder,
- Path.ChangeExtension(configFileName, extension)
- );
- }
-
- string MakeUnixFilePath(string extension)
- {
- return Path.Combine(
- "/etc",
- appSpecificFolder,
- Path.ChangeExtension(configFileName, extension)
- );
+ return EmitConfigSearchMessage(EnvironmentPath.CreatePath(Path.ChangeExtension(configFileName, extension)));
}
string EmitConfigSearchMessage(string path)
@@ -122,12 +108,9 @@ string EmitConfigSearchMessage(string path)
switch (Environment.OSVersion.Platform)
{
case PlatformID.Win32NT:
- cfg.AddJsonFile(EmitConfigSearchMessage(MakeWin32FilePath(jsonFileExt)), true);
- cfg.AddIniFile(EmitConfigSearchMessage(MakeWin32FilePath(iniFileExt)), true);
- break;
case PlatformID.Unix:
- cfg.AddJsonFile(EmitConfigSearchMessage(MakeUnixFilePath(jsonFileExt)), true);
- cfg.AddIniFile(EmitConfigSearchMessage(MakeUnixFilePath(iniFileExt)), true);
+ cfg.AddJsonFile(MakeFilePath(jsonFileExt), true);
+ cfg.AddIniFile(MakeFilePath(iniFileExt), true);
break;
}
}
diff --git a/src/App/Startup.cs b/src/App/Startup.cs
index 81658508..46201ce2 100644
--- a/src/App/Startup.cs
+++ b/src/App/Startup.cs
@@ -55,6 +55,7 @@ namespace App
using Microsoft.AspNetCore.SpaServices.Webpack;
using Models.DTO.Services;
+ using Support.DataProtection;
using Support.Diagnostics;
using Support.Mapping;
@@ -86,7 +87,9 @@ public void ConfigureServices(IServiceCollection services)
{
// Note the possible dangers for HTTPS: https://docs.microsoft.com/en-us/aspnet/core/performance/response-compression?tabs=aspnetcore2x#compression-with-secure-protocol
opts.EnableForHttps = true;
- });
+ });
+
+ services.AddConfiguredDataProtection(this.Configuration);
services.AddMvc(options =>
{
diff --git a/src/App/Support/DataProtection/AppBuilderExtensions.cs b/src/App/Support/DataProtection/AppBuilderExtensions.cs
new file mode 100644
index 00000000..d43b1d71
--- /dev/null
+++ b/src/App/Support/DataProtection/AppBuilderExtensions.cs
@@ -0,0 +1,167 @@
+// ******************************************************************************
+// © 2018 Sebastiaan Dammann | damsteen.nl
+//
+// File: : AppBuilderExtensions.cs
+// Project : App
+// ******************************************************************************
+
+namespace App.Support.DataProtection {
+ using System;
+ using System.IO;
+ using System.Security.Cryptography.X509Certificates;
+ using Microsoft.AspNetCore.DataProtection;
+ using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.DependencyInjection;
+
+ internal static class ServiceCollectionExtensions {
+ public static void AddConfiguredDataProtection(this IServiceCollection services, IConfiguration configuration)
+ {
+ DataProtectionOptions dataProtectionOptions = configuration.GetValue("DataProtection");
+
+ if (dataProtectionOptions == null)
+ {
+ // Automatic - let ASP.NET Core defaults in place
+ return;
+ }
+
+ IDataProtectionBuilder dataProtectionBuilder;
+ switch (dataProtectionOptions.StorageOptions?.Type ?? DataProtectionStorageType.InMemory)
+ {
+ case DataProtectionStorageType.InMemory:
+ dataProtectionBuilder = ConfigureForInMemory(services);
+ break;
+ case DataProtectionStorageType.File:
+ dataProtectionBuilder = ConfigureForFile(services, dataProtectionOptions);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ switch (dataProtectionOptions.Protection)
+ {
+ case DataProtectionProtectionType.None:
+ break;
+ case DataProtectionProtectionType.Certificate:
+ ConfigureForCertificate(dataProtectionBuilder, dataProtectionOptions);
+ break;
+ case DataProtectionProtectionType.Windows:
+ ConfigureWindowProtection(dataProtectionBuilder);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ if (dataProtectionOptions.Lifetime != null)
+ {
+ dataProtectionBuilder.SetDefaultKeyLifetime(dataProtectionOptions.Lifetime.Value);
+ }
+ }
+
+ private static void ConfigureWindowProtection(IDataProtectionBuilder dataProtectionBuilder) => dataProtectionBuilder.ProtectKeysWithDpapi();
+
+ private static IDataProtectionBuilder ConfigureForInMemory(IServiceCollection services) => services.AddConfiguredDataProtection().UseEphemeralDataProtectionProvider();
+
+ private static IDataProtectionBuilder ConfigureForFile(IServiceCollection services, DataProtectionOptions dataProtectionOptions)
+ {
+ IDataProtectionBuilder builder = services.AddConfiguredDataProtection();
+ DataProtectionStorageOptions storageOptions = dataProtectionOptions.File;
+
+ if (storageOptions == null)
+ {
+ throw new InvalidOperationException($"{nameof(DataProtectionOptions)}:{nameof(dataProtectionOptions.File)} not set");
+ }
+
+ if (storageOptions.Path == "auto")
+ {
+ storageOptions.Path = EnvironmentPath.CreatePath("key-store");
+ }
+
+ DirectoryInfo directory = new DirectoryInfo(storageOptions.Path);
+ directory.Create();
+
+ builder.PersistKeysToFileSystem(
+ directory
+ );
+
+ return builder;
+ }
+
+ private static void ConfigureForCertificate(IDataProtectionBuilder builder, DataProtectionOptions dataProtectionOptions)
+ {
+ CertificateDataProtectionOptions certificateOptions = dataProtectionOptions.Certificate;
+
+ if (certificateOptions == null)
+ {
+ throw new InvalidOperationException($"{nameof(DataProtectionOptions)}:{nameof(dataProtectionOptions.Certificate)} not set");
+ }
+
+ certificateOptions.Validate();
+
+ X509Certificate2 certificate = new X509Certificate2(certificateOptions.CertificatePath, certificateOptions.Password);
+
+ builder.ProtectKeysWithCertificate(
+ certificate
+ );
+ }
+
+ private static IDataProtectionBuilder AddConfiguredDataProtection(this IServiceCollection services)
+ {
+ return services.AddDataProtection(options => options.ApplicationDiscriminator = "financial-app");
+ }
+ }
+
+ internal sealed class DataProtectionOptions
+ {
+ public DataProtectionProtectionType Protection { get; set; } = DataProtectionProtectionType.None;
+
+ public DataProtectionStorageOptions File { get;set; }
+ public CertificateDataProtectionOptions Certificate { get;set; }
+
+ public DataProtectionStorageOptions StorageOptions { get; set; }
+
+ public TimeSpan? Lifetime { get; set; }
+ }
+
+ internal sealed class DataProtectionStorageOptions
+ {
+ public string Path { get; set; }
+
+ public DataProtectionStorageType Type { get; set; } = DataProtectionStorageType.InMemory;
+ }
+
+ internal sealed class CertificateDataProtectionOptions
+ {
+ ///
+ /// Path to PFX certificate
+ ///
+ public string CertificatePath { get; set; }
+
+ public string Password { get; set; }
+
+ public void Validate()
+ {
+ if (String.IsNullOrEmpty(this.CertificatePath) || !File.Exists(this.CertificatePath))
+ {
+ throw new InvalidOperationException($"Certificate path '{this.CertificatePath}' does not exist");
+ }
+
+ if (String.IsNullOrEmpty(this.Password))
+ {
+ throw new InvalidOperationException("No password has been given for certificate");
+ }
+ }
+ }
+
+ internal enum DataProtectionStorageType
+ {
+ InMemory,
+ File
+ }
+
+ internal enum DataProtectionProtectionType
+ {
+ None,
+ Certificate,
+ Windows
+ }
+}
diff --git a/src/App/Support/EnvironmentPath.cs b/src/App/Support/EnvironmentPath.cs
new file mode 100644
index 00000000..6b66427a
--- /dev/null
+++ b/src/App/Support/EnvironmentPath.cs
@@ -0,0 +1,47 @@
+// ******************************************************************************
+// © 2018 Sebastiaan Dammann | damsteen.nl
+//
+// File: : EnvironmentPath.cs
+// Project : App
+// ******************************************************************************
+
+namespace App.Support {
+ using System;
+ using System.IO;
+
+ public static class EnvironmentPath {
+ private const string AppSpecificFolder = "financial-app";
+
+ public static string CreatePath(string subPath)
+ {
+ switch (Environment.OSVersion.Platform)
+ {
+ case PlatformID.Win32NT:
+ return MakeWin32FilePath(subPath);
+ case PlatformID.Unix:
+ return MakeUnixFilePath(subPath);
+
+ default:
+ throw new InvalidOperationException($"Unsupported platform family '{Environment.OSVersion.Platform}' of {Environment.OSVersion}");
+ }
+ }
+
+ private static string MakeWin32FilePath(string subPath)
+ {
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
+ AppSpecificFolder,
+ subPath
+ );
+ }
+
+ private static string MakeUnixFilePath(string subPath)
+ {
+ return Path.Combine(
+ "/etc",
+ AppSpecificFolder,
+ subPath
+ );
+ }
+ }
+}