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

improve fault tolerance of dataprotection #158

Merged
merged 32 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5298a3e
Improved resilience in data protection mechanism
pawelvds Jan 11, 2024
9ba1d47
Enhanced the Decrypt method in LauncherConfiguration to handle except…
pawelvds Jan 11, 2024
e79e470
Enhance error handling in decryption of LauncherConfiguration
pawelvds Jan 11, 2024
73a3630
Added log warning
pawelvds Jan 12, 2024
d2951d6
Added try-catch block around ECDH curve loading
pawelvds Jan 12, 2024
da46b84
Updated Common.cs
pawelvds Jan 22, 2024
4d66c72
Enhanced resilience in LoadCurve with error handling and automatic re…
pawelvds Jan 23, 2024
8925726
Resolved merge conflict
pawelvds Jan 23, 2024
d867424
push
pawelvds Jan 23, 2024
a58571d
Handle curve loading and decryption errors in LoadCurve
pawelvds Jan 23, 2024
f13409a
Changes related to suggestions from review
pawelvds Jan 23, 2024
325a927
Fixed Common.cs
pawelvds Jan 29, 2024
d176c14
fix
pawelvds Jan 29, 2024
f8992d0
Another try fix
pawelvds Jan 29, 2024
2119715
f
pawelvds Jan 29, 2024
e5d0842
ff
pawelvds Jan 29, 2024
55afd78
fix conflicts
pawelvds Jan 29, 2024
25422da
fff
pawelvds Jan 29, 2024
0bb99c2
Refactor Common.cs for improved readability
pawelvds Jan 29, 2024
69e6e9f
Add conditional return in Common.cs
pawelvds Jan 29, 2024
779982b
Add conditional return in Common.cs
pawelvds Jan 29, 2024
2c3d294
changes
pawelvds Jan 29, 2024
18a51ef
Fix indentation in Common.cs file
pawelvds Jan 29, 2024
fb54d22
Refactor code
pawelvds Jan 29, 2024
4ddffed
Revert "changes"
pawelvds Jan 29, 2024
691283f
Refactor code formatting in fiskaltrust.Launcher
pawelvds Jan 29, 2024
f9fb2c9
Resolve diff
pawelvds Jan 29, 2024
f4f3c17
Refactor codein Common.cs
pawelvds Jan 29, 2024
83d3f80
Merge branch 'main' of github.com:fiskaltrust/middleware-launcher int…
pawelvds Jan 29, 2024
63118e5
Improve code
pawelvds Jan 29, 2024
4609cfb
Refactor curve loading
pawelvds Jan 29, 2024
b1783d5
Improved error handling in ECDH file loading
pawelvds Jan 29, 2024
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
42 changes: 27 additions & 15 deletions src/fiskaltrust.Launcher.Common/Configuration/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,20 @@ internal void SetAlternateNames(string text)
}
}
}

