2 Commits

Author SHA1 Message Date
Developer 02
65c64a3f9a chore(API): Aktualisiert auf 2.0.1 2024-12-02 11:35:33 +01:00
Developer 02
1d600aa453 refactor: Nullbarkeitsannotation zu ControllerExtensions-Methoden hinzugefügt 2024-12-02 11:33:52 +01:00
15 changed files with 154 additions and 178 deletions

View File

@@ -19,7 +19,7 @@ namespace DigitalData.Core.API
/// <param name="index">The key in the ViewData dictionary where the value will be stored.</param> /// <param name="index">The key in the ViewData dictionary where the value will be stored.</param>
/// <param name="value">The value to be stored in the ViewData dictionary.</param> /// <param name="value">The value to be stored in the ViewData dictionary.</param>
/// <returns>The same ViewResult object with updated ViewData, allowing for additional chained operations.</returns> /// <returns>The same ViewResult object with updated ViewData, allowing for additional chained operations.</returns>
public static ViewResult WithData(this ViewResult viewResult, string index, object value) public static ViewResult WithData(this ViewResult viewResult, string index, object? value)
{ {
viewResult.ViewData[index] = value; viewResult.ViewData[index] = value;
return viewResult; return viewResult;

View File

@@ -8,7 +8,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<Description>This package provides a comprehensive set of API controllers and related utilities for the DigitalData.Core library. It includes generic CRUD controllers, localization extensions, middleware for security policies, and application model conventions.</Description> <Description>This package provides a comprehensive set of API controllers and related utilities for the DigitalData.Core library. It includes generic CRUD controllers, localization extensions, middleware for security policies, and application model conventions.</Description>
<PackageId>DigitalData.Core.API</PackageId> <PackageId>DigitalData.Core.API</PackageId>
<Version>2.0.0.0</Version> <Version>2.0.1</Version>
<Authors>Digital Data GmbH</Authors> <Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company> <Company>Digital Data GmbH</Company>
<Product>DigitalData.Core.API</Product> <Product>DigitalData.Core.API</Product>
@@ -16,6 +16,8 @@
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl> <RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackageTags>digital data core api</PackageTags> <PackageTags>digital data core api</PackageTags>
<PackageIcon>core_icon.png</PackageIcon> <PackageIcon>core_icon.png</PackageIcon>
<AssemblyVersion>2.0.1</AssemblyVersion>
<FileVersion>2.0.1</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,9 +0,0 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymCryptService<TParams> : IRSAFactory<TParams>
{
IRSADecryptor this[string key] { get; }
bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor);
}
}

View File

@@ -0,0 +1,48 @@
using System.Security.Cryptography;
namespace DigitalData.Core.Abstractions.Security
{
public interface ICryptFactory
{
int KeySizeInBits { get; init; }
string PbePassword { init; }
PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; }
HashAlgorithmName PbeHashAlgorithmName { get; init; }
int PbeIterationCount { get; init; }
PbeParameters PbeParameters { get; }
string EncryptedPrivateKeyPemLabel { get; init; }
/// <summary>
/// Gets the formatter function for generating RSA key names.
/// This formatter takes an issuer, audience, isPrivate, and optional version and separator
/// to produce a formatted string used for the key naming convention.
/// </summary>
/// <param name="issuer">A string representing the issuer of the key. It should not contain invalid file name characters or the separator.</param>
/// <param name="audience">A string representing the audience for which the key is intended. It should not contain invalid file name characters or the separator.</param>
/// <param name="isPrivate">An bool to check if the key is private.</param>
/// <param name="version">An instance of the <see cref="Version?"/> interface, which is used to keep the version of Pbe password.</param>
/// <param name="separator">An optional string separator used to separate the issuer and audience. The default is "-_-". It should not be included in the issuer or audience strings.</param>
/// <returns>A formatted string combining the issuer, audience, and separator, which adheres to valid file naming rules.</returns>
/// <exception cref="ArgumentException">Thrown when the issuer, audience, or separator contains invalid characters or when the separator is present within the issuer or audience.</exception>
Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
string CreateRSAPrivateKeyPem(int? keySizeInBits = null);
string CreateEncryptedPrivateKeyPem(
int? keySizeInBits = null,
string? password = null,
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null);
IRSADecryptor this[string key] { get; }
bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor);
}
}

View File

