Skip to content

Commit

Permalink
add allocation-free hashing
Browse files Browse the repository at this point in the history
  • Loading branch information
saucecontrol committed Jun 18, 2018
1 parent 7e876fb commit 287a4aa
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 84 deletions.
42 changes: 32 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Blake2Fast
==========

These [RFC 7693](https://tools.ietf.org/html/rfc7693)-compliant Blake2 implementations have been tuned for high speed and low memory usage. The .NET Core 2.1 build supports the new X86 SIMD Intrinsics for even greater speed and `Span<T>` for even lower memory usage.
These [RFC 7693](https://tools.ietf.org/html/rfc7693)-compliant BLAKE2 implementations have been tuned for high speed and low memory usage. The .NET Core 2.1 build supports the new X86 SIMD Intrinsics for even greater speed and `Span<T>` for even lower memory usage.

Sample benchmark results comparing with built-in .NET algorithms, 10MiB input, .NET Core 2.1 x64 and x86 runtimes:

Expand All @@ -19,7 +19,7 @@ MD5 | X86 | 20.06 ms | 0.0996 ms | 0.0931 ms | 0 B |
SHA256 | X86 | 52.47 ms | 0.3252 ms | 0.3042 ms | 0 B |
SHA512 | X86 | 44.07 ms | 0.1643 ms | 0.1372 ms | 0 B |

You can find more detailed comparison between Blake2Fast and other .NET Blake2 implementations starting [here](https://photosauce.net/blog/post/fast-hashing-with-blake2-part-1-nuget-is-a-minefield)
You can find more detailed comparison between Blake2Fast and other .NET BLAKE2 implementations starting [here](https://photosauce.net/blog/post/fast-hashing-with-blake2-part-1-nuget-is-a-minefield)

Installation
------------
Expand All @@ -36,26 +36,29 @@ Usage
### All-at-Once Hashing

The simplest and lightest-weight way to calculate a hash is the all-at-once `ComputeHash` method.

```C#
Blake2b.ComputeHash(data);
var hash = Blake2b.ComputeHash(data);
```

Blake2 supports variable digest lengths from 1 to 32 bytes for `Blake2s` or 1 to 64 bytes for `Blake2b`.
BLAKE2 supports variable digest lengths from 1 to 32 bytes for BLAKE2s or 1 to 64 bytes for BLAKE2b.

```C#
Blake2b.ComputeHash(48, data);
var hash = Blake2b.ComputeHash(42, data);
```

Blake2 also natively supports keyed hashing.
BLAKE2 also natively supports keyed hashing.

```C#
Blake2b.ComputeHash(key, data);
var hash = Blake2b.ComputeHash(key, data);
```

### Incremental Hashing

Blake2 hashes can be incrementally updated if you do not have the data available all at once.
BLAKE2 hashes can be incrementally updated if you do not have the data available all at once.

```C#
async Task<byte[]> CalculateHashAsync(Stream data)
async Task<byte[]> ComputeHashAsync(Stream data)
{
var incHash = Blake2b.CreateIncrementalHasher();
var buffer = new byte[4096];
Expand All @@ -71,11 +74,30 @@ async Task<byte[]> CalculateHashAsync(Stream data)
}
```

### Allocation-Free Hashing

The output hash digest can be written to an existing buffer to avoid allocating a new array each time. This is especially useful when performing an iterative hash, as might be used in a [key derivation function](https://en.wikipedia.org/wiki/Key_derivation_function).

```C#
byte[] DeriveBytes(string password, byte[] salt)
{
// Create key from password, then hash the salt using the key
var pwkey = Blake2b.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
var hbuff = Blake2b.ComputeHash(pwkey, salt);

// Hash the hash lots of times, re-using the same buffer
for (int i = 0; i < 1_000_000; i++)
Blake2b.ComputeAndWriteHash(pwkey, hbuff, hbuff);

return hbuff;
}
```

### System.Security.Cryptography Interop

For interoperating with code that uses `System.Security.Cryptography` primitives, Blake2Fast can create a `HashAlgorithm` wrapper. The wrapper inherits from `HMAC` in case keyed hashing is required.

`HashAlgorithm` is less efficient than the above methods, so use it only when necessary.
`HashAlgorithm` is less efficient than the above methods, so use it only when necessary for compatibility.

```C#
byte[] WriteDataAndCalculateHash(byte[] data)
Expand Down
25 changes: 11 additions & 14 deletions src/Blake2Fast/Blake2Fast.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,32 @@
<AssemblyTitle>Blake2Fast</AssemblyTitle>
<TargetFrameworks>netstandard1.0;netstandard1.3;netstandard2.0;netcoreapp2.0;netcoreapp2.1;net45</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<LangVersion>latest</LangVersion>
<VersionPrefix>0.1.0</VersionPrefix>
<VersionPrefix>0.2.0</VersionPrefix>
<Authors>Clinton Ingram</Authors>
<Product>Blake2Fast</Product>
<Description>Optimized implementations of the Blake2b and Blake2s hashing algorithms. Uses SSE2-SSE4.1 intrinsics support on .NET Core 2.1</Description>
<Description>Optimized implementations of the BLAKE2b and BLAKE2s hashing algorithms. Uses SSE2-SSE4.1 Intrinsics support on .NET Core 2.1</Description>
<Copyright>Copyright © 2018 Clinton Ingram</Copyright>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/saucecontrol/Blake2Fast</RepositoryUrl>
<PackageIconUrl>https://photosauce.net/icon64x64.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/saucecontrol/Blake2Fast</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/saucecontrol/Blake2Fast/license</PackageLicenseUrl>
<PackageLicenseUrl>https://github.com/saucecontrol/Blake2Fast/blob/master/license</PackageLicenseUrl>
<PackageReleaseNotes>See https://github.com/saucecontrol/Blake2Fast/releases</PackageReleaseNotes>
<PackageTags>Blake2 Hash Blake2b Blake2s SSE SIMD HashAlgorithm HMAC</PackageTags>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageTags>BLAKE2 Hash BLAKE2b BLAKE2s SSE SIMD HashAlgorithm HMAC</PackageTags>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp2.0' Or '$(TargetFramework)'=='netcoreapp2.1'">
<DefineConstants>$(DefineConstants);IMPLICIT_BYTESPAN</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp2.1'">
<DefineConstants>$(DefineConstants);FAST_SPAN;USE_INTRINSICS</DefineConstants>
<PropertyGroup>
<DefineConstants Condition="'$(TargetFramework)'=='netcoreapp2.0' Or '$(TargetFramework)'=='netcoreapp2.1'">$(DefineConstants);IMPLICIT_BYTESPAN</DefineConstants>
<DefineConstants Condition="'$(TargetFramework)'=='netcoreapp2.1'">$(DefineConstants);FAST_SPAN;USE_INTRINSICS</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DocumentationFile>bin\$(Configuration)\$(TargetFrameWork)\SauceControl.Blake2Fast.xml</DocumentationFile>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<DocumentationFile>bin\$(Configuration)\$(TargetFrameWork)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>

<ItemGroup Condition="$(DefineConstants.Contains('USE_INTRINSICS'))">
Expand Down
77 changes: 65 additions & 12 deletions src/Blake2Fast/Blake2b.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
#if !NETSTANDARD1_0
using System;

#if !NETSTANDARD1_0
using System.Security.Cryptography;
#endif

#if FAST_SPAN
using ByteSpan = System.ReadOnlySpan<byte>;
using WriteableByteSpan = System.Span<byte>;
#else
using ByteSpan = System.ArraySegment<byte>;
using WriteableByteSpan = System.ArraySegment<byte>;
#endif

namespace SauceControl.Blake2Fast
{
/// <summary>Static helper methods for Blake2b hashing.</summary>
/// <summary>Static helper methods for BLAKE2b hashing.</summary>
public static class Blake2b
{
/// <summary>The default hash digest length in bytes. For BLAKE2b, this value is 64.</summary>
public const int DefaultDigestLength = Blake2bContext.HashBytes;

/// <inheritdoc cref="ComputeHash(int, ByteSpan, ByteSpan)"/>
public static byte[] ComputeHash(ByteSpan input) => ComputeHash(Blake2bContext.HashBytes, default, input);
public static byte[] ComputeHash(ByteSpan input) => ComputeHash(DefaultDigestLength, default, input);

/// <inheritdoc cref="ComputeHash(int, ByteSpan, ByteSpan)"/>
public static byte[] ComputeHash(int digestLength, ByteSpan input) => ComputeHash(digestLength, default, input);

/// <inheritdoc cref="ComputeHash(int, ByteSpan, ByteSpan)"/>
public static byte[] ComputeHash(ByteSpan key, ByteSpan input) => ComputeHash(Blake2bContext.HashBytes, key, input);
public static byte[] ComputeHash(ByteSpan key, ByteSpan input) => ComputeHash(DefaultDigestLength, key, input);

/// <summary>Perform an all-at-once Blake2b hash computation.</summary>
/// <summary>Perform an all-at-once BLAKE2b hash computation.</summary>
/// <remarks>If you have all the input available at once, this is the most efficient way to calculate the hash.</remarks>
/// <param name="digestLength">The hash digest length in bytes. Valid values are 1 to 64.</param>
/// <param name="key">0 to 64 bytes of input for initializing a keyed hash.</param>
Expand All @@ -36,16 +43,50 @@ public static byte[] ComputeHash(int digestLength, ByteSpan key, ByteSpan input)
return ctx.Finish();
}

/// <inheritdoc cref="ComputeAndWriteHash(ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(ByteSpan input, WriteableByteSpan output) => ComputeAndWriteHash(DefaultDigestLength, default, input, output);

/// <inheritdoc cref="ComputeAndWriteHash(int, ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(int digestLength, ByteSpan input, WriteableByteSpan output) => ComputeAndWriteHash(digestLength, default, input, output);

/// <summary>Perform an all-at-once BLAKE2b hash computation and write the hash digest to <paramref name="output" />.</summary>
/// <remarks>If you have all the input available at once, this is the most efficient way to calculate the hash.</remarks>
/// <param name="key">0 to 64 bytes of input for initializing a keyed hash.</param>
/// <param name="input">The message bytes to hash.</param>
/// <param name="output">Destination buffer into which the hash digest is written. The buffer must have a capacity of at least <see cref="DefaultDigestLength"/>(64) /> bytes.</param>
public static void ComputeAndWriteHash(ByteSpan key, ByteSpan input, WriteableByteSpan output) => ComputeAndWriteHash(DefaultDigestLength, key, input, output);

/// <summary>Perform an all-at-once BLAKE2b hash computation and write the hash digest to <paramref name="output" />.</summary>
/// <remarks>If you have all the input available at once, this is the most efficient way to calculate the hash.</remarks>
/// <param name="digestLength">The hash digest length in bytes. Valid values are 1 to 64.</param>
/// <param name="key">0 to 64 bytes of input for initializing a keyed hash.</param>
/// <param name="input">The message bytes to hash.</param>
/// <param name="output">Destination buffer into which the hash digest is written. The buffer must have a capacity of at least <paramref name="digestLength" /> bytes.</param>
public static void ComputeAndWriteHash(int digestLength, ByteSpan key, ByteSpan input, WriteableByteSpan output)
{
#if FAST_SPAN
if (output.Length < digestLength)
#else
if (output.Count < digestLength)
#endif
throw new ArgumentException($"Output buffer must have a capacity of at least {digestLength} bytes.", nameof(output));

var ctx = default(Blake2bContext);
ctx.Init(digestLength, key);
ctx.Update(input);
ctx.TryFinish(output, out int _);
}

/// <inheritdoc cref="CreateIncrementalHasher(int, ByteSpan)" />
public static IBlake2Incremental CreateIncrementalHasher() => CreateIncrementalHasher(Blake2bContext.HashBytes, default(ByteSpan));
public static IBlake2Incremental CreateIncrementalHasher() => CreateIncrementalHasher(DefaultDigestLength, default(ByteSpan));

/// <inheritdoc cref="CreateIncrementalHasher(int, ByteSpan)" />
public static IBlake2Incremental CreateIncrementalHasher(int digestLength) => CreateIncrementalHasher(digestLength, default(ByteSpan));

/// <inheritdoc cref="CreateIncrementalHasher(int, ByteSpan)" />
public static IBlake2Incremental CreateIncrementalHasher(ByteSpan key) => CreateIncrementalHasher(Blake2bContext.HashBytes, key);
public static IBlake2Incremental CreateIncrementalHasher(ByteSpan key) => CreateIncrementalHasher(DefaultDigestLength, key);

/// <summary>Create and initialize an incremental Blake2b hash computation.</summary>
/// <summary>Create and initialize an incremental BLAKE2b hash computation.</summary>
/// <remarks>If you will recieve the input in segments rather than all at once, this is the most efficient way to calculate the hash.</remarks>
/// <param name="digestLength">The hash digest length in bytes. Valid values are 1 to 64.</param>
/// <param name="key">0 to 64 bytes of input for initializing a keyed hash.</param>
Expand All @@ -70,6 +111,18 @@ public static IBlake2Incremental CreateIncrementalHasher(int digestLength, ByteS
/// <inheritdoc cref="ComputeHash(int, ByteSpan, ByteSpan)"/>
public static byte[] ComputeHash(int digestLength, byte[] key, byte[] input) => ComputeHash(digestLength, key.AsByteSpan(), input.AsByteSpan());

/// <inheritdoc cref="ComputeAndWriteHash(ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(byte[] input, byte[] output) => ComputeAndWriteHash(DefaultDigestLength, default, input.AsByteSpan(), output.AsByteSpan());

/// <inheritdoc cref="ComputeAndWriteHash(int, ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(int digestLength, byte[] input, byte[] output) => ComputeAndWriteHash(digestLength, default, input.AsByteSpan(), output.AsByteSpan());

/// <inheritdoc cref="ComputeAndWriteHash(ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(byte[] key, byte[] input, byte[] output) => ComputeAndWriteHash(DefaultDigestLength, key.AsByteSpan(), input.AsByteSpan(), output.AsByteSpan());

/// <inheritdoc cref="ComputeAndWriteHash(int, ByteSpan, ByteSpan, WriteableByteSpan)" />
public static void ComputeAndWriteHash(int digestLength, byte[] key, byte[] input, byte[] output) => ComputeAndWriteHash(digestLength, key.AsByteSpan(), input.AsByteSpan(), output.AsByteSpan());

/// <inheritdoc cref="CreateIncrementalHasher(int, ByteSpan)" />
public static IBlake2Incremental CreateIncrementalHasher(byte[] key) => CreateIncrementalHasher(key.AsByteSpan());

Expand All @@ -79,18 +132,18 @@ public static IBlake2Incremental CreateIncrementalHasher(int digestLength, ByteS

#if !NETSTANDARD1_0
/// <inheritdoc cref="CreateHashAlgorithm(int)" />
public static HashAlgorithm CreateHashAlgorithm() => CreateHashAlgorithm(Blake2bContext.HashBytes);
public static HashAlgorithm CreateHashAlgorithm() => CreateHashAlgorithm(DefaultDigestLength);

/// <summary>Creates and initializes a <see cref="HashAlgorithm" /> instance that implements Blake2b hashing.</summary>
/// <summary>Creates and initializes a <see cref="HashAlgorithm" /> instance that implements BLAKE2b hashing.</summary>
/// <remarks>Use this only if you require an implementation of <see cref="HashAlgorithm" />. It is less efficient than the direct methods.</remarks>
/// <param name="digestLength">The hash digest length in bytes. Valid values are 1 to 64.</param>
/// <returns>A <see cref="HashAlgorithm" /> instance.</returns>
public static HashAlgorithm CreateHashAlgorithm(int digestLength) => new Blake2HMAC(Blake2Algorithm.Blake2b, digestLength, default);

/// <inheritdoc cref="CreateHMAC(int, ByteSpan)" />
public static HMAC CreateHMAC(ByteSpan key) => CreateHMAC(Blake2bContext.HashBytes, key);
public static HMAC CreateHMAC(ByteSpan key) => CreateHMAC(DefaultDigestLength, key);

/// <summary>Creates and initializes an <see cref="HMAC" /> instance that implements Blake2b keyed hashing.</summary>
/// <summary>Creates and initializes an <see cref="HMAC" /> instance that implements BLAKE2b keyed hashing. Uses BLAKE2's built-in support for keyed hashing rather than the normal 2-pass approach.</summary>
/// <remarks>Use this only if you require an implementation of <see cref="HMAC" />. It is less efficient than the direct methods.</remarks>
/// <param name="digestLength">The hash digest length in bytes. Valid values are 1 to 64.</param>
/// <param name="key">0 to 64 bytes of input for initializing the keyed hash.</param>
Expand Down
14 changes: 9 additions & 5 deletions src/Blake2Fast/Blake2bContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,6 @@ public void Update(ByteSpan input)
}
}

#if !IMPLICIT_BYTESPAN
public void Update(byte[] input) => Update(input.AsByteSpan());
#endif

private void finish(WriteableByteSpan hash)
{
if (this.f[0] != 0)
Expand Down Expand Up @@ -212,10 +208,13 @@ public byte[] Finish()
return hash;
}

#if FAST_SPAN
public bool TryFinish(WriteableByteSpan output, out int bytesWritten)
{
#if FAST_SPAN
if (output.Length < outlen)
#else
if (output.Count < outlen)
#endif
{
bytesWritten = 0;
return false;
Expand All @@ -225,6 +224,11 @@ public bool TryFinish(WriteableByteSpan output, out int bytesWritten)
bytesWritten = (int)outlen;
return true;
}

#if !IMPLICIT_BYTESPAN
public void Update(byte[] input) => Update(input.AsByteSpan());

public bool TryFinish(byte[] output, out int bytesWritten) => TryFinish(output.AsByteSpan(), out bytesWritten);
#endif
}
}
Loading

0 comments on commit 287a4aa

Please sign in to comment.