private void MapFieldsWithAttribute<T>(Func<object?, object?> action)
private void MapFieldsWithAttribute<T>(Func<object?, string, object?> action)
{
var errors = new List<Exception>();

foreach (var field in GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
var value = field.GetValue(this);
var name = field.Name;

if (field.GetCustomAttributes(typeof(T)).Any())
if (field.GetCustomAttributes(typeof(T), false).Any())
{
try
{
field.SetValue(this, action(value));
field.SetValue(this, action(value, name));
}
catch (Exception e)
{
Expand All @@ -297,24 +297,37 @@ private void MapFieldsWithAttribute<T>(Func<object?, object?> action)
throw new AggregateException(errors);
}
}

public void Encrypt(IDataProtector dataProtector)
{
MapFieldsWithAttribute<EncryptAttribute>(value =>
MapFieldsWithAttribute<EncryptAttribute>((value, name) =>
{
if (value is null) { return null; }
if (value is null) return null;

return dataProtector.Protect((string)value);
try
{
return dataProtector.Protect((string)value);
}
catch (Exception e)
{
Log.Warning($"Failed to encrypt value of configuration field {name}. Consider using the 'config set' command to set the field's value.", name);
return null;
}
});
}

public void Decrypt(IDataProtector dataProtector)
{
MapFieldsWithAttribute<EncryptAttribute>((value) =>
MapFieldsWithAttribute<EncryptAttribute>((value, name) =>
{
if (value is null) { return null; }

return dataProtector.Unprotect((string)value);
try
{
if (value is null) return null;
return dataProtector.Unprotect((string)value);
}
catch (Exception e)
{
Log.Warning("Failed to decrypt value of configuration field {name}. Consider using the 'config set' command to set the fields value.", name);
return null;
}
});
}

Expand All @@ -328,7 +341,6 @@ public void Decrypt(IDataProtector dataProtector)
return null;
}
}

public record LauncherConfigurationInCashBoxConfiguration
{
[JsonPropertyName("launcher")]
Expand All @@ -349,4 +361,4 @@ public record LauncherConfigurationInCashBoxConfiguration
return configuration;
}
}
}
}
96 changes: 77 additions & 19 deletions src/fiskaltrust.Launcher/Commands/Common.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using fiskaltrust.Launcher.Common.Configuration;
Expand All @@ -10,6 +11,7 @@
using fiskaltrust.Launcher.Extensions;
using fiskaltrust.Launcher.Helpers;
using fiskaltrust.Launcher.Logging;
using fiskaltrust.Launcher.ServiceInstallation;
using fiskaltrust.storage.serialization.V0;
using Microsoft.AspNetCore.DataProtection;
using Serilog;
Expand Down Expand Up @@ -82,6 +84,7 @@ public static async Task<int> HandleAsync<O, S>(
IHost host,
Func<CommonOptions, CommonProperties, O, S, Task<int>> handler) where S : notnull
{
// Log messages will be save here and logged later when we have the configuration options to create the logger.
var collectionSink = new CollectionSink();
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(collectionSink)
Expand Down Expand Up @@ -131,6 +134,7 @@ public static async Task<int> HandleAsync<O, S>(

Log.Verbose("Merging launcher cli args.");
launcherConfiguration.OverwriteWith(options.ArgsLauncherConfiguration);
await EnsureServiceDirectoryExists(launcherConfiguration);

if (!launcherConfiguration.UseOffline!.Value && (launcherConfiguration.CashboxId is null || launcherConfiguration.AccessToken is null))
{
Expand All @@ -153,7 +157,7 @@ public static async Task<int> HandleAsync<O, S>(
ECDiffieHellman? clientEcdh = null;
try
{
clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value);
clientEcdh = await LoadCurve(launcherConfiguration.CashboxId!.Value, launcherConfiguration.AccessToken!, launcherConfiguration.ServiceFolder!, launcherConfiguration.UseOffline!.Value, useFallback: launcherConfiguration.UseLegacyDataProtection!.Value);
using var downloader = new ConfigurationDownloader(launcherConfiguration);
var exists = await downloader.DownloadConfigurationAsync(clientEcdh);
if (launcherConfiguration.UseOffline!.Value && !exists)
Expand All @@ -180,6 +184,7 @@ public static async Task<int> HandleAsync<O, S>(
}
catch (Exception e)
{
// will exit with non-zero exit code later.
Log.Fatal(e, "Could not read Cashbox configuration file.");
}

Expand All @@ -191,9 +196,11 @@ public static async Task<int> HandleAsync<O, S>(
}
catch (Exception e)
{
// will exit with non-zero exit code later.
Log.Fatal(e, "Could not parse Cashbox configuration.");
}

// Previous log messages will be logged here using this logger.
Log.Logger = new LoggerConfiguration()
.AddLoggingConfiguration(launcherConfiguration)
.AddFileLoggingConfiguration(launcherConfiguration, new[] { "fiskaltrust.Launcher", launcherConfiguration.CashboxId?.ToString() })
Expand All @@ -205,6 +212,9 @@ public static async Task<int> HandleAsync<O, S>(
Log.Write(logEvent);
}

// If any critical errors occured, we exit with a non-zero exit code.
// In many cases we don't want to immediately exit the application,
// but we want to log the error and continue and see what else is going on before we exit.
if (collectionSink.Events.Where(e => e.Level == LogEventLevel.Fatal).Any())
{
return 1;
Expand All @@ -229,6 +239,50 @@ public static async Task<int> HandleAsync<O, S>(
return await handler(options, new CommonProperties(launcherConfiguration, cashboxConfiguration, clientEcdh, dataProtectionProvider), specificOptions, host.Services.GetRequiredService<S>());
}

private static async Task EnsureServiceDirectoryExists(LauncherConfiguration config)
{
var serviceDirectory = config.ServiceFolder;
try
{
if (!Directory.Exists(serviceDirectory))
{
Directory.CreateDirectory(serviceDirectory);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var user = Environment.GetEnvironmentVariable("USER");
if (!string.IsNullOrEmpty(user))
{
var chownResult = await ProcessHelper.RunProcess("chown", new[] { user, serviceDirectory }, LogEventLevel.Debug);
if (chownResult.exitCode != 0)
{
Log.Warning("Failed to change owner of the service directory.");
}

var chmodResult = await ProcessHelper.RunProcess("chmod", new[] { "774", serviceDirectory }, LogEventLevel.Debug);
if (chmodResult.exitCode != 0)
{
Log.Warning("Failed to change permissions of the service directory.");
}
}
else
{
Log.Warning("Service user name is not set. Owner of the service directory will not be changed.");
}
}
else
{
Log.Debug("Changing owner and permissions is skipped on non-Unix operating systems.");
}
}
}
catch (UnauthorizedAccessException e)
{
// will exit with non-zero exit code later.
Log.Fatal(e, "Access to the path '{ServiceDirectory}' is denied. Please run the application with sufficient permissions.", serviceDirectory);
}
}

public static async Task<ECDiffieHellman> LoadCurve(Guid cashboxId, string accessToken, string serviceFolder, bool useOffline = false, bool dryRun = false, bool useFallback = false)
{
Log.Verbose("Loading Curve.");
Expand All @@ -237,34 +291,38 @@ public static async Task<ECDiffieHellman> LoadCurve(Guid cashboxId, string acces

if (File.Exists(clientEcdhPath))
{
return ECDiffieHellmanExt.Deserialize(dataProtector.Unprotect(await File.ReadAllTextAsync(clientEcdhPath)));
}
else
{
const string offlineClientEcdhPath = "/client.ecdh";
ECDiffieHellman clientEcdh;

if (!dryRun && useOffline && File.Exists(offlineClientEcdhPath))
try
{
clientEcdh = ECDiffieHellmanExt.Deserialize(await File.ReadAllTextAsync(offlineClientEcdhPath));
try
{
File.Delete(offlineClientEcdhPath);
}
catch { }
return ECDiffieHellmanExt.Deserialize(dataProtector.Unprotect(await File.ReadAllTextAsync(clientEcdhPath)));
}
else
catch (Exception e)
{
clientEcdh = CashboxConfigEncryption.CreateCurve();
Log.Warning($"Error loading or decrypting ECDH curve: {e.Message}. Regenerating new curve.");
}
}

if (!dryRun)
// Handling offline client ECDH path
const string offlineClientEcdhPath = "/client.ecdh";
if (!dryRun && useOffline && File.Exists(offlineClientEcdhPath))
{
var clientEcdh = ECDiffieHellmanExt.Deserialize(await File.ReadAllTextAsync(offlineClientEcdhPath));
try
{
await File.WriteAllTextAsync(clientEcdhPath, dataProtector.Protect(clientEcdh.Serialize()));
File.Delete(offlineClientEcdhPath);
}
catch { }

return clientEcdh;
}

// Regenerating the curve if it's not loaded or in case of an error
var newClientEcdh = CashboxConfigEncryption.CreateCurve();
if (!dryRun)
{
await File.WriteAllTextAsync(clientEcdhPath, dataProtector.Protect(newClientEcdh.Serialize()));
}

return newClientEcdh;
}
}
}
4 changes: 3 additions & 1 deletion src/fiskaltrust.Launcher/Commands/DoctorCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ public static async Task<int> HandleAsync(CommonOptions commonOptions, CommonPro
ftCashBoxConfiguration cashboxConfiguration = new();

if (clientEcdh is null)
{ }
{
Log.Warning("Failed to load ECDH curve. Skipping some related doctor checks.");
}
else
{
using var downloader = new ConfigurationDownloader(launcherConfiguration);
Expand Down
Loading