Help us understand the problem. What is going on with this article?

高校生がC#でSHA256暗号化アルゴリズムを完全独自実装してみた。

高校生の僕がSHA256暗号化アルゴリズムを外部ライブラリやランタイムに一切頼らず、独自で実装してみました。
当記事は、マイクロソフト様にケンカを売る記事ではございません。

前置き

image.png

この記事をご覧になっている方々は、様々な分野でお仕事をされている方がいらっしゃると思います。
そういった方々含め、プログラミングを実際にされていない方も「SHA256」という単語は一度でも目に入れた方も多くいらっしゃるのではないでしょうか。

SHA256とは、アメリカ国家安全保障局によって設計され、
2001年にアメリカ国立標準技術研究所によって連邦情報処理標準とされた暗号化アルゴリズムのひとつです。
SHA256の他にも、「SHA-1」や「SHA-512」といった多くの暗号化アルゴリズムがあります。

そのひとつである「SHA-1」は、2017年にGoogleによって攻撃手段が公開され、
それまでSHA-1を使用していたシステムは半強制的に、より安全な暗号化アルゴリズムへの移行を余儀なくされた。
という事件がありましたね。

該当記事: Googleがハッシュ関数「SHA-1」を破ることに成功

そういった背景もある暗号化アルゴリズム達ですが、今回はなかでも多くの場面で使われている
「SHA-256」をプログラミング言語 C# を使って独自で実装してみたいと思います。

ビットコインを支えているブロックチェーンでも、SHA-256は活躍しています。
ブロックチェーンに関しましては、下記記事をご参照頂くとわかりやすいと思います。

参考記事: 【Python】10分でイキリ高校生がブロックチェーンを初心者に解説してやる!!!

SHA256の特徴

weodnmgvbws.png

ここまでで、様々な暗号化アルゴリズムがあることがわかりました。
SHA256の特徴は

  • 一方向性
    • ▶復号化が困難である。

ここで、”困難”と言ったのは、総当り攻撃による突破が可能であるからです。
論理的には、2^80(2の80乗)という回数総当たりを仕掛ければどれかが当たる、ということです。
現在では、遥かに少ない2^63(2の63乗)という回数で突破できる手法が見つかっているそうです。

  • 衝突困難
    • ▶SHA-1で使用された攻撃手法。衝突させることが困難である。

簡単に言うと、異なる2つの平文から同一のハッシュ値が得られる脆弱性に強いということです。
詳しく知りたい方は、下記の記事を参照下さい。

参考記事: 巷で話題のGoogleのSHA-1衝突やってみた

実装してみる

ここからは、実際にプログラムを書いてC#で暗号化アルゴリズムを再現してみたいと思います。

大まかな処理フロー

かなり大雑把ではありますが、大まかな処理フローは以下の通りです。

sha256flow.png

1.平文(16進数)を2進数に変換

aであれば16進数61 2進数 1100001 に変換されます。

2.パディング処理

パディング(Padding)と呼ばれる空埋め処理を行います。
1ブロックのサイズは64byteですので、それ以下であれば空の部分を埋める必要があります。
但し、ただ埋めるだけではダメです。
例として以下のようなブロックがあったとします。
14byte分足りません。これを埋めます。

//50byte
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 

これを、ただ埋めると何が起こるかは安易に予想できます。
Xの部分を0で埋めたとすると、異なるブロックであっても全く同じメッセージブロックが生まれてしまいます。

//46byte
1111 0101 0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0100 0000 0110 0000 0000 0000 0000 XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX

//50byte
1111 0101 0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0100 0000 0110 0000 0000 0000 0000 0000 0000 XXXX XXXX XXXX XXXX XXXX XXXX XXXX

↓

全く同じブロックが生まれる。

そこで、もとのブロックのブロック長を末端に記録します。
Bの部分がブロック長を記録した部分です。

//46byte
1111 0101 0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0100 0000 0110 0000 0000 0000 0000 XXXX XXXX XXXX XXXX XXXX BBBB BBBB BBBB BBBB

//50byte
1111 0101 0111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0100 0000 0110 0000 0000 0000 0000 0000 0000 XXXX XXXX XXXX BBBB BBBB BBBB BBBB

ブロック長が64byte丁度だったり、万が一記録する余裕が無かっただった場合は次のブロックで記録します。

3.ブロックをメッセージブロックに分割

パディングが済んだ512byteのブロックを64byte長のメッセージブロックへと分割します。

4.初期ベクトルを使用してハッシュ値を算出

