75 Commits

Author SHA1 Message Date
Developer 02
ad8d15314f chore(Security): Description fixed 2025-01-14 20:20:25 +01:00
Developer 02
f1efbae6a4 feat(Sicherheit): Paketdefinition hinzugefügt. 2025-01-14 20:14:59 +01:00
Developer 02
051567aa0a refactor(Abstraktionen): hochgestuft auf 3.1 2025-01-14 19:51:58 +01:00
Developer 02
287871ddc6 refactor(TokenDescriptor): ApiRoute-Eigenschaft entfernt 2025-01-14 19:45:44 +01:00
Developer 02
a0ad8d732d refactor(RSATokenDescriptor): Lifetime-Eigenschaft hinzugefügt 2025-01-14 17:04:54 +01:00
Developer 02
3ad08e2a86 feat(AsymmetricTokenValidator): SecurityKey-Eigenschaft hinzugefügt. 2025-01-13 09:52:52 +01:00
Developer 02
b90a52412c feat(IAsymmetricTokenDescriptor): Methode Validator.get mit Lazy Loading hinzugefügt. 2025-01-10 23:32:53 +01:00
Developer 02
39091ff5cf refactor(IAsymmetricKey): Id nullbar gemacht.
- Benennung der perm-Datei aktualisiert.
2025-01-10 15:47:02 +01:00
Developer 02
22040cf1e7 fix(CryptoFactoryParams): Die Zuweisung des Standard-Depotnamens wurde unter das Ereignis afterCreate verschoben. 2025-01-09 23:59:58 +01:00
Developer 02
af4b7d5438 fix(CryptoFactoryParams): Aktualisiert, um den ersten Decryptor zu verwenden, um die Standard-Depot-ID zu setzen. 2025-01-09 23:39:42 +01:00
Developer 02
211064d44e refactor(RSATokenDescriptor): Standard-Id mit Issuer und Audience hinzugefügt.
- Issuer und Audience erforderlich gemacht.
2025-01-09 23:25:17 +01:00
Developer 02
66e3c771dd refactor(IAsymmetricKey): Die Implementierung von IUniqueSecurityContext wurde entfernt und stattdessen die Eigenschaft Id hinzugefügt.
- Aktualisierte verwandte Implementierungen.
2025-01-09 22:57:04 +01:00
Developer 02
97c4f7bf8f refactor(CryptographParams): umbenannt in CryptoFactoryParams 2025-01-09 22:33:56 +01:00
Developer 02
5981ba7a8d refactor(Abstractions.Security): Unnötige public Schlüsselwörter in Schnittstellen entfernt. 2025-01-09 22:27:33 +01:00
Developer 02
21e164ceb7 refactor(IAsymmetricKey): Unnötige Initor-Methoden entfernt. 2025-01-09 22:15:45 +01:00
Developer 02
1875bf46fa feat(DIExtensions): Umbenennung der AddCryptograph-Methoden in AddCryptoFactory 2025-01-09 20:38:15 +01:00
Developer 02
7f9459f6cf feat(SecurityExtensions): Erforderliche WriteToken-Methoden über Erweiterungen anstelle der Schnittstellenimplementierung hinzugefügt 2025-01-09 20:30:38 +01:00
Developer 02
079f0c69c7 fix(Kryptograph): Umbenannt in Krypto-Fabrik. 2025-01-09 20:10:45 +01:00
Developer 02
d98b3f2867 fix(JwtSignatureHandler): Aktualisierte Methoden, um IAsymmetricTokenDescriptor verwenden zu können 2025-01-09 19:54:05 +01:00
Developer 02
3761c13dba refactor(ICryptograph): Eigenschaft „TokenDescriptors“ hinzugefügt. 2025-01-09 19:34:29 +01:00
Developer 02
8acbbaeb2e refactor(IAsymmetricTokenDescriptor): Erforderliche Proportionen für SecurityTokenDescriptor-Zuordnung hinzugefügt. 2025-01-09 19:16:56 +01:00
Developer 02
60e1ec78b3 refactor(Kryptograph): Entfernte Verschlüsselungen. 2025-01-09 18:52:59 +01:00
Developer 02
e623575fe8 refactor(Kryptograph): Aktualisiert, um TokenDescriptors aus CryptographParams hinzuzufügen. 2025-01-09 18:33:51 +01:00
Developer 02
60ae8de550 refactor(CryptographParams): Aktualisierung, um TokenDeskriptoren mit Decryptoren zu initialisieren 2025-01-09 18:28:20 +01:00
Developer 02
87ad45f42a refactor(RSATokenDescriptor): Die Klasse RSA Token Descriptor wurde erstellt und die Funktionen wurden dorthin verschoben, um eine einfache und saubere Konfiguration zu ermöglichen. 2025-01-09 17:59:13 +01:00
Developer 02
2557525f06 refactor(PrivateKeyTokenDescriptor): superwarning zur Deaktivierung der Warnung vor Null 2025-01-09 11:08:06 +01:00
Developer 02
7a938f0379 refactor(Privatekey): Die Klasse encryptor wurde erstellt und die Verschlüsselungsfunktionen wurden zur einfachen und sauberen Konfiguration dorthin verschoben. 2025-01-08 20:03:25 +01:00
Developer 02
9f0facc487 refactor(Privatekey): Die Klasse decryptor wurde erstellt und die Verschlüsselungsfunktionen für eine einfache und saubere Konfiguration dorthin verschoben. 2025-01-08 18:45:36 +01:00
Developer 02
608d266d1c refactor(IAsymmetricKey): Umwandlung von RsaSecurityKey in SecurityKey zur besseren Abstraktion.
- RSAEncryptionPadding entfernen
 - Pem als Inhalt Content