@@ -1,18 +0,0 @@
using System.Security.Cryptography;
namespace DigitalData.Core.Abstractions.Security
{
public interface IRSAFactory<TParams>
{
string CreateRSAPrivateKeyPem(int? keySizeInBits = null);
string CreateEncryptedPrivateKeyPem(
int? keySizeInBits = null,
string? password = null,
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null);
Task<IRSADecryptor> ReadRSADecryptorAsync(string path, Version? version = null, CancellationToken cancellationToken = default);
}
}

View File

@@ -12,7 +12,13 @@ namespace DigitalData.Core.Security.Extensions
rsa.ImportFromPem(pem); rsa.ImportFromPem(pem);
return rsa; return rsa;
} }
public static IRSADecryptor GetRSADecryptor(this ICryptFactory factory, string issuer, string audience, Version? version = null, string? seperator = null)
=> factory[factory.RSAKeyNameFormatter(issuer, audience, true, version, seperator)];
public static bool TryGetRSADecryptor(this ICryptFactory factory, string issuer, string audience, out IRSADecryptor? decryptor, Version? version = null, string? seperator = null)
=> factory.TryGetRSADecryptor(factory.RSAKeyNameFormatter(issuer, audience, true, version, seperator), out decryptor);
private static string CreatePath(string filename, string? directory = null) private static string CreatePath(string filename, string? directory = null)
{ {
directory ??= Environment.CurrentDirectory; directory ??= Environment.CurrentDirectory;

View File

@@ -1,7 +0,0 @@
namespace DigitalData.Core.Security
{
public class AsymCryptParams : RSAFactoryParams
{
public IEnumerable<ReadOrCreateDirectory> ReadOrCreateDirs { get; init; } = new List<ReadOrCreateDirectory>();
}
}

View File

@@ -1,18 +1,22 @@
using DigitalData.Core.Abstractions.Security; using DigitalData.Core.Abstractions.Security;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DigitalData.Core.Security namespace DigitalData.Core.Security
{ {
public class AsymCryptService<TAsymCryptParams> : RSAFactory<TAsymCryptParams>, IAsymCryptService<TAsymCryptParams>, IRSAFactory<TAsymCryptParams> where TAsymCryptParams : AsymCryptParams public class CryptFactory : RSAFactory, ICryptFactory
{ {
private readonly Dictionary<string, IRSADecryptor> _decryptors; private readonly IDictionary<string, IRSADecryptor> _decryptors;
public IRSADecryptor this[string key] { get => _decryptors[key]; set => _decryptors[key] = value; } public IRSADecryptor this[string key] { get => _decryptors[key]; set => _decryptors[key] = value; }
public AsymCryptService(IOptions<TAsymCryptParams> options, ILogger<AsymCryptService<TAsymCryptParams>>? logger = null) : base(options) public Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
public CryptFactory(ILogger<CryptFactory> logger, IDictionary<string, IRSADecryptor> decryptors, Func<string, string, bool, Version?, string?, string> rsaKeyNameFormatter) : base()
{ {
_decryptors = new(); _decryptors = decryptors ?? new Dictionary<string, IRSADecryptor>();
RSAKeyNameFormatter = rsaKeyNameFormatter;
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy")); logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
} }

View File

@@ -1,12 +0,0 @@
namespace DigitalData.Core.Security
{
namespace DigitalData.Core.Security
{
public enum CryptographicKeyType
{
PrivateKey,
EncryptedPrivateKey,
PublicKey
}
}
}

View File

@@ -1,41 +1,16 @@
using DigitalData.Core.Abstractions.Security; using DigitalData.Core.Abstractions.Security;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace DigitalData.Core.Security namespace DigitalData.Core.Security
{ {
public static class DIExtensions public static class DIExtensions
{ {
private static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services) public static IServiceCollection AddSecurity(this IServiceCollection services)
where TAsymCryptParams : AsymCryptParams
{ {
services.TryAddScoped<IAsymCryptService<TAsymCryptParams>, AsymCryptService<TAsymCryptParams>>(); services.TryAddScoped<ICryptFactory, CryptFactory>();
return services; return services;
} }
public static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, IConfigurationSection section)
where TAsymCryptParams : AsymCryptParams
=> services.Configure<TAsymCryptParams>(section).AddAsymCryptService<TAsymCryptParams>();
public static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, TAsymCryptParams param)
where TAsymCryptParams : AsymCryptParams
=> services.AddSingleton(Options.Create(param)).AddAsymCryptService<TAsymCryptParams>();
private static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services)
where TRSAFactoryParams : RSAFactoryParams
{
services.TryAddScoped<IRSAFactory<TRSAFactoryParams>, RSAFactory<TRSAFactoryParams>>();
return services;
}
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, IConfigurationSection section)
where TRSAFactoryParams : RSAFactoryParams
=> services.Configure<TRSAFactoryParams>(section).AddRSAFactory<TRSAFactoryParams>();
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, TRSAFactoryParams param)
where TRSAFactoryParams : RSAFactoryParams
=> services.AddSingleton(Options.Create(param)).AddRSAFactory<TRSAFactoryParams>();
} }
} }

