diff --git a/DigitalData.Core.Abstractions/Security/CryptographerExtensions.cs b/DigitalData.Core.Abstractions/Security/CryptographerExtensions.cs new file mode 100644 index 0000000..2301e78 --- /dev/null +++ b/DigitalData.Core.Abstractions/Security/CryptographerExtensions.cs @@ -0,0 +1,21 @@ +namespace DigitalData.Core.Abstractions.Security +{ + public static class CryptographerExtensions + { + public static IEnumerable GetByIssuer(this IEnumerable cryptographers, string issuer) where TRSACryptographer: IRSACryptographer + => cryptographers.Where(c => c.Issuer == issuer); + + public static IEnumerable GetByAudience(this IEnumerable cryptographers, string audience) where TRSACryptographer : IRSACryptographer + => cryptographers.Where(c => c.Audience == audience); + + public static TRSACryptographer Get(this IEnumerable cryptographers, string issuer, string audience) where TRSACryptographer : IRSACryptographer + => cryptographers.Where(c => c.Issuer == issuer && c.Audience == audience).SingleOrDefault() + ?? throw new InvalidOperationException($"No {typeof(TRSACryptographer).GetType().Name.TrimStart('I')} found with Issuer: {issuer} and Audience: {audience}."); + + public static bool TryGet(this IEnumerable cryptographers, string issuer, string audience, out TRSACryptographer? cryptographer) where TRSACryptographer : IRSACryptographer + { + cryptographer = cryptographers.SingleOrDefault(c => c.Issuer == issuer && c.Audience == audience); + return cryptographer is not null; + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Abstractions/Security/IAsymCryptService.cs b/DigitalData.Core.Abstractions/Security/IAsymCryptService.cs new file mode 100644 index 0000000..81088ee --- /dev/null +++ b/DigitalData.Core.Abstractions/Security/IAsymCryptService.cs @@ -0,0 +1,9 @@ +namespace DigitalData.Core.Abstractions.Security +{ + public interface IAsymCryptService : IRSAFactory + { + public IEnumerable Decryptors { get; } + + public IEnumerable Encryptors { get; } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Abstractions/Security/ICryptFactory.cs b/DigitalData.Core.Abstractions/Security/ICryptFactory.cs deleted file mode 100644 index 94c3109..0000000 --- a/DigitalData.Core.Abstractions/Security/ICryptFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -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; } - - /// - /// 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. - /// - /// A string representing the issuer of the key. It should not contain invalid file name characters or the separator. - /// A string representing the audience for which the key is intended. It should not contain invalid file name characters or the separator. - /// An bool to check if the key is private. - /// An instance of the interface, which is used to keep the version of Pbe password. - /// 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. - /// A formatted string combining the issuer, audience, and separator, which adheres to valid file naming rules. - /// Thrown when the issuer, audience, or separator contains invalid characters or when the separator is present within the issuer or audience. - Func 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); - } -} \ No newline at end of file diff --git a/DigitalData.Core.Abstractions/Security/IRSACryptographer.cs b/DigitalData.Core.Abstractions/Security/IRSACryptographer.cs index acf7320..f2d29bd 100644 --- a/DigitalData.Core.Abstractions/Security/IRSACryptographer.cs +++ b/DigitalData.Core.Abstractions/Security/IRSACryptographer.cs @@ -7,5 +7,15 @@ namespace DigitalData.Core.Abstractions.Security public string Pem { get; init; } public RSAEncryptionPadding Padding { get; init; } + + public string? Directory { get; set; } + + public string? FileName { get; set; } + + public string Issuer { get; init; } + + public string Audience { get; init; } + + public void Init(); } } \ No newline at end of file diff --git a/DigitalData.Core.Abstractions/Security/IRSADecryptor.cs b/DigitalData.Core.Abstractions/Security/IRSADecryptor.cs index 376684e..4aa3ad9 100644 --- a/DigitalData.Core.Abstractions/Security/IRSADecryptor.cs +++ b/DigitalData.Core.Abstractions/Security/IRSADecryptor.cs @@ -2,11 +2,7 @@ { public interface IRSADecryptor : IRSACryptographer { - (string Value, Version Version)? VersionedPassword { init; } - - Version? PasswordVersion { get; } - - bool HasEncryptedPem { get; } + public bool Encrypt { get; init; } IRSAEncryptor Encryptor { get; } diff --git a/DigitalData.Core.Abstractions/Security/IRSAFactory.cs b/DigitalData.Core.Abstractions/Security/IRSAFactory.cs new file mode 100644 index 0000000..7ff812e --- /dev/null +++ b/DigitalData.Core.Abstractions/Security/IRSAFactory.cs @@ -0,0 +1,16 @@ +using System.Security.Cryptography; + +namespace DigitalData.Core.Abstractions.Security +{ + public interface IRSAFactory + { + string CreateRSAPrivateKeyPem(int? keySizeInBits = null); + + string CreateEncryptedPrivateKeyPem( + int? keySizeInBits = null, + string? password = null, + PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null, + HashAlgorithmName? hashAlgorithmName = null, + int? iterationCount = null); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security.Extensions/RSAExtensions.cs b/DigitalData.Core.Security.Extensions/RSAExtensions.cs index 0599cfa..47dcef5 100644 --- a/DigitalData.Core.Security.Extensions/RSAExtensions.cs +++ b/DigitalData.Core.Security.Extensions/RSAExtensions.cs @@ -12,13 +12,7 @@ namespace DigitalData.Core.Security.Extensions rsa.ImportFromPem(pem); 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) { directory ??= Environment.CurrentDirectory; diff --git a/DigitalData.Core.Security/AsymCryptService.cs b/DigitalData.Core.Security/AsymCryptService.cs new file mode 100644 index 0000000..cf6f008 --- /dev/null +++ b/DigitalData.Core.Security/AsymCryptService.cs @@ -0,0 +1,20 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.Config; +using DigitalData.Core.Security.Cryptographer; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DigitalData.Core.Security +{ + public class AsymCryptService : RSAFactory, IAsymCryptService, IRSAFactory where TAsymCryptParams : AsymCryptParams + { + public IEnumerable Decryptors => _params.Decryptors; + + public IEnumerable Encryptors => _params.Encryptors; + + public AsymCryptService(IOptions options, ILogger>? logger = null) : base(options) + { + logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy")); + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/Config/AsymCryptParams.cs b/DigitalData.Core.Security/Config/AsymCryptParams.cs new file mode 100644 index 0000000..5bbc93f --- /dev/null +++ b/DigitalData.Core.Security/Config/AsymCryptParams.cs @@ -0,0 +1,59 @@ +using DigitalData.Core.Abstractions.Security; + +namespace DigitalData.Core.Security.Config +{ + public class AsymCryptParams : RSAFactoryParams + { + public string Directory { get; init; } = string.Empty; + + /// + /// 0: Issuer - 1: Audience - 2: Type tag - 3: Version + /// + public string FileNameFormat { get; init; } = "{0}_-_{1}_-_{2}_-_{3}.pem"; + + public string EncryptorTag { get; init; } = "public"; + + public string DecryptorTag { get; init; } = "private"; + + public string EncryptedDecryptorTag { get; init; } = "enc-private"; + + public IEnumerable Decryptors { get; init; } = new List(); + + public IEnumerable Encryptors { get; init; } = new List(); + + private string TypeTagOf(IRSACryptographer crypt) + { + if (crypt is IRSAEncryptor) + return EncryptorTag; + else if (crypt is IRSADecryptor decryptor) + return decryptor.Encrypt ? EncryptedDecryptorTag : DecryptorTag; + else + throw new InvalidOperationException( + "Unknown cryptographer type. The crypt parameter must be either IRSAEncryptor or IRSADecryptor."); + } + + public override void OnDeserialized() + { + base.OnDeserialized(); + + var cryptographers = Encryptors.Cast().Concat(Decryptors.Cast()); + + foreach (var crypt in cryptographers) + { + // set default path + if (crypt.Pem is null) + { + crypt.Directory ??= Directory; + crypt.FileName ??= string.Format( + FileNameFormat, + crypt.Issuer, + crypt.Audience, + TypeTagOf(crypt), + Secrets.Version); + } + + crypt.Init(); + } + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/Config/RSAFactoryParams.cs b/DigitalData.Core.Security/Config/RSAFactoryParams.cs new file mode 100644 index 0000000..5ab57f2 --- /dev/null +++ b/DigitalData.Core.Security/Config/RSAFactoryParams.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text.Json.Serialization; + +namespace DigitalData.Core.Security.Config +{ + public class RSAFactoryParams : IJsonOnDeserialized + { + 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 PbeParameters? _pbeParameters; + + [JsonIgnore] + public PbeParameters PbeParameters => _pbeParameters!; + + public virtual void OnDeserialized() => _pbeParameters = new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/CryptFactory.cs b/DigitalData.Core.Security/CryptFactory.cs deleted file mode 100644 index 0549cd2..0000000 --- a/DigitalData.Core.Security/CryptFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using Microsoft.Extensions.Logging; - -namespace DigitalData.Core.Security -{ - public class CryptFactory : RSAFactory, ICryptFactory - { - private readonly IDictionary _decryptors; - - public IRSADecryptor this[string key] { get => _decryptors[key]; set => _decryptors[key] = value; } - - public Func RSAKeyNameFormatter { get; } - - public CryptFactory(ILogger logger, IDictionary decryptors, Func rsaKeyNameFormatter) : base() - { - _decryptors = decryptors ?? new Dictionary(); - - RSAKeyNameFormatter = rsaKeyNameFormatter; - - logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy")); - } - - public bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor) => _decryptors.TryGetValue(key, out decryptor); - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/Cryptographer/RSACryptographer.cs b/DigitalData.Core.Security/Cryptographer/RSACryptographer.cs new file mode 100644 index 0000000..a38fd1e --- /dev/null +++ b/DigitalData.Core.Security/Cryptographer/RSACryptographer.cs @@ -0,0 +1,53 @@ +using DigitalData.Core.Abstractions.Security; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.Cryptographer +{ + public class RSACryptographer : IRSACryptographer + { + protected string? _pem; + + public string Pem + { + get => _pem + ?? throw new InvalidOperationException($"Pem is not initialized. Please ensure that the PEM is set or properly loaded from the file. Issuer: {Issuer}, Audience: {Audience}."); + init => _pem = value; + } + + public string? PemPath => FileName is null ? null : Path.Combine(Directory ?? string.Empty, FileName); + + public string? Directory { get; set; } + + public string? FileName { get; set; } + + public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; + + protected virtual RSA RSA { get; } = RSA.Create(); + + public string Issuer { get; init; } = string.Empty; + + public string Audience { get; init; } = string.Empty; + + internal RSACryptographer() { } + + public virtual void UnableToInitPemEvent() => throw new InvalidOperationException( + $"Pem is not initialized and pem file is null. Issuer is {Issuer} and audience {Audience}."); + + public virtual void FileNotFoundEvent() => throw new FileNotFoundException( + $"Pem is not initialized and pem file is not found in {PemPath}. Issuer is {Issuer} and audience {Audience}."); + + // TODO: make file read asynchronous, consider multiple routing + public virtual void Init() + { + if(_pem is null) + { + if(PemPath is null) + UnableToInitPemEvent(); + if (File.Exists(PemPath)) + _pem = File.ReadAllText(PemPath); + else + FileNotFoundEvent(); + } + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/Cryptographer/RSADecryptor.cs b/DigitalData.Core.Security/Cryptographer/RSADecryptor.cs new file mode 100644 index 0000000..f99ac5d --- /dev/null +++ b/DigitalData.Core.Security/Cryptographer/RSADecryptor.cs @@ -0,0 +1,55 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.Config; +using DigitalData.Core.Security.Extensions; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.Cryptographer +{ + public class RSADecryptor : RSACryptographer, IRSADecryptor, IRSACryptographer + { + public bool Encrypt { get; init; } + + private readonly Lazy _lazyEncryptor; + + public IRSAEncryptor Encryptor => _lazyEncryptor.Value; + + public RSADecryptor() + { + _lazyEncryptor = new(() => new RSAEncryptor() + { + Pem = RSA.ExportRSAPublicKeyPem(), + Padding = Padding + }); + } + + public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding); + + public string Decrypt(string data) => RSA.Decrypt(data.Base64ToByte(), Padding).BytesToString(); + + public override void Init() + { + base.Init(); + if (Encrypt) + RSA.ImportFromEncryptedPem(Pem, Secrets.PBE_PASSWORD.AsSpan()); + else + RSA.ImportFromPem(Pem); + } + + public override void FileNotFoundEvent() + { + var new_decryptor = new RSADecryptor() + { + Pem = RSAFactory.Static.CreateRSAPrivateKeyPem(), + Encrypt = Encrypt + }; + + _pem = new_decryptor.Pem; + + if (PemPath is not null) + Task.Run(async () => + { + await File.WriteAllTextAsync(_pem, PemPath); + }); + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/Cryptographer/RSAEncryptor.cs b/DigitalData.Core.Security/Cryptographer/RSAEncryptor.cs new file mode 100644 index 0000000..be21823 --- /dev/null +++ b/DigitalData.Core.Security/Cryptographer/RSAEncryptor.cs @@ -0,0 +1,37 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.Config; +using DigitalData.Core.Security.Extensions; + +namespace DigitalData.Core.Security.Cryptographer +{ + public class RSAEncryptor : RSACryptographer, IRSAEncryptor, IRSACryptographer + { + public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding); + + public string Encrypt(string data) => RSA.Encrypt(data.Base64ToByte(), Padding).BytesToString(); + + public bool Verify(string data, string signature) => Encrypt(data) == signature; + + public override void Init() + { + base.Init(); + RSA.ImportFromPem(base.Pem); + } + + public override void FileNotFoundEvent() + { + var new_decryptor = new RSADecryptor() + { + Pem = RSAFactory.Static.CreateRSAPrivateKeyPem() + }; + + _pem = new_decryptor.Encryptor.Pem; + + if (PemPath is not null) + Task.Run(async () => + { + await File.WriteAllTextAsync(_pem, PemPath); + }); + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/Cryptographer/RSAFactory.cs b/DigitalData.Core.Security/Cryptographer/RSAFactory.cs new file mode 100644 index 0000000..ee4cca9 --- /dev/null +++ b/DigitalData.Core.Security/Cryptographer/RSAFactory.cs @@ -0,0 +1,44 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.Config; +using Microsoft.Extensions.Options; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.Cryptographer +{ + public class RSAFactory : IRSAFactory where TRSAFactoryParams : RSAFactoryParams + { + private static readonly Lazy> LazyInstance = new(() => new(Options.Create(new()))); + + public static RSAFactory Static => LazyInstance.Value; + + protected readonly TRSAFactoryParams _params; + + public RSAFactory(IOptions options) => _params = options.Value; + + public string CreateRSAPrivateKeyPem(int? keySizeInBits = null) + => RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportRSAPrivateKeyPem(); + + public string CreateEncryptedPrivateKeyPem( + int? keySizeInBits = null, + string? password = null, + PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null, + HashAlgorithmName? hashAlgorithmName = null, + int? iterationCount = null) + { + password ??= _params.PbePassword; + + var pbeParameters = pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null + ? new PbeParameters( + pbeEncryptionAlgorithm ?? _params.PbeEncryptionAlgorithm, + hashAlgorithmName ?? _params.PbeHashAlgorithmName, + iterationCount ?? _params.PbeIterationCount) + : _params.PbeParameters; + + var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); + + var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey); + + return new string(pemChars); + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/DIExtensions.cs b/DigitalData.Core.Security/DIExtensions.cs index 699f1c6..652394d 100644 --- a/DigitalData.Core.Security/DIExtensions.cs +++ b/DigitalData.Core.Security/DIExtensions.cs @@ -1,16 +1,55 @@ using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.Config; +using DigitalData.Core.Security.Cryptographer; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DigitalData.Core.Security { public static class DIExtensions { - public static IServiceCollection AddSecurity(this IServiceCollection services) + public static JsonSerializerOptions AddCryptographerConverter(this JsonSerializerOptions options) { - services.TryAddScoped(); + if (!options.Converters.OfType().Any()) + options.Converters.Add(new HashAlgorithmNameConverter()); + if (!options.Converters.OfType().Any()) + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } + + private static IServiceCollection AddAsymCryptService(this IServiceCollection services) + where TAsymCryptParams : AsymCryptParams + { + services.TryAddScoped, AsymCryptService>(); return services; } + + public static IServiceCollection AddAsymCryptService(this IServiceCollection services, IConfigurationSection section) + where TAsymCryptParams : AsymCryptParams + => services.Configure(section).AddAsymCryptService(); + + public static IServiceCollection AddAsymCryptService(this IServiceCollection services, TAsymCryptParams param) + where TAsymCryptParams : AsymCryptParams + => services.AddSingleton(Options.Create(param)).AddAsymCryptService(); + + private static IServiceCollection AddRSAFactory(this IServiceCollection services) + where TRSAFactoryParams : RSAFactoryParams + { + services.TryAddScoped, RSAFactory>(); + return services; + } + + public static IServiceCollection AddRSAFactory(this IServiceCollection services, IConfigurationSection section) + where TRSAFactoryParams : RSAFactoryParams + => services.Configure(section).AddRSAFactory(); + + public static IServiceCollection AddRSAFactory(this IServiceCollection services, TRSAFactoryParams param) + where TRSAFactoryParams : RSAFactoryParams + => services.AddSingleton(Options.Create(param)).AddRSAFactory(); } } \ No newline at end of file diff --git a/DigitalData.Core.Security/DigitalData.Core.Security.csproj b/DigitalData.Core.Security/DigitalData.Core.Security.csproj index 3e2378e..c198a40 100644 --- a/DigitalData.Core.Security/DigitalData.Core.Security.csproj +++ b/DigitalData.Core.Security/DigitalData.Core.Security.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/DigitalData.Core.Security/HashAlgorithmNameConverter.cs b/DigitalData.Core.Security/HashAlgorithmNameConverter.cs new file mode 100644 index 0000000..8fe7a53 --- /dev/null +++ b/DigitalData.Core.Security/HashAlgorithmNameConverter.cs @@ -0,0 +1,13 @@ +using System.Security.Cryptography; +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace DigitalData.Core.Security +{ + public class HashAlgorithmNameConverter : JsonConverter + { + public override HashAlgorithmName Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new(reader.GetString() ?? string.Empty); + + public override void Write(Utf8JsonWriter writer, HashAlgorithmName value, JsonSerializerOptions options) => writer.WriteStringValue(value.Name); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSACryptographer.cs b/DigitalData.Core.Security/RSACryptographer.cs deleted file mode 100644 index 55161cc..0000000 --- a/DigitalData.Core.Security/RSACryptographer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using System.Security.Cryptography; - -namespace DigitalData.Core.Security -{ - public class RSACryptographer : IRSACryptographer - { - public required virtual string Pem { get; init; } - - public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; - - protected virtual RSA RSA { get; } = RSA.Create(); - - internal RSACryptographer() { } - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSADecryptor.cs b/DigitalData.Core.Security/RSADecryptor.cs deleted file mode 100644 index a527fe5..0000000 --- a/DigitalData.Core.Security/RSADecryptor.cs +++ /dev/null @@ -1,58 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using DigitalData.Core.Security.Extensions; -using System.Security.Cryptography; - -namespace DigitalData.Core.Security -{ - public class RSADecryptor : RSACryptographer, IRSADecryptor, IRSACryptographer - { - public (string Value, Version Version)? VersionedPassword - { - init - { - _password = value?.Value; - PasswordVersion = value?.Version; - } - } - - private string? _password; - - public Version? PasswordVersion { get; private init; } = null; - - public bool HasEncryptedPem => _password is not null; - - public bool IsEncrypted => _password is not null; - - private readonly Lazy _lazyEncryptor; - - public IRSAEncryptor Encryptor => _lazyEncryptor.Value; - - private readonly Lazy lazyRSA; - - protected override RSA RSA => lazyRSA.Value; - - public RSADecryptor() - { - _lazyEncryptor = new(() => new RSAEncryptor() - { - Pem = RSA.ExportRSAPublicKeyPem(), - Padding = Padding - }); - - lazyRSA = new(() => - { - var rsa = RSA.Create(); - if (_password is null) - RSA.ImportFromPem(Pem); - else - RSA.ImportFromEncryptedPem(Pem, _password.AsSpan()); - - return rsa; - }); - } - - public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding); - - public string Decrypt(string data) => RSA.Decrypt(data.Base64ToByte(), Padding).BytesToString(); - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAEncryptor.cs b/DigitalData.Core.Security/RSAEncryptor.cs deleted file mode 100644 index 7783902..0000000 --- a/DigitalData.Core.Security/RSAEncryptor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using DigitalData.Core.Security.Extensions; - -namespace DigitalData.Core.Security -{ - public class RSAEncryptor : RSACryptographer, IRSAEncryptor, IRSACryptographer - { - public override required string Pem - { - get => base.Pem; - init - { - RSA.ImportFromPem(base.Pem); - base.Pem = value; - } - } - - public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding); - - public string Encrypt(string data) => RSA.Encrypt(data.Base64ToByte(), Padding).BytesToString(); - - public bool Verify(string data, string signature) => Encrypt(data) == signature; - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAFactory.cs b/DigitalData.Core.Security/RSAFactory.cs deleted file mode 100644 index 9425dbb..0000000 --- a/DigitalData.Core.Security/RSAFactory.cs +++ /dev/null @@ -1,132 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using System.Security.Cryptography; -using System.Text; - -namespace DigitalData.Core.Security -{ - public class RSAFactory - { - private static readonly Lazy LazyInstance = new(() => new()); - - public static RSAFactory Static => LazyInstance.Value; - - public static readonly string DefaultEncryptedPrivateKeyFileTag = "enc-private"; - - public static readonly string DefaultPrivateKeyFileTag = "private"; - - public static readonly string DefaultPublicKeyFileTag = "public"; - - public static readonly IEnumerable KeyFileTags = new string[] { DefaultEncryptedPrivateKeyFileTag, DefaultPrivateKeyFileTag, DefaultPublicKeyFileTag }; - - private static readonly Lazy> 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 _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) - => RSA.Create(keySizeInBits ?? KeySizeInBits).ExportRSAPrivateKeyPem(); - - public string CreateEncryptedPrivateKeyPem( - int? keySizeInBits = null, - string? password = null, - PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null, - HashAlgorithmName? hashAlgorithmName = null, - int? iterationCount = null) - { - password ??= PbePassword; - - var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null) - ? new PbeParameters( - pbeEncryptionAlgorithm ?? PbeEncryptionAlgorithm, - hashAlgorithmName ?? PbeHashAlgorithmName, - iterationCount ?? PbeIterationCount) - : PbeParameters; - - var encryptedPrivateKey = RSA.Create(keySizeInBits ?? KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters); - - var pemChars = PemEncoding.Write(EncryptedPrivateKeyPemLabel, encryptedPrivateKey); - - return new string(pemChars); - } - - public async Task ReadRSADecryptorAsync(string path, Version? version = null, CancellationToken cancellationToken = default) - { - var pem = await File.ReadAllTextAsync(path, cancellationToken); - - (string Value, Version Version)? versionedPassword = null; - - if(version is not null) - { - if (version != Secrets.Version) - throw new InvalidOperationException($"The provided version {version} does not match the expected version {Secrets.Version}."); - - versionedPassword = (Secrets.PBE_PASSWORD, Secrets.Version); - } - - return new RSADecryptor() - { - Pem = pem, - VersionedPassword = versionedPassword - }; - } - } -} \ No newline at end of file