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}||{Value.Salt}";
rochar marked this conversation as resolved.
Show resolved Hide resolved

/// <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>
public static void AppendBytes(ref byte[] target, byte[] source)
rochar marked this conversation as resolved.
Show resolved Hide resolved
{
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 @@ -29,5 +29,6 @@ public void Configure(EntityTypeBuilder<Country> builder)
builder.OwnsOne(e => e.LatLong).Ignore(p => p.Value);
builder.OwnsOne(e => e.GrossDomesticProduct).Ignore(p => p.Value);
builder.OwnsOne(e => e.DateTimeRange).Ignore(p => p.Value);
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 @@ -82,4 +82,9 @@ public sealed class Country
/// Gets or sets the longest hiking trail in meters.
/// </summary>
public Length LongestHikingTrailInMeters { 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 @@ -40,6 +40,7 @@ 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),
HashedText = HashedText.From("Test123."),
};
DbContext.Countries.Add(newItem);
DbContext.SaveChanges();
Expand All @@ -56,7 +57,7 @@ public void Countries_CanRead_LatLong()
[Fact]
public void AddedItemShouldGetGeneratedId()
{
var newItem = new Country() {
var newItem = new Country() {
Name = Text.From("Switzerland"),
LatLong = LatLong.From(46.802496, 8.234392),
Population = Number.From(8_703_654),
Expand All @@ -72,6 +73,7 @@ public void AddedItemShouldGetGeneratedId()
CountryCode3 = CountryCode3.From("CHE"),
IPAddress = IpAddress.From("102.129.143.255"),
LongestHikingTrailInMeters = Length.From(390_000),
HashedText = HashedText.From(("Test123.","salt"))
};
DbContext.Countries.Add(newItem);
DbContext.SaveChanges();
Expand Down Expand Up @@ -106,5 +108,7 @@ 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);
}
}
81 changes: 80 additions & 1 deletion tests/Nox.Types.Tests/Types/HashedText/HashedTextTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,90 @@
// ReSharper disable once CheckNamespace
using System.Security.Cryptography;

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);

Assert.NotNull(hashedText);
Assert.NotNull(hashedText.Salt);
Assert.NotEqual(text, hashedText.HashText);
}


[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 });

Assert.Equal(textHashedExpected, hashedText.HashText);
}

[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, ""));

Assert.True(hashedText.Equals(expectedHashedText));
}

[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);

Assert.False(hashedText.Equals(hashedTextNoSalting));
}

[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);

Assert.False(hashedText.Equals(hashedText1));
}

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

Assert.Equal(text, hashedText.HashText);
Assert.Equal(salt, hashedText.Salt);
}

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

Assert.Equal(text, hashedText.HashText);
Assert.Equal(salt, hashedText.Salt);
}
}