2025-01-07 16:53:05 +01:00
Developer 02
34e14fd2f5 refactor(RSATokenDescriptor): In die Abstraktionsschicht verschoben und in PrivateKeyTokenDescriptor umbenannt 2025-01-07 16:34:19 +01:00
Developer 02
dc45cf2c08 refactor(JwtSignatureHandler): Aktualisiert, um RSAPrivateKey anstelle des Deskriptors zu verwenden 2025-01-07 13:55:30 +01:00
Developer 02
09a31b5a3d refactor(TokenDescription): Nach RSAKey verschoben, um unter RSAPrivateKey definiert werden zu können 2025-01-07 13:22:45 +01:00
Developer 02
b5cecac745 refactor(DIExtensions): Umbenennung der Methode AddAsymCryptHandler in AddCryptograph 2025-01-07 12:19:42 +01:00
Developer 02
0f4b5430a3 refactor(AsymCryptParams): Umbenennen in CryptographParams. 2025-01-07 12:12:50 +01:00
Developer 02
7f2d2dadfa refactor(DigitalData.Core.Security): Umbenennung des Unternamensraums von Cryptographer in RSAKey 2025-01-07 12:09:34 +01:00
Developer 02
ac0b6f739b refactor(AsymCryptHandler): Renamed to Cryptograph 2025-01-07 12:03:01 +01:00
Developer 02
d9d61368e3 refactor(IAsymCryptHandler): Umbenannt in ICryptograph 2025-01-07 12:01:39 +01:00
Developer 02
e8c98115b6 refactor(IRSAFactory): umbenannt in IAsymmetricKey 2025-01-07 11:48:27 +01:00
Developer 02
09dae1b1ac refactor(IRSAEncryptor): umbenannt in RSAPublicKey 2025-01-07 11:39:12 +01:00
Developer 02
9aafc9e467 refactor(IRSAEncryptor): umbenannt in IAsymmetricPublicKey 2025-01-07 11:33:53 +01:00
Developer 02
4ce738957d refactor(RSADecryptor): umbenennen in RSAPrivateKey 2025-01-07 11:20:24 +01:00
Developer 02
5e1bf16b6d refactor(IRSADecryptor): Umbenennung in IAsymmetricPrivateKey 2025-01-07 11:16:12 +01:00
Developer 02
4f96d271f3 refactor(IRSACryptographer): Umbenennung in IAsymmetricKey 2025-01-07 11:03:14 +01:00
Developer 02
14485af448 fix(DIExtensions): Umbenennung der Methode AddAsymCryptService in AddAsymCryptHandler 2025-01-07 10:38:10 +01:00
Developer 02
c27e21a702 fix(JwtSignatureHandler): The nullability of TokenParams has been removed. 2025-01-07 10:36:17 +01:00
Developer 02
4874079b69 fix: TokenParams-Kaliber erstellt, um Token-Beschreibungen über IOptions zu konfigurieren 2025-01-07 10:21:25 +01:00
Developer 02
15e909064f feat(IJwtSignatureHandler): Unterstützung für die Erstellung von Token durch den Routenwert der Tokenbeschreibung hinzugefügt. 2025-01-07 09:35:09 +01:00
Developer 02
d17c5ca6cd feat(JwtSignatureHandler): Unterstützung für die Erstellung von Token durch den Routenwert der Tokenbeschreibung hinzugefügt. 2025-01-07 09:30:33 +01:00
Developer 02
592b949f57 feat: Unterstützung für Token-Beschreibungen im JwtSignatureHandler hinzugefügt
- Methoden eingeführt, um Token-Beschreibungen im DI-Container zu konfigurieren und zu registrieren.
- Überladungen zu `AddJwtSignatureHandler` hinzugefügt, um sowohl konfigurationsbasierte als auch Inline-Token-Beschreibungen zu unterstützen.
2025-01-06 16:32:32 +01:00
Developer 02
8850ac4ac9 refactor(DIExtensions): Methode AddJwtSignatureHandler hinzugefügt 2025-01-06 16:08:45 +01:00
Developer 02
8ccf6f31ae refactor(JwtSignatureHandler): Umbenennung von CreateAndWriteToken in WriteToken 2025-01-06 15:35:51 +01:00
Developer 02
9875d023e3 Revert "refactor(TokenDescription): In die Abstraktionsschicht verschoben."
This reverts commit 2cf0eb3977.
2025-01-06 15:31:26 +01:00
Developer 02
62afba7c23 refactor: Entfernen redundanter Methodenüberladungen in IJwtSignatureHandler
- Entfernte doppelte Methodensignaturen für CreateToken und CreateAndWriteToken, die TokenDescription akzeptieren.
- Vereinfachte das Interface, um sich auf die wesentlichen Methoden für die Token-Erstellung und -Schreibung zu konzentrieren.
2025-01-06 15:26:31 +01:00
Developer 02
1d4882cfbc refactor(JwtSignatureService): umbenannt in JwtSignatureHandler.
- seine Schnittstelle umbenannt
2025-01-06 15:15:46 +01:00
Developer 02
275b9ec858 feat(IJwtSignatureService): Erstellt und implementiert 2025-01-06 15:06:31 +01:00
Developer 02
2cf0eb3977 refactor(TokenDescription): In die Abstraktionsschicht verschoben. 2025-01-06 14:39:45 +01:00
Developer 02
4ab5393deb refactor(JwtSignatureService): Injizieren von IAsymCryptHandler anstelle von AsymCryptHandler zur Abstraktion. 2025-01-06 13:13:57 +01:00
Developer 02
a2dc59d5ef refactor(AsymCryptParams): Entfernte TokenDescriptions 2025-01-06 13:11:05 +01:00
Developer 02
ed041bf7cb refactor(JwtSignatureService): CreateToken und CreateAndWriteToken Methoden mit Issuer und Audience Inputs hinzugefügt 2025-01-06 12:13:41 +01:00
Developer 02
c70327e7f4 refactor(IRSADecryptor): Methode CreateSigningCredentials hinzugefügt 2025-01-06 11:32:17 +01:00
Developer 02
0a3ce89c0d refactor(IRSACryptographer): RsaSecurityKey-Eigenschaft hinzugefügt 2025-01-06 11:29:19 +01:00
Developer 02
389d64c25d refactor(AsymCryptService): umbenannt in AsymCryptHandler 2025-01-06 10:44:03 +01:00
Developer 02
a3931414e3 refactor(AsymCryptService): Indexer und IEnumerable-Implementierung zur Vereinfachung entfernt 2024-12-21 10:10:50 +01:00
Developer 02
0dd897625a feat(SecurityExtensions): Die Match- und TryMatch-Erweiterungsmethoden wurden hinzugefügt, um die Funktionalität in SecurityExtensions zu erweitern. Diese Methoden vereinfachen das Matching von IUniqueSecurityContext durch direkte Verwendung eines Lookup-Kontexts. 2024-12-20 23:34:21 +01:00
Developer 02
351a6732cf refactor(SecurityExtensions): Nullbarkeit des out-Wertes in der TryGet-Methode wurde entfernt.
- Warnung ist deaktiviert.
 - Null-Prüfung in der Verwendung entfernt.
2024-12-20 22:59:21 +01:00
Developer 02
5a1808c6a6 refactor(CryptographerExtensions): Aktualisiert zur Verwendung von IUniqueSecurityContext anstelle von IRSACryptographer.
- umbenannt in SecurityExtensions.
2024-12-20 18:54:33 +01:00
Developer 02
50c42e9cdd rename(ISecurityIdentifier): Umbenannt in IUniqueSecurityContext und Kommentare zur Dokumentation hinzugefügt. 2024-12-20 14:42:07 +01:00
Developer 02
ec126be2aa feat(ISecurityIdentifier): Implementiert in IRSACryptographer und TokenDescription 2024-12-20 14:35:10 +01:00
Developer 02
9953bbd2ef feat(ISecurityIdentifier): Wird als Basisbezeichner in den Sicherheitsklassen erstellt. 2024-12-20 14:11:16 +01:00
Developer 02
dbecfa92f4 refactor(Extension): Zusammenlegung der Erweiterungsklassen zur Vereinfachung.
- Methoden und Klasse intern gemacht, um Komplexität zu vermeiden.
2024-12-20 10:48:37 +01:00
Developer 02
e007f15bce refactor(JwtSignatureService): removed primary constructor because this feature is not available in C# 11.0.
- Added GlobalSuppressions to avoid editor to offer this.
2024-12-20 10:40:35 +01:00
Developer 02
79dffef528 Refactor: Entfernung der generischen IRSAFactory und IAsymCryptService.
- RSAFactory und AsymCryptService aktualisiert.
 - Aktualisierte DI-Erweiterungen
