本文へ移動
Hikari 仕様

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:bitsxor で合成し 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 をそのまま使ってはならない用途であり、専用の口が要る