diff --git a/DigitalData.Core.Security/Config/CryptoFactoryParams.cs b/DigitalData.Core.Security/Config/CryptoFactoryParams.cs index 5bd60a7..2f8b063 100644 --- a/DigitalData.Core.Security/Config/CryptoFactoryParams.cs +++ b/DigitalData.Core.Security/Config/CryptoFactoryParams.cs @@ -1,4 +1,5 @@ -using DigitalData.Core.Security.RSAKey; +using DigitalData.Core.Security.RSAKey.Auth; +using DigitalData.Core.Security.RSAKey.Crypto; namespace DigitalData.Core.Security.Config { diff --git a/DigitalData.Core.Security/RSAKey/Auth/RSATokenDescriptor.cs b/DigitalData.Core.Security/RSAKey/Auth/RSATokenDescriptor.cs new file mode 100644 index 0000000..9e4c02e --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Auth/RSATokenDescriptor.cs @@ -0,0 +1,119 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.RSAKey.Base; +using Microsoft.IdentityModel.Tokens; + +namespace DigitalData.Core.Security.RSAKey.Auth; + +/// +/// Contains some information which used to create a security token. Designed to abstract +/// +public class RSATokenDescriptor : RSAPrivateKey, IAsymmetricTokenDescriptor +{ + private readonly Lazy _lazyTokenValidator; + + public IAsymmetricTokenValidator Validator => _lazyTokenValidator.Value; + + public required TimeSpan Lifetime { get; init; } + + #region SecurityTokenDescriptor Map + /// + /// Gets or sets the value of the 'audience' claim. + /// + public required string Audience { get; set; } + + /// + /// Defines the compression algorithm that will be used to compress the JWT token payload. + /// + public string CompressionAlgorithm { get; set; } + + /// + /// Gets or sets the used to create a encrypted security token. + /// + public EncryptingCredentials EncryptingCredentials { get; set; } + + /// + /// Gets or sets the value of the 'expiration' claim. This value should be in UTC. + /// The expiration time is the sum of DateTime.Now and LifeTime. + /// + public DateTime? Expires => DateTime.Now.AddTicks(Lifetime.Ticks); + + /// + /// Gets or sets the issuer of this . + /// + public required string Issuer { get; set; } + + /// + /// Gets or sets the time the security token was issued. This value should be in UTC. + /// + public DateTime? IssuedAt { get; set; } + + /// + /// Gets or sets the notbefore time for the security token. This value should be in UTC. + /// + public DateTime? NotBefore { get; set; } + + /// + /// Gets or sets the token type. + /// If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'. + /// If also contains 'typ' header claim value, it will override the TokenType provided here. + /// This value is used only for JWT tokens and not for SAML/SAML2 tokens + /// + public string TokenType { get; set; } + + /// + /// Gets or sets the which contains any custom header claims that need to be added to the JWT token header. + /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , + /// , and/or provided and SHOULD NOT be included in this dictionary as this + /// will result in an exception being thrown. + /// These claims are only added to the outer header (in case of a JWE). + /// + public IDictionary AdditionalHeaderClaims { get; set; } + + /// + /// Gets or sets the which contains any custom header claims that need to be added to the inner JWT token header. + /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , + /// , and/or provided and SHOULD NOT be included in this dictionary as this + /// will result in an exception being thrown. + /// + /// For JsonWebTokenHandler, these claims are merged with while adding to the inner JWT header. + /// + /// + public IDictionary AdditionalInnerHeaderClaims { get; set; } + + /// + /// Gets or sets the used to create a security token. + /// + public SigningCredentials SigningCredentials => _lazySigningCredentials.Value; + #endregion SecurityTokenDescriptor + + private readonly Lazy _lazyRsaSecurityKey; + + public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value; + + private readonly Lazy _lazySigningCredentials; + + /// + /// Specifies the signature algorithm to be applied to the . + /// Default is . + /// + public string SigningAlgorithm { get; init; } = SecurityAlgorithms.RsaSha256; + + /// + /// Optionally specifies the digest algorithm to be applied during the signing process for the . + /// If not provided, the default algorithm is used. + /// + public string? SigningDigest { get; init; } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public RSATokenDescriptor() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + _lazyTokenValidator = new(CreatePublicKey); + + _lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA)); + + _lazySigningCredentials = new(() => SigningDigest is null + ? new(SecurityKey, SigningAlgorithm) + : new(SecurityKey, SigningAlgorithm, SigningDigest)); + } +} diff --git a/DigitalData.Core.Security/RSAKey/Auth/RSATokenValidator.cs b/DigitalData.Core.Security/RSAKey/Auth/RSATokenValidator.cs new file mode 100644 index 0000000..cdf9471 --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Auth/RSATokenValidator.cs @@ -0,0 +1,17 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.RSAKey.Base; +using Microsoft.IdentityModel.Tokens; + +namespace DigitalData.Core.Security.RSAKey.Auth; + +public class RSATokenValidator : RSAPublicKey, IAsymmetricTokenValidator +{ + private readonly Lazy _lazyRsaSecurityKey; + + public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value; + + public RSATokenValidator() + { + _lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA)); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSAKeyBase.cs b/DigitalData.Core.Security/RSAKey/Base/RSAKeyBase.cs similarity index 59% rename from DigitalData.Core.Security/RSAKey/RSAKeyBase.cs rename to DigitalData.Core.Security/RSAKey/Base/RSAKeyBase.cs index a8f47f9..a3c0472 100644 --- a/DigitalData.Core.Security/RSAKey/RSAKeyBase.cs +++ b/DigitalData.Core.Security/RSAKey/Base/RSAKeyBase.cs @@ -1,16 +1,15 @@ using DigitalData.Core.Abstractions.Security; using System.Security.Cryptography; -namespace DigitalData.Core.Security.RSAKey +namespace DigitalData.Core.Security.RSAKey.Base; + +public class RSAKeyBase : IAsymmetricKey { - public class RSAKeyBase : IAsymmetricKey - { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public virtual string Content { get; init; } + public virtual string Content { get; init; } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public string? Id { get; init; } + public string? Id { get; init; } - protected virtual RSA RSA { get; } = RSA.Create(); - } + protected virtual RSA RSA { get; } = RSA.Create(); } \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/Base/RSAPrivateKey.cs b/DigitalData.Core.Security/RSAKey/Base/RSAPrivateKey.cs new file mode 100644 index 0000000..6305d03 --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Base/RSAPrivateKey.cs @@ -0,0 +1,54 @@ +using DigitalData.Core.Abstractions.Security; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.RSAKey.Base; + +public class RSAPrivateKey : RSAKeyBase, IAsymmetricPrivateKey, IAsymmetricKey +{ + private string? _pem; + + public override string Content + { +#pragma warning disable CS8603 // Possible null reference return. + get => _pem; +#pragma warning restore CS8603 // Possible null reference return. + init + { + _pem = value; + Init(); + } + } + + public bool IsPemNull => _pem is null; + + public bool IsEncrypted { get; init; } + + protected TPublicKey CreatePublicKey() where TPublicKey : RSAPublicKey, new() + => new() { Content = RSA.ExportRSAPublicKeyPem() }; + + private readonly Lazy _lazyPublicKey; + + public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value; + + public RSAPrivateKey() + { + _lazyPublicKey = new(CreatePublicKey); + } + + internal void SetPem(string pem) + { + _pem = pem; + Init(); + } + + private void Init() + { + if (string.IsNullOrEmpty(_pem)) + throw new InvalidOperationException($"The content of RSA private key is null or empty. Id: {Id}."); + + if (IsEncrypted) + RSA.ImportFromEncryptedPem(Content, Secrets.PBE_PASSWORD.AsSpan()); + else + RSA.ImportFromPem(Content); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/Base/RSAPublicKey.cs b/DigitalData.Core.Security/RSAKey/Base/RSAPublicKey.cs new file mode 100644 index 0000000..50329b8 --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Base/RSAPublicKey.cs @@ -0,0 +1,16 @@ +using DigitalData.Core.Abstractions.Security; + +namespace DigitalData.Core.Security.RSAKey.Base; + +public class RSAPublicKey : RSAKeyBase, IAsymmetricPublicKey, IAsymmetricKey +{ + public override string Content + { + get => base.Content; + init + { + base.Content = value; + RSA.ImportFromPem(value); + } + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/Crypto/RSADecryptor.cs b/DigitalData.Core.Security/RSAKey/Crypto/RSADecryptor.cs new file mode 100644 index 0000000..4fea226 --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Crypto/RSADecryptor.cs @@ -0,0 +1,33 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.RSAKey.Base; +using System.Reflection; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.RSAKey.Crypto; + +public class RSADecryptor : RSAPrivateKey, IAsymmetricDecryptor +{ + public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; + + // TODO: add as json converter to IConfigurIConfiguration.Config + public string PaddingName + { + get => Padding.ToString(); + init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found."); + } + + public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding); + + private readonly Lazy _lazyEncryptor; + + public IAsymmetricEncryptor Encryptor => _lazyEncryptor.Value; + + public RSADecryptor() + { + _lazyEncryptor = new(() => new RSAEncryptor() + { + Content = RSA.ExportRSAPublicKeyPem(), + Padding = Padding + }); + } +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/Crypto/RSAEncryptor.cs b/DigitalData.Core.Security/RSAKey/Crypto/RSAEncryptor.cs new file mode 100644 index 0000000..c9b0d43 --- /dev/null +++ b/DigitalData.Core.Security/RSAKey/Crypto/RSAEncryptor.cs @@ -0,0 +1,20 @@ +using DigitalData.Core.Abstractions.Security; +using DigitalData.Core.Security.RSAKey.Base; +using System.Reflection; +using System.Security.Cryptography; + +namespace DigitalData.Core.Security.RSAKey.Crypto; + +public class RSAEncryptor : RSAPublicKey, IAsymmetricEncryptor +{ + public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; + + // TODO: add as json converter to IConfigurIConfiguration.Config + public string PaddingName + { + get => Padding.ToString(); + init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found."); + } + + public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding); +} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSADecryptor.cs b/DigitalData.Core.Security/RSAKey/RSADecryptor.cs deleted file mode 100644 index ba35d43..0000000 --- a/DigitalData.Core.Security/RSAKey/RSADecryptor.cs +++ /dev/null @@ -1,33 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using System.Reflection; -using System.Security.Cryptography; - -namespace DigitalData.Core.Security.RSAKey -{ - public class RSADecryptor : RSAPrivateKey, IAsymmetricDecryptor - { - public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; - - // TODO: add as json converter to IConfigurIConfiguration.Config - public string PaddingName - { - get => Padding.ToString(); - init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found."); - } - - public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding); - - private readonly Lazy _lazyEncryptor; - - public IAsymmetricEncryptor Encryptor => _lazyEncryptor.Value; - - public RSADecryptor() - { - _lazyEncryptor = new(() => new RSAEncryptor() - { - Content = RSA.ExportRSAPublicKeyPem(), - Padding = Padding - }); - } - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSAEncryptor.cs b/DigitalData.Core.Security/RSAKey/RSAEncryptor.cs deleted file mode 100644 index 8fb87e8..0000000 --- a/DigitalData.Core.Security/RSAKey/RSAEncryptor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using System.Reflection; -using System.Security.Cryptography; - -namespace DigitalData.Core.Security.RSAKey -{ - public class RSAEncryptor : RSAPublicKey, IAsymmetricEncryptor - { - public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256; - - // TODO: add as json converter to IConfigurIConfiguration.Config - public string PaddingName - { - get => Padding.ToString(); - init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found."); - } - - public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding); - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSAPrivateKey.cs b/DigitalData.Core.Security/RSAKey/RSAPrivateKey.cs deleted file mode 100644 index 67d701a..0000000 --- a/DigitalData.Core.Security/RSAKey/RSAPrivateKey.cs +++ /dev/null @@ -1,55 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using System.Security.Cryptography; - -namespace DigitalData.Core.Security.RSAKey -{ - public class RSAPrivateKey : RSAKeyBase, IAsymmetricPrivateKey, IAsymmetricKey - { - private string? _pem; - - public override string Content - { -#pragma warning disable CS8603 // Possible null reference return. - get => _pem; -#pragma warning restore CS8603 // Possible null reference return. - init - { - _pem = value; - Init(); - } - } - - public bool IsPemNull => _pem is null; - - public bool IsEncrypted { get; init; } - - protected TPublicKey CreatePublicKey() where TPublicKey : RSAPublicKey, new() - => new() { Content = RSA.ExportRSAPublicKeyPem() }; - - private readonly Lazy _lazyPublicKey; - - public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value; - - public RSAPrivateKey() - { - _lazyPublicKey = new(CreatePublicKey); - } - - internal void SetPem(string pem) - { - _pem = pem; - Init(); - } - - private void Init() - { - if (string.IsNullOrEmpty(_pem)) - throw new InvalidOperationException ($"The content of RSA private key is null or empty. Id: {Id}."); - - if (IsEncrypted) - RSA.ImportFromEncryptedPem(Content, Secrets.PBE_PASSWORD.AsSpan()); - else - RSA.ImportFromPem(Content); - } - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSAPublicKey.cs b/DigitalData.Core.Security/RSAKey/RSAPublicKey.cs deleted file mode 100644 index 96a7210..0000000 --- a/DigitalData.Core.Security/RSAKey/RSAPublicKey.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DigitalData.Core.Abstractions.Security; - -namespace DigitalData.Core.Security.RSAKey -{ - public class RSAPublicKey : RSAKeyBase, IAsymmetricPublicKey, IAsymmetricKey - { - public override string Content - { - get => base.Content; - init - { - base.Content = value; - RSA.ImportFromPem(value); - } - } - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/RSAKey/RSATokenDescriptor.cs b/DigitalData.Core.Security/RSAKey/RSATokenDescriptor.cs deleted file mode 100644 index 44ce805..0000000 --- a/DigitalData.Core.Security/RSAKey/RSATokenDescriptor.cs +++ /dev/null @@ -1,119 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using Microsoft.IdentityModel.Tokens; - -namespace DigitalData.Core.Security.RSAKey -{ - /// - /// Contains some information which used to create a security token. Designed to abstract - /// - public class RSATokenDescriptor : RSAPrivateKey, IAsymmetricTokenDescriptor - { - private readonly Lazy _lazyTokenValidator; - - public IAsymmetricTokenValidator Validator => _lazyTokenValidator.Value; - - public required TimeSpan Lifetime { get; init; } - - #region SecurityTokenDescriptor Map - /// - /// Gets or sets the value of the 'audience' claim. - /// - public required string Audience { get; set; } - - /// - /// Defines the compression algorithm that will be used to compress the JWT token payload. - /// - public string CompressionAlgorithm { get; set; } - - /// - /// Gets or sets the used to create a encrypted security token. - /// - public EncryptingCredentials EncryptingCredentials { get; set; } - - /// - /// Gets or sets the value of the 'expiration' claim. This value should be in UTC. - /// The expiration time is the sum of DateTime.Now and LifeTime. - /// - public DateTime? Expires => DateTime.Now.AddTicks(Lifetime.Ticks); - - /// - /// Gets or sets the issuer of this . - /// - public required string Issuer { get; set; } - - /// - /// Gets or sets the time the security token was issued. This value should be in UTC. - /// - public DateTime? IssuedAt { get; set; } - - /// - /// Gets or sets the notbefore time for the security token. This value should be in UTC. - /// - public DateTime? NotBefore { get; set; } - - /// - /// Gets or sets the token type. - /// If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'. - /// If also contains 'typ' header claim value, it will override the TokenType provided here. - /// This value is used only for JWT tokens and not for SAML/SAML2 tokens - /// - public string TokenType { get; set; } - - /// - /// Gets or sets the which contains any custom header claims that need to be added to the JWT token header. - /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , - /// , and/or provided and SHOULD NOT be included in this dictionary as this - /// will result in an exception being thrown. - /// These claims are only added to the outer header (in case of a JWE). - /// - public IDictionary AdditionalHeaderClaims { get; set; } - - /// - /// Gets or sets the which contains any custom header claims that need to be added to the inner JWT token header. - /// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the , - /// , and/or provided and SHOULD NOT be included in this dictionary as this - /// will result in an exception being thrown. - /// - /// For JsonWebTokenHandler, these claims are merged with while adding to the inner JWT header. - /// - /// - public IDictionary AdditionalInnerHeaderClaims { get; set; } - - /// - /// Gets or sets the used to create a security token. - /// - public SigningCredentials SigningCredentials => _lazySigningCredentials.Value; - #endregion SecurityTokenDescriptor - - private readonly Lazy _lazyRsaSecurityKey; - - public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value; - - private readonly Lazy _lazySigningCredentials; - - /// - /// Specifies the signature algorithm to be applied to the . - /// Default is . - /// - public string SigningAlgorithm { get; init; } = SecurityAlgorithms.RsaSha256; - - /// - /// Optionally specifies the digest algorithm to be applied during the signing process for the . - /// If not provided, the default algorithm is used. - /// - public string? SigningDigest { get; init; } - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public RSATokenDescriptor() -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - { - _lazyTokenValidator = new(CreatePublicKey); - - _lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA)); - - _lazySigningCredentials = new(() => SigningDigest is null - ? new(SecurityKey, SigningAlgorithm) - : new(SecurityKey, SigningAlgorithm, SigningDigest)); - } - } -} diff --git a/DigitalData.Core.Security/RSAKey/RSATokenValidator.cs b/DigitalData.Core.Security/RSAKey/RSATokenValidator.cs deleted file mode 100644 index 9786dd8..0000000 --- a/DigitalData.Core.Security/RSAKey/RSATokenValidator.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DigitalData.Core.Abstractions.Security; -using Microsoft.IdentityModel.Tokens; - -namespace DigitalData.Core.Security.RSAKey -{ - public class RSATokenValidator : RSAPublicKey, IAsymmetricTokenValidator - { - private readonly Lazy _lazyRsaSecurityKey; - - public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value; - - public RSATokenValidator() - { - _lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA)); - } - } -} \ No newline at end of file diff --git a/DigitalData.Core.Security/Services/PemFileInitalizer.cs b/DigitalData.Core.Security/Services/PemFileInitalizer.cs index 98f062b..cc575b7 100644 --- a/DigitalData.Core.Security/Services/PemFileInitalizer.cs +++ b/DigitalData.Core.Security/Services/PemFileInitalizer.cs @@ -1,5 +1,6 @@ using DigitalData.Core.Security.Config; -using DigitalData.Core.Security.RSAKey; +using DigitalData.Core.Security.RSAKey.Auth; +using DigitalData.Core.Security.RSAKey.Base; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/DigitalData.Core.Security/Services/RSAFactory.cs b/DigitalData.Core.Security/Services/RSAFactory.cs index 27b50b4..7a021a1 100644 --- a/DigitalData.Core.Security/Services/RSAFactory.cs +++ b/DigitalData.Core.Security/Services/RSAFactory.cs @@ -1,6 +1,6 @@ using DigitalData.Core.Abstractions.Security; using DigitalData.Core.Security.Config; -using DigitalData.Core.Security.RSAKey; +using DigitalData.Core.Security.RSAKey.Crypto; using System.Security.Cryptography; namespace DigitalData.Core.Security.Services;