Compare commits
164 Commits
feat/api
...
608d266d1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
608d266d1c | ||
|
|
34e14fd2f5 | ||
|
|
dc45cf2c08 | ||
|
|
09a31b5a3d | ||
|
|
b5cecac745 | ||
|
|
0f4b5430a3 | ||
|
|
7f2d2dadfa | ||
|
|
ac0b6f739b | ||
|
|
d9d61368e3 | ||
|
|
e8c98115b6 | ||
|
|
09dae1b1ac | ||
|
|
9aafc9e467 | ||
|
|
4ce738957d | ||
|
|
5e1bf16b6d | ||
|
|
4f96d271f3 | ||
|
|
14485af448 | ||
|
|
c27e21a702 | ||
|
|
4874079b69 | ||
|
|
15e909064f | ||
|
|
d17c5ca6cd | ||
|
|
592b949f57 | ||
|
|
8850ac4ac9 | ||
|
|
8ccf6f31ae | ||
|
|
9875d023e3 | ||
|
|
62afba7c23 | ||
|
|
1d4882cfbc | ||
|
|
275b9ec858 | ||
|
|
2cf0eb3977 | ||
|
|
4ab5393deb | ||
|
|
a2dc59d5ef | ||
|
|
ed041bf7cb | ||
|
|
c70327e7f4 | ||
|
|
0a3ce89c0d | ||
|
|
389d64c25d | ||
|
|
a3931414e3 | ||
|
|
0dd897625a | ||
|
|
351a6732cf | ||
|
|
5a1808c6a6 | ||
|
|
50c42e9cdd | ||
|
|
ec126be2aa | ||
|
|
9953bbd2ef | ||
|
|
dbecfa92f4 | ||
|
|
e007f15bce | ||
|
|
79dffef528 | ||
|
|
af478e974c | ||
|
|
435c91955c | ||
|
|
4142d2d948 | ||
|
|
06260e0edb | ||
|
|
2d675a16ad | ||
|
|
5469b20e4f | ||
|
|
6f5b4efefb | ||
|
|
b6b12c7702 | ||
|
|
ce716d2bab | ||
|
|
bf672d8b8c | ||
|
|
ed29c9f990 | ||
|
|
66ed34b664 | ||
|
|
d7b4c382cd | ||
|
|
4f6ca3524a | ||
|
|
bd1ae4246d | ||
|
|
d92475c230 | ||
|
|
15705cccc4 | ||
|
|
a8403087f6 | ||
|
|
0235c83075 | ||
|
|
63aeba982f | ||
|
|
514495fc8d | ||
|
|
9752fb14ec | ||
|
|
b3629661a1 | ||
|
|
f38bad8531 | ||
|
|
154478c318 | ||
|
|
155eb563d1 | ||
|
|
4aacc3f650 | ||
|
|
f40c86ed63 | ||
|
|
b32f0df125 | ||
|
|
324a5bdb1e | ||
|
|
e0a6787a87 | ||
|
|
c6a4038eab | ||
|
|
58c8520c08 | ||
|
|
eced1a5afc | ||
|
|
7da93c6719 | ||
|
|
6a92466490 | ||
|
|
5d9d756b91 | ||
|
|
f14aaa75e1 | ||
|
|
249f5a0ae5 | ||
|
|
30177cf0c7 | ||
|
|
68ef0a7537 | ||
|
|
fe2ee78d14 | ||
|
|
53e6f37a09 | ||
|
|
7ec85b4e30 | ||
|
|
a9ebc406f3 | ||
|
|
d013d3edfa | ||
|
|
f267fe955b | ||
|
|
644283cf8f | ||
|
|
82aa8d1143 | ||
|
|
7459f05748 | ||
|
|
36f75d003a | ||
|
|
76ce64691a | ||
|
|
7c03282066 | ||
|
|
7ae95b729f | ||
|
|
9ee8a51664 | ||
|
|
b1d1a898b8 | ||
|
|
4ed3e79565 | ||
|
|
8d9de4502e | ||
|
|
7dd91c73c4 | ||
|
|
988d1e2b16 | ||
|
|
4e0e907313 | ||
|
|
0bfec426d4 | ||
|
|
08ffe821ff | ||
|
|
fa5d0f1b26 | ||
|
|
38bd23d012 | ||
|
|
50e2581727 | ||
|
|
5c09d7775b | ||
|
|
dbfee49dee | ||
|
|
0c6c84852d | ||
|
|
3f61b5064c | ||
|
|
f79d2e2352 | ||
|
|
201da81aa5 | ||
|
|
bea57a25e8 | ||
|
|
0ff89b4906 | ||
|
|
600d17ef40 | ||
|
|
16565eca4d | ||
|
|
8787c04917 | ||
|
|
b3568216a0 | ||
|
|
6f520732dd | ||
|
|
8003cffb9b | ||
|
|
b02f93b38d | ||
|
|
2f0c6a905a | ||
|
|
baf1f5e045 | ||
|
|
b8a4a1f2b5 | ||
|
|
a69f610ef4 | ||
|
|
016d8bdcf2 | ||
|
|
738005f5dc | ||
|
|
c96af25e23 | ||
|
|
35e2fef046 | ||
|
|
b8fb45d4a3 | ||
|
|
fa60147507 | ||
|
|
e9d408a717 | ||
|
|
5fd3fa2fc6 | ||
|
|
0d5bcedc01 | ||
|
|
2e68a37944 | ||
|
|
8076efb934 | ||
|
|
c38f7dcf72 | ||
|
|
6e4942c885 | ||
|
|
d0dfd834b0 | ||
|
|
aa9951f242 | ||
|
|
506685a0b5 | ||
|
|
c9548238bb | ||
|
|
3ffdd49a47 | ||
|
|
609cd29dc5 | ||
|
|
cc3d1f58d3 | ||
|
|
c03f39c1a9 | ||
|
|
750f7bc20c | ||
|
|
65989b23b3 | ||
|
|
c895d2df0e | ||
|
|
0c451cb834 | ||
|
|
9396f48f46 | ||
|
|
1a941b4728 | ||
|
|
c6942164e2 | ||
|
|
343560ed62 | ||
|
|
6873bac8a1 | ||
|
|
09406ca505 | ||
|
|
3aa5ad782f | ||
|
|
5991444efd | ||
|
|
f720ea9cd6 | ||
|
|
a4b96c2f3e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -410,3 +410,4 @@ FodyWeavers.xsd
|
||||
/DigitalData.Core.ConsoleApp/FooHttpOptions.cs
|
||||
/DigitalData.Core.Tests/obj/
|
||||
/DigitalData.Core.Terminal
|
||||
/DigitalData.Core.Tests.API
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace DigitalData.Core.API
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class BasicCRUDControllerBase<TCRUDService, TDto, TEntity, TId> : CRUDControllerBase<TCRUDService, TDto, TDto, TDto, TEntity, TId>
|
||||
where TCRUDService : ICRUDService<TDto, TDto, TDto, TEntity, TId>
|
||||
where TCRUDService : ICRUDService<TDto, TDto, TEntity, TId>
|
||||
where TDto : class, IUnique<TId>
|
||||
where TEntity : class, IUnique<TId>
|
||||
{
|
||||
|
||||
@@ -11,13 +11,12 @@ namespace DigitalData.Core.API
|
||||
/// <typeparam name="TCRUDService">The derived CRUD service type implementing ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>.</typeparam>
|
||||
/// <typeparam name="TCreateDto">The Data Transfer Object type for create operations.</typeparam>
|
||||
/// <typeparam name="TReadDto">The Data Transfer Object type for read operations.</typeparam>
|
||||
/// <typeparam name="TUpdateDto">The Data Transfer Object type for update operations.</typeparam>
|
||||
/// <typeparam name="TEntity">The entity type CRUD operations will be performed on.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity's identifier.</typeparam>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CRUDControllerBase<TCRUDService, TCreateDto, TReadDto, TUpdateDto, TEntity, TId> : ControllerBase
|
||||
where TCRUDService : ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>
|
||||
where TCRUDService : ICRUDService<TCreateDto, TReadDto, TEntity, TId>
|
||||
where TCreateDto : class
|
||||
where TReadDto : class
|
||||
where TUpdateDto : class, IUnique<TId>
|
||||
|
||||
@@ -12,13 +12,12 @@ namespace DigitalData.Core.API
|
||||
/// <typeparam name="TCRUDService">The derived CRUD service type implementing ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>.</typeparam>
|
||||
/// <typeparam name="TCreateDto">The Data Transfer Object type for create operations.</typeparam>
|
||||
/// <typeparam name="TReadDto">The Data Transfer Object type for read operations.</typeparam>
|
||||
/// <typeparam name="TUpdateDto">The Data Transfer Object type for update operations.</typeparam>
|
||||
/// <typeparam name="TEntity">The entity type CRUD operations will be performed on.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity's identifier.</typeparam>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CRUDControllerBaseWithErrorHandling<TCRUDService, TCreateDto, TReadDto, TUpdateDto, TEntity, TId> : ControllerBase
|
||||
where TCRUDService : ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>
|
||||
where TCRUDService : ICRUDService<TCreateDto, TReadDto, TEntity, TId>
|
||||
where TCreateDto : class
|
||||
where TReadDto : class
|
||||
where TUpdateDto : class, IUnique<TId>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<Description>This package provides a comprehensive set of API controllers and related utilities for the DigitalData.Core library. It includes generic CRUD controllers, localization extensions, middleware for security policies, and application model conventions.</Description>
|
||||
<PackageId>DigitalData.Core.API</PackageId>
|
||||
<Version>2.0.1</Version>
|
||||
<Version>2.1.1</Version>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DigitalData.Core.API</Product>
|
||||
@@ -16,8 +16,8 @@
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackageTags>digital data core api</PackageTags>
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<AssemblyVersion>2.0.1</AssemblyVersion>
|
||||
<FileVersion>2.0.1</FileVersion>
|
||||
<AssemblyVersion>2.1.1</AssemblyVersion>
|
||||
<FileVersion>2.1.1</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace DigitalData.Core.Abstractions.Application
|
||||
/// This interface is useful for entities that do not require different DTOs for different operations,
|
||||
/// allowing for a more concise and maintainable codebase when implementing services for such entities.
|
||||
/// </remarks>
|
||||
public interface IBasicCRUDService<TDto, TEntity, TId> : ICRUDService<TDto, TDto, TDto, TEntity, TId>
|
||||
public interface IBasicCRUDService<TDto, TEntity, TId> : ICRUDService<TDto, TDto, TEntity, TId>
|
||||
where TDto : class, IUnique<TId> where TEntity : class, IUnique<TId>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Application
|
||||
{
|
||||
public interface ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId> : IReadService<TReadDto, TEntity, TId>
|
||||
where TCreateDto : class where TReadDto : class where TUpdateDto : IUnique<TId> where TEntity : class, IUnique<TId>
|
||||
public interface ICRUDService<TCreateDto, TReadDto, TEntity, TId> : IReadService<TReadDto, TEntity, TId>
|
||||
where TCreateDto : class where TReadDto : class where TEntity : class, IUnique<TId>
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously creates a new entity based on the provided <paramref name="createDto"/> and returns the identifier of the created entity wrapped in a <see cref="DataResult{TId}"/>.
|
||||
@@ -20,6 +20,6 @@ namespace DigitalData.Core.Abstractions.Application
|
||||
/// </summary>
|
||||
/// <param name="updateDto">The updateDTO with updated values for the entity.</param>
|
||||
/// <returns>An Result indicating the outcome of the update operation, with an appropriate message.</returns>
|
||||
Task<Result> UpdateAsync(TUpdateDto updateDto);
|
||||
Task<Result> UpdateAsync<TUpdateDto>(TUpdateDto updateDto) where TUpdateDto : IUnique<TId>;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackAsTool>False</PackAsTool>
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<Version>2.2.1</Version>
|
||||
<AssemblyVersion>2.2.1</AssemblyVersion>
|
||||
<FileVersion>2.2.1</FileVersion>
|
||||
<Version>3.0.0</Version>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
16
DigitalData.Core.Abstractions/Security/IAsymmetricKey.cs
Normal file
16
DigitalData.Core.Abstractions/Security/IAsymmetricKey.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricKey : IUniqueSecurityContext
|
||||
{
|
||||
public string Content { get; init; }
|
||||
|
||||
public new string Issuer { get; init; }
|
||||
|
||||
public new string Audience { get; init; }
|
||||
|
||||
public SecurityKey SecurityKey { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricKeyFactory
|
||||
{
|
||||
string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
IAsymmetricPrivateKey CreatePrivateKey(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricPrivateKey : IAsymmetricKey
|
||||
{
|
||||
public bool IsEncrypted { get; init; }
|
||||
|
||||
IAsymmetricPublicKey PublicKey { get; }
|
||||
|
||||
PrivateKeyTokenDescriptor? TokenDescriptor { get; init; }
|
||||
|
||||
byte[] Decrypt(byte[] data);
|
||||
|
||||
string Decrypt(string data);
|
||||
|
||||
SigningCredentials CreateSigningCredentials(string algorithm = SecurityAlgorithms.RsaSha256, string? digest = null);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IRSAEncryptor : IRSACryptographer
|
||||
public interface IAsymmetricPublicKey : IAsymmetricKey
|
||||
{
|
||||
public byte[] Encrypt(byte[] data);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatter function for generating RSA key names.
|
||||
/// This formatter takes an issuer, audience, isPrivate, and optional version and separator
|
||||
/// to produce a formatted string used for the key naming convention.
|
||||
/// </summary>
|
||||
/// <param name="issuer">A string representing the issuer of the key. It should not contain invalid file name characters or the separator.</param>
|
||||
/// <param name="audience">A string representing the audience for which the key is intended. It should not contain invalid file name characters or the separator.</param>
|
||||
/// <param name="isPrivate">An bool to check if the key is private.</param>
|
||||
/// <param name="version">An instance of the <see cref="Version?"/> interface, which is used to keep the version of Pbe password.</param>
|
||||
/// <param name="separator">An optional string separator used to separate the issuer and audience. The default is "-_-". It should not be included in the issuer or audience strings.</param>
|
||||
/// <returns>A formatted string combining the issuer, audience, and separator, which adheres to valid file naming rules.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the issuer, audience, or separator contains invalid characters or when the separator is present within the issuer or audience.</exception>
|
||||
Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
|
||||
|
||||
string CreateRSAPrivateKeyPem(int? keySizeInBits = null);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
int? keySizeInBits = null,
|
||||
string? password = null,
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null);
|
||||
|
||||
IRSADecryptor this[string key] { get; }
|
||||
|
||||
bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor);
|
||||
}
|
||||
}
|
||||
11
DigitalData.Core.Abstractions/Security/ICryptograph.cs
Normal file
11
DigitalData.Core.Abstractions/Security/ICryptograph.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface ICryptograph : IAsymmetricKeyFactory
|
||||
{
|
||||
IEnumerable<IAsymmetricPrivateKey> PrivateKeys { get; }
|
||||
|
||||
IAsymmetricPrivateKey VaultPrivateKey { get; }
|
||||
|
||||
IEnumerable<IAsymmetricPublicKey> PublicKeys { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IJwtSignatureHandler<TPrincipal>
|
||||
{
|
||||
SecurityToken CreateToken(SecurityTokenDescriptor tokenDescriptor);
|
||||
|
||||
SecurityToken CreateToken(TPrincipal subject, string issuer, string audience);
|
||||
|
||||
SecurityToken CreateToken(TPrincipal subject, string apiRoute);
|
||||
|
||||
string WriteToken(SecurityToken token);
|
||||
|
||||
string WriteToken(SecurityTokenDescriptor descriptor);
|
||||
|
||||
string WriteToken(TPrincipal subject, string issuer, string audience);
|
||||
|
||||
string WriteToken(TPrincipal subject, string apiRoute);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IRSACryptographer
|
||||
{
|
||||
public string Pem { get; init; }
|
||||
|
||||
public RSAEncryptionPadding Padding { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IRSADecryptor : IRSACryptographer
|
||||
{
|
||||
(string Value, Version Version)? VersionedPassword { init; }
|
||||
|
||||
Version? PasswordVersion { get; }
|
||||
|
||||
bool HasEncryptedPem { get; }
|
||||
|
||||
IRSAEncryptor Encryptor { get; }
|
||||
|
||||
byte[] Decrypt(byte[] data);
|
||||
|
||||
string Decrypt(string data);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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 class PrivateKeyTokenDescriptor : IUniqueSecurityContext
|
||||
{
|
||||
public string? ApiRoute { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'audience' claim.
|
||||
/// </summary>
|
||||
public new string Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the compression algorithm that will be used to compress the JWT token payload.
|
||||
/// </summary>
|
||||
public string CompressionAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
|
||||
/// </summary>
|
||||
public EncryptingCredentials EncryptingCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? Expires { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer of this <see cref="PrivateKeyTokenDescriptor"/>.
|
||||
/// </summary>
|
||||
public new string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the security token was issued. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? NotBefore { get; set; }
|
||||
|
||||
/// <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>
|
||||
public string TokenType { get; set; }
|
||||
|
||||
/// <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>
|
||||
public IDictionary<string, object> AdditionalHeaderClaims { get; set; }
|
||||
|
||||
/// <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>
|
||||
public IDictionary<string, object> AdditionalInnerHeaderClaims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
|
||||
/// </summary>
|
||||
public SigningCredentials SigningCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the signature algorithm to be applied to the <see cref="SigningCredentials"/>.
|
||||
/// Default is <see cref="SecurityAlgorithms.RsaSha256"/>.
|
||||
/// </summary>
|
||||
public string SigningAlgorithm { get; internal set; } = SecurityAlgorithms.RsaSha256;
|
||||
|
||||
/// <summary>
|
||||
/// 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 { get; init; }
|
||||
}
|
||||
}
|
||||
29
DigitalData.Core.Abstractions/Security/SecurityExtensions.cs
Normal file
29
DigitalData.Core.Abstractions/Security/SecurityExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public static class SecurityExtensions
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace DigitalData.Core.Application
|
||||
/// and a culture-specific translation service for any necessary text translations, ensuring a versatile and internationalized approach to CRUD operations.
|
||||
/// </remarks>
|
||||
public class BasicCRUDService<TCRUDRepository, TDto, TEntity, TId> :
|
||||
CRUDService<TCRUDRepository, TDto, TDto, TDto, TEntity, TId>, IBasicCRUDService<TDto, TEntity, TId>
|
||||
CRUDService<TCRUDRepository, TDto, TDto, TEntity, TId>, IBasicCRUDService<TDto, TEntity, TId>
|
||||
where TCRUDRepository : ICRUDRepository<TEntity, TId> where TDto : class, IUnique<TId> where TEntity : class, IUnique<TId>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -12,11 +12,10 @@ namespace DigitalData.Core.Application
|
||||
/// </summary>
|
||||
/// <typeparam name="TCreateDto">The DTO type for create operations.</typeparam>
|
||||
/// <typeparam name="TReadDto">The DTO type for read operations.</typeparam>
|
||||
/// <typeparam name="TUpdateDto">The DTO type for update operations.</typeparam>
|
||||
/// <typeparam name="TEntity">The entity type.</typeparam>
|
||||
/// <typeparam name="TId">The type of the identifier for the entity.</typeparam>
|
||||
public class CRUDService<TCRUDRepository, TCreateDto, TReadDto, TUpdateDto, TEntity, TId> : ReadService<TCRUDRepository, TReadDto, TEntity, TId>, ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>
|
||||
where TCRUDRepository : ICRUDRepository<TEntity, TId> where TCreateDto : class where TReadDto : class where TUpdateDto : IUnique<TId> where TEntity : class, IUnique<TId>
|
||||
public class CRUDService<TCRUDRepository, TCreateDto, TReadDto, TEntity, TId> : ReadService<TCRUDRepository, TReadDto, TEntity, TId>, ICRUDService<TCreateDto, TReadDto, TEntity, TId>
|
||||
where TCRUDRepository : ICRUDRepository<TEntity, TId> where TCreateDto : class where TReadDto : class where TEntity : class, IUnique<TId>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
@@ -45,7 +44,7 @@ namespace DigitalData.Core.Application
|
||||
/// </summary>
|
||||
/// <param name="updateDto">The DTO to update an entity from.</param>
|
||||
/// <returns>A service message indicating success or failure.</returns>
|
||||
public virtual async Task<Result> UpdateAsync(TUpdateDto updateDto)
|
||||
public virtual async Task<Result> UpdateAsync<TUpdateDto>(TUpdateDto updateDto) where TUpdateDto : IUnique<TId>
|
||||
{
|
||||
var currentEntitiy = await _repository.ReadByIdAsync(updateDto.Id);
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ namespace DigitalData.Core.Application
|
||||
/// <typeparam name="TCRUDRepository">The repository type that provides CRUD operations for entities of type TEntity.</typeparam>
|
||||
/// <typeparam name="TCreateDto">The DTO type used for create operations.</typeparam>
|
||||
/// <typeparam name="TReadDto">The DTO type used for read operations.</typeparam>
|
||||
/// <typeparam name="TUpdateDto">The DTO type used for update operations.</typeparam>
|
||||
/// <typeparam name="TEntity">The entity type corresponding to the DTOs.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity's identifier.</typeparam>
|
||||
/// <typeparam name="TProfile">The AutoMapper profile type for configuring mappings between the DTOs and the entity.</typeparam>
|
||||
@@ -48,9 +47,9 @@ namespace DigitalData.Core.Application
|
||||
/// <param name="configureService">An optional action to configure additional services for the CRUD service.</param>
|
||||
/// <returns>The original <see cref="IServiceCollection"/> instance, allowing further configuration.</returns>
|
||||
public static IServiceCollection AddCleanCRUDService<TCRUDRepository, TCreateDto, TReadDto, TUpdateDto, TEntity, TId, TProfile>(this IServiceCollection services, Action<IServiceCollection>? configureService = null)
|
||||
where TCRUDRepository : ICRUDRepository<TEntity, TId> where TCreateDto : class where TReadDto : class where TUpdateDto : class, IUnique<TId> where TEntity : class, IUnique<TId> where TProfile : Profile
|
||||
where TCRUDRepository : ICRUDRepository<TEntity, TId> where TCreateDto : class where TReadDto : class, IUnique<TId> where TEntity : class, IUnique<TId> where TProfile : Profile
|
||||
{
|
||||
services.AddScoped<ICRUDService<TCreateDto, TReadDto, TUpdateDto, TEntity, TId>, CRUDService<TCRUDRepository, TCreateDto, TReadDto, TUpdateDto, TEntity, TId>>();
|
||||
services.AddScoped<ICRUDService<TCreateDto, TReadDto, TEntity, TId>, CRUDService<TCRUDRepository, TCreateDto, TReadDto, TEntity, TId>>();
|
||||
configureService?.Invoke(services);
|
||||
|
||||
services.AddAutoMapper(typeof(TProfile).Assembly);
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackageTags>digital data core application clean architecture</PackageTags>
|
||||
<Version>2.0.0.0</Version>
|
||||
<Version>3.0.1</Version>
|
||||
<AssemblyVersion>3.0.1</AssemblyVersion>
|
||||
<FileVersion>3.0.1</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace DigitalData.Core.Security.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.Extensions
|
||||
{
|
||||
public static class RSAExtensions
|
||||
{
|
||||
public static RSA ToRSA(this string pem)
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
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;
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
return Path.Combine(directory, $"{filename}.pem");
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> FileLocks = new();
|
||||
|
||||
public static void SavePem(this IRSACryptographer decryptor, string key, string? directory = null)
|
||||
{
|
||||
var filePath = CreatePath(filename: key, directory : directory);
|
||||
var fileLock = FileLocks.GetOrAdd(filePath, _ => new (1, 1));
|
||||
fileLock.Wait();
|
||||
try
|
||||
{
|
||||
File.WriteAllText(filePath, decryptor.Pem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SavePemAsync(this IRSACryptographer decryptor, string key, string? directory = null)
|
||||
{
|
||||
var filePath = CreatePath(filename: key, directory: directory);
|
||||
var fileLock = FileLocks.GetOrAdd(filePath, _ => new (1, 1));
|
||||
await fileLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(filePath, decryptor.Pem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
DigitalData.Core.Security/Config/ClaimDescriptor.cs
Normal file
11
DigitalData.Core.Security/Config/ClaimDescriptor.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
public Func<TPrincipal, IDictionary<string, object>>? CreateClaims { get; init; }
|
||||
|
||||
public Func<TPrincipal, ClaimsIdentity>? CreateSubject { get; init; }
|
||||
}
|
||||
}
|
||||
86
DigitalData.Core.Security/Config/CryptographParams.cs
Normal file
86
DigitalData.Core.Security/Config/CryptographParams.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class CryptographParams : RSAFactoryParams
|
||||
{
|
||||
public string PemDirectory { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the separator used to concatenate the components of a file-related token string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The resulting file-related token string is constructed as follows:
|
||||
/// <c>string.Join(FileNameSeparator, 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>FileNameSeparator = "_-_"</c>, the output might look like:
|
||||
/// <c>"Issuer_-_Audience_-_Secret_version"</c>.
|
||||
/// </example>
|
||||
public string FileNameSeparator { get; init; } = "_-_";
|
||||
|
||||
public string FileExtension { get; init; } = "pem";
|
||||
|
||||
/// <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 />
|
||||
/// - <see cref="StringExtensions.ToTag(DateTime, string)" /> is used when converting to tag.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
public string DateTagFormat { get; init; } = "MM//2";
|
||||
|
||||
public IEnumerable<RSAPrivateKey> PrivateKeys { get; init; } = new List<RSAPrivateKey>();
|
||||
|
||||
public RSAPrivateKey? VaultPrivateKey { get; init; }
|
||||
|
||||
public CryptographParams()
|
||||
{
|
||||
// init decryptors
|
||||
AfterCreate += () =>
|
||||
{
|
||||
// Create root folder if it does not exist
|
||||
if (!Directory.Exists(PemDirectory))
|
||||
Directory.CreateDirectory(PemDirectory);
|
||||
|
||||
foreach (var decryptor in PrivateKeys)
|
||||
{
|
||||
// set default path
|
||||
if (decryptor.IsPemNull)
|
||||
{
|
||||
var file_name_params = new List<object> { decryptor.Issuer, decryptor.Audience, KeySizeInBits, DateTime.Now.ToTag(DateTagFormat) };
|
||||
if (decryptor.IsEncrypted)
|
||||
file_name_params.Add(Secrets.Version);
|
||||
|
||||
var file_name = $"{string.Join(FileNameSeparator, file_name_params)}.{FileExtension}";
|
||||
|
||||
var path = Path.Combine(PemDirectory, file_name);
|
||||
|
||||
if (File.Exists(path))
|
||||
decryptor.SetPem(File.ReadAllText(path));
|
||||
else
|
||||
{
|
||||
var pem = decryptor.IsEncrypted
|
||||
? Instance.RSAFactory.CreateEncryptedPrivateKeyPem(pbeParameters: PbeParameters, keySizeInBits: KeySizeInBits, password: Secrets.PBE_PASSWORD)
|
||||
: Instance.RSAFactory.CreatePrivateKeyPem(keySizeInBits: KeySizeInBits);
|
||||
|
||||
decryptor.SetPem(pem);
|
||||
|
||||
// Save file in background
|
||||
Task.Run(async () => await File.WriteAllTextAsync(path: path, pem));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
DigitalData.Core.Security/Config/MappingProfile.cs
Normal file
14
DigitalData.Core.Security/Config/MappingProfile.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<PrivateKeyTokenDescriptor, SecurityTokenDescriptor>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class ParamsConfigureOptions<TParams> : IConfigureOptions<TParams> where TParams : RSAFactoryParams
|
||||
{
|
||||
public void Configure(TParams options) => options.Init();
|
||||
}
|
||||
}
|
||||
59
DigitalData.Core.Security/Config/RSAFactoryParams.cs
Normal file
59
DigitalData.Core.Security/Config/RSAFactoryParams.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Reflection;
|
||||
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 PbeHashAlgorithm { get; init; } = HashAlgorithmName.SHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PbeHashAlgorithmName
|
||||
{
|
||||
get => PbeHashAlgorithm.ToString();
|
||||
init => PbeHashAlgorithm = (typeof(HashAlgorithmName).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) is HashAlgorithmName hashAlgorithmName)
|
||||
? hashAlgorithmName
|
||||
: new(value);
|
||||
}
|
||||
|
||||
public int PbeIterationCount { get; init; } = 100_000;
|
||||
|
||||
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
|
||||
|
||||
private PbeParameters? _pbeParameters;
|
||||
|
||||
[JsonIgnore]
|
||||
public PbeParameters PbeParameters => _pbeParameters!;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a thread-safe initialization mechanism using Lazy initialization.
|
||||
/// </summary>
|
||||
private readonly Lazy<bool> _lazyInitializer;
|
||||
|
||||
public bool IsInitialized => _lazyInitializer.IsValueCreated;
|
||||
|
||||
public RSAFactoryParams()
|
||||
{
|
||||
_lazyInitializer = new(() =>
|
||||
{
|
||||
AfterCreate?.Invoke();
|
||||
return true;
|
||||
});
|
||||
|
||||
AfterCreate += () => _pbeParameters = new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithm, PbeIterationCount);
|
||||
}
|
||||
|
||||
protected event Action AfterCreate;
|
||||
|
||||
public void Init() => _ = _lazyInitializer.Value;
|
||||
|
||||
public void OnDeserialized() => Init();
|
||||
}
|
||||
}
|
||||
@@ -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<string, IRSADecryptor> _decryptors;
|
||||
|
||||
public IRSADecryptor this[string key] { get => _decryptors[key]; set => _decryptors[key] = value; }
|
||||
|
||||
public Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
|
||||
|
||||
public CryptFactory(ILogger<CryptFactory> logger, IDictionary<string, IRSADecryptor> decryptors, Func<string, string, bool, Version?, string?, string> rsaKeyNameFormatter) : base()
|
||||
{
|
||||
_decryptors = decryptors ?? new Dictionary<string, IRSADecryptor>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
42
DigitalData.Core.Security/Cryptograph.cs
Normal file
42
DigitalData.Core.Security/Cryptograph.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
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 Cryptograph : RSAFactory<CryptographParams>, ICryptograph, IAsymmetricKeyFactory
|
||||
{
|
||||
public IEnumerable<IAsymmetricPrivateKey> PrivateKeys { get; }
|
||||
|
||||
/// <summary>
|
||||
/// It is a separate decryptor for permanently stored encrypted data. It is assigned to the first Default decryptor by default.
|
||||
/// </summary>
|
||||
public IAsymmetricPrivateKey VaultPrivateKey { get; }
|
||||
|
||||
private readonly Lazy<IEnumerable<IAsymmetricPublicKey>> _lazyPublicKeys;
|
||||
|
||||
public IEnumerable<IAsymmetricPublicKey> PublicKeys => _lazyPublicKeys.Value;
|
||||
|
||||
public IEnumerable<PrivateKeyTokenDescriptor> TokenDescriptions { get; init; } = new List<PrivateKeyTokenDescriptor>();
|
||||
|
||||
public Cryptograph(IOptions<CryptographParams> options, ILogger<Cryptograph>? logger = null) : base(options)
|
||||
{
|
||||
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
|
||||
|
||||
if (!_params.PrivateKeys.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."
|
||||
);
|
||||
|
||||
PrivateKeys = _params.PrivateKeys;
|
||||
|
||||
VaultPrivateKey = _params.VaultPrivateKey ?? PrivateKeys.First();
|
||||
|
||||
_lazyPublicKeys = new(PrivateKeys.Select(decryptor => decryptor.PublicKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,71 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddSecurity(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddScoped<ICryptFactory, CryptFactory>();
|
||||
private static IServiceCollection AddParamsConfigureOptions<TParams>(this IServiceCollection services) where TParams : RSAFactoryParams
|
||||
=> services.AddSingleton<IConfigureOptions<TParams>, ParamsConfigureOptions<TParams>>();
|
||||
|
||||
return services;
|
||||
private static IServiceCollection AddCryptograph(this IServiceCollection services) => services
|
||||
.AddParamsConfigureOptions<CryptographParams>()
|
||||
.AddAutoMapper(typeof(MappingProfile).Assembly)
|
||||
.AddSingleton<ICryptograph, Cryptograph>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="section"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddCryptograph(this IServiceCollection services, IConfigurationSection section) => services
|
||||
.Configure<CryptographParams>(section)
|
||||
.AddCryptograph();
|
||||
|
||||
/// <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 AddCryptograph(this IServiceCollection services, CryptographParams? asymCryptParams = null) => services
|
||||
.AddSingleton(Options.Create(asymCryptParams ?? new()))
|
||||
.AddCryptograph();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom RSA Factory with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="section"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
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,
|
||||
Func<TPrincipal, ClaimsIdentity>? subjectMapper = null)
|
||||
{
|
||||
var descriptor = new ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
CreateClaims = claimsMapper,
|
||||
CreateSubject = subjectMapper
|
||||
};
|
||||
|
||||
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>>();
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
98
DigitalData.Core.Security/Extension.cs
Normal file
98
DigitalData.Core.Security/Extension.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
internal static class Extension
|
||||
{
|
||||
internal static string ToBase64String(this byte[] bytes) => Convert.ToBase64String(bytes);
|
||||
|
||||
internal static byte[] Base64ToByte(this string base64String) => Convert.FromBase64String(base64String);
|
||||
|
||||
internal static byte[] ToBytes(this string str) => System.Text.Encoding.UTF8.GetBytes(str);
|
||||
|
||||
internal 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 />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateTime"/> value to be formatted.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <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>
|
||||
internal static string ToTag(this DateTime date, string format)
|
||||
{
|
||||
if (format is not null && format.Contains("//"))
|
||||
{
|
||||
var subStrings = format.Split("//");
|
||||
|
||||
if (subStrings.Length != 2)
|
||||
throw new ArgumentException($"Date tag format {format} is invalid. It must contain exactly one '//' separator.", nameof(format));
|
||||
|
||||
var formattedLeft = date.ToString(subStrings[0]);
|
||||
|
||||
if (!int.TryParse(formattedLeft, out var dateValue))
|
||||
throw new FormatException($"The left-side value ({formattedLeft}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (!int.TryParse(subStrings[1], out var divisor))
|
||||
throw new FormatException($"The right-side value ({divisor}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (divisor == 0)
|
||||
throw new DivideByZeroException($"Date tag format {format} includes division by zero, which is not allowed.");
|
||||
|
||||
var result = (dateValue - 1) / divisor + 1;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return date.ToString(format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateOnly"/> value to be formatted. It will convert to DateTime to use the method shared with DateTime.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <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>
|
||||
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, PrivateKeyTokenDescriptor description)
|
||||
=> mapper.Map(description, new SecurityTokenDescriptor());
|
||||
}
|
||||
}
|
||||
8
DigitalData.Core.Security/GlobalSuppressions.cs
Normal file
8
DigitalData.Core.Security/GlobalSuppressions.cs
Normal 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)")]
|
||||
14
DigitalData.Core.Security/Instance.cs
Normal file
14
DigitalData.Core.Security/Instance.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public static class Instance
|
||||
{
|
||||
private static readonly Lazy<RSAFactory<RSAFactoryParams>> LazyInstance = new(() => new(Options.Create<RSAFactoryParams>(new())));
|
||||
|
||||
public static IAsymmetricKeyFactory RSAFactory => LazyInstance.Value;
|
||||
}
|
||||
}
|
||||
59
DigitalData.Core.Security/JwtSignatureHandler.cs
Normal file
59
DigitalData.Core.Security/JwtSignatureHandler.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
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 ICryptograph _cryptograph;
|
||||
|
||||
public JwtSignatureHandler(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, IMapper mapper, ICryptograph cryptograph)
|
||||
{
|
||||
_claimDescriptor = claimDescriptorOptions.Value;
|
||||
_mapper = mapper;
|
||||
_cryptograph = cryptograph;
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, RSAPrivateKey key)
|
||||
{
|
||||
if(key.TokenDescriptor is null)
|
||||
throw new InvalidOperationException($"No descriptor found for issuer '{key.Issuer}' and audience '{key.Audience}'.");
|
||||
var descriptor = _mapper.Map(key.TokenDescriptor);
|
||||
descriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
|
||||
descriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
|
||||
return CreateToken(descriptor);
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, string issuer, string audience)
|
||||
{
|
||||
var key = _cryptograph.PrivateKeys?.Get(issuer: issuer, audience: audience)
|
||||
?? throw new InvalidOperationException($"No or multiple token description found for issuer '{issuer}' and audience '{audience}'.");
|
||||
return CreateToken(subject: subject, key: (RSAPrivateKey)key);
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, string apiRoute)
|
||||
{
|
||||
var key = _cryptograph.PrivateKeys.SingleOrDefault(key => ((RSAPrivateKey)key).TokenDescriptor?.ApiRoute == apiRoute)
|
||||
?? throw new InvalidOperationException($"No or multiple token description found for api route '{apiRoute}'.");
|
||||
|
||||
return CreateToken(subject: subject, key: (RSAPrivateKey)key);
|
||||
}
|
||||
|
||||
public string WriteToken(SecurityTokenDescriptor descriptor) => WriteToken(CreateToken(descriptor));
|
||||
|
||||
public string WriteToken(TPrincipal subject, RSAPrivateKey key) => WriteToken(CreateToken(subject: subject, key: key));
|
||||
|
||||
public string WriteToken(TPrincipal subject, string issuer, string audience) => WriteToken(CreateToken(subject: subject, issuer: issuer, audience: audience));
|
||||
|
||||
public string WriteToken(TPrincipal subject, string apiRoute) => WriteToken(CreateToken(subject: subject, apiRoute: apiRoute));
|
||||
}
|
||||
}
|
||||
@@ -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() { }
|
||||
}
|
||||
}
|
||||
@@ -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<IRSAEncryptor> _lazyEncryptor;
|
||||
|
||||
public IRSAEncryptor Encryptor => _lazyEncryptor.Value;
|
||||
|
||||
private readonly Lazy<RSA> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<RSAFactory> 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<string> KeyFileTags = new string[] { DefaultEncryptedPrivateKeyFileTag, DefaultPrivateKeyFileTag, DefaultPublicKeyFileTag };
|
||||
|
||||
private static readonly Lazy<IEnumerable<string>> LazyLowerFileTags = new(() => KeyFileTags.Select(tag => tag.ToLower()));
|
||||
|
||||
public static readonly string DefaultRSAKeyNameSeparator = "-_-";
|
||||
|
||||
//TODO: make the validation using regex
|
||||
public static string DefaultRSAKeyNameFormatter(string issuer, string audience, bool isPrivate = true, Version? passwordVersion = null, string? separator = null)
|
||||
{
|
||||
separator ??= DefaultRSAKeyNameSeparator;
|
||||
|
||||
void ValidateForbidden(string value, string paramName)
|
||||
{
|
||||
if (Path.GetInvalidFileNameChars().Any(value.Contains) || LazyLowerFileTags.Value.Any(tag => value.ToLower().Contains(tag)))
|
||||
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains forbidden characters that are not allowed in file naming.", paramName);
|
||||
}
|
||||
|
||||
static void ValidateSeparator(string value, string paramName, string separator)
|
||||
{
|
||||
if (value.Contains(separator))
|
||||
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains separator characters ({separator}) that are not allowed in file naming.", paramName);
|
||||
}
|
||||
|
||||
ValidateForbidden(issuer, nameof(issuer));
|
||||
ValidateForbidden(audience, nameof(audience));
|
||||
ValidateForbidden(separator, nameof(separator));
|
||||
|
||||
ValidateSeparator(issuer, nameof(issuer), separator);
|
||||
ValidateSeparator(audience, nameof(audience), separator);
|
||||
|
||||
var sb = new StringBuilder(issuer.Length + audience.Length + separator.Length * 2 + 20);
|
||||
sb.Append(issuer).Append(separator).Append(audience).Append(separator);
|
||||
|
||||
if (passwordVersion is null && isPrivate)
|
||||
sb.Append(DefaultPrivateKeyFileTag);
|
||||
else if (isPrivate)
|
||||
sb.Append(DefaultEncryptedPrivateKeyFileTag).Append(separator).Append(passwordVersion);
|
||||
else if (passwordVersion is null)
|
||||
sb.Append(DefaultPublicKeyFileTag);
|
||||
else
|
||||
sb.Append(DefaultPublicKeyFileTag).Append(separator).Append(passwordVersion);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public int KeySizeInBits { get; init; } = 2048;
|
||||
|
||||
public string PbePassword { private get; init; } = Secrets.PBE_PASSWORD;
|
||||
|
||||
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
|
||||
|
||||
public HashAlgorithmName PbeHashAlgorithmName { get; init; } = HashAlgorithmName.SHA256;
|
||||
|
||||
public int PbeIterationCount { get; init; } = 100_000;
|
||||
|
||||
private readonly Lazy<PbeParameters> _lazyPbeParameters;
|
||||
|
||||
public PbeParameters PbeParameters => _lazyPbeParameters.Value;
|
||||
|
||||
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
|
||||
|
||||
internal RSAFactory()
|
||||
{
|
||||
_lazyPbeParameters = new(() => new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount));
|
||||
}
|
||||
|
||||
public string CreateRSAPrivateKeyPem(int? keySizeInBits = null)
|
||||
=> 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<IRSADecryptor> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
68
DigitalData.Core.Security/RSAKey/RSAFactory.cs
Normal file
68
DigitalData.Core.Security/RSAKey/RSAFactory.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAFactory<TRSAFactoryParams> : IAsymmetricKeyFactory where TRSAFactoryParams : RSAFactoryParams
|
||||
{
|
||||
protected readonly TRSAFactoryParams _params;
|
||||
|
||||
public RSAFactory(IOptions<TRSAFactoryParams> options)
|
||||
{
|
||||
options.Value.Init();
|
||||
_params = options.Value;
|
||||
}
|
||||
|
||||
public string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false) => encrypt
|
||||
? CreateEncryptedPrivateKeyPem(keySizeInBits: keySizeInBits)
|
||||
: RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportRSAPrivateKeyPem();
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= _params.PbePassword;
|
||||
|
||||
var pbeParameters = pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null
|
||||
? new PbeParameters(
|
||||
pbeEncryptionAlgorithm ?? _params.PbeEncryptionAlgorithm,
|
||||
hashAlgorithmName ?? _params.PbeHashAlgorithm,
|
||||
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);
|
||||
}
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= _params.PbePassword;
|
||||
|
||||
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
|
||||
|
||||
var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
|
||||
|
||||
return new string(pemChars);
|
||||
}
|
||||
|
||||
public IAsymmetricPrivateKey CreatePrivateKey(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null) => new RSAPrivateKey()
|
||||
{
|
||||
Content = pem,
|
||||
Issuer = issuer ?? string.Empty,
|
||||
Audience = audience ?? string.Empty,
|
||||
IsEncrypted = encrypt,
|
||||
Padding = padding ?? RSAEncryptionPadding.OaepSHA256
|
||||
};
|
||||
}
|
||||
}
|
||||
38
DigitalData.Core.Security/RSAKey/RSAKeyBase.cs
Normal file
38
DigitalData.Core.Security/RSAKey/RSAKeyBase.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAKeyBase : IAsymmetricKey
|
||||
{
|
||||
public virtual string Content { 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 SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
internal RSAKeyBase()
|
||||
{
|
||||
_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.
|
||||
}
|
||||
}
|
||||
83
DigitalData.Core.Security/RSAKey/RSAPrivateKey.cs
Normal file
83
DigitalData.Core.Security/RSAKey/RSAPrivateKey.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
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; }
|
||||
|
||||
private readonly Lazy<IAsymmetricPublicKey> _lazyPublicKey;
|
||||
|
||||
public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value;
|
||||
|
||||
private PrivateKeyTokenDescriptor? _tokenDescriptor;
|
||||
|
||||
private readonly Lazy<PrivateKeyTokenDescriptor?> _descriptorInitiator;
|
||||
|
||||
public PrivateKeyTokenDescriptor? TokenDescriptor { get => _descriptorInitiator.Value; init => _tokenDescriptor = value; }
|
||||
|
||||
public RSAPrivateKey()
|
||||
{
|
||||
_lazyPublicKey = new(() => new RSAPublicKey()
|
||||
{
|
||||
Content = RSA.ExportRSAPublicKeyPem(),
|
||||
Padding = Padding
|
||||
});
|
||||
|
||||
_descriptorInitiator = new(() =>
|
||||
{
|
||||
if(_tokenDescriptor is not null)
|
||||
{
|
||||
_tokenDescriptor.Issuer = Issuer;
|
||||
_tokenDescriptor.Audience = Audience;
|
||||
_tokenDescriptor.SigningCredentials = CreateSigningCredentials();
|
||||
}
|
||||
return _tokenDescriptor;
|
||||
});
|
||||
}
|
||||
|
||||
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(Content, Secrets.PBE_PASSWORD.AsSpan());
|
||||
else
|
||||
RSA.ImportFromPem(Content);
|
||||
}
|
||||
|
||||
private InvalidOperationException PemIsNullException => new($"Content is null or empty. Issuer: {Issuer}, Audience: {Audience}.");
|
||||
|
||||
public SigningCredentials CreateSigningCredentials(string algorithm = SecurityAlgorithms.RsaSha256, string? digest = null)
|
||||
=> digest is null ? new(SecurityKey, algorithm) : new(SecurityKey, algorithm, digest);
|
||||
}
|
||||
}
|
||||
23
DigitalData.Core.Security/RSAKey/RSAPublicKey.cs
Normal file
23
DigitalData.Core.Security/RSAKey/RSAPublicKey.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Legacy.Cli
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Security", "DigitalData.Core.Security\DigitalData.Core.Security.csproj", "{47D80C65-74A2-4EB8-96A5-D571A9108FB3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Security.Extensions", "DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj", "{D740182D-82DA-480A-9F87-BFB4A8620A00}"
|
||||
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.Terminal", "DigitalData.Core.Terminal\DigitalData.Core.Terminal.csproj", "{0FA93730-8084-4907-B172-87D610323796}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Tests.API", "DigitalData.Core.Tests.API\DigitalData.Core.Tests.API.csproj", "{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -72,14 +72,14 @@ Global
|
||||
{47D80C65-74A2-4EB8-96A5-D571A9108FB3}.Debug|Any CPU.Build.0 = Debug|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
|
||||
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0FA93730-8084-4907-B172-87D610323796}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0FA93730-8084-4907-B172-87D610323796}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0FA93730-8084-4907-B172-87D610323796}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0FA93730-8084-4907-B172-87D610323796}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user