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 (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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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
|
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
|
||||||
|
|
Loading…
Reference in a new issue