2024-12-20 10:30:12 +01:00
Developer 02
af478e974c refactor(TokenDescriptorProvider): entfernt und eine Mapper-Erweiterungsmethode mit derselben Funktionalität zur Vereinfachung hinzugefügt. 2024-12-20 09:44:04 +01:00
Developer 02
435c91955c refactor(JwtSignatureService): verwendete primäre Struktur. 2024-12-20 09:29:49 +01:00
Developer 02
4142d2d948 refactor(TokenDescriptorProvider): verwendete Primärstruktur. 2024-12-20 09:27:08 +01:00
42 changed files with 629 additions and 506 deletions

View File

@@ -9,7 +9,7 @@
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>DigitalData.Core.Abstractions</Product>
<Description>This package contains abstractions for the DigitalData.Core.Abstractions library, developed according to the principles of Clean Architecture. It promotes separation of concerns and enables independent core logic.</Description>
<Description>This package contains abstractions for the DigitalData.Core library, developed according to the principles of Clean Architecture. It promotes separation of concerns and enables independent core logic.</Description>
<PackageTags>digital data core abstractions clean architecture</PackageTags>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Copyright>Copyright 2024</Copyright>
@@ -17,9 +17,9 @@
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackAsTool>False</PackAsTool>
<PackageIcon>core_icon.png</PackageIcon>
<Version>3.0.0</Version>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.0.0</FileVersion>
<Version>3.1.0</Version>
<AssemblyVersion>3.1.0</AssemblyVersion>
<FileVersion>3.1.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,21 +0,0 @@
namespace DigitalData.Core.Abstractions.Security
{
public static class CryptographerExtensions
{
public static IEnumerable<TRSACryptographer> GetByIssuer<TRSACryptographer>(this IEnumerable<TRSACryptographer> cryptographers, string issuer) where TRSACryptographer: IRSACryptographer
=> cryptographers.Where(c => c.Issuer == issuer);
public static IEnumerable<TRSACryptographer> GetByAudience<TRSACryptographer>(this IEnumerable<TRSACryptographer> cryptographers, string audience) where TRSACryptographer : IRSACryptographer
=> cryptographers.Where(c => c.Audience == audience);
public static TRSACryptographer Get<TRSACryptographer>(this IEnumerable<TRSACryptographer> 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<TRSACryptographer>(this IEnumerable<TRSACryptographer> 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;
}
}
}

View File

@@ -1,15 +0,0 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymCryptService : IRSAFactory, IEnumerable<IRSADecryptor>
{
IEnumerable<IRSADecryptor> Decryptors { get; }
IRSADecryptor Vault { get; }
IRSADecryptor this[string key] { get; }
IEnumerable<IRSAEncryptor> Encryptors { get; }
}
public interface IAsymCryptService<TParams> : IAsymCryptService, IRSAFactory<TParams> { }
}

View File

@@ -0,0 +1,9 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricDecryptor : IAsymmetricPrivateKey
{
byte[] Decrypt(byte[] data);
IAsymmetricEncryptor Encryptor { get; }
}
}

View File

@@ -0,0 +1,7 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricEncryptor : IAsymmetricPublicKey
{
byte[] Encrypt(byte[] data);
}
}

View File

@@ -0,0 +1,9 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricKey
{
string? Id { get; }
string Content { get; }
}
}

View File

@@ -2,7 +2,7 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IRSAFactory
public interface IAsymmetricKeyFactory
{
string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false);
@@ -18,8 +18,6 @@ namespace DigitalData.Core.Abstractions.Security
int? keySizeInBits = null,
string? password = null);
IRSADecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null);
IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null);
}
public interface IRSAFactory<TParams> : IRSAFactory { }
}

View File

@@ -0,0 +1,9 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricPrivateKey : IAsymmetricKey
{
bool IsEncrypted { get; }
IAsymmetricPublicKey PublicKey { get; }
}
}

View File

@@ -0,0 +1,6 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricPublicKey : IAsymmetricKey
{
}
}

View File

@@ -0,0 +1,74 @@
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Abstractions.Security
{
/// <summary>
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
/// </summary>
public interface IAsymmetricTokenDescriptor : IAsymmetricPrivateKey, IUniqueSecurityContext
{
IAsymmetricTokenValidator Validator { get; }
TimeSpan Lifetime { get; init; }
#region SecurityTokenDescriptor Map
/// <summary>
/// Defines the compression algorithm that will be used to compress the JWT token payload.
/// </summary>
string CompressionAlgorithm { get; }
/// <summary>
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
/// </summary>
EncryptingCredentials EncryptingCredentials { get; }
/// <summary>
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
/// </summary>
DateTime? Expires { get; }
/// <summary>
/// Gets or sets the time the security token was issued. This value should be in UTC.
/// </summary>
DateTime? IssuedAt { get; }
/// <summary>
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
/// </summary>
DateTime? NotBefore { get; }
/// <summary>
/// Gets or sets the token type.
/// <remarks> 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 <see cref="AdditionalHeaderClaims"/> 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</remarks>
/// </summary>
string TokenType { get; }
/// <summary>
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> 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 <see cref="SigningCredentials"/>,
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
/// will result in an exception being thrown.
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
/// </summary>
IDictionary<string, object> AdditionalHeaderClaims { get; }
/// <summary>
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> 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 <see cref="SigningCredentials"/>,
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
/// will result in an exception being thrown.
/// <remarks>
/// For JsonWebTokenHandler, these claims are merged with <see cref="AdditionalHeaderClaims"/> while adding to the inner JWT header.
/// </remarks>
/// </summary>
IDictionary<string, object> AdditionalInnerHeaderClaims { get; }
/// <summary>
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
/// </summary>
SigningCredentials SigningCredentials { get; }
#endregion SecurityTokenDescriptor
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Abstractions.Security
{
public interface IAsymmetricTokenValidator : IAsymmetricPublicKey
{
SecurityKey SecurityKey { get; }
}
}

View File

@@ -0,0 +1,11 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface ICryptoFactory : IAsymmetricKeyFactory
{
IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
IAsymmetricDecryptor VaultDecryptor { get; }
IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; }
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Abstractions.Security
{
public interface IJwtSignatureHandler<TPrincipal>
{
SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor);
SecurityToken CreateToken(TPrincipal subject, IAsymmetricTokenDescriptor descriptor);
SecurityToken CreateToken(TPrincipal subject, string issuer, string audience);
string WriteToken(SecurityToken token);
}
}

View File

@@ -1,15 +0,0 @@
using System.Security.Cryptography;
namespace DigitalData.Core.Abstractions.Security
{
public interface IRSACryptographer
{
public string Pem { get; init; }
public RSAEncryptionPadding Padding { get; init; }
public string Issuer { get; init; }
public string Audience { get; init; }
}
}

View File

@@ -1,13 +0,0 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IRSADecryptor : IRSACryptographer
{
public bool IsEncrypted { get; init; }
IRSAEncryptor Encryptor { get; }
byte[] Decrypt(byte[] data);
string Decrypt(string data);
}
}

