Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

Feature/hashedtext type #126

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/Nox.Types/Enums/NoxType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum NoxType
[CompoundType] StreetAddress,
[CompoundType] TranslatedText,
[CompoundType] DateTimeRange,
[CompoundType] HashedText,

// Simple Types
Area,
Expand All @@ -41,7 +42,6 @@ public enum NoxType
File,
Formula,
Guid,
HashedText,
Html,
Image,
ImageJpg,
Expand Down
110 changes: 107 additions & 3 deletions src/Nox.Types/Types/HashedText/HashedText.cs
rochar marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,9 +1,113 @@
using System;
using System.Security.Cryptography;
using System.Text;

namespace Nox.Types;

/// <summary>
/// Represents a Nox <see cref="HashedText"/> type and value object.
/// </summary>
public sealed class HashedText : ValueObject<(string HashText, string Salt), HashedText>
{
public string HashText
{
get => Value.HashText;
private set => Value = (HashText: value, Salt: Value.Salt);
}
public string Salt
{
get => Value.Salt;
private set => Value = (HashText: Value.HashText, Salt: value);
}

public HashedText() { Value = (string.Empty, string.Empty); }

public static HashedText From(string value, HashedTextTypeOptions options)
{
options ??= new HashedTextTypeOptions();

var newObject = GetHashText(value, options);

var validationResult = newObject.Validate();

if (!validationResult.IsValid)
{
throw new TypeValidationException(validationResult.Errors);
}

return newObject;
}

public static HashedText From(string value)
=> From(value, new HashedTextTypeOptions());

public override string ToString() => $"{Value.HashText}";

/// <summary>
/// Creates hashed value of plainText using HashedTextTypeOptions
/// </summary>
/// <param name="plainText"></param>
/// <param name="hashedTextTypeOptions"></param>
/// <returns>Hashed value of plainText</returns>
private static HashedText GetHashText(string plainText, HashedTextTypeOptions hashedTextTypeOptions)
{
string hashedText = string.Empty;
string salt = string.Empty;

using (var hasher = CreateHasher(hashedTextTypeOptions.HashingAlgorithm))
{
byte[] saltBytes = GetSalt(hashedTextTypeOptions.Salt);
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
AppendBytes(ref plainTextBytes, saltBytes);
byte[] hashBytes = hasher.ComputeHash(plainTextBytes);

hashedText = Convert.ToBase64String(hashBytes);
salt = Convert.ToBase64String(saltBytes);
}

return new HashedText { Value = (hashedText, salt) };
}

/// <summary>
/// Returns hasher with sent algorithm
/// </summary>
/// <param name="hashAlgorithm"></param>
/// <returns></returns>
/// <exception cref="CryptographicException"></exception>
private static HashAlgorithm CreateHasher(HashingAlgorithm hashAlgorithm)
{
HashAlgorithm hasher = HashAlgorithm.Create(hashAlgorithm.ToString());

return hasher ?? throw new CryptographicException("Invalid hash algorithm");
}

/// <summary>
/// Creates salt byte array with length byteCount
/// </summary>
/// <param name="byteCount">array length</param>
/// <returns></returns>
private static byte[] GetSalt(int byteCount)
{
byte[] salt = new byte[byteCount];
RNGCryptoServiceProvider rng = new();
rng.GetBytes(salt);

return salt;
}

/// <summary>
/// Represents a Nox <see cref="HashedText"/> type and value object.
/// Merges two byte arrays
/// </summary>
/// <remarks>Placeholder, needs to be implemented</remarks>
public sealed class HashedText : ValueObject<uint, HashedText>
/// <param name="target"></param>
/// <param name="source"></param>
private static void AppendBytes(ref byte[] target, byte[] source)
{
int targetLength = target.Length;
int sourceLength = source.Length;
if (sourceLength != 0)
{
Array.Resize(ref target, targetLength + sourceLength);
Array.Copy(source, 0, target, targetLength, sourceLength);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Nox.Types;

public class HashedTextTypeOptions
{
public HashingAlgorithm HashingAlgorithm { get; set; } = HashingAlgorithm.SHA256;
public int Salt { get; set; } = 64;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

namespace Nox.Types;

public enum HashingAlgorithm
{
SHA256,
SHA512
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ public void Configure(EntityTypeBuilder<Country> builder)
.Ignore(p => p.Value)
.Property(x => x.CountryId)
.HasConversion<CountryCode2Converter>();
builder.OwnsOne(e => e.HashedText).Ignore(p => p.Value);
}
}
5 changes: 5 additions & 0 deletions tests/Nox.Types.Tests/EntityFrameworkTests/Models/Country.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ public sealed class Country
/// Gets or sets the StreetAddress.
/// </summary>
public StreetAddress StreetAddress { get; set; } = null!;

/// <summary>
/// Gets or sets hashed value
/// </summary>
public HashedText HashedText { get; set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public void Countries_CanRead_LatLong()
IPAddress = IpAddress.From("102.129.143.255"),
DateTimeRange = DateTimeRange.From(new DateTime(2023, 01, 01), new DateTime(2023, 02, 01)),
LongestHikingTrailInMeters = Length.From(390_000),
StreetAddress = CreateStreetAddress()
StreetAddress = CreateStreetAddress(),
HashedText = HashedText.From("Test123."),
};
DbContext.Countries.Add(newItem);
DbContext.SaveChanges();
Expand Down Expand Up @@ -74,7 +75,8 @@ public void AddedItemShouldGetGeneratedId()
CountryCode3 = CountryCode3.From("CHE"),
IPAddress = IpAddress.From("102.129.143.255"),
LongestHikingTrailInMeters = Length.From(390_000),
StreetAddress = streetAddress
StreetAddress = streetAddress,
HashedText = HashedText.From(("Test123.","salt"))
};
DbContext.Countries.Add(newItem);
DbContext.SaveChanges();
Expand Down Expand Up @@ -109,6 +111,8 @@ public void AddedItemShouldGetGeneratedId()
Assert.Equal(new DateTime(2023, 02, 01), item.DateTimeRange.End);
Assert.Equal(390_000, item.LongestHikingTrailInMeters.Value);
Assert.Equal(LengthTypeUnit.Meter, item.LongestHikingTrailInMeters.Unit);
Assert.Equal(newItem.HashedText.HashText, item.HashedText.HashText);
Assert.Equal(newItem.HashedText.Salt, item.HashedText.Salt);

AssertStreetAddress(streetAddress, item.StreetAddress);
}
Expand Down
70 changes: 69 additions & 1 deletion tests/Nox.Types.Tests/Types/HashedText/HashedTextTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,79 @@
// ReSharper disable once CheckNamespace
using System.Security.Cryptography;
using FluentAssertions;

namespace Nox.Types.Tests.Types;

public class HashedTextTests
{

rochar marked this conversation as resolved.
Show resolved Hide resolved
[Fact]
public void HashedText_Constructor_WithoutOptions_ReturnsHashedValue()
{
string text = "Text to hash";
var hashedText = HashedText.From(text);

hashedText.Should().NotBeNull();
hashedText.Value.Salt.Should().NotBeNull().And.NotBe(text);
}


[Fact]
public void HashedText_Constructor_WithOptions_ReturnsHashedValue()
{
string text = "Text to hash";
byte[] textData = System.Text.Encoding.UTF8.GetBytes(text);
byte[] hash = SHA512.HashData(textData);
var textHashedExpected = Convert.ToBase64String(hash);

var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt = 0 });

hashedText.Value.HashText.Should().Be(textHashedExpected);
}