初回ラウンドでは、初期ベクトル(IV)を使用します。

/// <summary>
/// 初期ハッシュ
/// </summary>
private uint[] initial_hash = 
{ 
    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 
    0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 
};

5.合計64ラウンド処理を行う

行う処理は簡単に以下の通りです。

sha256flow2.png

論理代数

ここからは論理代数の分野です。
正直なところ、僕もふわっとしか理解できていません。
一通り調べてはみましたが、情報そのものが正しいものであるか区別する手段がありませんでしたので、
あくまで参考として記載します。
詳しい方がいらっしゃいましたら、コメント欄にて解説をお願い致します。

参考記事: 論理代数を学ぼう
参考記事: 論理代数と論理関数 湊 真一

Ch

Ch(E,F,G) = (E \Lambda F) Xor (\rightharpoondown E \Lambda G)
private uint Ch(uint x, uint y, uint z)
{
    return (x & y) ^ (~x & z);
}

Maj (変数多数決関数)

Maj(A,B,C) = (A \Lambda B) Xor (A \Lambda C) Xor (B \Lambda C)
private uint Maj(uint x, uint y, uint z)
{
    return (x & y) ^ (x & z) ^ (y & z);
}

RotR (Rotation Right)

private uint Rot_R(uint x, byte n)
{
    return (x >> n) | (x << (32 - n));
}

RotL (Rotation Left)

private uint Rot_L(uint x, byte n)
{
    return (x << n) | (x >> (32 - n));
}

Σ0 (Sigma0)

\sum 0 (A) = (A \ggg 2) Xor (A \ggg 13) Xor (A \ggg 22)
private uint Sigma0(uint x)
{
    return Rot_R(x, 2) ^ Rot_R(x, 13) ^ Rot_R(x, 22);
}

Σ1 (Sigma1)

\sum 1 (E) = (E \ggg 6) Xor (E \ggg 11) Xor (E \ggg 25)
private uint Sigma1(uint x)
{
    return Rot_R(x, 6) ^ Rot_R(x, 11) ^ Rot_R(x, 25);
}

C#ソースコード

ここからは、実際のソースコードを紹介します。

定数K

/// <summary>
/// Kと呼ばれる定数
/// </summary>
private readonly uint[] const_k = new uint[64] 
{
    0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 
    0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
    0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 
    0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
    0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 
    0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
    0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 
    0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
    0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 
    0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
    0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 
    0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
    0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 
    0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
    0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 
    0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
};

ComputeHash() 関数 - ハッシュ値を算出

/// <summary>
/// SHA256ハッシュを生成します。
/// </summary>
/// <param name="plainText">暗号化したい文字列</param>
/// <returns>暗号化された文字列</returns>
public string ComputeHash(string plainText)
{
    var p = Padding(plainText);

    var block_list = Parse(p);
    var s = new uint[8];
    Array.Copy(initial_hash, s, initial_hash.Length);

    //ブロックリストの中のブロックにスコープを当てる
    foreach (var block in block_list)
    {
        var pair = MakePair(s);
        var expanded_block = ExpandBlock(block);

        for (int n = 0; n < 64; ++n)
        {
            var CH = Ch(pair["e"], pair["f"], pair["g"]);
            var MAJ = Maj(pair["a"], pair["b"], pair["c"]);
            var SIG0 = Sigma0(pair["a"]);
            var SIG1 = Sigma1(pair["e"]);

            var WJ_KJ = (const_k[n] + expanded_block[n]);
            var T1_TEMP = (pair["h"] + WJ_KJ + CH);
            var T1 = (T1_TEMP + SIG1);
            var T2 = (SIG0 + MAJ);

            pair["h"] = pair["g"];
            pair["g"] = pair["f"];
            pair["f"] = pair["e"];
            pair["e"] = (pair["d"] + T1);
            pair["d"] = pair["c"];
            pair["c"] = pair["b"];
            pair["b"] = pair["a"];
            pair["a"] = (T1 + T2);
        }

        s[0] = (pair["a"] + s[0]);
        s[1] = (pair["b"] + s[1]);
        s[2] = (pair["c"] + s[2]);
        s[3] = (pair["d"] + s[3]);
        s[4] = (pair["e"] + s[4]);
        s[5] = (pair["f"] + s[5]);
        s[6] = (pair["g"] + s[6]);
        s[7] = (pair["h"] + s[7]);
    }
    return MakeHash(s);
}

ToUInt32Array() 関数 - 2進数に変換