View File

@@ -1,11 +0,0 @@
namespace DigitalData.Core.Abstractions.Security
{
public interface IRSAEncryptor : IRSACryptographer
{
public byte[] Encrypt(byte[] data);
public string Encrypt(string data);
public bool Verify(string data, string signature) => Encrypt(data) == signature;
}
}

View File

@@ -0,0 +1,24 @@
namespace DigitalData.Core.Abstractions.Security
{
/// <summary>
/// Represents a unique security context that identifies an issuer and an audience.
/// </summary>
public interface IUniqueSecurityContext
{
/// <summary>
/// Gets the issuer identifier for this security context.
/// </summary>
/// <remarks>
/// The issuer typically represents the entity that issues a token or a cryptographic key.
/// </remarks>
string Issuer { get; }
/// <summary>
/// Gets the audience identifier for this security context.
/// </summary>
/// <remarks>
/// The audience typically represents the intended recipient or target of a token or cryptographic operation.
/// </remarks>
string Audience { get; }
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace DigitalData.Core.Abstractions.Security
{
public static class SecurityExtensions
{
#region Unique Security Context
public static IEnumerable<TUniqueSecurityContext> GetByIssuer<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer) where TUniqueSecurityContext: IUniqueSecurityContext
=> contextes.Where(c => c.Issuer == issuer);
public static IEnumerable<TUniqueSecurityContext> GetByAudience<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
=> contextes.Where(c => c.Audience == audience);
public static TUniqueSecurityContext Get<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
=> contextes.Where(c => c.Issuer == issuer && c.Audience == audience).SingleOrDefault()
?? throw new InvalidOperationException($"Exactly one {typeof(TUniqueSecurityContext).Name} must exist with Issuer: '{issuer}' and Audience: '{audience}'.");
public static bool TryGet<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
{
#pragma warning disable CS8601 // Possible null reference assignment.
context = contextes.SingleOrDefault(c => c.Issuer == issuer && c.Audience == audience);
#pragma warning restore CS8601 // Possible null reference assignment.
return context is not null;
}
public static TUniqueSecurityContext Match<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext) where TUniqueSecurityContext : IUniqueSecurityContext
=> contextes.Get(lookupContext.Issuer, lookupContext.Audience);
public static bool TryMatch<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
=> contextes.TryGet(lookupContext.Issuer, lookupContext.Audience, out context);
#endregion Unique Security Context
#region De/serilization
internal static byte[] Base64ToByte(this string base64String) => Convert.FromBase64String(base64String);
internal static string BytesToString(this byte[] bytes) => Encoding.UTF8.GetString(bytes);
internal static string ToBase64String(this byte[] bytes) => Convert.ToBase64String(bytes);
internal static byte[] ToBytes(this string str) => System.Text.Encoding.UTF8.GetBytes(str);
public static string Decrypt(this IAsymmetricDecryptor decryptor, string data) => decryptor
.Decrypt(data.Base64ToByte()).BytesToString();
#endregion De/serilization
#region Asymmetric Encryptor
public static string Encrypt(this IAsymmetricEncryptor encryptor, string data) => encryptor.Encrypt(data.ToBytes()).ToBase64String();
#endregion Asymmetric Encryptor
#region Jwt Signature Handler
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, SecurityTokenDescriptor descriptor)
=> handler.WriteToken(handler.CreateToken(descriptor));
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
=> handler.WriteToken(handler.CreateToken(subject: subject, descriptor: descriptor));
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, string issuer, string audience)
=> handler.WriteToken(handler.CreateToken(subject: subject, issuer: issuer, audience: audience));
#endregion Jwt Signature Handler
}
}

View File

@@ -1,70 +0,0 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using DigitalData.Core.Security.Cryptographer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections;
namespace DigitalData.Core.Security
{
public class AsymCryptService<TAsymCryptParams> : RSAFactory<TAsymCryptParams>, IAsymCryptService<TAsymCryptParams>, IRSAFactory<TAsymCryptParams>, IEnumerable<IRSADecryptor>
where TAsymCryptParams : AsymCryptParams
{
public IEnumerable<IRSADecryptor> Decryptors { get; }
/// <summary>
/// It is a separate decryptor for permanently stored encrypted data. It is assigned to the first Default decryptor by default.
/// </summary>
public IRSADecryptor Vault { get; }
public IRSADecryptor this[string key]
{
get
{
var key_params = key.Split(_params.KeyNameSeparator);
if (key_params.Length != 2)
throw new ArgumentException($"Invalid key format. Expected two segments separated by '{_params.KeyNameSeparator}', but received: '{key}'.", nameof(key));
return _params.Decryptors.FirstOrDefault(d => d.Issuer == key_params[0] && d.Audience == key_params[1])
?? throw new KeyNotFoundException($"No decryptor found matching the issuer '{key_params[0]}' and audience '{key_params[1]}'.");
}
}
public IRSADecryptor this[int index] => index < 0 || index >= Decryptors.Count()
? Decryptors.ElementAt(index)
: throw new ArgumentOutOfRangeException(
nameof(index),
index,
$"The index {index} is out of range. The valid indices for {GetType()} are between 0 and {Decryptors.Count() - 1} (inclusive). Please ensure the index is within this range.");
public AsymCryptService(IOptions<TAsymCryptParams> options, ILogger<AsymCryptService<TAsymCryptParams>>? logger = null) : base(options)
{
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
if (!_params.Decryptors.Any())
throw new InvalidOperationException(
"Any decryptor is not found. Ensure that at least one decryptor is configured in the provided parameters. " +
"This issue typically arises if the configuration for decryptors is incomplete or missing. " +
"Check the 'Decryptors' collection in the configuration and verify that it contains valid entries."
);
Decryptors = _params.Decryptors;
Vault = _params.Vault ?? Decryptors.First();
}
public IEnumerator<IRSADecryptor> GetEnumerator() => Decryptors.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Decryptors.GetEnumerator();
public IEnumerable<IRSAEncryptor> Encryptors
{
get
{
foreach (var decryptor in Decryptors)
yield return decryptor.Encryptor;
}
}
}
}

View File

