From 10f6adb297f70a3dc41e112a54614b44c15e0264 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 9 Apr 2022 15:13:14 +0200 Subject: [PATCH] Blake2B support. --- .../GenericHashBlake2BTest.cs | 107 ++++++++++ .../SpaceWizards.Sodium.Tests.csproj | 9 +- .../CryptoGenericHashBlake2B.cs | 202 ++++++++++++++++++ 3 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 SpaceWizards.Sodium.Tests/GenericHashBlake2BTest.cs create mode 100644 SpaceWizards.Sodium/CryptoGenericHashBlake2B.cs diff --git a/SpaceWizards.Sodium.Tests/GenericHashBlake2BTest.cs b/SpaceWizards.Sodium.Tests/GenericHashBlake2BTest.cs new file mode 100644 index 0000000..07eff1b --- /dev/null +++ b/SpaceWizards.Sodium.Tests/GenericHashBlake2BTest.cs @@ -0,0 +1,107 @@ +using System; +using System.Text; +using NUnit.Framework; + +namespace SpaceWizards.Sodium.Tests; + +[TestFixture] +[TestOf(typeof(CryptoGenericHashBlake2B))] +[Parallelizable(ParallelScope.All)] +public sealed class GenericHashBlake2BTest +{ + [Test] + [TestCase("foobar", null, "93a0e84a8cdd4166267dbe1263e937f08087723ac24e7dcc35b3d5941775ef47")] + [TestCase("foobar", null, + "8df31f60d6aeabd01b7dc83f277d0e24cbe104f7290ff89077a7eb58646068edfe1a83022866c46f65fb91612e516e0ecfa5cb25fc16b37d2c8d73732fe74cb2")] + [TestCase("foobar", "baz", "5f49889d5da33b8a04b242f19986193f0401d8fe087040ed79ae955119638a45")] + public void TestHash(string inputStr, string? keyStr, string expectedHex) + { + var input = Encoding.UTF8.GetBytes(inputStr); + var key = keyStr == null ? null : Encoding.UTF8.GetBytes(keyStr); + var expected = Convert.FromHexString(expectedHex); + + var output = CryptoGenericHashBlake2B.Hash(expected.Length, input, key); + + Assert.That(output, Is.EquivalentTo(expected)); + } + + [Test] + // @formatter:off + [TestCase("foobar", null, null, null, "93a0e84a8cdd4166267dbe1263e937f08087723ac24e7dcc35b3d5941775ef47")] + [TestCase("foobar", null, null, null, "8df31f60d6aeabd01b7dc83f277d0e24cbe104f7290ff89077a7eb58646068edfe1a83022866c46f65fb91612e516e0ecfa5cb25fc16b37d2c8d73732fe74cb2")] + [TestCase("foobar", "baz", null, null, "5f49889d5da33b8a04b242f19986193f0401d8fe087040ed79ae955119638a45")] + [TestCase("foobar", "baz", "AAAABBBBCCCCDDDD", null, "d4898e5ec36873d27d87ab00a464be00a7c8be03b9b5c01defd6e9d1e7150ebb")] + [TestCase("foobar", "baz", "AAAABBBBCCCCDDDD", "DDDDCCCCBBBBAAAA", "ccfa20580f3162c9312aa9ba39e88bc1e6857ebb5dcad2726d3835207cf8d735")] + [TestCase("foobar", "baz", null, "DDDDCCCCBBBBAAAA", "75d3b6c777060f178299a5fb16846013fb97354305598636117493a57b117282")] + [TestCase("foobar", null, null, "DDDDCCCCBBBBAAAA", "f5f01f9992ce6db03a37b9485d17bdc8f26154d6bc70e0524124b9bbeafd0269")] + // @formatter:on + public void TestHashSaltPersonal( + string inputStr, + string? keyStr, + string? saltStr, + string? personalStr, + string expectedHex) + { + var input = Encoding.UTF8.GetBytes(inputStr); + var key = keyStr == null ? null : Encoding.UTF8.GetBytes(keyStr); + var salt = saltStr == null ? null : Encoding.UTF8.GetBytes(saltStr); + var personal = personalStr == null ? null : Encoding.UTF8.GetBytes(personalStr); + var expected = Convert.FromHexString(expectedHex); + + var output = CryptoGenericHashBlake2B.HashSaltPersonal(expected.Length, input, key, salt, personal); + + Assert.That(output, Is.EquivalentTo(expected)); + } + + [Test] + [TestCase("foobar", null, "93a0e84a8cdd4166267dbe1263e937f08087723ac24e7dcc35b3d5941775ef47")] + [TestCase("foobar", null, + "8df31f60d6aeabd01b7dc83f277d0e24cbe104f7290ff89077a7eb58646068edfe1a83022866c46f65fb91612e516e0ecfa5cb25fc16b37d2c8d73732fe74cb2")] + [TestCase("foobar", "baz", "5f49889d5da33b8a04b242f19986193f0401d8fe087040ed79ae955119638a45")] + public void TestHashIncremental(string inputStr, string? keyStr, string expectedHex) + { + var input = Encoding.UTF8.GetBytes(inputStr); + var key = keyStr == null ? null : Encoding.UTF8.GetBytes(keyStr); + var expected = Convert.FromHexString(expectedHex); + + var output = new byte[expected.Length]; + CryptoGenericHashBlake2B.State state; + CryptoGenericHashBlake2B.Init(ref state, key, expected.Length); + CryptoGenericHashBlake2B.Update(ref state, input); + CryptoGenericHashBlake2B.Final(ref state, output); + + Assert.That(output, Is.EquivalentTo(expected)); + } + + [Test] + // @formatter:off + [TestCase("foobar", null, null, null, "93a0e84a8cdd4166267dbe1263e937f08087723ac24e7dcc35b3d5941775ef47")] + [TestCase("foobar", null, null, null, "8df31f60d6aeabd01b7dc83f277d0e24cbe104f7290ff89077a7eb58646068edfe1a83022866c46f65fb91612e516e0ecfa5cb25fc16b37d2c8d73732fe74cb2")] + [TestCase("foobar", "baz", null, null, "5f49889d5da33b8a04b242f19986193f0401d8fe087040ed79ae955119638a45")] + [TestCase("foobar", "baz", "AAAABBBBCCCCDDDD", null, "d4898e5ec36873d27d87ab00a464be00a7c8be03b9b5c01defd6e9d1e7150ebb")] + [TestCase("foobar", "baz", "AAAABBBBCCCCDDDD", "DDDDCCCCBBBBAAAA", "ccfa20580f3162c9312aa9ba39e88bc1e6857ebb5dcad2726d3835207cf8d735")] + [TestCase("foobar", "baz", null, "DDDDCCCCBBBBAAAA", "75d3b6c777060f178299a5fb16846013fb97354305598636117493a57b117282")] + [TestCase("foobar", null, null, "DDDDCCCCBBBBAAAA", "f5f01f9992ce6db03a37b9485d17bdc8f26154d6bc70e0524124b9bbeafd0269")] + // @formatter:on + public void TestHashSaltPersonalIncremental( + string inputStr, + string? keyStr, + string? saltStr, + string? personalStr, + string expectedHex) + { + var input = Encoding.UTF8.GetBytes(inputStr); + var key = keyStr == null ? null : Encoding.UTF8.GetBytes(keyStr); + var salt = saltStr == null ? null : Encoding.UTF8.GetBytes(saltStr); + var personal = personalStr == null ? null : Encoding.UTF8.GetBytes(personalStr); + var expected = Convert.FromHexString(expectedHex); + + var output = new byte[expected.Length]; + CryptoGenericHashBlake2B.State state; + CryptoGenericHashBlake2B.InitSaltPersonal(ref state, key, expected.Length, salt, personal); + CryptoGenericHashBlake2B.Update(ref state, input); + CryptoGenericHashBlake2B.Final(ref state, output); + + Assert.That(output, Is.EquivalentTo(expected)); + } +} diff --git a/SpaceWizards.Sodium.Tests/SpaceWizards.Sodium.Tests.csproj b/SpaceWizards.Sodium.Tests/SpaceWizards.Sodium.Tests.csproj index eeb8bf9..d039a0c 100644 --- a/SpaceWizards.Sodium.Tests/SpaceWizards.Sodium.Tests.csproj +++ b/SpaceWizards.Sodium.Tests/SpaceWizards.Sodium.Tests.csproj @@ -8,9 +8,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -18,7 +18,8 @@ - + + diff --git a/SpaceWizards.Sodium/CryptoGenericHashBlake2B.cs b/SpaceWizards.Sodium/CryptoGenericHashBlake2B.cs new file mode 100644 index 0000000..918e053 --- /dev/null +++ b/SpaceWizards.Sodium/CryptoGenericHashBlake2B.cs @@ -0,0 +1,202 @@ +using SpaceWizards.Sodium.Interop; + +namespace SpaceWizards.Sodium; + +using static Interop.Libsodium; + +/// +/// Wrappers around the crypto_generichash_blake2b_ APIs. +/// +public static class CryptoGenericHashBlake2B +{ + static CryptoGenericHashBlake2B() + { + SodiumCore.EnsureInit(); + } + + public const int BytesMin = (int)crypto_generichash_blake2b_BYTES_MIN; + public const int BytesMax = (int)crypto_generichash_blake2b_BYTES_MAX; + public const int Bytes = (int)crypto_generichash_blake2b_BYTES; + + public const int KeyBytesMin = (int)crypto_generichash_blake2b_KEYBYTES_MIN; + public const int KeyBytesMax = (int)crypto_generichash_blake2b_KEYBYTES_MAX; + public const int KeyBytes = (int)crypto_generichash_blake2b_BYTES; + + public const int SaltBytes = (int)crypto_generichash_blake2b_SALTBYTES; + public const int PersonalBytes = (int)crypto_generichash_blake2b_PERSONALBYTES; + + public static unsafe void Keygen(Span key) + { + if (key.Length != KeyBytes) + throw new ArgumentException($"Key must be {nameof(KeyBytes)} bytes"); + + fixed (byte* k = key) + { + crypto_generichash_blake2b_keygen(k); + } + } + + public static byte[] Keygen() + { + var key = new byte[KeyBytes]; + Keygen(key); + return key; + } + + public static byte[] Hash( + int outputLength, + ReadOnlySpan input, + ReadOnlySpan key) + { + var output = new byte[outputLength]; + + Hash(output, input, key); + + return output; + } + + public static unsafe bool Hash(Span output, ReadOnlySpan input, ReadOnlySpan key) + { + if (key.Length > KeyBytesMax) + throw new ArgumentException("Key too large"); + + if (output.Length is < BytesMin or > BytesMax) + throw new ArgumentException("Output is invalid size"); + + fixed (byte* i = input) + fixed (byte* o = output) + fixed (byte* k = key) + { + var ret = crypto_generichash_blake2b( + o, (nuint)output.Length, + i, (ulong)input.Length, + k, (nuint)key.Length); + + return ret == 0; + } + } + + public static byte[] HashSaltPersonal( + int outputLength, + ReadOnlySpan input, + ReadOnlySpan key, + ReadOnlySpan salt, + ReadOnlySpan personal) + { + var output = new byte[outputLength]; + + HashSaltPersonal(output, input, key, salt, personal); + + return output; + } + + + public static unsafe bool HashSaltPersonal( + Span output, + ReadOnlySpan input, + ReadOnlySpan key, + ReadOnlySpan salt, + ReadOnlySpan personal) + { + if (key.Length > KeyBytesMax) + throw new ArgumentException("Key too large"); + + if (output.Length is < BytesMin or > BytesMax) + throw new ArgumentException("Output is invalid size"); + + if (salt.Length != SaltBytes && salt.Length != 0) + throw new ArgumentException($"Salt must be {nameof(SaltBytes)} bytes or empty"); + + if (personal.Length != PersonalBytes && personal.Length != 0) + throw new ArgumentException($"Personalization must be {nameof(PersonalBytes)} bytes or empty"); + + fixed (byte* i = input) + fixed (byte* o = output) + fixed (byte* k = key) + fixed (byte* s = salt) + fixed (byte* p = personal) + { + var ret = crypto_generichash_blake2b_salt_personal( + o, (nuint)output.Length, + i, (ulong)input.Length, + k, (nuint)key.Length, + s, + p); + + return ret == 0; + } + } + + public static unsafe bool Init(ref State state, ReadOnlySpan key, int outputLength) + { + if (key.Length > KeyBytesMax) + throw new ArgumentException("Key too large"); + + if (outputLength is < BytesMin or > BytesMax) + throw new ArgumentException("Output is invalid size"); + + fixed (crypto_generichash_blake2b_state* s = &state.Data) + fixed (byte* k = key) + { + var ret = crypto_generichash_blake2b_init(s, k, (nuint)key.Length, (nuint)outputLength); + return ret == 0; + } + } + + public static unsafe bool InitSaltPersonal( + ref State state, + ReadOnlySpan key, + int outputLength, + ReadOnlySpan salt, + ReadOnlySpan personal) + { + if (key.Length > KeyBytesMax) + throw new ArgumentException("Key too large"); + + if (outputLength is < BytesMin or > BytesMax) + throw new ArgumentException("Output is invalid size"); + + fixed (crypto_generichash_blake2b_state* s = &state.Data) + fixed (byte* k = key) + fixed (byte* saltPtr = salt) + fixed (byte* p = personal) + { + var ret = crypto_generichash_blake2b_init_salt_personal( + s, + k, (nuint)key.Length, + (nuint)outputLength, + saltPtr, + p); + + return ret == 0; + } + } + + public static unsafe bool Update(ref State state, ReadOnlySpan input) + { + fixed (crypto_generichash_blake2b_state* s = &state.Data) + fixed (byte* i = input) + { + var ret = crypto_generichash_blake2b_update(s, i, (nuint)input.Length); + return ret == 0; + } + } + + public static unsafe bool Final(ref State state, Span output) + { + if (output.Length is < BytesMin or > BytesMax) + throw new ArgumentException("Output is invalid size"); + + fixed (crypto_generichash_blake2b_state* s = &state.Data) + fixed (byte* o = output) + { + var ret = crypto_generichash_blake2b_final(s, o, (nuint)output.Length); + return ret == 0; + } + } + + public struct State + { + public crypto_generichash_blake2b_state Data; + } +}