View File

@@ -6,10 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" /> <ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
<ProjectReference Include="..\DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj" /> <ProjectReference Include="..\DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj" />

View File

@@ -1,6 +1,5 @@
using DigitalData.Core.Abstractions.Security; using DigitalData.Core.Abstractions.Security;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json;
namespace DigitalData.Core.Security namespace DigitalData.Core.Security
{ {
@@ -12,39 +11,6 @@ namespace DigitalData.Core.Security
protected virtual RSA RSA { get; } = RSA.Create(); protected virtual RSA RSA { get; } = RSA.Create();
public string? Issuer { get; init; }
public string? Audience { get; init; }
private DateOnly? _expiration;
public DateOnly? Expiration
{
get => _expiration;
init
{
if (value <= DateOnly.FromDateTime(DateTime.Now))
throw new InvalidOperationException($"Cryptographer expiration date has already passed. Cryptographer: {JsonSerializer.Serialize(this)}");
_expiration = value;
}
}
private Version? _version;
public Version? Version
{
get => _version;
init
{
if (value != Secrets.Version)
throw new InvalidOperationException($"Cryptographer version ({value}) does not match the expected version ({Secrets.Version}). Cryptographer: {JsonSerializer.Serialize(this)}");
_version = value;
}
}
internal RSACryptographer() { } internal RSACryptographer() { }
} }
} }

View File