/// <summary>
/// Stringを2進数に変換します。
/// </summary>
/// <param name="plain_text">暗号化する文字列</param>
/// <returns>2進数配列</returns>
private uint[] ToUInt32Array(string plain_text)
{
    //文字列を16進数に変換. 実体はbyte配列
    var a = Encoding.ASCII.GetBytes(plain_text);

    //結果を格納する配列
    uint[] result = { };

    foreach (var n in a)
    {
        //16進数を2進数に変換
        var j = int.Parse(Convert.ToString(n, 2));
        var len = j.ToString().Length;
        var fill_len = 0;

        //2進数を8桁に揃える  0を先頭に追加
        if(len < 8)
        {
            //2進数が8桁以下であったとき、埋めるべき数は |8 - len| である
            fill_len = Math.Abs(8 - len);
            while (fill_len > 0)
            {
                fill_len--;
                SelfAppend(ref result, 0u);
            }
        }

        SelfConcat(ref result, ToArray((uint)j));
    }
    return result;
}

CalculateK() 関数 - Kを算出

/// <summary>
/// 与えられた配列から定数Kを算出します。
/// </summary>
/// <param name="plain_bits">算出された数</param>
/// <returns>定数K</returns>
private uint CalculateK(uint[] plain_bits)
{
    uint k = 0;
    var length = plain_bits.Length;

    while ((length + 1 + k) % 512 != 448)
    {
        k += 1;
    }

    return k;
}

Padding() 関数 - パディング

/// <summary>
/// パディングと呼ばれる、空埋め処理を行います。
/// </summary>
/// <param name="plain_text">パディングする2進数配列</param>
/// <returns>パディングされた2進数配列</returns>
private uint[] Padding(string plain_text)
{
    var plain_bits = ToUInt32Array(plain_text);
    var length = plain_bits.Length;
    var k = CalculateK(plain_bits);

    //処理する値を保持するバッファ
    uint[] buf = { };

    buf = Extend<uint>(plain_bits, 1);

    for(int r = 0; r < k; r++)
    {
        SelfAppend(ref buf, 0u);
    }

    var bytStr = Convert.ToString(length, 2);

    //64桁右寄せゼロ埋め
    //0000000000000000000000000000000000000000000000000000000000011000
    bytStr = bytStr.ToString().PadLeft(64, '0');

    //↑で得た64桁の数列を配列に変換
    //(64)[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 ]
    uint[] bytStr_array = { };
    for(int x = 0; x <= 63; x++)
    {
        var num_str = bytStr.Substring(x, 1);
        var num = uint.Parse(num_str);
        SelfAppend(ref bytStr_array, num);
    }

    //位取り バッファにAppend
    foreach(var b in bytStr_array)
    {
        SelfAppend(ref buf, b);
    }

#if DEBUG
    //ブロックサイズはパディング後512である
    //Assert(buf.Length != 512)
    PrintArray(buf); //512
#endif

    return buf;
}

Parse() 関数 - ブロックの分割

/// <summary>
/// パディングされた配列を512バイトのブロック長に分割します。
/// </summary>
/// <param name="plain_bits">分割する2進数配列</param>
/// <returns>分割された2進数ブロック一覧を格納したリスト</returns>
private List<uint[]> Parse(uint[] plain_bits)
{
    var result = new List<uint[]>();
    const int BLOCK_SIZE = 512;
    var length = plain_bits.Length;
    var num_blocks = length / BLOCK_SIZE;

    for(int n = 0; n < num_blocks; n++)
    {
        var block = new uint[BLOCK_SIZE];
        Array.Copy(plain_bits, n * BLOCK_SIZE, block, 0, BLOCK_SIZE);

        //ブロックリストに追加
        result.Add(block);
    }

    return result;
}

ExpandBlock() 関数 - ブロックのプレ処理

/// <summary>
/// ブロックを処理します。
/// </summary>
/// <param name="block">64バイトの2進数ブロック配列</param>
/// <returns>処理された2進数ブロック配列</returns>
private uint[] ExpandBlock(uint[] block)
{
    uint[] result = { };

    for(int x = 0; x < 16; x++)
    {
        //例
        //バイナリ -> ヘックスデシマル -> uint OR バイナリ -> demical
        //↓チャンクバイナリ 01100001011000100110001110000000 は ヘックスデミカル 0x61626380 である.
        //CHUNK: [ 32 ] [0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]
        //= 0x61626380 したがって uint は 1633837952

        //コピー先のチャンク配列
        uint[] chunk_array = new uint[32];
        //ブロックの x*32 から 32byte先までをコピー
        Array.Copy(block, x*32, chunk_array, 0, 32);
        //バイナリに変換
        var chunk_binary_str = ToBinary(chunk_array);

        var chunk_decimal = Convert.ToUInt32(chunk_binary_str, 2);

        SelfAppend(ref result, chunk_decimal);
    }

    for(int y = 16; y < 64; y++)
    {
        var T1 = Sub0(result[y - 15]) + result[y - 16];
        var T2 = T1 + result[y - 7];
        var T3 = T2 + Sub1(result[y - 2]);

        SelfAppend(ref result, T3);
    }

    return result;
}

