6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【C#】Span<byte>を用いて少しずつハッシュ値計算(IncrementalHash)

Posted at

はじめに

数年前に作ったハッシュ値を計算しながらファイル(Stream)を暗号化する汎用的なメソッドを、最近のC#コードへ持っていくにあたって、

  • byte[]を使ったレガシーなストリームの読み取り処理になっていた部分をSpan<byte>を使ったものに更新
  • Obsolete属性が付いたAesCryptoServiceProviderクラスを使っていた部分はAes.Create()に変更
  • Obsolete属性が付いたMD5CryptoServiceProviderクラスを使っていた部分はMD5.Create()に変更

・・・と思ったら、MD5クラス(ICryptoTransformインターフェース)のTransformBlockメソッドとTransformFinalBlockメソッドはbyte[]にしか対応しておらずSpan<byte>が使えない・・・
クラス一覧を眺めていて代替クラスを見つけたもののクラス名でググっても、まさかの日本語サイトはMicrosoftのドキュメントだけ。英語の情報もかなり少なめ・・・
「これは有りそうで無い情報を見つけたのでは!?」 と思ったので数年振りにメモ。

ストリームのハッシュ値計算だけならHashAlgorithmクラスのComputeHashメソッドに突っ込むだけで計算できるから意外と需要が無いのかな?

IncrementalHashクラス

このクラスを使えば超簡単にできた。

/// <summary>
/// ストリームのデータを読み取ってハッシュ値を計算します。
/// </summary>
/// <param name="stream">ハッシュ値を計算するストリーム</param>
/// <param name="hashAlgorithm">計算に使用する使用するアルゴリズム</param>
/// <returns>ハッシュ値</returns>
public static ReadOnlySpan<byte> ComuputeHash(Stream stream, HashAlgorithmName hashAlgorithm)
{
	// 指定されたアルゴリズムでIncrementalHashを生成
	using var incHash = IncrementalHash.CreateHash(hashAlgorithm);

	Span<byte> buff = stackalloc byte[1024];
	for (; ; )
	{
		// ストリームから読み込み
		int readLen = stream.Read(buff);
		if (readLen == 0)
			break;

		// ハッシュ計算
		incHash.AppendData(buff[..readLen]);
	}

	// 計算したハッシュを取得
	return incHash.GetHashAndReset().AsSpan();
}

/// <summary>
/// ストリームのデータを読み取ってハッシュ値を計算します。
/// </summary>
/// <param name="stream">ハッシュ値を計算するストリーム</param>
/// <returns>ハッシュ値</returns>
public static ReadOnlySpan<byte> ComuputeHash(Stream stream)
	=> ComuputeHash(stream, HashAlgorithmName.SHA256);

(実際には読み取ったデータをそのまま CryptoStream クラスで書き込んだりしてるけど、省略して単純なハッシュ計算処理のみ掲載)

FinalBlockかどうかとかも気にする必要がなく、ただデータを突っ込んでいって GetHashAndReset() メソッドで計算結果を取得するだけのお手軽さ。
あと、引数で簡単にアルゴリズムが変えられたので、非推奨なMD5からSHA256に変えた。

(余談)
無限ループは、while(true)よりリテラルを一切書かないfor(;;)派。
一応、言語(コンパイラ?)によっては、while(true)だとループのたびに条件を評価しちゃうとかなんとかを聞いたことがあるような・・・
てか、このコードだとfor文の中に組み込んでしまう手も・・・

for (int readLen = stream.Read(buff);
	 readLen != 0;
	 readLen = stream.Read(buff))
{
	// ハッシュ計算
	incHash.AppendData(buff[..readLen]);
}

見やすいかどうかはさておき()

おまけ(高速ハッシュ値比較)

以前は2つのハッシュ値を格納した byte[] を比較するのに、forで1バイトずつ比較したり、LINQのEnumerable.SequenceEqualを使ったり、強引にmemcmpしたり、どれもイマイチしっくりこなかったけど、今はSpan<byte>同士を拡張メソッドのMemoryExtensions.SequenceEqualで高速比較できて超便利・・・

if (!hash1.SequenceEqual(hash2))
{
	// 不一致
	throw new InvalidDataException("ハッシュ値が一致しません。");
}

参考:MemoryExtensions.SequenceEqual(SpanHelpers.SequenceEqual)の高速性について

さいごに

長年C#のbyte[]を使うたびに「もう少し何とかならんのか・・・」と思いつつ「低レベルな実装だから仕方ないのかなぁ」と思っていたところ、Span<T>Memory<T>の登場で大幅に改善されたのが感動的すぎて(特にSliceが泣けた)、過去・現在・未来の全てのソースからbyte[]を消し去りたい今日この頃・・・

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?