@@ -1,9 +1,8 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Cryptographer;
using DigitalData.Core.Security.RSAKey;
namespace DigitalData.Core.Security.Config
{
public class AsymCryptParams : RSAFactoryParams
public class CryptoFactoryParams : RSAFactoryParams
{
public string PemDirectory { get; init; } = string.Empty;
@@ -23,20 +22,6 @@ namespace DigitalData.Core.Security.Config
public string FileExtension { get; init; } = "pem";
/// <summary>
/// Represents the separator used to concatenate the components of a key-related token string.
/// </summary>
/// <remarks>
/// The resulting key-related token string is constructed as follows:
/// <c>string.Join(KeyNameSeparator, Issuer, Audience, Secret_version)</c>.
/// If <c>Secret_version</c> is not null, it will be included in the concatenation.
/// </remarks>
/// <example>
/// For example, if <c>KeyNameSeparator = ":"</c>, the output might look like:
/// <c>"Issuer:Audience:Secret_version"</c>.
/// </example>
public string KeyNameSeparator { get; init; } = ":";
/// <summary>
///This is the subtext of the pem file name. For the file to be automatically renewed, the name must be assigned to change periodically. For example, by default MM/2 will be refreshed every 2 months.
/// <br />
@@ -56,11 +41,11 @@ namespace DigitalData.Core.Security.Config
public IEnumerable<RSADecryptor> Decryptors { get; init; } = new List<RSADecryptor>();
public IEnumerable<TokenDescription> TokenDescriptions { get; init; } = new List<TokenDescription>();
public IEnumerable<RSATokenDescriptor> TokenDescriptors { get; init; } = new List<RSATokenDescriptor>();
public RSADecryptor? Vault { get; init; }
public RSADecryptor? VaultDecryptor { get; init; }
public AsymCryptParams()
public CryptoFactoryParams()
{
// init decryptors
AfterCreate += () =>
@@ -69,13 +54,29 @@ namespace DigitalData.Core.Security.Config
if (!Directory.Exists(PemDirectory))
Directory.CreateDirectory(PemDirectory);
foreach (var decryptor in Decryptors)
var privateKeys = new List<RSAPrivateKey>();
privateKeys.AddRange(Decryptors);
privateKeys.AddRange(TokenDescriptors);
if (VaultDecryptor is not null)
privateKeys.Add(VaultDecryptor);
foreach (var privateKey in privateKeys)
{
// set default path
if (decryptor.IsPemNull)
if (privateKey.IsPemNull)
{
var file_name_params = new List<object> { decryptor.Issuer, decryptor.Audience, KeySizeInBits, DateTime.Now.ToTag(DateTagFormat) };
if (decryptor.IsEncrypted)
// file name
var file_name_params = new List<object>();
if (privateKey.Id is not null)
file_name_params.Add(privateKey.Id);
else if (privateKey is RSATokenDescriptor descriptor)
file_name_params.Add(descriptor.Issuer);
file_name_params.Add(KeySizeInBits);
file_name_params.Add(DateTime.Now.ToTag(DateTagFormat));
if (privateKey.IsEncrypted)
file_name_params.Add(Secrets.Version);
var file_name = $"{string.Join(FileNameSeparator, file_name_params)}.{FileExtension}";
@@ -83,14 +84,14 @@ namespace DigitalData.Core.Security.Config
var path = Path.Combine(PemDirectory, file_name);
if (File.Exists(path))
decryptor.SetPem(File.ReadAllText(path));
privateKey.SetPem(File.ReadAllText(path));
else
{
var pem = decryptor.IsEncrypted
var pem = privateKey.IsEncrypted
? Instance.RSAFactory.CreateEncryptedPrivateKeyPem(pbeParameters: PbeParameters, keySizeInBits: KeySizeInBits, password: Secrets.PBE_PASSWORD)
: Instance.RSAFactory.CreatePrivateKeyPem(keySizeInBits: KeySizeInBits);
decryptor.SetPem(pem);
privateKey.SetPem(pem);
// Save file in background
Task.Run(async () => await File.WriteAllTextAsync(path: path, pem));
@@ -98,19 +99,6 @@ namespace DigitalData.Core.Security.Config
}
}
};
// set signing credentials of token descriptions
AfterCreate += () =>
{
foreach(var tDesc in TokenDescriptions)
{
if (!Decryptors.TryGet(issuer: tDesc.Issuer, tDesc.Audience, out var decryptor) || decryptor is null)
throw new InvalidOperationException(
$"Decryptor for Issuer '{tDesc.Issuer}' and Audience '{tDesc.Audience}' could not be found or is null.");
tDesc.SigningCredentials = decryptor.CreateSigningCredentials(algorithm: tDesc.SigningAlgorithm, digest: tDesc.SigningDigest);
}
};
}
}
}

View File

@@ -1,4 +1,5 @@
using AutoMapper;
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security.Config
@@ -7,7 +8,7 @@ namespace DigitalData.Core.Security.Config
{
public MappingProfile()
{
CreateMap<TokenDescription, SecurityTokenDescriptor>();
CreateMap<IAsymmetricTokenDescriptor, SecurityTokenDescriptor>();
}
}
}

View File

@@ -0,0 +1,38 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using DigitalData.Core.Security.RSAKey;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace DigitalData.Core.Security
{
public class CryptoFactory : RSAFactory<CryptoFactoryParams>, ICryptoFactory, IAsymmetricKeyFactory
{
public IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
/// <summary>
/// It is a separate decryptor for permanently stored encrypted data. It is assigned to the first Default decryptor by default.
/// </summary>
public IAsymmetricDecryptor VaultDecryptor { get; }
public IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; init; } = new List<IAsymmetricTokenDescriptor>();
public CryptoFactory(IOptions<CryptoFactoryParams> options, ILogger<CryptoFactory>? logger = null) : base(options)
{
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
if (!_params.Decryptors.Any())
throw new InvalidOperationException(
"Any decryptor is not found. Ensure that at least one decryptor is configured in the provided parameters. " +
"This issue typically arises if the configuration for decryptors is incomplete or missing. " +
"Check the 'Decryptors' collection in the configuration and verify that it contains valid entries."
);
Decryptors = _params.Decryptors;
TokenDescriptors = _params.TokenDescriptors;
VaultDecryptor = _params.VaultDecryptor ?? Decryptors.First();
}
}
}

View File

@@ -1,39 +0,0 @@
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
using System.Reflection;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.Cryptographer
{
//TODO: Abstract RSA for future updates (using ECC, El Gamal or Lattice-based Cryptography)
public class RSACryptographer : IRSACryptographer
{
public virtual string Pem { get; init; }
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.");
}
protected virtual RSA RSA { get; } = RSA.Create();
public string Issuer { get; init; } = string.Empty;
public string Audience { get; init; } = string.Empty;
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
public RsaSecurityKey RsaSecurityKey => _lazyRsaSecurityKey.Value;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
internal RSACryptographer()
{
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
}
}

View File