[Fact]
public void HashedText_Equals_ReturnsTrue()
{
string text = "Text to hash";
byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(text);
byte[] hash = SHA512.HashData(textBytes);
var textHashedExpected = Convert.ToBase64String(hash);

var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt = 0 });
var expectedHashedText = HashedText.From((textHashedExpected, ""));

hashedText.Equals(expectedHashedText).Should().BeTrue();
}

[Fact]
public void HashedText_Equals_ReturnsFalse_Salting()
{
string text = "Text to hash";
var hashedText = HashedText.From(text, new HashedTextTypeOptions() { HashingAlgorithm = HashingAlgorithm.SHA512, Salt = 0 });
var hashedTextNoSalting = HashedText.From(text);

hashedText.Equals(hashedTextNoSalting).Should().BeFalse();
}

[Fact]
public void HashedText_Equals_ReturnsFalse()
{
string text = "Text to hash";
string text1 = $"{text} 1";
var hashedText1 = HashedText.From(text1);
var hashedText = HashedText.From(text);

hashedText.Equals(hashedText1).Should().BeFalse();
}

[Fact]
public void When_Create_Should()
public void HashedText_FromHashedValue_Delimiter_NoSalt()
{
string text = "Text to hash";
string salt = "Salt";
var hashedText = HashedText.From((text, salt));

hashedText.Value.HashText.Should().Be(text);
hashedText.Value.Salt.Should().Be(salt);
}
}