MakePair() 関数 - ペアを生成

/// <summary>
/// 計算をより楽に、分かりやすくするためにStringとのペアを生成します。
/// </summary>
/// <param name="hash">ペアと対になる2進数配列</param>
/// <returns>ペアを格納したディクショナリ</returns>
private Dictionary<string, uint> MakePair(uint[] hash)
{
    var dictionary = new Dictionary<string, uint>();
    dictionary.Add("a", hash[0]);
    dictionary.Add("b", hash[1]);
    dictionary.Add("c", hash[2]);
    dictionary.Add("d", hash[3]);
    dictionary.Add("e", hash[4]);
    dictionary.Add("f", hash[5]);
    dictionary.Add("g", hash[6]);
    dictionary.Add("h", hash[7]);

    return dictionary;
}

MakeHash() 関数 - 16進数に変換

追記(2)より、一度byte[32]に変換しないと0が抜け落ちる模様。

/// <summary>
/// 2進数を16進数の文字列に変換します。
/// </summary>
/// <param name="s">変換したい2進数配列</param>
/// <returns>変換された文字列</returns>
private string MakeHash(uint[] s)
{
    var s_byte_array = s.SelectMany((v) => BitConverter.GetBytes(v).Reverse()).ToArray();
    var result_str = string.Join("", s_byte_array.Select(v => $"{v:X2}"));
    return result_str.ToLower();
}

ToBinary() 関数 - バイナリに変換

/// <summary>
/// チャンクをバイナリに変換します。
/// </summary>
/// <param name="chunk"></param>
/// <returns>変換されたバイナリ</returns>
private string ToBinary(uint[] chunk)
{
    string result = string.Empty;

    foreach(var n in chunk)
    {
        result += n.ToString();
    }

    return result;
}

BinaryTodecimal() 関数 - デシマルに変換

Convert.ToString(value, 16) を使う方法もあります。

/// <summary>
/// バイナリをデシマル Integer に変換します。
/// </summary>
/// <param name="binary_str">バイナリ</param>
/// <returns>デシマル Integer</returns>
private int BinaryToDecimal(string binary_str)
{
    char[] binary_char_array = binary_str.ToCharArray();
    Array.Reverse(binary_char_array);
    int result = 0;

    for (int i = 0; i < binary_char_array.Length; i++)
    {
        if (binary_char_array[i] == '1')
        {
            if (i == 0)
            {
                result += 1;
            }
            else
            {
                result += (int)Math.Pow(2, i);
            }
        }
    }

    return result;
}

自作便利関数

やっていて、色々と不便に感じた部分や簡略化したい部分を関数化したものです。

Extend() 関数 - 配列を拡張

C#では型付けが厳しい為、定義後に配列の大きさを柔軟に変更することができません。
そこで、既存の配列を拡張する関数を作ってみました。

/// <summary>
/// 配列を右辺へ拡張し、値を格納します。
/// </summary>
/// <typeparam name="T">アンマネージド型</typeparam>
/// <param name="source">ソース元である配列</param>
/// <param name="num">格納する値</param>
/// <returns>拡張された配列</returns>
private T[] Extend<T>(IList<T> source, T num)
{
    var result = new T[source.Count+1];
    for (int n = 0; n < source.Count; n++)
    {
        result[n] = source[n];
    }
    result[source.Count] = num;
    return result;
}

使い方:

uint[] test = { 1 };
//before [ 1 ]
uint[] res = Extend<uint>(test, 3);
//after [ 1, 3 ]

PrintArray() 関数 - 配列をいい感じに出力

C#では、Console.WriteLine()で大体のことはできますが、
配列をそのまま出力しようとするとSystem.Int32と、型がそのまま出力されてしまいます。
そのままではデバッグするのもストレスなので、いい感じに出力してくれる関数を作ってみました。

