std:crypto — 暗号ハッシュ
標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。
import c := "std:crypto" は暗号ハッシュ 3 つ・鍵付きハッシュ 1 つ・定数時間の比較 1 つを slot に持つ namespace object を c に束縛する。モジュール名を内容ちょうどの hash ではなく crypto と広く採るのは、鍵付きハッシュや将来の署名等の受け皿にするためである。
std:math / std:bits と同じく I/O を伴わず、純粋・同期 (Future を返さない)。この不変条件は本書の契約である — 入力が同じなら出力も同じで、外界を読まない。エントロピーを引く口はこの契約と両立しないので、別モジュール std:crypto/random (crypto/random.md) が持つ。鍵とトークンを作るのはあちら、それを署名するのがこちらである。
import c := "std:crypto" c.sha256("abc".to_bytes!) #> <32 bytes> (ba7816bf…) c.sha1("abc".to_bytes!) #> <20 bytes> c.md5("".to_bytes!) #> <16 bytes> (d41d8cd9…) mac := c.hmac_sha256(key, msg) #> <32 bytes> c.constant_time_equal(mac, want)
以下では import c := "std:crypto" で束縛したものとして記す。
1. slot 一覧
| 名前 | 形 | 意味 |
|---|---|---|
sha1 |
c.sha1(bytes) |
SHA-1 ダイジェスト (20 bytes) |
sha256 |
c.sha256(bytes) |
SHA-256 ダイジェスト (32 bytes) |
md5 |
c.md5(bytes) |
MD5 ダイジェスト (16 bytes) |
hmac_sha256 |
c.hmac_sha256(key, msg) |
HMAC-SHA-256 (32 bytes)。§2 |
constant_time_equal |
c.constant_time_equal(a, b) |
内容で分岐しない Bytes の比較 → Bool。長さが違えば false だがそのときも走査は同じだけ行う。§3 |
本モジュールは効果を1 つも持たない(../language-spec.md §17.9)。
- 入力は Bytes のみ。String は
s.to_bytes!を経由する (引数型を 1 つに保つ)。非 Bytes はError。 - 返り値は生のダイジェスト Bytes (16 進文字列ではない)。
- SHA-1 と MD5 は衝突攻撃に対して脆弱であり、新規のセキュリティ用途には SHA-256 を使う。SHA-1 / MD5 はレガシープロトコル互換 (例: MySQL の
mysql_native_password認証は SHA-1 を要求する) のために提供する。
2. hmac_sha256
c.hmac_sha256(key: Bytes, msg: Bytes) は RFC 2104 の HMAC を SHA-256 で構成し、32 bytes を返す。
- 鍵の長さに制限を設けない。 ブロック長 (64 bytes) を超える鍵は SHA-256 で縮めてから使い、短い鍵は 0 で右詰めする (RFC 2104 の規定どおり)。呼び手が鍵を整える必要は無い。
- 空の鍵・空のメッセージも受ける。
- RFC 4231 のテストベクターで固定する。 自前で組める計算だが、正しさがそのまま安全性なので、値を試験が押さえる。
純 Hikari で書かずに std へ置く理由は 2 つある。第 1 に、sha256 を 2 回呼ぶだけの計算に見えて、鍵の縮め方・詰め方・内外のパッドの取り違えが黙って通る(出力の見た目は変わらない)。第 2 に、std:crypto が Bytes の連結を持たないので、純 Hikari で書くと List への開き閉じが 1 回の署名ごとに 4 回入る。
3. constant_time_equal
c.constant_time_equal(a: Bytes, b: Bytes) は 2 つの Bytes が等しいかを Bool で返す。最初に違うバイトの位置で早期に帰らない。
- 長さが違えば
false。そのときも走査は同じだけ行う — 長さの差から内容を推し量られないためである。 ==との違いは分岐の仕方だけで、返す値は同じである。- MAC・トークン・パスワードのハッシュの照合にはこれを使う。
==は最初の差で帰るので、比較にかかる時間から「どこまで合っていたか」が漏れ、1 バイトずつ総当たりで正しい値を組み立てられる。 - 逆に、秘密でない値の比較に使う値打ちは無い。
==より遅く、読み手に「これは秘密である」と誤って伝える。
4. 利用例 — MySQL 認証スクランブル
mysql_native_password のスクランブル SHA1(pw) XOR SHA1(salt ++ SHA1(SHA1(pw))) は、Bytes を Int の List に開いて std:bits の xor で合成し bytes().unwrap! で閉じる:
import b := "std:bits" import c := "std:crypto" xor_bytes := { x, y | xl := x.to_list() yl := y.to_list() bytes(range(0, xl.length!).map { i | b.xor(xl.[i], yl.[i]) }).unwrap! # XOR 結果は常に 0..255 なので None にならない }
Bytes 同士の連結も同様に List 側で行う (x.to_list() を伸ばして bytes().unwrap! で確定)。Bytes 直接の連結・zip は持たない (language-spec.md §7.2.2)。
5. 将来枠
- 他のダイジェストによる HMAC (
hmac_sha1等)。SHA-1 の HMAC は SHA-1 の衝突耐性に依らないので、レガシープロトコルの互換に要ることがある - RSA / 署名 / 検証 (TLS は net.md が担うので、ここに署名系を置く動機が無い)
- パスワードハッシュ (argon2 / bcrypt)。
sha256をそのまま使ってはならない用途であり、専用の口が要る