diff --git a/IT.Hashing.Gost.Native/HashAlgorithms.cs b/IT.Hashing.Gost.Native/HashAlgorithms.cs index 5a2be34..d63b6ff 100644 --- a/IT.Hashing.Gost.Native/HashAlgorithms.cs +++ b/IT.Hashing.Gost.Native/HashAlgorithms.cs @@ -1,5 +1,8 @@ using IT.Hashing.Gost.Native.Internal; using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; namespace IT.Hashing.Gost.Native; @@ -54,7 +57,9 @@ private abstract class Resetable_Gost3411 : IHashAlgorithm { private SafeHashHandleImpl _handle; - public virtual int Size => _handle.Size; + public abstract int Size { get; } + + public abstract int SizeInBase64 { get; } protected Resetable_Gost3411() { @@ -82,6 +87,23 @@ public void Reset() public bool TryGetHash(Span hash, out int length) => _handle.TryGetHash(hash, out length); + public bool TryGetHashInBase64(Span destination, out int length) + { + length = SizeInBase64; + if (destination.Length < length) + return false; + + var isTrue = _handle.TryGetHash(destination, out var written); + Debug.Assert(isTrue); + Debug.Assert(written == Size); + + var status = Base64.EncodeToUtf8InPlace(destination, written, out written); + Debug.Assert(status == OperationStatus.Done); + Debug.Assert(written == length); + + return true; + } + protected abstract SafeHashHandleImpl CreateHandle(); } @@ -89,6 +111,8 @@ private class Resetable_Gost3411_94 : Resetable_Gost3411 { public override int Size => 32; + public override int SizeInBase64 => 44; + protected override SafeHashHandleImpl CreateHandle() => CryptoApiHelper.CreateHash_3411_94(_provider!); } @@ -97,6 +121,8 @@ private class Resetable_Gost3411_2012_256 : Resetable_Gost3411 { public override int Size => 32; + public override int SizeInBase64 => 44; + protected override SafeHashHandleImpl CreateHandle() => CryptoApiHelper.CreateHash_3411_2012_256(_provider!); } @@ -105,6 +131,8 @@ private class Resetable_Gost3411_2012_512 : Resetable_Gost3411 { public override int Size => 64; + public override int SizeInBase64 => 88; + protected override SafeHashHandleImpl CreateHandle() => CryptoApiHelper.CreateHash_3411_2012_512(_provider!); } diff --git a/IT.Hashing.Gost.Native/IT.Hashing.Gost.Native.csproj b/IT.Hashing.Gost.Native/IT.Hashing.Gost.Native.csproj index 3510e14..d8b39e6 100644 --- a/IT.Hashing.Gost.Native/IT.Hashing.Gost.Native.csproj +++ b/IT.Hashing.Gost.Native/IT.Hashing.Gost.Native.csproj @@ -13,7 +13,7 @@ true Ivan Tikhonov Ivan Tikhonov © 2026 - 2.0.2 + 2.0.3 Gost Hashing Native CryptoPro VipNet Readme.md Icon.png diff --git a/IT.Hashing.Gost.Native/Internal/SafeHashHandleImpl.cs b/IT.Hashing.Gost.Native/Internal/SafeHashHandleImpl.cs index 222edad..5b980d5 100644 --- a/IT.Hashing.Gost.Native/Internal/SafeHashHandleImpl.cs +++ b/IT.Hashing.Gost.Native/Internal/SafeHashHandleImpl.cs @@ -1,5 +1,7 @@ using Microsoft.Win32.SafeHandles; using System; +using System.Buffers; +using System.Buffers.Text; using System.Security; namespace IT.Hashing.Gost.Native.Internal; @@ -14,6 +16,8 @@ internal class SafeHashHandleImpl : SafeHandleZeroOrMinusOneIsInvalid, IHashAlgo public int Size => CryptoApiHelper.GetEndHashDataLength(this); + public int SizeInBase64 => Base64.GetMaxEncodedToUtf8Length(Size); + public SafeHashHandleImpl() : base(true) { } @@ -63,6 +67,30 @@ public bool TryGetHash(Span hash, out int length) return CryptoApiHelper.TryGetEndHashData(this, hash, out length); } + [SecurityCritical] + public bool TryGetHashInBase64(Span hash, out int length) + { + if (!CryptoApiHelper.TryGetEndHashData(this, hash, out length)) + { + length = Base64.GetMaxEncodedToUtf8Length(length); + return false; + } + + var status = Base64.EncodeToUtf8InPlace(hash, length, out var written); + if (status != OperationStatus.Done) + { + if (status == OperationStatus.DestinationTooSmall) + { + hash.Slice(0, length).Clear(); + length = Base64.GetMaxEncodedToUtf8Length(length); + return false; + } + throw new InvalidOperationException($"Status is {status}"); + } + length = written; + return true; + } + public void Reset() { throw new NotImplementedException(); diff --git a/IT.Hashing.Gost/Gost3411_2012_256.cs b/IT.Hashing.Gost/Gost3411_2012_256.cs index 8481c13..5530edb 100644 --- a/IT.Hashing.Gost/Gost3411_2012_256.cs +++ b/IT.Hashing.Gost/Gost3411_2012_256.cs @@ -1,5 +1,8 @@ using IT.Hashing.Gost.Internal; using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace IT.Hashing.Gost; @@ -10,6 +13,8 @@ public class Gost3411_2012_256 : Gost3411_2012_512 public override int Size => 32; + public override int SizeInBase64 => 44; + /// Thrown when the instance has been disposed. [MethodImpl(MethodImplOptionsEx.OptimizedLoop)] public override bool TryGetHash(Span destination, out int length) @@ -18,7 +23,30 @@ public override bool TryGetHash(Span destination, out int length) if (destination.Length < length) return false; - BinarySpans.WriteUInt64LittleEndian(HashFinal().AsSpan(HalfBlockSizeWords, HalfBlockSizeWords), destination); + Span hash = stackalloc ulong[BlockSizeWords]; + HashFinal(hash); + + BinarySpans.WriteUInt64LittleEndian(hash.Slice(HalfBlockSizeWords, HalfBlockSizeWords), destination); + + return true; + } + + /// Thrown when the instance has been disposed. + [MethodImpl(MethodImplOptionsEx.OptimizedLoop)] + public override bool TryGetHashInBase64(Span destination, out int length) + { + length = 44; + if (destination.Length < length) + return false; + + Span hash = stackalloc ulong[BlockSizeWords]; + HashFinal(hash); + + BinarySpans.WriteUInt64LittleEndian(hash.Slice(HalfBlockSizeWords, HalfBlockSizeWords), destination); + + var status = Base64.EncodeToUtf8InPlace(destination, 32, out var written); + Debug.Assert(status == OperationStatus.Done); + Debug.Assert(written == length); return true; } diff --git a/IT.Hashing.Gost/Gost3411_2012_512.cs b/IT.Hashing.Gost/Gost3411_2012_512.cs index 862d452..95c733f 100644 --- a/IT.Hashing.Gost/Gost3411_2012_512.cs +++ b/IT.Hashing.Gost/Gost3411_2012_512.cs @@ -2,7 +2,10 @@ using IT.Hashing.Gost.Internal; using System; +using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics; +using System.Buffers.Text; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -196,6 +199,8 @@ static Gost3411_2012_512() public virtual int Size => 64; + public virtual int SizeInBase64 => 88; + public Gost3411_2012_512() { _h = new ulong[BlockSizeWords]; @@ -270,7 +275,30 @@ public virtual bool TryGetHash(Span destination, out int length) if (destination.Length < length) return false; - BinarySpans.WriteUInt64LittleEndian(HashFinal(), destination); + Span hash = stackalloc ulong[BlockSizeWords]; + HashFinal(hash); + BinarySpans.WriteUInt64LittleEndian(hash, destination); + + return true; + } + + /// Thrown when the instance has been disposed. + [MethodImpl(MethodImplOptionsEx.OptimizedLoop)] + public virtual bool TryGetHashInBase64(Span destination, out int length) + { + if (_disposed) throw new ObjectDisposedException(GetType().FullName); + + length = 88; + if (destination.Length < length) + return false; + + Span hash = stackalloc ulong[BlockSizeWords]; + HashFinal(hash); + BinarySpans.WriteUInt64LittleEndian(hash, destination); + + var status = Base64.EncodeToUtf8InPlace(destination, 64, out var written); + Debug.Assert(status == OperationStatus.Done); + Debug.Assert(written == length); return true; } @@ -287,6 +315,44 @@ public void Dispose() } } + [MethodImpl(MethodImplOptionsEx.OptimizedLoop)] + protected void HashFinal(Span h) + { + if (_disposed) throw new ObjectDisposedException(GetType().FullName); + + // Pad the final block: m || 1 || 0...0 + Span paddedBlock = stackalloc byte[64]; + paddedBlock.Clear(); + _buffer.AsSpan(0, _bufferLength).CopyTo(paddedBlock); + paddedBlock[_bufferLength] = 0x01; + + // Convert padded block to ulongs + Span p = stackalloc ulong[BlockSizeWords]; + BinarySpans.ReadUInt64LittleEndian(paddedBlock, p); + + Debug.Assert(h.Length == _h.Length); + _h.CopyTo(h); + + Span n = stackalloc ulong[BlockSizeWords]; + _n.CopyTo(n); + + Span sigma = stackalloc ulong[BlockSizeWords]; + _sigma.CopyTo(sigma); + + // Stage 3: Process padded block + GN(h, n, p); + + // Update N with the bit count of the final partial block + AddModulus512(n, _bufferLength * 8); + + // Update Sigma with padded block + AddBlock512(sigma, p); + + // Stage 4: Final compressions with zero + GN(h, _zeroArray, n); + GN(h, _zeroArray, sigma); + } + [MethodImpl(MethodImplOptionsEx.OptimizedLoop)] protected ulong[] HashFinal() { @@ -421,7 +487,7 @@ private static void E(ReadOnlySpan k, ReadOnlySpan m, ref Vector51 /// Uses Miyaguchi-Preneel construction. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void GN(ulong[] h, ReadOnlySpan n, ReadOnlySpan m) + private static void GN(Span h, ReadOnlySpan n, ReadOnlySpan m) { Span k = stackalloc ulong[BlockSizeWords]; Span t = stackalloc ulong[BlockSizeWords]; @@ -542,7 +608,7 @@ private static void ApplyLPS(Span data) /// Add a number of bits to the 512-bit counter (little-endian arithmetic). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddModulus512(ulong[] counter, int bits) + private static void AddModulus512(Span counter, int bits) { unchecked { @@ -583,7 +649,7 @@ private static void AddModulus512(ulong[] counter, int bits) /// Add two 512-bit blocks as little-endian integers modulo 2^512. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AddBlock512(ulong[] a, ReadOnlySpan b) + private static void AddBlock512(Span a, ReadOnlySpan b) { unchecked { diff --git a/IT.Hashing.Gost/IHashAlgorithm.cs b/IT.Hashing.Gost/IHashAlgorithm.cs index a05b6cc..fc9f983 100644 --- a/IT.Hashing.Gost/IHashAlgorithm.cs +++ b/IT.Hashing.Gost/IHashAlgorithm.cs @@ -6,6 +6,8 @@ public interface IHashAlgorithm : IDisposable { int Size { get; } + int SizeInBase64 { get; } + void Append(byte value); void Append(ReadOnlySpan span); @@ -14,5 +16,7 @@ public interface IHashAlgorithm : IDisposable bool TryGetHash(Span hash, out int length); + bool TryGetHashInBase64(Span hash, out int length); + void Reset(); } \ No newline at end of file diff --git a/IT.Hashing.Gost/IT.Hashing.Gost.csproj b/IT.Hashing.Gost/IT.Hashing.Gost.csproj index c2caba0..695b2fc 100644 --- a/IT.Hashing.Gost/IT.Hashing.Gost.csproj +++ b/IT.Hashing.Gost/IT.Hashing.Gost.csproj @@ -13,7 +13,7 @@ true Ivan Tikhonov Ivan Tikhonov © 2026 - 2.0.2 + 2.0.3 Gost Hashing Managed Readme.md Icon.png diff --git a/IT.Hashing.Tests/Gost.cs b/IT.Hashing.Tests/Gost.cs index 797d70e..d16acaa 100644 --- a/IT.Hashing.Tests/Gost.cs +++ b/IT.Hashing.Tests/Gost.cs @@ -1,6 +1,7 @@ using IT.Hashing.Gost; using IT.Hashing.Gost.Native; using Org.BouncyCastle.Security; +using System.Text; namespace IT.Hashing.Tests; @@ -20,7 +21,13 @@ public void Gost94() { _random.NextBytes(bytes); - var hash = CalcAlgorithm(nativeAlg, bytes); + nativeAlg.Append(bytes); + + var hashBase64 = new byte[nativeAlg.SizeInBase64]; + nativeAlg.TryGetHashInBase64(hashBase64, out _); + + var hash = ToHashAndReset(nativeAlg); + Assert.That(Encoding.UTF8.GetString(hashBase64), Is.EqualTo(Convert.ToBase64String(hash))); var hash1 = gostNative.ComputeHash(bytes); @@ -44,13 +51,20 @@ public void Gost512() { _random.NextBytes(bytes); - var hash = CalcAlgorithm(nativeAlg, bytes); + nativeAlg.Append(bytes); + gostManaged.Append(bytes); + + var hashBase64 = new byte[nativeAlg.SizeInBase64]; + nativeAlg.TryGetHashInBase64(hashBase64, out _); + + var hash = ToHashAndReset(nativeAlg); + Assert.That(Encoding.UTF8.GetString(hashBase64), Is.EqualTo(Convert.ToBase64String(hash))); var hash1 = gostNative.ComputeHash(bytes); var hash2 = DigestUtilities.CalculateDigest("GOST3411_2012_512", bytes); - var hash3 = CalcAlgorithm(gostManaged, bytes); + var hash3 = ToHashAndReset(gostManaged); Assert.That(hash.SequenceEqual(hash1), Is.True); Assert.That(hash.SequenceEqual(hash2), Is.True); @@ -71,13 +85,20 @@ public void Gost256() { _random.NextBytes(bytes); - var hash = CalcAlgorithm(nativeAlg, bytes); + nativeAlg.Append(bytes); + gostManaged.Append(bytes); + + var hashBase64 = new byte[nativeAlg.SizeInBase64]; + nativeAlg.TryGetHashInBase64(hashBase64, out _); + + var hash = ToHashAndReset(nativeAlg); + Assert.That(Encoding.UTF8.GetString(hashBase64), Is.EqualTo(Convert.ToBase64String(hash))); var hash1 = gostNative.ComputeHash(bytes); var hash2 = DigestUtilities.CalculateDigest("GOST3411_2012_256", bytes); - var hash3 = CalcAlgorithm(gostManaged, bytes); + var hash3 = ToHashAndReset(gostManaged); Assert.That(hash.SequenceEqual(hash1), Is.True); Assert.That(hash.SequenceEqual(hash2), Is.True); @@ -85,15 +106,18 @@ public void Gost256() } } - private static byte[] CalcAlgorithm(IHashAlgorithm alg, ReadOnlySpan data) + private static byte[] ToHashAndReset(IHashAlgorithm alg) { - alg.Append(data); - var hash = new byte[alg.Size]; - alg.TryGetHash(hash, out _); - alg.Reset(); + + var hash2 = new byte[alg.Size]; + alg.TryGetHash(hash2, out _); + Assert.That(hash.SequenceEqual(hash2), Is.True); + + alg.Reset(); + return hash; } } \ No newline at end of file