/// <summary>
/// 配列をいい感じに出力します。
/// </summary>
/// <typeparam name="T">アンマネージド型</typeparam>
/// <param name="source">ソース元である配列</param>
private void PrintArray<T>(IList<T> source)
{
    Console.WriteLine(string.Format("({0})", source.Count) + "[ " + string.Join(", ", source) + " ]");
}
//こんな配列が
int[] hoge = { 0, 1, 2, 3, 4 };

//こうすると
Console.WriteLine(hoge);

//こうなるから
//Output: System.Int32

//自作関数でラップすると
PrintArray(hoge);

//いい感じに出力される
//Output: [ 0, 1, 2, 3, 4 ]

StrMid() 関数 - 数値の中から数値を取り出す

例えば、こんな数字から

123456

3のインデックス部分を取り出したいとします。
パッと思いつくのは、stringにしてSubString()する方法でした。

//int文字列から特定の位置にある数字を取り出す
//12345
// ↑indexN
//return 2

/// <summary>
/// Integer から指定されたインデックスにある数値を取り出します。
/// 12345678 の インデックス 5 の場合 返り値は 5 です。
/// </summary>
/// <param name="source">ソース元である数値</param>
/// <param name="indexN">インデックス</param>
/// <returns>取り出された数値</returns>
private int StrMid(int source, int indexN)
{
    var s = source.ToString().Substring(indexN, 1);
    return int.Parse(s);
}

実行してみた!

image.png

環境は .NET Framework 4.7.2 です。
WinFormで適当にそれっぽいものを作って実際に実行してみます。

使用例:

using (SHA256NotManaged sha256 = SHA256NotManaged.Create())
{
    var plainText = textBox1.Text;
    var encryptedText = sha256.ComputeHash(plainText);
    textBox2.Text = encryptedText;
}

実行結果:

afqbu-uhkdc.gif

出てきたハッシュをSHA256データベースで検索してみます。

image.png

正しいようです。

処理時間

今回は、アルゴリズムに集点をおいて、可読性を優先してコードを書いたのもあり
速度については一切考慮しておりませんが、一応ランタイムのものと比較してみます。

比較にあたって、下記記事を参考にソースコードを拝借いたしました。
ありがとうございました。

参考記事: C#でStopwatchを使った時間計測を1行でできるようにする

using (SHA256NotManaged sha256 = SHA256NotManaged.Create())
{
    TimeSpan elapsed = StopwatchEx.Context(() => { sha256.ComputeHash("abc"); }, 1000);
    Console.WriteLine($"自作: {elapsed.TotalMilliseconds}");
}

using (System.Security.Cryptography.SHA256 sha256_ = System.Security.Cryptography.SHA256.Create())
{
    TimeSpan elapsed = StopwatchEx.Context(() => { sha256_.ComputeHash(abc); }, 1000);
    Console.WriteLine($"本家: {elapsed.TotalMilliseconds}");
}

今回は5回ほど計測してみました。
それぞれ1000回の試行にかかった平均時間(ミリ秒)です。
ランタイムのものと比較すると、約800倍近く時間がかかっていますね。
手も足も出ませんでした。

正直ランタイムといい勝負するのではないかと期待していた 打ち砕かれました。

自作: 857.9315
本家: 1.1778

自作: 920.8264
本家: 1.8223

自作: 804.2666
本家: 1.009

自作: 803.6653
本家: 1.0139

自作: 821.0931
本家: 1.0187

最後に

一番苦戦したのは理論代数の分野の理解です。
一向に理解できないせいで計算があわず、2時間格闘しました。。。
結局ふわっとしか理解できていません。
詳しい方がいらっしゃれば、些細な情報でもコメント欄にて解説頂けるとありがたいです。

フルソースコードは僕のGithubリポジトリにあります。
コピペで動きます。
image.png

当記事内で間違いや誤字脱字等ありましたら、お手数ですがコメント欄にてご指摘いただけますと幸いです。

最後まで読んでいただき、ありがとうございました。

追記 (不具合修正)

  • 一定の文字数を超えると、生成されるハッシュの値が異なる現象
  • その他一部で意図しない動作

を確認しました。

本題です。
なぜこのプログラムで一定以上の文字数を超えると算出されるハッシュ値が異なるか?
結論から申し上げますと、ブロック長の記録の例外処理を完全に忘れていました。
「何らかの理由で記録するスペースがなかった場合、次のブロックで記録する」
という処理です。

ただのヒューマンエラーでした。

最初、この謎現象の原因を掴むべくデバッグしていましたら

image.png