@@ -1,67 +0,0 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.Cryptographer
{
public class RSADecryptor : RSACryptographer, IRSADecryptor, IRSACryptographer
{
private string? _pem;
public override string Pem
{
#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; }
private readonly Lazy<IRSAEncryptor> _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();
internal void SetPem(string pem)
{
_pem = pem;
Init();
}
private void Init()
{
if (string.IsNullOrEmpty(_pem))
throw PemIsNullException;
if (IsEncrypted)
RSA.ImportFromEncryptedPem(Pem, Secrets.PBE_PASSWORD.AsSpan());
else
RSA.ImportFromPem(Pem);
}
private InvalidOperationException PemIsNullException => new($"Pem is null or empty. Issuer: {Issuer}, Audience: {Audience}.");
public SigningCredentials CreateSigningCredentials(string algorithm = SecurityAlgorithms.RsaSha256, string? digest = null)
=> digest is null ? new(RsaSecurityKey, algorithm) : new(RsaSecurityKey, algorithm, digest);
}
}

View File

@@ -1,24 +0,0 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
namespace DigitalData.Core.Security.Cryptographer
{
public class RSAEncryptor : RSACryptographer, IRSAEncryptor, IRSACryptographer
{
public override string Pem
{
get => base.Pem;
init
{
base.Pem = value;
RSA.ImportFromPem(value);
}
}
public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding);
public string Encrypt(string data) => RSA.Encrypt(data.ToBytes(), Padding).ToBase64String();
public bool Verify(string data, string signature) => Encrypt(data) == signature;
}
}

View File

@@ -1,6 +1,6 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using DigitalData.Core.Security.Cryptographer;
using DigitalData.Core.Security.RSAKey;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -10,122 +10,43 @@ namespace DigitalData.Core.Security
{
public static class DIExtensions
{
private static (bool Added, object Lock) _mappingProfile = (false, new());
private static IServiceCollection AddMappingProfile(this IServiceCollection services)
{
if (_mappingProfile.Added)
return services;
lock (_mappingProfile.Lock)
{
if (_mappingProfile.Added)
return services;
_mappingProfile.Added = true;
return services
.AddAutoMapper(typeof(MappingProfile).Assembly)
.AddSingleton<TokenDescriptorProvider>();
}
}
private static IServiceCollection AddParamsConfigureOptions<TParams>(this IServiceCollection services) where TParams : RSAFactoryParams
=> services.AddSingleton<IConfigureOptions<TParams>, ParamsConfigureOptions<TParams>>();
private static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, bool setAsDefault = false) where TAsymCryptParams : AsymCryptParams
{
services.AddParamsConfigureOptions<TAsymCryptParams>().AddMappingProfile();
return setAsDefault
? services.AddSingleton<IAsymCryptService, AsymCryptService<TAsymCryptParams>>()
: services.AddSingleton<IAsymCryptService<TAsymCryptParams>, AsymCryptService<TAsymCryptParams>>();
}
private static IServiceCollection AddCryptoFactory(this IServiceCollection services) => services
.AddParamsConfigureOptions<CryptoFactoryParams>()
.AddAutoMapper(typeof(MappingProfile).Assembly)
.AddSingleton<ICryptoFactory, CryptoFactory>();
/// <summary>
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
/// </summary>
/// <typeparam name="TAsymCryptParams"></typeparam>
/// <param name="services"></param>
/// <param name="section"></param>
/// <param name="setAsDefault">If true, the factory is registered as the default <see cref="IRSAFactory"/>. Otherwise, it is registered as <see cref="IRSAFactory{TRSAFactoryParams}"/>.</param>
/// <returns></returns>
public static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, IConfigurationSection section, bool setAsDefault = false) where TAsymCryptParams : AsymCryptParams => services
.Configure<TAsymCryptParams>(section)
.AddAsymCryptService<TAsymCryptParams>(setAsDefault: setAsDefault);
/// <summary>
/// Registers a custom asym crypt service with default parameters from the given configuration section.
/// </summary>
/// <param name="services"></param>
/// <param name="section"></param>
/// <param name="setAsDefault"></param>
/// <returns></returns>
public static IServiceCollection AddAsymCryptService(this IServiceCollection services, IConfigurationSection section, bool setAsDefault = false)
=> services.Configure<AsymCryptParams>(section).AddAsymCryptService<AsymCryptParams>(setAsDefault: setAsDefault);
/// <summary>
/// Registers an asym crypt service with the specified parameters from the given instance. Optionally, sets it as the default factory.
/// </summary>
/// <typeparam name="TAsymCryptParams"></typeparam>
/// <param name="services"></param>
/// <param name="param"></param>
/// <param name="setAsDefault">If true, the factory is registered as the default <see cref="IRSAFactory"/>. Otherwise, it is registered as <see cref="IRSAFactory{TRSAFactoryParams}"/>.</param>
/// <returns></returns>
public static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, TAsymCryptParams param, bool setAsDefault = false) where TAsymCryptParams : AsymCryptParams => services
.AddSingleton(Options.Create(param))
.AddAsymCryptService<TAsymCryptParams>(setAsDefault: setAsDefault);
/// <summary>
/// Registers default asym crypt service with the specified parameters from the given instance.
/// </summary>
/// <param name="services"></param>
/// <param name="param"></param>
/// <returns></returns>
public static IServiceCollection AddAsymCryptService(this IServiceCollection services, AsymCryptParams param) => services
.AddAsymCryptService(param: param, setAsDefault: true);
/// <summary>
/// Registers default RSA Factory instance with default params
/// </summary>
/// <param name="services"></param>
/// <param name="factoryParams"></param>
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
public static IServiceCollection AddRSAFactory(this IServiceCollection services, RSAFactoryParams? factoryParams = null) => services
.AddParamsConfigureOptions<RSAFactoryParams>()
.AddMappingProfile()
.AddScoped<IRSAFactory>(_ => new RSAFactory<RSAFactoryParams>(Options.Create(factoryParams ?? new())));
public static IServiceCollection AddCryptoFactory(this IServiceCollection services, IConfigurationSection section) => services
.Configure<CryptoFactoryParams>(section)
.AddCryptoFactory();
/// <summary>
/// Registers an asym crypt service with the specified parameters from the given instance.
/// </summary>
/// <param name="services"></param>
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
public static IServiceCollection AddCryptoFactory(this IServiceCollection services, CryptoFactoryParams? factoryParams = null) => services
.AddSingleton(Options.Create(factoryParams ?? new()))
.AddCryptoFactory();
/// <summary>
/// Registers a custom RSA Factory with specified parameters from the given configuration section.
/// </summary>
/// <typeparam name="TRSAFactoryParams"></typeparam>
/// <param name="services"></param>
/// <param name="section"></param>
/// <param name="setAsDefault">If true, the factory is registered as the default <see cref="IRSAFactory"/>. Otherwise, it is registered as <see cref="IRSAFactory{TRSAFactoryParams}"/>.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, IConfigurationSection section, bool setAsDefault = false)
where TRSAFactoryParams : RSAFactoryParams
{
services.AddMappingProfile().AddParamsConfigureOptions<TRSAFactoryParams>().Configure<TRSAFactoryParams>(section);
return setAsDefault
? services.AddSingleton<IRSAFactory, RSAFactory<TRSAFactoryParams>>()
: services.AddSingleton<IRSAFactory<TRSAFactoryParams>, RSAFactory<TRSAFactoryParams>>();
}
/// <summary>
/// Registers an RSA Factory with the specified parameters from the given instance. Optionally, sets it as the default factory.
/// </summary>
/// <typeparam name="TRSAFactoryParams">The type of the RSA factory parameters.</typeparam>
/// <param name="services"></param>
/// <param name="rsaParams"></param>
/// <param name="setAsDefault">If true, the factory is registered as the default <see cref="IRSAFactory"/>. Otherwise, it is registered as <see cref="IRSAFactory{TRSAFactoryParams}"/>.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, TRSAFactoryParams rsaParams, bool setAsDefault = false)
where TRSAFactoryParams : RSAFactoryParams
{
services.AddMappingProfile().AddSingleton(Options.Create(rsaParams));
return setAsDefault
? services.AddParamsConfigureOptions<TRSAFactoryParams>().AddSingleton<IRSAFactory, RSAFactory<TRSAFactoryParams>>()
: services.AddParamsConfigureOptions<TRSAFactoryParams>().AddSingleton<IRSAFactory<TRSAFactoryParams>, RSAFactory<TRSAFactoryParams>>();
}
public static IServiceCollection AddRSAFactory(this IServiceCollection services, IConfigurationSection section) => services
.AddParamsConfigureOptions<RSAFactoryParams>()
.Configure<RSAFactoryParams>(section)
.AddSingleton<IAsymmetricKeyFactory, RSAFactory<RSAFactoryParams>>();
private static IServiceCollection AddClaimDescriptor<TPrincipal>(this IServiceCollection services,
Func<TPrincipal, IDictionary<string, object>>? claimsMapper = null,
@@ -139,5 +60,12 @@ namespace DigitalData.Core.Security
return services.AddSingleton(sp => Options.Create(descriptor));
}
public static IServiceCollection AddJwtSignatureHandler<TPrincipal>(this IServiceCollection services,
Func<TPrincipal, IDictionary<string, object>>? claimsMapper = null,
Func<TPrincipal, ClaimsIdentity>? subjectMapper = null)
=> services
.AddClaimDescriptor(claimsMapper: claimsMapper, subjectMapper: subjectMapper)
.AddSingleton<IJwtSignatureHandler<TPrincipal>, JwtSignatureHandler<TPrincipal>>();
}
}

