パスワードの保存や、データの改ざん検知など、現代のセキュリティに欠かせないのが、ハッシュ関数です。今回は、実際にRustでSHA-256を実装して、ハッシュ関数に関する理解を深めてましょう。
ハッシュ関数とSHA-256とは
ハッシュ関数とは、入力データを一定の手順で変換し、固定長のハッシュ値と呼ばれるデータにする仕組みのことです。入力データが少し変わるだけで、生成されるハッシュ値が大きく変化します。ハッシュ値から元のデータの推測が難しいことから、厳格なセキュリティが必要な場面で利用されます。具体的に言えば、パスワードの保存や、データの改ざん検知で利用されます。
そして、今回Rustで作るのは、現代セキュリティで幅広く利用されている「SHA-256」です。「SHA-256」(Secure Hash Algorithm 256)とは、SHA-2のバリエーションの一つで、256ビットのハッシュ長を持つ暗号的ハッシュ関数です。2001年にアメリカ国立標準技術研究所(NIST)によって標準化されました。
SHA-256の仕組み
SHA-256の仕組みですが、入力データを固定長のブロック(512ビット)に分割して、繰り返し定数を用いて計算を行って、最終的に256ビットのハッシュ値を計算します。
SHA-256の計算手順は次の通りです。
(1) 入力データをパディング処理してブロックに分割
(2) ハッシュ値を初期化
(3) ブロックごとに繰り返し圧縮関数を適用してハッシュ値を更新
最初に、(1)のパディング処理は、入力データを512ビットの倍数になるようにパディング処理を行います。末尾にデータ長の情報を付加します。そして、(2)では、ハッシュ値の初期値を設定します。(3)では、パディング処理した入力データを、512ビットごとのブロックに分割し、ビット回転やXORを組み合わせた圧縮関数を64回実行して、ハッシュ値を更新します。
これを図にすると次のようになります。入力データをパディング処理してブロックに分割し、ブロックの数だけ、圧縮関数を繰り返し実行して、最終的なハッシュ値を計算します。
Rustで実装してみよう
それでは、実際にRustでSHA-256を実装してみましょう。
まずは、cargoコマンドを利用してプロジェクトを作ってみましょう。ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、下記のコマンドを実行して、プロジェクトを作成しましょう。
# 新規プロジェクト「sha256」を作成
mkdir sha256
cd sha256
cargo init
すると、次のようなプロジェクトのテンプレートが生成されます。
.
├── Cargo.toml
└── src
└── main.rs
src/main.rsに、こちらのGist(https://gist.github.com/kujirahand/a29f81eed3ed5f7bc8b6c956b008d470)にあるプログラムを貼り付けましょう。
プログラムを貼り付けたら、ターミナル上で下記を実行しましょう。
cargo run
すると、次のように表示されます。これは上から順に「hello」「world」「cat」「dog」のSHA-256ハッシュ値となっています。
$ cargo run
…省略…
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
77af778b51abd4a3c51c5ddd97204a9c3ae614ebccb75a606c3b6865aed6744e
cd6357efdd966de8c0cb2f876cc89ec74ce35f0968e11743987084bd42fb8944
次の画像は、実際にコマンドを実行してみたところです。
SHA-256のプログラムを確認してみよう
ここから、SHA-256のプログラムにおけるポイントとなる部分のプログラムを確認してみましょう。下記のプログラムは、定数KとH0を抜粋掲載したものです。SHA-256の計算では、256ビットの初期ハッシュ値に対して、入力データを用いて、繰り返し圧縮関数を適用していきます。下記の定数H0が初期ハッシュ値です。そして、定数Kは、圧縮関数の中で利用します。なお、定数H0のデータ型[u32; 8]は、要素が8個の32ビット符号なし整数の配列という意味になります。
// ラウンド定数の配列の初期値
const K: [u32; 64] = [
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
…省略…
];
// 初期ハッシュ値(IV)
const H0: [u32; 8] = [
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
];
続いて、圧縮関数を適用して、ハッシュ値を更新する処理を確認しましょう。以下の部分が、SHA-256のハッシュ値を計算するメイン部分となります。
// ハッシュ値初期化 (H0をコピー)
let mut state = H0;
// 各ブロックの圧縮関数を適用
for i in 0..block_count {
// 512ビット(64バイト)ブロックを取り出し
let block = &padded[i * 64..(i + 1) * 64];
let mut w = [0u32; 64];
// 最初の16個を展開
for j in 0..16 {
w[j] = u32::from_be_bytes([
block[j * 4],
block[j * 4 + 1],
block[j * 4 + 2],
block[j * 4 + 3]]);
}
// 残りの48個(w[16]からw[63]まで)を計算
for j in 16..64 {
w[j] = small_sigma1(w[j - 2])
.wrapping_add(w[j - 7])
.wrapping_add(small_sigma0(w[j - 15]))
.wrapping_add(w[j - 16]);
}
// 作業変数 a,b,c,d,e,f,g,h に現在の state をコピー
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h) = (
state[0], state[1], state[2], state[3],
state[4],state[5], state[6], state[7]);
// 64回の繰り返し
for j in 0..64 {
let temp1 = h
.wrapping_add(big_sigma1(e))
.wrapping_add(ch(e, f, g))
.wrapping_add(K[j])
.wrapping_add(w[j]);
let temp2 = big_sigma0(a).wrapping_add(maj(a, b, c));
h = g;
g = f;
f = e;
e = d.wrapping_add(temp1);
d = c;
c = b;
b = a;
a = temp1.wrapping_add(temp2);
}
// 計算結果を現在の state に加算
state[0] = state[0].wrapping_add(a);
state[1] = state[1].wrapping_add(b);
state[2] = state[2].wrapping_add(c);
state[3] = state[3].wrapping_add(d);
state[4] = state[4].wrapping_add(e);
state[5] = state[5].wrapping_add(f);
state[6] = state[6].wrapping_add(g);
state[7] = state[7].wrapping_add(h);
}
まとめ
SHA-256は、セキュリティやデータ検証の基盤技術として広く利用されていますが、ここで紹介したように、SHA-256を求めるプログラムは、110行以下で記述することができます。そのため、頑張ってプログラムの流れを追うなら、その仕組みの概要を理解することができるでしょう。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。