右が正しいものです。
ブロック末端&チャンクが異なっています。
デバッグを続けていくと、この現象は
一定の文字数以上(ブロック数2以上)で発生することがわかりました。
したがって、ブロック分割部分で何かしらの問題が発生していると予想できます。

そこで、実際に中身を見てました。

image.png

予想的中。
1ブロック目と2ブロック目のブロックがそれぞれ同一の値となっていることが確認できます。
問題の分割関数を覗いてみます。

5799d40641a72eedc85e110d8d2b3e14.png

問題箇所を特定できました。
人為的ミスは、一歩引いてソースを見てみないと中々発見できませんね。。
2進数のプレーンテキスト配列の0から512までを毎回コピーしていたことが原因でした。
ブロックが複数ある場合にのみ発生する不具合だったので、発見に時間がかかりました。
修正後のソースコードは以下の通りです。

//ブロックが複数ある場合も想定されるので、
//n*512から512先までをコピーしなければいけない。
Array.Copy(plain_bits, n * BLOCK_SIZE, block, 0, BLOCK_SIZE);

以上、折角の機会なので不具合に対する適切な対処法と僕なりのデバッグ手法を紹介してみました。

追記(2) 不具合について

既知の不具合として、コメント欄にてご指摘いただきました。
どうやら、プレーンテキストの文字数が5文字であり、特定の文字が含まれていると発生する模様。

image.png

▼2文字分、0が抜け落ちてます。
image.png

だけど、abcdhは大丈夫。

image.png

abcdgもダメ。

image.png

生成されるプレーンテキストに0が含まれていると発生するみたいです。
計算に間違いがあれば生成されるハッシュ全体が変わるはずですから、
この特定の文字が局所的に変化する現象はハッシュを16進数に変換する課程で問題が発生していると思われます。
5文字以上である時は特に問題は無い様子?
↑ははっきりと原因がわからない。

とりあえず、デバッグプリントしてみます。

自作Bytes: (32)[ 191, 22, 120, 186, 234, 207, 1, 143, 222, 64, 65, 65, 35, 34, 174, 93, 163, 97, 3, 176, 156, 122, 23, 150, 97, 255, 16, 180, 173, 21, 0, 242 ]
本家Bytes: (32)[ 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173 ]

4桁区切りでの規則性が見られます。
191, 22, 120, 186186, 120, 22, 191
uintからbyte[4]の変換の際に配列をリバースする必要がある様です。

そこで、こんな感じにリバースしてみました。

var test = s.SelectMany((v) => BitConverter.GetBytes(v).Reverse()).ToArray();
自作Bytes: (32)[ 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173 ]
本家Bytes: (32)[ 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173 ]

OKっぽい。
肝心のabcdeの中身を見てみます。

自作Bytes: (32)[ 54, 187, 229, 14, 217, 104, 65, 209, 4, 67, 188, 182, 112, 214, 85, 79, 10, 52, 183, 97, 190, 103, 236, 156, 74, 138, 210, 192, 196, 76, 164, 44 ]
本家Bytes: (32)[ 54, 187, 229, 14, 217, 104, 65, 209, 4, 67, 188, 182, 112, 214, 85, 79, 10, 52, 183, 97, 190, 103, 236, 156, 74, 138, 210, 192, 196, 76, 164, 44 ]

。。。おろ?問題ないっぽいです。
これを16進数文字列に変換してみます。

読みにくいコードでごめんなさい。
やっていることは、
uintbyteReverse()Concat()bytestring(64)
こんな感じです。

var test = s.SelectMany((v) => BitConverter.GetBytes(v).Reverse()).ToArray();
var makehash_fixed_test = test.SelectMany((v) => Convert.ToString(v, 16)).ToArray();
var t = string.Join("", test.Select(v => $"{v:X2}"));
Console.WriteLine(string.Format("MakeHashFixed?: ({0}) : {1}", t.Length, t.ToLower()));

無事解決しました!

MakeHashFixed?: (64) : 36bbe50ed96841d10443bcb670d6554f0a34b761be67ec9c4a8ad2c0c44ca42c

image.png

やはり、予想通りエンコーディングの問題でした。
具体的な原因は謎ですが、
下記のコードでも不具合が発生することを考えると、フレームワークの不具合なのかな?と思ったり。
とにかく、byteを噛ませないとダメみたいですね。

//これも不具合発生.
private string MakeHash(uint[] s)
{
    var n = s.Select((v) => $"{v:X2}");
    return string.Join("", n).ToLower();
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away