View File

@@ -4,8 +4,28 @@
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>DigitalData.Core.Security</PackageId>
<Version>1.0.0</Version>
<Company>Digital Data GmbH</Company>
<Product>Digital Data GmbH</Product>
<Description>This package provides RSA-based security functionalities as an implementation of the DigitalData.Core.Abstractions.Security library. It supports robust encryption and decryption operations, as well as JWT signing and validation for secure authentication and data integrity.</Description>
<Authors>Digital Data GmbH</Authors>
<Copyright>Copyright 2025</Copyright>
<PackageProjectUrl></PackageProjectUrl>
<PackageIcon>core_icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackageTags>digital data core security</PackageTags>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\nuget-package-icons\core_icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />

View File

@@ -1,15 +1,11 @@
namespace DigitalData.Core.Security.Config
using AutoMapper;
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security
{
internal static class StringExtensions
internal static class Extension
{
public static string ToBase64String(this byte[] bytes) => Convert.ToBase64String(bytes);
public static byte[] Base64ToByte(this string base64String) => Convert.FromBase64String(base64String);
public static byte[] ToBytes(this string str) => System.Text.Encoding.UTF8.GetBytes(str);
public static string BytesToString(this byte[] bytes) => System.Text.Encoding.UTF8.GetString(bytes);
/// <summary>
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
/// <br />
@@ -30,7 +26,7 @@
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
public static string ToTag(this DateTime date, string format)
internal static string ToTag(this DateTime date, string format)
{
if (format is not null && format.Contains("//"))
{
@@ -78,6 +74,16 @@
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
public static string ToTag(this DateOnly date, string format) => date.ToDateTime(new()).ToTag(format);
internal static string ToTag(this DateOnly date, string format) => date.ToDateTime(new()).ToTag(format);
/// <summary>
/// Maps a <see cref="RSATokenDescriptor"/> to a <see cref="SecurityTokenDescriptor"/>.
/// </summary>
/// <param name="mapper">The <see cref="IMapper"/> instance used for mapping.</param>
/// <param name="description">The <see cref="RSATokenDescriptor"/> instance to be mapped.</param>
/// <returns>A <see cref="SecurityTokenDescriptor"/> instance populated with the mapped values.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="mapper"/> or <paramref name="description"/> is <c>null</c>.</exception>
internal static SecurityTokenDescriptor Map(this IMapper mapper, IAsymmetricTokenDescriptor description)
=> mapper.Map(description, new SecurityTokenDescriptor());
}
}

View File

@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "<Pending>", Scope = "member", Target = "~M:DigitalData.Core.Security.JwtSignatureHandler`1.#ctor(Microsoft.Extensions.Options.IOptions{DigitalData.Core.Security.Config.ClaimDescriptor{`0}},AutoMapper.IMapper)")]

View File

@@ -1,6 +1,6 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using DigitalData.Core.Security.Cryptographer;
using DigitalData.Core.Security.RSAKey;
using Microsoft.Extensions.Options;
namespace DigitalData.Core.Security
@@ -9,6 +9,6 @@ namespace DigitalData.Core.Security
{
private static readonly Lazy<RSAFactory<RSAFactoryParams>> LazyInstance = new(() => new(Options.Create<RSAFactoryParams>(new())));
public static IRSAFactory RSAFactory => LazyInstance.Value;
public static IAsymmetricKeyFactory RSAFactory => LazyInstance.Value;
}
}

View File

@@ -0,0 +1,40 @@
using AutoMapper;
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace DigitalData.Core.Security
{
public class JwtSignatureHandler<TPrincipal> : JwtSecurityTokenHandler, IJwtSignatureHandler<TPrincipal>
{
private readonly ClaimDescriptor<TPrincipal> _claimDescriptor;
private readonly IMapper _mapper;
private readonly ICryptoFactory _cryptoFactory;
public JwtSignatureHandler(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, IMapper mapper, ICryptoFactory cryptoFactory)
{
_claimDescriptor = claimDescriptorOptions.Value;
_mapper = mapper;
_cryptoFactory = cryptoFactory;
}
public SecurityToken CreateToken(TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
{
var sDescriptor = _mapper.Map(descriptor);
sDescriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
sDescriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
return CreateToken(sDescriptor);
}
public SecurityToken CreateToken(TPrincipal subject, string issuer, string audience)
{
var descriptor = _cryptoFactory.TokenDescriptors.Get(issuer: issuer, audience: audience)
?? throw new InvalidOperationException($"No or multiple token description found for issuer '{issuer}' and audience '{audience}'.");
return CreateToken(subject: subject, descriptor: descriptor);
}
}
}

View File

@@ -1,30 +0,0 @@
using DigitalData.Core.Security.Config;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace DigitalData.Core.Security
{
public class JwtSignatureService<TPrincipal> : JwtSecurityTokenHandler
{
private readonly ClaimDescriptor<TPrincipal> _claimDescriptor;
private readonly TokenDescriptorProvider _descriptorProvider;
public JwtSignatureService(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, TokenDescriptorProvider descriptorProvider)
{
_claimDescriptor = claimDescriptorOptions.Value;
_descriptorProvider = descriptorProvider;
}
public SecurityToken CreateToken(TPrincipal subject, TokenDescription description)
{
var descriptor = _descriptorProvider.Create(description: description);
descriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
descriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
return CreateToken(descriptor);
}
public string CreateAndWriteToken(TPrincipal subject, TokenDescription description) => WriteToken(CreateToken(subject, description));
}
}

View File

@@ -0,0 +1,33 @@
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<IAsymmetricEncryptor> _lazyEncryptor;
public IAsymmetricEncryptor Encryptor => _lazyEncryptor.Value;
public RSADecryptor()
{
_lazyEncryptor = new(() => new RSAEncryptor()
{
Content = RSA.ExportRSAPublicKeyPem(),
Padding = Padding
});
}
}
}

