diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e2a98a3..d039a87 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -22,6 +22,13 @@ "certificate-tool" ], "rollForward": false + }, + "docfx": { + "version": "2.78.2", + "commands": [ + "docfx" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d554ec1..e2414cc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,8 +34,10 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - - run: dotnet tool update -g docfx - - run: docfx docs/docfx.json + - name: Build Documentation + run: > + dotnet tool restore --configfile nuget.config + dotnet docfx docs/docfx.json - name: Upload Artifact uses: actions/upload-pages-artifact@v3.0.1 diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/.husky/pre-push b/.husky/pre-push old mode 100644 new mode 100755 diff --git a/AdvancedSystems.Security.Abstractions/IHashService.cs b/AdvancedSystems.Security.Abstractions/IHashService.cs index cab1229..d1b87fd 100644 --- a/AdvancedSystems.Security.Abstractions/IHashService.cs +++ b/AdvancedSystems.Security.Abstractions/IHashService.cs @@ -28,7 +28,7 @@ public interface IHashService /// /// Raised if the specified is not implemented. /// - byte[] Compute(HashFunction hashFunction, byte[] buffer); + Span Compute(HashFunction hashFunction, Span buffer); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IKDFService.cs b/AdvancedSystems.Security.Abstractions/IKDFService.cs index 0804187..a566d73 100644 --- a/AdvancedSystems.Security.Abstractions/IKDFService.cs +++ b/AdvancedSystems.Security.Abstractions/IKDFService.cs @@ -1,4 +1,6 @@ -namespace AdvancedSystems.Security.Abstractions; +using System; + +namespace AdvancedSystems.Security.Abstractions; /// /// Represents a contract employing for key derivation functions. @@ -51,7 +53,7 @@ public interface IKDFService /// /// Additionally, some platforms may support SHA3-equivalent hash functions. /// - bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, out byte[]? pbkdf2); + bool TryComputePBKDF2(HashFunction hashFunction, Span password, Span salt, int hashSize, int iterations, out byte[]? pbkdf2); #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs b/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs deleted file mode 100644 index 28bda07..0000000 --- a/AdvancedSystems.Security.Abstractions/IRSACryptoService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; - -namespace AdvancedSystems.Security.Abstractions; - -/// -/// Represents a contract for performing RSA-based asymmetric operations. -/// -public interface IRSACryptoService : IDisposable -{ - #region Properties - - X509Certificate2 Certificate { get; } - - HashAlgorithmName HashAlgorithmName { get; } - - RSAEncryptionPadding EncryptionPadding { get; } - - RSASignaturePadding SignaturePadding { get; } - - Encoding Encoding { get; } - - #endregion - - #region Methods - - byte[] Encrypt(byte[] message); - - byte[] Decrypt(byte[] cipher); - - byte[] SignData(byte[] data); - - bool VerifyData(byte[] data, byte[] signature); - - #endregion -} \ No newline at end of file diff --git a/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs new file mode 100644 index 0000000..56b2f3b --- /dev/null +++ b/AdvancedSystems.Security.Abstractions/RSACryptoContract.cs @@ -0,0 +1,104 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace AdvancedSystems.Security.Abstractions; + +/// +/// Represents a contract for performing RSA-based asymmetric operations. +/// +public abstract class RSACryptoContract +{ + #region Properties + + /// + public abstract X509Certificate2 Certificate { get; } + + /// + public abstract HashFunction HashFunction { get; set; } + + /// + public abstract RSAEncryptionPadding EncryptionPadding { get; set; } + + /// + public abstract RSASignaturePadding SignaturePadding { get; set; } + + #endregion + + #region Methods + + /// + /// Encrypts the input . + /// + /// + /// The data to encrypt. + /// + /// + /// The encrypted data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the public key of the specified certificate is null. + /// + public abstract Span Encrypt(Span data); + + /// + /// Decrypts the . + /// + /// + /// The data to decrypt. + /// + /// + /// The decrypted data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the private key of the specified certificate is null. + /// + public abstract Span Decrypt(Span cipher); + + /// + /// Computes the hash value of the specified data and signs it. + /// + /// + /// The input data to hash and sign. + /// + /// + /// The RSA signature for the specified data. + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the private key of the specified certificate is null. + /// + public abstract Span SignData(Span data); + + /// + /// Verifies that a digital signature is valid by calculating the + /// hash value of the specified data using the specified hash algorithm + /// and padding, and comparing it to the provided signature. + /// + /// + /// The signed data. + /// + /// + /// The signature data to be verified. + /// + /// + /// if the signature is valid; otherwise, . + /// + /// + /// Raised if this object has already been disposed. + /// + /// + /// Raised if the public key of the specified certificate is null. + /// + public abstract bool VerifyData(Span data, Span signature); + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj index c652eeb..f13a7c1 100644 --- a/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj +++ b/AdvancedSystems.Security.Tests/AdvancedSystems.Security.Tests.csproj @@ -10,15 +10,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,7 +26,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs index 5fd78c9..37d98f3 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HMACTests.cs @@ -44,11 +44,11 @@ public sealed class HMACTests public void TestHMAC_Value(HashFunction hashFunction, string text, string expectedMac) { // Arrange - byte[] key = "secret".GetBytes(Format.String); - byte[] buffer = text.GetBytes(Format.String); + Span key = "secret".GetBytes(Format.String); + Span buffer = text.GetBytes(Format.String); // Act - byte[] actualMac = HMACProvider.Compute(hashFunction, key, buffer); + Span actualMac = HMACProvider.Compute(hashFunction, key, buffer); // Assert Assert.Equal(expectedMac.GetBytes(Format.Hex), actualMac); @@ -79,7 +79,7 @@ public void TestHMAC_Size(HashFunction hashFunction, string text) { // Arrange int keySize = 32; - byte[] buffer = text.GetBytes(Format.String); + Span buffer = text.GetBytes(Format.String); int expectedMacSize = hashFunction.GetSize(); // Act diff --git a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs index 292508e..2874968 100644 --- a/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs +++ b/AdvancedSystems.Security.Tests/Cryptography/HashTests.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System; +using System.Security.Cryptography; using System.Text; using AdvancedSystems.Security.Abstractions; @@ -37,10 +38,10 @@ public void TestMD5Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.MD5, buffer); + Span hash = HashProvider.Compute(HashFunction.MD5, buffer); string md5 = hash.ToString(format); // Assert @@ -68,10 +69,10 @@ public void TestSHA1Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA1, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA1, buffer); string sha1 = hash.ToString(format); // Assert @@ -99,10 +100,10 @@ public void TestSHA256Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA256, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA256, buffer); string sha256 = hash.ToString(format); // Assert @@ -130,10 +131,10 @@ public void TestSHA384Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA384, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA384, buffer); string sha384 = hash.ToString(format); // Assert @@ -161,10 +162,10 @@ public void TestSHA512Hash(string input, string expected, Format format) { // Arrange Encoding encoding = Encoding.UTF8; - byte[] buffer = encoding.GetBytes(input); + Span buffer = encoding.GetBytes(input); // Act - byte[] hash = HashProvider.Compute(HashFunction.SHA512, buffer); + Span hash = HashProvider.Compute(HashFunction.SHA512, buffer); string sha512 = hash.ToString(format); // Assert diff --git a/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs new file mode 100644 index 0000000..af846ef --- /dev/null +++ b/AdvancedSystems.Security.Tests/Cryptography/RSACryptoProviderTests.cs @@ -0,0 +1,67 @@ +using System; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Tests.Fixtures; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Cryptography; + +/// +/// Tests the default implementation of +/// as a provider class (). +/// +public sealed class RSACryptoProviderTests : IClassFixture +{ + private readonly RSACryptoProviderFixture _sut; + + public RSACryptoProviderTests(RSACryptoProviderFixture rsaCryptoProviderFixture) + { + this._sut = rsaCryptoProviderFixture; + } + + #region Tests + + /// + /// Tests that encrypts an array of bytes correctly + /// by using a pre-configured certificate. + /// + [Fact] + public void TestEncryptionDecryption_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + Span buffer = message.GetBytes(Format.String); + + // Act + Span cipher = this._sut.RSACryptoProvider.Encrypt(buffer); + Span source = this._sut.RSACryptoProvider.Decrypt(cipher); + string decryptedMessage = source.ToString(Format.String); + + // Assert + Assert.Equal(message, decryptedMessage); + } + + /// + /// Tests that signs and verifies an array of bytes + /// correctly by using a pre-configured certificate. + /// + [Fact] + public void TestSigningVerification_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + Span buffer = message.GetBytes(Format.String); + + // Act + Span signature = this._sut.RSACryptoProvider.SignData(buffer); + bool verified = this._sut.RSACryptoProvider.VerifyData(buffer, signature); + + // Assert + Assert.True(verified); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 3332569..8e2fd81 100644 --- a/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -1,8 +1,10 @@ -using System.Security.Cryptography.X509Certificates; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.DependencyInjection; +using AdvancedSystems.Security.Options; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -24,23 +26,20 @@ public sealed class ServiceCollectionExtensionsTests /// /// Tests that can be initialized through dependency injection. /// + /// + /// Although of little practical value, this test verifies that + /// can be registered without having a strong dependency on during + /// the initialization phase. + /// [Fact] public async Task TestAddCertificateService_FromOptions() { // Arrange - string storeService = "my/CurrentUser"; - using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() .ConfigureServices(services => { - services.AddCertificateStore(storeService, options => - { - options.Location = StoreLocation.CurrentUser; - options.Name = StoreName.My; - }); - services.AddCertificateService(); }) .Configure(app => @@ -62,7 +61,8 @@ public async Task TestAddCertificateService_FromOptions() #region AddCertificateStore Tests /// - /// Tests that can be initialized through dependency injection from configuration options. + /// Tests that can be initialized through dependency injection + /// from configuration options. /// [Fact] public async Task TestAddCertificateStore_FromOptions() @@ -96,24 +96,34 @@ public async Task TestAddCertificateStore_FromOptions() } /// - /// Tests that can be initialized through dependency injection from configuration sections. + /// Tests that can be initialized through dependency injection + /// from configuration sections. /// [Fact] public async Task TestAddCertificateStore_FromAppSettings() { // Arrange - string storeService = "my/CurrentUser"; + string section = Sections.CERTIFICATE_STORE; + string storeService = "MyStoreService"; + string storeLocation = "CurrentUser"; + string storeName = "My"; + + var appSettings = new Dictionary + { + { $"{section}:{nameof(CertificateStoreOptions.Location)}", storeLocation }, + { $"{section}:{nameof(CertificateStoreOptions.Name)}", storeName }, + }; + + var configurationRoot = new ConfigurationBuilder() + .AddInMemoryCollection(appSettings) + .Build(); using var hostBuilder = await new HostBuilder() .ConfigureWebHost(builder => builder .UseTestServer() - .ConfigureAppConfiguration(config => - { - config.AddJsonFile("appsettings.json", optional: false); - }) .ConfigureServices((context, services) => { - var storeSettings = context.Configuration.GetSection(Sections.CERTIFICATE_STORE); + var storeSettings = configurationRoot.GetRequiredSection(section); services.AddCertificateStore(storeService, storeSettings); }) .Configure(app => diff --git a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs index 09f42e6..fc2001a 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CertificateExtensionsTests.cs @@ -15,11 +15,11 @@ namespace AdvancedSystems.Security.Tests.Extensions; /// /// Tests the public methods in . /// -public sealed class CertificateExtensionsTests : IClassFixture +public sealed class CertificateExtensionsTests : IClassFixture { - private readonly CertificateFixture _certificateFixture; + private readonly HostFixture _certificateFixture; - public CertificateExtensionsTests(CertificateFixture certificateFixture) + public CertificateExtensionsTests(HostFixture certificateFixture) { this._certificateFixture = certificateFixture; } diff --git a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs index d3905f0..b7178da 100644 --- a/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs +++ b/AdvancedSystems.Security.Tests/Extensions/CoreExtensionsTests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -18,11 +19,11 @@ public sealed class CoreExtensionsTests public void TestStringFormatting(string input) { // Arrange - byte[] buffer = input.GetBytes(Format.String); + Span buffer = input.GetBytes(Format.String); // Act string fromBytes = buffer.ToString(Format.String); - byte[] @string = fromBytes.GetBytes(Format.String); + Span @string = fromBytes.GetBytes(Format.String); // Assert Assert.Equal(buffer, @string); @@ -35,11 +36,11 @@ public void TestStringFormatting(string input) public void TestBase64Formatting(int size) { // Arrange - byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + Span buffer = CryptoRandomProvider.GetBytes(size).ToArray(); // Act string base64 = buffer.ToString(Format.Base64); - byte[] fromBytes = base64.GetBytes(Format.Base64); + Span fromBytes = base64.GetBytes(Format.Base64); // Assert Assert.Equal(buffer, fromBytes); @@ -52,11 +53,11 @@ public void TestBase64Formatting(int size) public void TestHexFormatting(int size) { // Arrange - byte[] buffer = CryptoRandomProvider.GetBytes(size).ToArray(); + Span buffer = CryptoRandomProvider.GetBytes(size).ToArray(); // Act string hexadecimal = buffer.ToString(Format.Hex); - byte[] fromBytes = hexadecimal.GetBytes(Format.Hex); + Span fromBytes = hexadecimal.GetBytes(Format.Hex); // Assert Assert.Equal(buffer, fromBytes); diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs index d64cd4e..918b201 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CertificateStoreFixture.cs @@ -1,11 +1,14 @@ -using System.Security.Cryptography.X509Certificates; +using System; +using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Services; namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CertificateStoreFixture +public sealed class CertificateStoreFixture : IDisposable { + private bool _isDisposed = false; + public CertificateStoreFixture() { this.CertificateStore = new CertificateStore(StoreName.My, StoreLocation.CurrentUser); @@ -16,4 +19,26 @@ public CertificateStoreFixture() public CertificateStore CertificateStore { get; set; } #endregion + + #region Methods + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (this._isDisposed) return; + + if (disposing) + { + this.CertificateStore.Dispose(); + } + + this._isDisposed = true; + } + + #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs similarity index 79% rename from AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs index 9540026..3d5a766 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/CryptoRandomServiceFixture.cs @@ -3,9 +3,9 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CryptoRandomFixture +public sealed class CryptoRandomServiceFixture { - public CryptoRandomFixture() + public CryptoRandomServiceFixture() { this.CryptoRandomService = new CryptoRandomService(); } diff --git a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs similarity index 80% rename from AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs index 461f097..a2904d2 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/HMACFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/HMACServiceFixture.cs @@ -3,9 +3,9 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class HMACFixture +public sealed class HMACServiceFixture { - public HMACFixture() + public HMACServiceFixture() { this.HMACService = new HMACService(); } diff --git a/AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HashServiceFixture.cs similarity index 100% rename from AdvancedSystems.Security.Tests/Fixtures/HashFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HashServiceFixture.cs diff --git a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs similarity index 96% rename from AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs rename to AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs index a7717d9..788abda 100644 --- a/AdvancedSystems.Security.Tests/Fixtures/CertificateFixture.cs +++ b/AdvancedSystems.Security.Tests/Fixtures/HostFixture.cs @@ -13,7 +13,7 @@ namespace AdvancedSystems.Security.Tests.Fixtures; -public sealed class CertificateFixture : IAsyncLifetime +public sealed class HostFixture : IAsyncLifetime { #region Properties diff --git a/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs b/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs new file mode 100644 index 0000000..c4ff2fe --- /dev/null +++ b/AdvancedSystems.Security.Tests/Fixtures/RSACryptoProviderFixture.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.Tests.Helpers; + +namespace AdvancedSystems.Security.Tests.Fixtures; + +public sealed class RSACryptoProviderFixture : IDisposable +{ + public RSACryptoProviderFixture() + { + using var store = new CertificateStore(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + + this.PasswordCertificate = store.Certificates + .Find(X509FindType.FindByThumbprint, Certificates.PasswordCertificateThumbprint, validOnly: false) + .OfType() + .First(); + + this.RSACryptoProvider = new RSACryptoProvider(this.PasswordCertificate); + } + + #region Properties + + public X509Certificate2 PasswordCertificate { get; private set; } + + public RSACryptoProvider RSACryptoProvider { get; private set; } + + #endregion + + #region Methods + + public void Dispose() + { + this.PasswordCertificate.Dispose(); + this.RSACryptoProvider.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Helpers/Certificates.cs b/AdvancedSystems.Security.Tests/Helpers/Certificates.cs new file mode 100644 index 0000000..ace7269 --- /dev/null +++ b/AdvancedSystems.Security.Tests/Helpers/Certificates.cs @@ -0,0 +1,19 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace AdvancedSystems.Security.Tests.Helpers; + +internal static class Certificates +{ + internal const string PasswordCertificateThumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + + internal static X509Certificate2 CreateSelfSignedCertificate(string subject) + { + using var csdsa = ECDsa.Create(); + var request = new CertificateRequest(subject, csdsa, HashAlgorithmName.SHA256); + var validFrom = DateTimeOffset.UtcNow; + X509Certificate2 certificate = request.CreateSelfSigned(validFrom, validFrom.AddHours(1)); + return certificate; + } +} \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs index 6c7605f..cc1dc23 100644 --- a/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs +++ b/AdvancedSystems.Security.Tests/Helpers/UserSecrets.cs @@ -5,5 +5,5 @@ internal static class UserSecrets /// /// Retrieves the certificate password from the secrets store. /// - public const string CERTIFICATE_PASSWORD = "CertificatePassword"; + internal const string CERTIFICATE_PASSWORD = "CertificatePassword"; } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs index e4ad01d..80caf63 100644 --- a/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CertificateServiceTests.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using AdvancedSystems.Security.Abstractions; @@ -18,11 +17,11 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// Tests the public methods in . /// -public sealed class CertificateServiceTests : IClassFixture +public sealed class CertificateServiceTests : IClassFixture { - private readonly CertificateFixture _sut; + private readonly HostFixture _sut; - public CertificateServiceTests(CertificateFixture certificateFixture) + public CertificateServiceTests(HostFixture certificateFixture) { this._sut = certificateFixture; } @@ -123,7 +122,7 @@ public void TestGetCertificate_ByThumbprint() { // Arrange string storeService = this._sut.ConfiguredStoreService; - string thumbprint = "2BFC1C18AC1A99E4284D07F1D2F0312C8EAB33FC"; + string thumbprint = Certificates.PasswordCertificateThumbprint; // Act var certificateService = this._sut.Host?.Services.GetService(); @@ -148,7 +147,7 @@ public void TestAddRemoveCertificate() { // Arrange string storeService = this._sut.ConfiguredStoreService; - var certificate1 = CreateSelfSignedCertificate("O=AdvancedSystems"); + var certificate1 = Certificates.CreateSelfSignedCertificate("O=AdvancedSystems"); string thumbprint = certificate1.Thumbprint; // Act @@ -168,17 +167,4 @@ public void TestAddRemoveCertificate() } #endregion - - #region Helpers - - private static X509Certificate2 CreateSelfSignedCertificate(string subject) - { - using var csdsa = ECDsa.Create(); - var request = new CertificateRequest(subject, csdsa, HashAlgorithmName.SHA256); - var validFrom = DateTimeOffset.UtcNow; - X509Certificate2 certificate = request.CreateSelfSigned(validFrom, validFrom.AddHours(1)); - return certificate; - } - - #endregion } \ No newline at end of file diff --git a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs index df2f796..e10f2a1 100644 --- a/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/CryptoRandomServiceTests.cs @@ -14,11 +14,11 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// These methods are more exhaustively tested by the underlying provider class. /// -public sealed class CryptoRandomServiceTests : IClassFixture +public sealed class CryptoRandomServiceTests : IClassFixture { - private readonly CryptoRandomFixture _sut; + private readonly CryptoRandomServiceFixture _sut; - public CryptoRandomServiceTests(CryptoRandomFixture fixture) + public CryptoRandomServiceTests(CryptoRandomServiceFixture fixture) { this._sut = fixture; } diff --git a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs index 7c773ca..37c2bc5 100644 --- a/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HMACServiceTests.cs @@ -12,12 +12,12 @@ namespace AdvancedSystems.Security.Tests.Services; /// /// Tests the public methods in . /// -public sealed class HMACServiceTests : IClassFixture, IClassFixture +public sealed class HMACServiceTests : IClassFixture, IClassFixture { private readonly IHMACService _sut; private readonly ICryptoRandomService _cryptoRandomService; - public HMACServiceTests(HMACFixture fixture, CryptoRandomFixture cryptoRandomFixture) + public HMACServiceTests(HMACServiceFixture fixture, CryptoRandomServiceFixture cryptoRandomFixture) { this._sut = fixture.HMACService; this._cryptoRandomService = cryptoRandomFixture.CryptoRandomService; @@ -35,13 +35,13 @@ public void TestCompute() // Arrange var sha256 = HashFunction.SHA256; Span key = this._cryptoRandomService.GetBytes(32); - byte[] data = "Hello, World".GetBytes(Format.String); + Span data = "Hello, World".GetBytes(Format.String); // Act - byte[] mac = this._sut.Compute(sha256, key, data); + Span mac = this._sut.Compute(sha256, key, data); // Assert - Assert.NotEmpty(mac); + Assert.NotEmpty(mac.ToArray()); } #endregion diff --git a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs index 3984d09..156f033 100644 --- a/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/HashServiceTests.cs @@ -32,7 +32,7 @@ public HashServiceTests(HashServiceFixture fixture) #region Tests /// - /// Tests that returns the expected hash, + /// Tests that returns the expected hash, /// and that the log warning message is called on or . /// /// diff --git a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs index 2683601..c630e82 100644 --- a/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs +++ b/AdvancedSystems.Security.Tests/Services/KDFServiceTests.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Abstractions; +using System; + +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; using AdvancedSystems.Security.Tests.Fixtures; @@ -22,7 +24,7 @@ public KDFServiceTests(KDFServiceFixture kdfServiceFixture) #region Tests /// - /// Tests that + /// Tests that /// returns a non-empty hash with success state . /// [Fact] @@ -32,8 +34,8 @@ public void TestTryComputePBKDF2() var sha256 = HashFunction.SHA256; int iterations = 30_000; int saltSize = 128; - byte[] password = "REDACTED".GetBytes(Format.String); - byte[] salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); + Span password = "REDACTED".GetBytes(Format.String); + Span salt = CryptoRandomProvider.GetBytes(saltSize).ToArray(); // Act bool success = this._sut.TryComputePBKDF2(sha256, password, salt, sha256.GetSize(), iterations, out byte[]? pbkdf2); diff --git a/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs new file mode 100644 index 0000000..d2e7fac --- /dev/null +++ b/AdvancedSystems.Security.Tests/Services/RSACryptoServiceTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Security.Cryptography; + +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Cryptography; +using AdvancedSystems.Security.Extensions; +using AdvancedSystems.Security.Options; +using AdvancedSystems.Security.Services; +using AdvancedSystems.Security.Tests.Fixtures; +using AdvancedSystems.Security.Tests.Helpers; + +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Xunit; + +namespace AdvancedSystems.Security.Tests.Services; + +/// +/// Tests the default implementation of +/// as a service class (). +/// +public sealed class RSACryptoServiceTests : IClassFixture +{ + private readonly HostFixture _certificateFixture; + private readonly Mock _certificateService = new(); + private readonly RSACryptoService _sut; + + public RSACryptoServiceTests(HostFixture certificateFixture) + { + this._certificateFixture = certificateFixture; + + var rsaOptions = new RSACryptoOptions + { + HashFunction = HashFunction.SHA256, + EncryptionPadding = RSAEncryptionPadding.OaepSHA256, + SignaturePadding = RSASignaturePadding.Pss, + Thumbprint = Certificates.PasswordCertificateThumbprint, + StoreService = this._certificateFixture.ConfiguredStoreService, + }; + + ICertificateService certificateService = this._certificateFixture.Host?.Services.GetService() + ?? throw new InvalidOperationException($"Failed to retrieve {nameof(ICertificateService)} from DI container."); + + // NOTE: Use invalid certificates for testing purposes only + this._certificateService.Setup(x => x.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, true)) + .Returns(certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, false)); + + this._sut = new RSACryptoService( + this._certificateService.Object, + Microsoft.Extensions.Options.Options.Create(rsaOptions) + ); + + this._certificateService.VerifyAll(); + } + + #region Tests + + /// + /// Tests that encrypts an array of bytes correctly + /// by using a pre-configured certificate. + /// + [Fact] + public void TestEncryptionDecryption_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + Span buffer = message.GetBytes(Format.String); + + // Act + Span cipher = this._sut.Encrypt(buffer); + Span source = this._sut.Decrypt(cipher); + string decryptedMessage = source.ToString(Format.String); + + // Assert + Assert.Equal(message, decryptedMessage); + } + + /// + /// Tests that signs and verifies an array of bytes + /// correctly by using a pre-configured certificate. + /// + [Fact] + public void TestSigningVerification_Roundtrip() + { + // Arrange + string message = "Hello, World!"; + Span buffer = message.GetBytes(Format.String); + + // Act + Span signature = this._sut.SignData(buffer); + bool verified = this._sut.VerifyData(buffer, signature); + + // Assert + Assert.True(verified); + } + + #endregion +} \ No newline at end of file diff --git a/AdvancedSystems.Security/AdvancedSystems.Security.csproj b/AdvancedSystems.Security/AdvancedSystems.Security.csproj index 5c765a1..857521f 100644 --- a/AdvancedSystems.Security/AdvancedSystems.Security.csproj +++ b/AdvancedSystems.Security/AdvancedSystems.Security.csproj @@ -13,12 +13,12 @@ - + - - + + diff --git a/AdvancedSystems.Security/Cryptography/HashProvider.cs b/AdvancedSystems.Security/Cryptography/HashProvider.cs index a96fa9b..b88638d 100644 --- a/AdvancedSystems.Security/Cryptography/HashProvider.cs +++ b/AdvancedSystems.Security/Cryptography/HashProvider.cs @@ -10,8 +10,8 @@ namespace AdvancedSystems.Security.Cryptography; /// public static class HashProvider { - /// - public static byte[] Compute(HashFunction hashFunction, byte[] buffer) + /// + public static Span Compute(HashFunction hashFunction, Span buffer) { return hashFunction switch { diff --git a/AdvancedSystems.Security/Cryptography/KDFProvider.cs b/AdvancedSystems.Security/Cryptography/KDFProvider.cs index 41dbc5f..c809bfa 100644 --- a/AdvancedSystems.Security/Cryptography/KDFProvider.cs +++ b/AdvancedSystems.Security/Cryptography/KDFProvider.cs @@ -12,7 +12,7 @@ namespace AdvancedSystems.Security.Cryptography; /// public static class KDFProvider { - /// + /// public static bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { try diff --git a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs index 656910e..a4cf147 100644 --- a/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs +++ b/AdvancedSystems.Security/Cryptography/RSACryptoProvider.cs @@ -1,112 +1,116 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; -using AdvancedSystems.Core.Extensions; +using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Extensions; namespace AdvancedSystems.Security.Cryptography; /// /// Represents a class for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoProvider +public sealed class RSACryptoProvider : RSACryptoContract, IDisposable { - private static readonly HashAlgorithmName DEFAULT_HASH_ALGORITHM_NAME = HashAlgorithmName.SHA256; - private static readonly RSAEncryptionPadding DEFAULT_RSA_ENCRYPTION_PADDING = RSAEncryptionPadding.OaepSHA256; - private static readonly RSASignaturePadding DEFAULT_RSA_SIGNATURE_PADDING = RSASignaturePadding.Pss; - private static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8; - - public RSACryptoProvider(X509Certificate2 certificate, HashAlgorithmName hashAlgorithm, RSAEncryptionPadding encryptionPadding, RSASignaturePadding signaturePadding, Encoding encoding) - { - this.Certificate = certificate; - this.HashAlgorithmName = hashAlgorithm; - this.EncryptionPadding = encryptionPadding; - this.SignaturePadding = signaturePadding; - this.Encoding = encoding; - } + private bool _isDisposed = false; public RSACryptoProvider(X509Certificate2 certificate) { this.Certificate = certificate; - this.HashAlgorithmName = RSACryptoProvider.DEFAULT_HASH_ALGORITHM_NAME; - this.EncryptionPadding = RSACryptoProvider.DEFAULT_RSA_ENCRYPTION_PADDING; - this.SignaturePadding = RSACryptoProvider.DEFAULT_RSA_SIGNATURE_PADDING; - this.Encoding = RSACryptoProvider.DEFAULT_ENCODING; } #region Properties - public X509Certificate2 Certificate { get; private set; } - - public HashAlgorithmName HashAlgorithmName { get; set; } + /// + public override X509Certificate2 Certificate { get; } - public RSAEncryptionPadding EncryptionPadding { get; set; } + /// + public override HashFunction HashFunction { get; set; } = HashFunction.SHA256; - public RSASignaturePadding SignaturePadding { get; set; } + /// + public override RSAEncryptionPadding EncryptionPadding { get; set; } = RSAEncryptionPadding.OaepSHA256; - public Encoding Encoding { get; set; } + /// + public override RSASignaturePadding SignaturePadding { get; set; } = RSASignaturePadding.Pss; #endregion - #region Public Methods + #region Methods + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (this._isDisposed) return; + + if (disposing) + { + this.Certificate.Dispose(); + } + + this._isDisposed = true; + } - public string Encrypt(string message, Encoding? encoding = null) + /// + public override Span Encrypt(Span data) { - encoding ??= this.Encoding; + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); - using RSA? publicKey = this.Certificate.GetRSAPublicKey(); - ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); + using RSA publicKey = this.Certificate.GetRSAPublicKey() + ?? throw new CryptographicException("Public Key is null."); - byte[] buffer = encoding.GetBytes(message); - byte[] cipher = publicKey.Encrypt(buffer, this.EncryptionPadding); - return Convert.ToBase64String(cipher); + Span cipher = publicKey.Encrypt(data, this.EncryptionPadding); + return cipher; } - public string Decrypt(string cipher, Encoding? encoding = null) + /// + public override Span Decrypt(Span cipher) { + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + if (!this.Certificate.HasPrivateKey) { - throw new CryptographicException($"Certificate with thumbprint '{this.Certificate.Thumbprint}' has no private key."); + throw new CryptographicException($"Certificate with thumbprint \"{this.Certificate.Thumbprint}\" has no private key."); } - encoding ??= this.Encoding; - - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); - ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); + using RSA privateKey = this.Certificate.GetRSAPrivateKey() + ?? throw new CryptographicException("Private Key is null."); - byte[] buffer = Convert.FromBase64String(cipher); - byte[] source = privateKey.Decrypt(buffer, this.EncryptionPadding); - return encoding.GetString(source); + Span source = privateKey.Decrypt(cipher, this.EncryptionPadding); + return source; } - public string SignData(string data, Encoding? encoding = null) + /// + public override Span SignData(Span data) { - if (data.IsNullOrEmpty()) + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + if (!this.Certificate.HasPrivateKey) { - throw new ArgumentNullException(nameof(data)); + throw new CryptographicException($"Certificate with thumbprint \"{this.Certificate.Thumbprint}\" has no private key."); } - using RSA? privateKey = this.Certificate.GetRSAPrivateKey(); - ArgumentNullException.ThrowIfNull(privateKey, nameof(privateKey)); - - encoding ??= this.Encoding; - byte[] buffer = encoding.GetBytes(data); + using RSA privateKey = this.Certificate.GetRSAPrivateKey() + ?? throw new CryptographicException("Private Key is null."); - byte[] signature = privateKey.SignData(buffer, this.HashAlgorithmName, this.SignaturePadding); - return Convert.ToBase64String(signature); + Span signature = privateKey.SignData(data, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); + return signature; } - public bool VerifyData(string data, string signature, Encoding? encoding = null) + /// + public override bool VerifyData(Span data, Span signature) { - using RSA? publicKey = this.Certificate.GetRSAPublicKey(); - ArgumentNullException.ThrowIfNull(publicKey, nameof(publicKey)); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); - encoding ??= this.Encoding; - byte[] buffer = encoding.GetBytes(data); - byte[] signedBuffer = Convert.FromBase64String(signature); + using RSA publicKey = this.Certificate.GetRSAPublicKey() + ?? throw new CryptographicException("Public Key is null."); - bool isVerified = publicKey.VerifyData(buffer, signedBuffer, this.HashAlgorithmName, this.SignaturePadding); + bool isVerified = publicKey.VerifyData(data, signature, this.HashFunction.ToHashAlgorithmName(), this.SignaturePadding); return isVerified; } diff --git a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs index 714f54c..7687d31 100644 --- a/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs +++ b/AdvancedSystems.Security/DependencyInjection/ServiceCollectionExtensions.cs @@ -177,11 +177,13 @@ public static IServiceCollection AddKDFService(this IServiceCollection services) private static IServiceCollection AddRSACryptoService(this IServiceCollection services) { - throw new NotImplementedException(); + services.TryAdd(ServiceDescriptor.Transient()); + + return services; } /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. @@ -200,7 +202,7 @@ public static IServiceCollection AddRSACryptoService(this IServiceCollection ser } /// - /// Adds the default implementation of to . + /// Adds the default implementation of to . /// /// /// The service collection containing the service. diff --git a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs index c0dc448..04c43a6 100644 --- a/AdvancedSystems.Security/Extensions/CertificateExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CertificateExtensions.cs @@ -11,7 +11,7 @@ namespace AdvancedSystems.Security.Extensions; /// Defines functions for interacting with X.509 certificates. /// /// -public static partial class CertificateExtensions +public static class CertificateExtensions { /// /// Attempts to parse the specified distinguished name (DN) string into a object. diff --git a/AdvancedSystems.Security/Extensions/CoreExtensions.cs b/AdvancedSystems.Security/Extensions/CoreExtensions.cs index 86327d8..e8cb9de 100644 --- a/AdvancedSystems.Security/Extensions/CoreExtensions.cs +++ b/AdvancedSystems.Security/Extensions/CoreExtensions.cs @@ -11,7 +11,7 @@ namespace AdvancedSystems.Security.Extensions; /// public static class CoreExtensions { - public static string ToString(this byte[] array, Format format) + public static string ToString(this Span array, Format format) { return format switch { @@ -22,7 +22,7 @@ public static string ToString(this byte[] array, Format format) }; } - public static byte[] GetBytes(this string @string, Format format) + public static Span GetBytes(this string @string, Format format) { return format switch { diff --git a/AdvancedSystems.Security/Extensions/StringExtensions.cs b/AdvancedSystems.Security/Extensions/StringExtensions.cs deleted file mode 100644 index 1aba33b..0000000 --- a/AdvancedSystems.Security/Extensions/StringExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Text; - -using AdvancedSystems.Security.Cryptography; - -namespace AdvancedSystems.Security.Extensions; - -public static class StringExtensions -{ - -} \ No newline at end of file diff --git a/AdvancedSystems.Security/Options/RSACryptoOptions.cs b/AdvancedSystems.Security/Options/RSACryptoOptions.cs index 6cbea4a..6f7c985 100644 --- a/AdvancedSystems.Security/Options/RSACryptoOptions.cs +++ b/AdvancedSystems.Security/Options/RSACryptoOptions.cs @@ -1,23 +1,43 @@ using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; -using System.Text; + +using AdvancedSystems.Security.Abstractions; namespace AdvancedSystems.Security.Options; +/// +/// Configures options for the . +/// public sealed record RSACryptoOptions { + /// + /// + /// [Required] - public required HashAlgorithmName HashAlgorithmName { get; set; } + [EnumDataType(typeof(HashFunction))] + public required HashFunction HashFunction { get; set; } + /// + /// + /// [Required] public required RSAEncryptionPadding EncryptionPadding { get; set; } + /// + /// + /// [Required] public required RSASignaturePadding SignaturePadding { get; set; } + /// + /// The string representing the thumbprint of the encryption certificate to retrieve. + /// [Required] - public required Encoding Encoding { get; set; } + public required string Thumbprint { get; set; } + /// + /// + /// [Required] - public required string Thumbprint { get; set; } + public required string StoreService { get; set; } } \ No newline at end of file diff --git a/AdvancedSystems.Security/Services/CertificateService.cs b/AdvancedSystems.Security/Services/CertificateService.cs index e570f05..73651af 100644 --- a/AdvancedSystems.Security/Services/CertificateService.cs +++ b/AdvancedSystems.Security/Services/CertificateService.cs @@ -160,7 +160,7 @@ public IEnumerable GetCertificate(string storeService) } catch (ArgumentNullException) { - return Enumerable.Empty(); + return []; } finally { diff --git a/AdvancedSystems.Security/Services/HashService.cs b/AdvancedSystems.Security/Services/HashService.cs index 52e35f8..c468478 100644 --- a/AdvancedSystems.Security/Services/HashService.cs +++ b/AdvancedSystems.Security/Services/HashService.cs @@ -1,4 +1,6 @@ -using AdvancedSystems.Security.Abstractions; +using System; + +using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Extensions; @@ -21,7 +23,7 @@ public HashService(ILogger logger) #region Methods /// - public byte[] Compute(HashFunction hashFunction, byte[] buffer) + public Span Compute(HashFunction hashFunction, Span buffer) { if (hashFunction is HashFunction.MD5 or HashFunction.SHA1) { diff --git a/AdvancedSystems.Security/Services/KDFService.cs b/AdvancedSystems.Security/Services/KDFService.cs index e445215..05d77d5 100644 --- a/AdvancedSystems.Security/Services/KDFService.cs +++ b/AdvancedSystems.Security/Services/KDFService.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using AdvancedSystems.Security.Abstractions; using AdvancedSystems.Security.Cryptography; @@ -13,9 +14,9 @@ public sealed class KDFService : IKDFService #region Methods /// - public bool TryComputePBKDF2(HashFunction hashFunction, byte[] password, byte[] salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) + public bool TryComputePBKDF2(HashFunction hashFunction, Span password, Span salt, int hashSize, int iterations, [NotNullWhen(true)] out byte[]? pbkdf2) { - return KDFProvider.TryComputePBKDF2(hashFunction, password, salt, hashSize, iterations, out pbkdf2); + return KDFProvider.TryComputePBKDF2(hashFunction, password.ToArray(), salt.ToArray(), hashSize, iterations, out pbkdf2); } #endregion diff --git a/AdvancedSystems.Security/Services/RSACryptoService.cs b/AdvancedSystems.Security/Services/RSACryptoService.cs index 2fb4368..8ce38c2 100644 --- a/AdvancedSystems.Security/Services/RSACryptoService.cs +++ b/AdvancedSystems.Security/Services/RSACryptoService.cs @@ -1,13 +1,12 @@ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Text; using AdvancedSystems.Security.Abstractions; +using AdvancedSystems.Security.Abstractions.Exceptions; using AdvancedSystems.Security.Cryptography; using AdvancedSystems.Security.Options; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace AdvancedSystems.Security.Services; @@ -15,78 +14,67 @@ namespace AdvancedSystems.Security.Services; /// /// Represents a service for performing RSA-based asymmetric operations. /// -public sealed class RSACryptoService +public sealed class RSACryptoService : RSACryptoContract, IDisposable { - private readonly ILogger _logger; - private readonly ICertificateService _certificateService; - private readonly RSACryptoOptions _rsaOptions; - - private bool _disposed = false; - private readonly X509Certificate2 _certificate; + private bool _isDisposed = false; private readonly RSACryptoProvider _provider; - public RSACryptoService(ILogger logger, ICertificateService certificateService, IOptions rsaOptions) + public RSACryptoService(ICertificateService certificateService, IOptions options) { - this._logger = logger; - this._certificateService = certificateService; - this._rsaOptions = rsaOptions.Value; - - this._certificate = this._certificateService.GetCertificate("default", this._rsaOptions.Thumbprint, validOnly: true) - ?? throw new ArgumentNullException(); - - this._provider = new RSACryptoProvider( - this._certificate, - this._rsaOptions.HashAlgorithmName, - this._rsaOptions.EncryptionPadding, - this._rsaOptions.SignaturePadding, - this._rsaOptions.Encoding - ); + RSACryptoOptions rsaOptions = options.Value; + + this.Certificate = certificateService.GetCertificate(rsaOptions.StoreService, rsaOptions.Thumbprint, validOnly: true) + ?? throw new CertificateNotFoundException($"Failed to retrieve certificate with options {nameof(RSACryptoOptions.StoreService)}=\"{rsaOptions.StoreService}\" and {nameof(RSACryptoOptions.Thumbprint)}=\"{rsaOptions.Thumbprint}\"."); + + this._provider = new RSACryptoProvider(this.Certificate) + { + HashFunction = rsaOptions.HashFunction, + EncryptionPadding = rsaOptions.EncryptionPadding, + SignaturePadding = rsaOptions.SignaturePadding + }; } #region Properties - /// - public X509Certificate2 Certificate + /// + public override X509Certificate2 Certificate { get; } + + /// + public override HashFunction HashFunction { get { - return this._certificate; + return this._provider.HashFunction; } - } - - /// - public HashAlgorithmName HashAlgorithmName - { - get + set { - return this._provider.HashAlgorithmName; + this._provider.HashFunction = value; } } - /// - public RSAEncryptionPadding EncryptionPadding + /// + public override RSAEncryptionPadding EncryptionPadding { get { return this._provider.EncryptionPadding; } + set + { + this._provider.EncryptionPadding = value; + } } - /// - public RSASignaturePadding SignaturePadding + /// + public override RSASignaturePadding SignaturePadding { get { return this._provider.SignaturePadding; } - } - - /// - public Encoding Encoding - { - get + set { - return this._provider.Encoding; + this._provider.SignaturePadding = value; } } @@ -94,45 +82,56 @@ public Encoding Encoding #region Methods - /// - + /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } - /// - public void Dispose(bool disposing) + private void Dispose(bool disposing) { - if (this._disposed || !disposing) return; + if (this._isDisposed) return; - this._certificate.Dispose(); - this._disposed = true; + if (disposing) + { + this.Certificate.Dispose(); + this._provider.Dispose(); + } + + this._isDisposed = true; } - /// - public string Encrypt(string message, Encoding? encoding = null) + /// + public override Span Encrypt(Span buffer) { - return this._provider.Encrypt(message, encoding); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + return this._provider.Encrypt(buffer); } - /// - public string Decrypt(string cipher, Encoding? encoding = null) + /// + public override Span Decrypt(Span cipher) { - return this._provider.Decrypt(cipher, encoding); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + return this._provider.Decrypt(cipher); } - /// - public string SignData(string data, Encoding? encoding = null) + /// + public override Span SignData(Span data) { - return this._provider.SignData(data, encoding); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + return this._provider.SignData(data); } - /// - public bool VerifyData(string data, string signature, Encoding? encoding = null) + /// + public override bool VerifyData(Span data, Span signature) { - return this._provider.VerifyData(data, signature, encoding); + ObjectDisposedException.ThrowIf(this._isDisposed, nameof(this.Certificate)); + + return this._provider.VerifyData(data, signature); } #endregion diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8d7194e --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +MAIN_PROJECT = ./AdvancedSystems.Security +TEST_PROJECT = ./AdvancedSystems.Security.Tests + +import-certificate: + # password certificate authority + dotnet certificate-tool add --file ./development/AdvancedSystems-CA.pfx \ + --store-name My \ + --store-location CurrentUser \ + --password $(password); + + # password certificate + # TODO: read values from appsettings.json + dotnet certificate-tool add --file ./development/AdvancedSystems-PasswordCertificate.pem \ + --store-name My \ + --store-location CurrentUser; + +install: import-certificate + # main project + dotnet restore --configfile nuget.config + dotnet tool restore --configfile nuget.config + dotnet husky install + + # unit test project + dotnet user-secrets init --project $(TEST_PROJECT) + dotnet user-secrets set CertificatePassword $(password) --project $(TEST_PROJECT) + +build: + dotnet build $(MAIN_PROJECT) --configuration $(configuration) --no-restore /warnAsError + +test: build + dotnet test $(TEST_PROJECT) --configuration $(configuration) --verbosity normal + +lint: + dotnet format + +documentation: + dotnet tool restore --configfile nuget.config + + if [ "$(serve)" = "true" ]; then \ + dotnet docfx ./docs/docfx.json --serve --open-browser; \ + else \ + dotnet docfx ./docs/docfx.json; \ + fi + +clean: + dotnet clean + dotnet clean --configuration Release + @clear + +$(V).SILENT: \ No newline at end of file diff --git a/readme.md b/readme.md index 764c837..ffd74d5 100644 --- a/readme.md +++ b/readme.md @@ -34,21 +34,17 @@ for debugging .NET assemblies. Configure local user secrets for the test suite (optional): ```powershell -$Password = Read-Host -Prompt "AdvancedSystems-CA.pfx Password" -dotnet user-secrets set CertificatePassword $Password --project ./AdvancedSystems.Tests +$Password = Read-Host "Password" -MaskInput +make install password=$Password ``` Run test suite: ```powershell -dotnet test ./AdvancedSystems.Core.Tests --configuration Release +make test configuration=Release ``` -In addition to unit testing, this project also uses stryker for mutation testing, which is setup to be installed with - -```powershell -dotnet tool restore --configfile nuget.config -``` +In addition to unit testing, this project also uses stryker for mutation testing, which is set up to be installed with Run stryker locally: @@ -56,8 +52,8 @@ Run stryker locally: dotnet stryker ``` -Build and serve documentation locally (`http://localhost:8080`): +Build and serve documentation locally: ```powershell -docfx ./docs/docfx.json --serve +make documentation serve=true ```