@@ -1,21 +1,89 @@
using DigitalData.Core.Abstractions.Security; using DigitalData.Core.Abstractions.Security;
using Microsoft.Extensions.Options;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
namespace DigitalData.Core.Security namespace DigitalData.Core.Security
{ {
public class RSAFactory<TRSAFactoryParams> : IRSAFactory<TRSAFactoryParams> where TRSAFactoryParams : RSAFactoryParams public class RSAFactory
{ {
private static readonly Lazy<RSAFactory<RSAFactoryParams>> LazyInstance = new(() => new(Options.Create<RSAFactoryParams>(new()))); private static readonly Lazy<RSAFactory> LazyInstance = new(() => new());
public static RSAFactory<RSAFactoryParams> Static => LazyInstance.Value; public static RSAFactory Static => LazyInstance.Value;
protected readonly TRSAFactoryParams _params; public static readonly string DefaultEncryptedPrivateKeyFileTag = "enc-private";
public RSAFactory(IOptions<TRSAFactoryParams> options) => _params = options.Value; public static readonly string DefaultPrivateKeyFileTag = "private";
public static readonly string DefaultPublicKeyFileTag = "public";
public static readonly IEnumerable<string> KeyFileTags = new string[] { DefaultEncryptedPrivateKeyFileTag, DefaultPrivateKeyFileTag, DefaultPublicKeyFileTag };
private static readonly Lazy<IEnumerable<string>> LazyLowerFileTags = new(() => KeyFileTags.Select(tag => tag.ToLower()));
public static readonly string DefaultRSAKeyNameSeparator = "-_-";
//TODO: make the validation using regex
public static string DefaultRSAKeyNameFormatter(string issuer, string audience, bool isPrivate = true, Version? passwordVersion = null, string? separator = null)
{
separator ??= DefaultRSAKeyNameSeparator;
void ValidateForbidden(string value, string paramName)
{
if (Path.GetInvalidFileNameChars().Any(value.Contains) || LazyLowerFileTags.Value.Any(tag => value.ToLower().Contains(tag)))
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains forbidden characters that are not allowed in file naming.", paramName);
}
static void ValidateSeparator(string value, string paramName, string separator)
{
if (value.Contains(separator))
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains separator characters ({separator}) that are not allowed in file naming.", paramName);
}
ValidateForbidden(issuer, nameof(issuer));
ValidateForbidden(audience, nameof(audience));
ValidateForbidden(separator, nameof(separator));
ValidateSeparator(issuer, nameof(issuer), separator);
ValidateSeparator(audience, nameof(audience), separator);
var sb = new StringBuilder(issuer.Length + audience.Length + separator.Length * 2 + 20);
sb.Append(issuer).Append(separator).Append(audience).Append(separator);
if (passwordVersion is null && isPrivate)
sb.Append(DefaultPrivateKeyFileTag);
else if (isPrivate)
sb.Append(DefaultEncryptedPrivateKeyFileTag).Append(separator).Append(passwordVersion);
else if (passwordVersion is null)
sb.Append(DefaultPublicKeyFileTag);
else
sb.Append(DefaultPublicKeyFileTag).Append(separator).Append(passwordVersion);
return sb.ToString();
}
public int KeySizeInBits { get; init; } = 2048;
public string PbePassword { private get; init; } = Secrets.PBE_PASSWORD;
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
public HashAlgorithmName PbeHashAlgorithmName { get; init; } = HashAlgorithmName.SHA256;
public int PbeIterationCount { get; init; } = 100_000;
private readonly Lazy<PbeParameters> _lazyPbeParameters;
public PbeParameters PbeParameters => _lazyPbeParameters.Value;
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
internal RSAFactory()
{
_lazyPbeParameters = new(() => new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount));
}
public string CreateRSAPrivateKeyPem(int? keySizeInBits = null) public string CreateRSAPrivateKeyPem(int? keySizeInBits = null)
=> RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportRSAPrivateKeyPem(); => RSA.Create(keySizeInBits ?? KeySizeInBits).ExportRSAPrivateKeyPem();
public string CreateEncryptedPrivateKeyPem( public string CreateEncryptedPrivateKeyPem(
int? keySizeInBits = null, int? keySizeInBits = null,
@@ -24,18 +92,18 @@ namespace DigitalData.Core.Security
HashAlgorithmName? hashAlgorithmName = null, HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null) int? iterationCount = null)
{ {
password ??= _params.PbePassword; password ??= PbePassword;
var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null) var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null)
? new PbeParameters( ? new PbeParameters(
pbeEncryptionAlgorithm ?? _params.PbeEncryptionAlgorithm, pbeEncryptionAlgorithm ?? PbeEncryptionAlgorithm,
hashAlgorithmName ?? _params.PbeHashAlgorithmName, hashAlgorithmName ?? PbeHashAlgorithmName,
iterationCount ?? _params.PbeIterationCount) iterationCount ?? PbeIterationCount)
: _params.PbeParameters; : PbeParameters;
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); var encryptedPrivateKey = RSA.Create(keySizeInBits ?? KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey); var pemChars = PemEncoding.Write(EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
return new string(pemChars); return new string(pemChars);
} }

View File

@@ -1,34 +0,0 @@
using System.Security.Cryptography;
namespace DigitalData.Core.Security
{
public class RSAFactoryParams
{
public string EncryptedPrivateKeyFileTag { get; init; } = "enc-private";
public string PrivateKeyFileTag { get; init; } = "private";
public string PublicKeyFileTag { get; init; } = "public";
public string RSAKeyNameSeparator { get; init; } = "-_-";
public int KeySizeInBits { get; init; } = 2048;
public string PbePassword { internal get; init; } = Secrets.PBE_PASSWORD;
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
public HashAlgorithmName PbeHashAlgorithmName { get; init; } = HashAlgorithmName.SHA256;
public int PbeIterationCount { get; init; } = 100_000;
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
private readonly Lazy<PbeParameters> _lazyPbeParameters;
public PbeParameters PbeParameters => _lazyPbeParameters.Value;
public RSAFactoryParams()
=> _lazyPbeParameters = new(() => new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount));
}
}

View File

@@ -1,9 +0,0 @@
namespace DigitalData.Core.Security
{
public class ReadOrCreateDirectory
{
public required string Dir { get; init; }
public IEnumerable<string> ReadOrCreateFiles { get; init; } = new List<string>();
}
}