View File

@@ -0,0 +1,20 @@
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);
}
}

View File

@@ -3,9 +3,9 @@ using DigitalData.Core.Security.Config;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.Cryptographer
namespace DigitalData.Core.Security.RSAKey
{
public class RSAFactory<TRSAFactoryParams> : IRSAFactory<TRSAFactoryParams> where TRSAFactoryParams : RSAFactoryParams
public class RSAFactory<TRSAFactoryParams> : IAsymmetricKeyFactory where TRSAFactoryParams : RSAFactoryParams
{
protected readonly TRSAFactoryParams _params;
@@ -56,11 +56,9 @@ namespace DigitalData.Core.Security.Cryptographer
return new string(pemChars);
}
public IRSADecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null) => new RSADecryptor()
public IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null) => new RSADecryptor()
{
Pem = pem,
Issuer = issuer ?? string.Empty,
Audience = audience ?? string.Empty,
Content = pem,
IsEncrypted = encrypt,
Padding = padding ?? RSAEncryptionPadding.OaepSHA256
};

View File

@@ -0,0 +1,16 @@
using DigitalData.Core.Abstractions.Security;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.RSAKey
{
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; }
#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; }
protected virtual RSA RSA { get; } = RSA.Create();
}
}

View File

@@ -0,0 +1,55 @@
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<TPublicKey>() where TPublicKey : RSAPublicKey, new()
=> new() { Content = RSA.ExportRSAPublicKeyPem() };
private readonly Lazy<RSAPublicKey> _lazyPublicKey;
public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value;
public RSAPrivateKey()
{
_lazyPublicKey = new(CreatePublicKey<RSAPublicKey>);
}
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);
}
}
}

View File

@@ -0,0 +1,17 @@
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);
}
}
}
}

View File

@@ -1,16 +1,24 @@
using Microsoft.IdentityModel.Tokens;
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security.Config
namespace DigitalData.Core.Security.RSAKey
{
/// <summary>
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
/// </summary>
public class TokenDescription
public class RSATokenDescriptor : RSAPrivateKey, IAsymmetricTokenDescriptor
{
private readonly Lazy<RSATokenValidator> _lazyTokenValidator;
public IAsymmetricTokenValidator Validator => _lazyTokenValidator.Value;
public required TimeSpan Lifetime { get; init; }
#region SecurityTokenDescriptor Map
/// <summary>
/// Gets or sets the value of the 'audience' claim.
/// </summary>
public string Audience { get; set; }
public required string Audience { get; set; }
/// <summary>
/// Defines the compression algorithm that will be used to compress the JWT token payload.
@@ -24,13 +32,14 @@ namespace DigitalData.Core.Security.Config
/// <summary>
/// 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.
/// </summary>
public DateTime? Expires { get; set; }
public DateTime? Expires => DateTime.Now.AddTicks(Lifetime.Ticks);
/// <summary>
/// Gets or sets the issuer of this <see cref="ITokenDescription"/>.
/// Gets or sets the issuer of this <see cref="SecurityTokenDescriptor"/>.
/// </summary>
public string Issuer { get; set; }
public required string Issuer { get; set; }
/// <summary>
/// Gets or sets the time the security token was issued. This value should be in UTC.
@@ -73,7 +82,14 @@ namespace DigitalData.Core.Security.Config
/// <summary>
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
public SigningCredentials SigningCredentials => _lazySigningCredentials.Value;
#endregion SecurityTokenDescriptor
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
private readonly Lazy<SigningCredentials> _lazySigningCredentials;
/// <summary>
/// Specifies the signature algorithm to be applied to the <see cref="SigningCredentials"/>.
@@ -85,6 +101,19 @@ namespace DigitalData.Core.Security.Config
/// Optionally specifies the digest algorithm to be applied during the signing process for the <see cref="SigningCredentials"/>.
/// If not provided, the default algorithm is used.
/// </summary>
public string? SigningDigest = null;
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<RSATokenValidator>);
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
_lazySigningCredentials = new(() => SigningDigest is null
? new(SecurityKey, SigningAlgorithm)
: new(SecurityKey, SigningAlgorithm, SigningDigest));
}
}
}
}

View File

@@ -0,0 +1,17 @@
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security.RSAKey
{
public class RSATokenValidator : RSAPublicKey, IAsymmetricTokenValidator
{
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
public RSATokenValidator()
{
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
}
}
}

View File

@@ -1,19 +0,0 @@
using AutoMapper;
using DigitalData.Core.Security.Config;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security
{
public class TokenDescriptorProvider
{
private readonly IMapper _mapper;
public TokenDescriptorProvider(IMapper mapper)
{
_mapper = mapper;
}
public SecurityTokenDescriptor Create(TokenDescription description)
=> _mapper.Map(description, new SecurityTokenDescriptor());
}
}

View File

@@ -25,7 +25,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Security",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Terminal", "DigitalData.Core.Terminal\DigitalData.Core.Terminal.csproj", "{0FA93730-8084-4907-B172-87D610323796}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Tests.API", "DigitalData.Core.Tests.API\DigitalData.Core.Tests.API.csproj", "{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Tests.API", "DigitalData.Core.Tests.API\DigitalData.Core.Tests.API.csproj", "{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -68,8 +68,8 @@ Global
{E009A053-A9F4-48F2-984F-EF5C376A9B14}.Debug|Any CPU.Build.0 = Release|Any CPU
{E009A053-A9F4-48F2-984F-EF5C376A9B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E009A053-A9F4-48F2-984F-EF5C376A9B14}.Release|Any CPU.Build.0 = Release|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Debug|Any CPU.Build.0 = Release|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Release|Any CPU.Build.0 = Release|Any CPU
{0FA93730-8084-4907-B172-87D610323796}.Debug|Any CPU.ActiveCfg = Debug|Any CPU