Extend CryptographyService.cs and add tests

This commit is contained in:
Mike 2024-12-14 18:50:32 +00:00
parent 20fea63405
commit 84cf848e90
9 changed files with 266 additions and 1 deletions

View file

@ -0,0 +1,21 @@
// <copyright file="PublicKeyType.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.Services.Constants;
/// <summary>
/// Public key types.
/// </summary>
public enum PublicKeyType
{
/// <summary>
/// SPKI/x509 format.
/// </summary>
SpkiX509PublicKey,
/// <summary>
/// PKCS #1 type.
/// </summary>
Pkcs1PublicKey
}

View file

@ -2,6 +2,8 @@
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
using Astral.Services.Constants;
namespace Astral.Services.Interfaces; namespace Astral.Services.Interfaces;
/// <summary> /// <summary>
@ -48,4 +50,19 @@ public interface ICryptographyService
/// <param name="length">Length of string to create.</param> /// <param name="length">Length of string to create.</param>
/// <returns>A random string.</returns> /// <returns>A random string.</returns>
string GenerateRandomString(int length); string GenerateRandomString(int length);
/// <summary>
/// Convert public key.
/// </summary>
/// <param name="pkcs1Key">PKCS #1 key.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>The converted key.</returns>
string ConvertPublicKey(byte[] pkcs1Key, PublicKeyType type);
/// <summary>
/// Strip out header, footer and newlines from PEM RSA key.
/// </summary>
/// <param name="pemKey">Key to simplify.</param>
/// <returns>Simplified form.</returns>
string SimplifyPemKey(string pemKey);
} }

View file

@ -4,10 +4,12 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Astral.Services.Constants;
using Astral.Services.Interfaces; using Astral.Services.Interfaces;
using Astral.Services.Options; using Astral.Services.Options;
using Injectio.Attributes; using Injectio.Attributes;
using Konscious.Security.Cryptography; using Konscious.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Astral.Services.Services; namespace Astral.Services.Services;
@ -17,14 +19,19 @@ namespace Astral.Services.Services;
public class CryptographyService : ICryptographyService public class CryptographyService : ICryptographyService
{ {
private readonly PwdHashOptions _configuration; private readonly PwdHashOptions _configuration;
private readonly ILogger<CryptographyService> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CryptographyService" /> class. /// Initializes a new instance of the <see cref="CryptographyService" /> class.
/// </summary> /// </summary>
/// <param name="pwdHashSettings">Instance of <see cref="IOptions{PwdHashOptions}" />.</param> /// <param name="pwdHashSettings">Instance of <see cref="IOptions{PwdHashOptions}" />.</param>
public CryptographyService(IOptions<PwdHashOptions> pwdHashSettings) /// <param name="logger">Instance of <see cref="ILogger{CryptographyService}" />.</param>
public CryptographyService(
IOptions<PwdHashOptions> pwdHashSettings,
ILogger<CryptographyService> logger)
{ {
_configuration = pwdHashSettings.Value; _configuration = pwdHashSettings.Value;
_logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -67,4 +74,52 @@ public class CryptographyService : ICryptographyService
const string availableChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; const string availableChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
return RandomNumberGenerator.GetString(availableChars, length); return RandomNumberGenerator.GetString(availableChars, length);
} }
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
} }

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<CodeAnalysisRuleSet>../Alveus.ruleset</CodeAnalysisRuleSet>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Astral.Services\Astral.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="TestData\" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -0,0 +1 @@
global using Xunit;

View file

@ -0,0 +1,117 @@
// <copyright file="CryptographyServiceTests.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
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;
/// <summary>
/// Tests for <see cref="ICryptographyService"/>.
/// </summary>
public class CryptographyServiceTests
{
private readonly CryptographyService _service;
/// <summary>
/// Initializes a new instance of the <see cref="CryptographyServiceTests"/> class.
/// </summary>
public CryptographyServiceTests()
{
var pwdHashOptions = new PwdHashOptions()
{
DegreeOfParallelism = 4,
HashSize = 128,
SaltSize = 64,
MemoryToUseKb = 16,
NumberOfIterations = 3
};
_service = new CryptographyService(
new OptionsWrapper<PwdHashOptions>(pwdHashOptions),
new NullLogger<CryptographyService>());
}
/// <summary>
/// Password hashing and verification tests.
/// </summary>
[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));
});
}
/// <summary>
/// Public key conversion tests.
/// </summary>
[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);
}
}

Binary file not shown.

View file

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQr8BExTMA6CREusv/SoiwlvMM
cvDNvPIuo0Tm70Kmm/Slle0pjszdHFbR3rUiniyhlmVIuu7hEkzMOIzE4KZ9cnOC
1DtkeEOhQkF2l3A4HX4OYPzUPzv89Hj+jAFI+LbBtzLb7wf2b4our+Z44w+i1YWN
ff59tv8PqXa/wuSD/QIDAQAB
-----END PUBLIC KEY-----

View file

@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{EB
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Astral.Tests", "Astral.Tests\Astral.Tests.csproj", "{F0CBECAA-F279-4D94-9F83-E9EBC7A5C08C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{7594740E-7061-46C9-A870-7E3687D3BC36}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal