Extend CryptographyService.cs and add tests
This commit is contained in:
parent
20fea63405
commit
84cf848e90
9 changed files with 266 additions and 1 deletions
21
Astral.Services/Constants/PublicKeyType.cs
Normal file
21
Astral.Services/Constants/PublicKeyType.cs
Normal 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
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
||||
// </copyright>
|
||||
|
||||
using Astral.Services.Constants;
|
||||
|
||||
namespace Astral.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
|
@ -48,4 +50,19 @@ public interface ICryptographyService
|
|||
/// <param name="length">Length of string to create.</param>
|
||||
/// <returns>A random string.</returns>
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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<CryptographyService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CryptographyService" /> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -67,4 +74,52 @@ public class CryptographyService : ICryptographyService
|
|||
const string availableChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
42
Astral.Tests/Astral.Tests.csproj
Normal file
42
Astral.Tests/Astral.Tests.csproj
Normal 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>
|
1
Astral.Tests/GlobalUsings.cs
Normal file
1
Astral.Tests/GlobalUsings.cs
Normal file
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
117
Astral.Tests/ServiceTests/CryptographyServiceTests.cs
Normal file
117
Astral.Tests/ServiceTests/CryptographyServiceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
BIN
Astral.Tests/TestData/public-key.der
Normal file
BIN
Astral.Tests/TestData/public-key.der
Normal file
Binary file not shown.
6
Astral.Tests/TestData/public-key.pem
Normal file
6
Astral.Tests/TestData/public-key.pem
Normal file
|
@ -0,0 +1,6 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCQr8BExTMA6CREusv/SoiwlvMM
|
||||
cvDNvPIuo0Tm70Kmm/Slle0pjszdHFbR3rUiniyhlmVIuu7hEkzMOIzE4KZ9cnOC
|
||||
1DtkeEOhQkF2l3A4HX4OYPzUPzv89Hj+jAFI+LbBtzLb7wf2b4our+Z44w+i1YWN
|
||||
ff59tv8PqXa/wuSD/QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue