diff --git a/Astral.Services/Constants/PublicKeyType.cs b/Astral.Services/Constants/PublicKeyType.cs
new file mode 100644
index 0000000..846ff42
--- /dev/null
+++ b/Astral.Services/Constants/PublicKeyType.cs
@@ -0,0 +1,21 @@
+//
+// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
+//
+
+namespace Astral.Services.Constants;
+
+///
+/// Public key types.
+///
+public enum PublicKeyType
+{
+ ///
+ /// SPKI/x509 format.
+ ///
+ SpkiX509PublicKey,
+
+ ///
+ /// PKCS #1 type.
+ ///
+ Pkcs1PublicKey
+}
diff --git a/Astral.Services/Interfaces/ICryptographyService.cs b/Astral.Services/Interfaces/ICryptographyService.cs
index f09911a..c1a2074 100644
--- a/Astral.Services/Interfaces/ICryptographyService.cs
+++ b/Astral.Services/Interfaces/ICryptographyService.cs
@@ -2,6 +2,8 @@
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
//
+using Astral.Services.Constants;
+
namespace Astral.Services.Interfaces;
///
@@ -48,4 +50,19 @@ public interface ICryptographyService
/// Length of string to create.
/// A random string.
string GenerateRandomString(int length);
+
+ ///
+ /// Convert public key.
+ ///
+ /// PKCS #1 key.
+ /// The type to convert to.
+ /// The converted key.
+ string ConvertPublicKey(byte[] pkcs1Key, PublicKeyType type);
+
+ ///
+ /// Strip out header, footer and newlines from PEM RSA key.
+ ///
+ /// Key to simplify.
+ /// Simplified form.
+ string SimplifyPemKey(string pemKey);
}
diff --git a/Astral.Services/Services/CryptographyService.cs b/Astral.Services/Services/CryptographyService.cs
index 56a4042..323e6a9 100644
--- a/Astral.Services/Services/CryptographyService.cs
+++ b/Astral.Services/Services/CryptographyService.cs
@@ -4,10 +4,12 @@
using System.Security.Cryptography;
using System.Text;
+using Astral.Services.Constants;
using Astral.Services.Interfaces;
using Astral.Services.Options;
using Injectio.Attributes;
using Konscious.Security.Cryptography;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Astral.Services.Services;
@@ -17,14 +19,19 @@ namespace Astral.Services.Services;
public class CryptographyService : ICryptographyService
{
private readonly PwdHashOptions _configuration;
+ private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// Instance of .
- public CryptographyService(IOptions pwdHashSettings)
+ /// Instance of .
+ public CryptographyService(
+ IOptions pwdHashSettings,
+ ILogger logger)
{
_configuration = pwdHashSettings.Value;
+ _logger = logger;
}
///
@@ -67,4 +74,52 @@ public class CryptographyService : ICryptographyService
const string availableChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
return RandomNumberGenerator.GetString(availableChars, length);
}
+
+ ///
+ public string ConvertPublicKey(byte[] pkcs1Key, PublicKeyType type)
+ {
+ try
+ {
+ var rsa = RSA.Create();
+ var bytesRead = 0;
+ switch (type)
+ {
+ case PublicKeyType.SpkiX509PublicKey:
+ rsa.ImportSubjectPublicKeyInfo(pkcs1Key, out bytesRead);
+ break;
+ case PublicKeyType.Pkcs1PublicKey:
+ rsa.ImportRSAPublicKey(pkcs1Key, out bytesRead);
+ break;
+ }
+ var pem = "";
+ if (bytesRead == 0)
+ {
+ _logger.LogError(
+ "An error occured converting RSA public key from binary to SPKI (PEM). Bytes read: 0");
+ }
+ else
+ {
+ pem = rsa.ExportSubjectPublicKeyInfoPem();
+ }
+
+ rsa.Clear();
+ return pem;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError("An error occured converting RSA public key from binary to SPKI (PEM). {exception}", e);
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ public string SimplifyPemKey(string pemKey)
+ {
+ pemKey = pemKey.Replace("-----BEGIN PUBLIC KEY-----", string.Empty);
+ pemKey = pemKey.Replace("-----END PUBLIC KEY-----", string.Empty);
+ pemKey = pemKey.Replace("\r", string.Empty);
+ pemKey = pemKey.Replace("\n", string.Empty);
+ return pemKey;
+ }
}
diff --git a/Astral.Tests/Astral.Tests.csproj b/Astral.Tests/Astral.Tests.csproj
new file mode 100644
index 0000000..bd091ac
--- /dev/null
+++ b/Astral.Tests/Astral.Tests.csproj
@@ -0,0 +1,42 @@
+
+
+
+ net8.0
+ enable
+ ../Alveus.ruleset
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/Astral.Tests/GlobalUsings.cs b/Astral.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..8c927eb
--- /dev/null
+++ b/Astral.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/Astral.Tests/ServiceTests/CryptographyServiceTests.cs b/Astral.Tests/ServiceTests/CryptographyServiceTests.cs
new file mode 100644
index 0000000..5e9f13f
--- /dev/null
+++ b/Astral.Tests/ServiceTests/CryptographyServiceTests.cs
@@ -0,0 +1,117 @@
+//
+// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
+//
+
+using Astral.Services.Constants;
+using Astral.Services.Interfaces;
+using Astral.Services.Options;
+using Astral.Services.Services;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Astral.Tests.ServiceTests;
+
+///
+/// Tests for .
+///
+public class CryptographyServiceTests
+{
+ private readonly CryptographyService _service;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CryptographyServiceTests()
+ {
+ var pwdHashOptions = new PwdHashOptions()
+ {
+ DegreeOfParallelism = 4,
+ HashSize = 128,
+ SaltSize = 64,
+ MemoryToUseKb = 16,
+ NumberOfIterations = 3
+ };
+
+ _service = new CryptographyService(
+ new OptionsWrapper(pwdHashOptions),
+ new NullLogger());
+ }
+
+ ///
+ /// Password hashing and verification tests.
+ ///
+ [Fact]
+ public void HashingAndVerificationTests()
+ {
+ const string password = "Password1234!";
+ const string badPassword = "Password123!";
+
+ var salt1 = _service.GenerateSalt();
+ var salt2 = _service.GenerateSalt();
+ var salt3 = _service.GenerateSalt();
+
+ var hash1 = _service.HashPassword(password, salt1);
+ var hash2 = _service.HashPassword(password, salt2);
+ var hash3 = _service.HashPassword(password, salt3);
+
+ Assert.Multiple(() =>
+ {
+ Assert.NotNull(hash1);
+ Assert.NotNull(hash2);
+ Assert.NotNull(hash3);
+ });
+
+ // Ensure unique hashes.
+ Assert.Multiple(() =>
+ {
+ Assert.NotEqual(hash1, hash2);
+ Assert.NotEqual(hash1, hash3);
+ Assert.NotEqual(hash2, hash3);
+ });
+
+ // Ensure passwords can be verified.
+ Assert.Multiple(() =>
+ {
+ Assert.True(_service.VerifyPassword(password, salt1, hash1));
+ Assert.True(_service.VerifyPassword(password, salt2, hash2));
+ Assert.True(_service.VerifyPassword(password, salt3, hash3));
+ });
+
+ // Ensure bad password/hash combinations do not get verified.
+ Assert.Multiple(() =>
+ {
+ Assert.False(_service.VerifyPassword(password, salt1, hash2));
+ Assert.False(_service.VerifyPassword(password, salt1, hash3));
+ Assert.False(_service.VerifyPassword(password, salt2, hash1));
+ Assert.False(_service.VerifyPassword(password, salt2, hash3));
+ Assert.False(_service.VerifyPassword(password, salt3, hash1));
+ Assert.False(_service.VerifyPassword(password, salt3, hash2));
+ });
+
+ // Ensure bad password do not get verified.
+ Assert.Multiple(() =>
+ {
+ Assert.False(_service.VerifyPassword(badPassword, salt1, hash1));
+ Assert.False(_service.VerifyPassword(badPassword, salt2, hash2));
+ Assert.False(_service.VerifyPassword(badPassword, salt3, hash3));
+ });
+ }
+
+ ///
+ /// Public key conversion tests.
+ ///
+ [Fact]
+ public async Task PublicKeyConversionTests()
+ {
+ var publicKeyDer = await File.ReadAllBytesAsync("./TestData/public-key.der");
+ var publicKeyPem = await File.ReadAllTextAsync("./TestData/public-key.pem");
+
+ publicKeyPem = _service.SimplifyPemKey(publicKeyPem);
+
+ var generatedPem = _service.ConvertPublicKey(publicKeyDer, PublicKeyType.SpkiX509PublicKey);
+
+ generatedPem = _service.SimplifyPemKey(generatedPem);
+
+ Assert.Equal(generatedPem, publicKeyPem);
+ }
+}
diff --git a/Astral.Tests/TestData/public-key.der b/Astral.Tests/TestData/public-key.der
new file mode 100644
index 0000000..dc967eb
Binary files /dev/null and b/Astral.Tests/TestData/public-key.der differ
diff --git a/Astral.Tests/TestData/public-key.pem b/Astral.Tests/TestData/public-key.pem
new file mode 100644
index 0000000..76a5fc5
--- /dev/null
+++ b/Astral.Tests/TestData/public-key.pem
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQr8BExTMA6CREusv/SoiwlvMM
+cvDNvPIuo0Tm70Kmm/Slle0pjszdHFbR3rUiniyhlmVIuu7hEkzMOIzE4KZ9cnOC
+1DtkeEOhQkF2l3A4HX4OYPzUPzv89Hj+jAFI+LbBtzLb7wf2b4our+Z44w+i1YWN
+ff59tv8PqXa/wuSD/QIDAQAB
+-----END PUBLIC KEY-----
diff --git a/AstralApi.sln b/AstralApi.sln
index b5c4f2a..426f74e 100644
--- a/AstralApi.sln
+++ b/AstralApi.sln
@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{EB
README.md = README.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.Tests", "Astral.Tests\Astral.Tests.csproj", "{F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -37,5 +39,9 @@ Global
{7594740E-7061-46C9-A870-7E3687D3BC36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7594740E-7061-46C9-A870-7E3687D3BC36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7594740E-7061-46C9-A870-7E3687D3BC36}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal