はじめに
数年前に作ったハッシュ値を計算しながらファイル(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[]
を消し去りたい今日この頃・・・