ハッシュ化
Hashing ・ はっしゅか
任意のデータから、元に戻せない固定長の値を作る処理。改ざん検知とパスワード保存の要。
✦ 30秒で試す — 航海同じパスワードの2人が、漏れた表の上では別人に見える出力の256ビットのうち、変わるのは?予想してみる概要
ハッシュ化とは、どんな長さのデータからでも、決まった長さの値(ハッシュ値、ダイジェスト)を計算する処理です。1文字のテキストでも数GBの動画でも、たとえば SHA-256 なら必ず256ビット(16進数で64文字)になります。大事な性質は2つあります。同じ入力からは必ず同じハッシュ値が出ること、そしてハッシュ値から元のデータを復元することが事実上できないこと(一方向性)です。
「元に戻せないなら何の役に立つのか」と思うかもしれませんが、この性質こそが価値です。元データを持っていなくても、「2つのデータが同じかどうか」をハッシュ値の比較だけで確かめられるからです。パスワードの照合、ダウンロードしたファイルの改ざん検知、Git のコミット ID、キャッシュのキー、データベースのインデックスの一種であるハッシュインデックスと、ハッシュはシステムのあらゆる場所に顔を出します。
なお、鍵があれば元に戻せる暗号化とはまったく別の技術です。この2つを取り違えると、とくにパスワードの保存で致命的な設計ミスにつながります。
なぜ生まれたか
原点のひとつは「大量のデータから目的のものを速く見つけたい」という探索の問題です。先頭から順に調べると時間がかかりますが、データから計算した小さな値を格納場所の番地として使えば、一度で目的の場所にたどり着けます。これが1950年代に生まれたハッシュテーブルで、いまも最も使われるデータ構造のひとつです。この用途では、速くて値がよく散らばれば十分でした。
もうひとつの系譜がセキュリティです。ネットワーク越しに受け取ったファイルが途中で書き換えられていないかを確かめるのに、全データを別経路で照合するのは現実的ではありませんが、短いハッシュ値だけなら比べられます。ただしこの用途では、攻撃者が「同じハッシュ値になる偽物」を狙って作れないことが必要です。そこで意図的な衝突を作れないよう設計された「暗号学的ハッシュ関数」が整備されました。MD5(1992年)、SHA-1(1995年)と続きましたが、MD5 は2004年に、SHA-1 は2017年に実際の衝突が示され、いまは SHA-2(SHA-256 など)と SHA-3 が使われています。
さらに深刻だったのがパスワード保存です。パスワードを平文でデータベースに保存すると、漏れた瞬間に全ユーザーの認証情報がそのまま流出します。2009年には平文で保存された約3,200万件のパスワードが流出する事件があり、2012年にはソルトなしの SHA-1 で保存されていた大手 SNS のハッシュが大量に解読されました。「サーバの管理者ですら元のパスワードを知り得ない形で、しかも解読に莫大な時間がかかるように保存する」ことが求められ、パスワード専用の遅いハッシュ関数が標準になっていきます。
詳細
暗号学的ハッシュ関数の性質
セキュリティ用途のハッシュ関数には、固定長に変換する以上の性質が求められます。ハッシュ値から元の入力を見つけられないこと(原像計算困難性)、同じハッシュ値になる別の入力の組を作れないこと(衝突耐性)、そして入力が1ビット変わるだけで出力が全面的に変わること(雪崩効果)です。雪崩効果がないと、「出力が少し似ている入力は、元の入力も少し似ている」という手がかりから答えに近づけてしまいます。代表的なアルゴリズムは SHA-2 ファミリ(SHA-256・SHA-512)と SHA-3 です。MD5 と SHA-1 は衝突が実証されているので、署名や改ざん検知には使いません。
性質は手で触ると早く腑に落ちます。下の部品で入力のビットを1つずつ反転させ、出力の何ビットが入れ替わるかを数えてください。何度押しても、その数は256の半分のまわりに集まります。
上の入力「chiseizu」のマス(ビット)をどれか1つ押して反転させてください。入力は1ビットしか変わらないのに、下の 256 ビットの出力がどれだけ変わるかを数えます。
パスワード保存 — ハッシュ化が鉄則である理由
パスワードは暗号化ではなくハッシュ化して保存します。暗号化は鍵があれば戻せるので、鍵が漏れれば全員のパスワードが平文に戻ってしまいます。ハッシュ化なら、データベースが丸ごと漏れても、そこにあるのは元に戻せない値だけです。ログイン時の照合は「入力されたパスワードを同じ方法でハッシュ化し、保存値と比べる」ことで行います。登録時とログイン時の2つの経路が、同じハッシュ計算を通って比較の一点で合流する構造です。
ソルトと遅いハッシュ — それぞれが防ぐもの
ただし、SHA-256 を1回かけるだけでは足りません。漏れたハッシュ値の表を手にした攻撃者には、2つの攻め方があるからです。1つ目は「事前計算」です。よく使われるパスワードのハッシュ値をあらかじめ大量に計算した表(レインボーテーブルはこれを省メモリにした形)を用意しておけば、漏れた値を表で引くだけで元のパスワードが分かります。しかも同じパスワードの利用者は同じハッシュ値になるので、1回引けば全員がまとめて割れます。これを防ぐのが「ソルト」です。ユーザーごとにランダムな値を生成してパスワードに混ぜてからハッシュ化し、ソルトはハッシュ値と一緒に保存します。ソルトは秘密ではありませんが、ソルトごとに計算し直しが必要になるので、事前計算の表は役に立たなくなり、同じパスワードでもハッシュ値が別々になります。
2つ目は「総当たり」です。候補を片っ端からハッシュ化して照合する攻撃で、ソルトでは防げません。SHA-256 は速さを売りにした関数なので、GPU 1枚で毎秒百億回ほど試せてしまいます。そこで使うのが、わざと遅く作られたパスワード専用のハッシュ関数です。bcrypt(1999年)、scrypt、Argon2(2015年のパスワードハッシュ競技会で選ばれた方式)は、1回の計算に時間やメモリを食うよう設計されていて、計算の重さ(コスト)を後から引き上げることもできます。bcrypt をコスト12で使えば、GPU でも毎秒千回程度しか試せません。新規に作るなら Argon2id(または bcrypt)を選び、ライブラリの既定値かそれ以上のコストで使うのが今の定石です。
下の部品で、漏れたパスワード表に攻撃者の道具を落としてみてください。保存方式を切り替えると、ソルトが止めるものと、遅いハッシュが止めるもの、そしてどちらでも救えないもの(辞書に載っている弱いパスワード)が分かれて見えます。
左は、あるサービスから丸ごと漏れたパスワード表です。右の攻撃者の道具「事前計算表」をつかんで、表の上に落としてください。
さらに一段守りを足すのが「ペッパー」です。全ユーザー共通の秘密の値をハッシュ計算に混ぜ、データベースとは別の場所(シークレット管理の仕組みや HSM)に置きます。データベースだけが漏れた場合、攻撃者はペッパーを知らないので総当たりすら始められません。ただしペッパーを失うと誰もログインできなくなるので、鍵と同じ扱いの管理が要ります。
改ざん検知・MAC・署名
ハッシュのもうひとつの主戦場が完全性(データが書き換えられていないこと)の確認です。ソフトウェアの配布サイトがファイルと一緒に SHA-256 の値を載せているのは、受け取ったファイルが壊れたり改ざんされたりしていないかを確かめられるようにするためです。ただし、ハッシュ値そのものを同じ経路で配っていれば、攻撃者はファイルとハッシュ値を両方差し替えられます。ハッシュ値は「別の信頼できる経路」で手に入れて初めて意味を持ちます。
そこで、鍵を知る者だけが正しい値を作れるようにしたのが MAC(メッセージ認証コード)で、ハッシュ関数に鍵を組み込んだ HMAC が代表です。JWT の HS256 署名や Webhook の署名検証、API リクエストの署名に使われています。さらに、共通の鍵ではなく公開鍵暗号の秘密鍵で「データのハッシュ値」に署名するのが電子署名です。署名は長いデータ全体ではなくハッシュ値に対して行うので、ここでも衝突耐性が土台になります。MD5 の衝突を悪用して正規の証明書を偽造した事件(2012年のマルウェア Flame)は、この土台が崩れると何が起きるかを示しました。
TLS との関係は時代で変わっています。TLS 1.2 までの一部の暗号スイートは、通信の改ざん検知に HMAC を使っていました。TLS 1.3 では、通信の中身は認証付き暗号(AES-GCM や ChaCha20-Poly1305)が暗号化と改ざん検知を一体で受け持ち、HMAC は鍵を導出する手順(HKDF)の中で使われています。
内容から名前を作る
「内容から一意な名前を作る」用途も広く使われます。Git のコミット ID はコミット内容のハッシュ値そのもので、内容が1文字でも違えば別の ID になります(Git は長く SHA-1 を使ってきたので、衝突を検出する仕組みを入れたうえで、SHA-256 への移行も進めています)。バンドラが出力するファイル名に app.a3f8c1.js のようなハッシュを埋め込むのも、内容が変わったときだけ URL が変わるようにして、CDN やブラウザのキャッシュを長期間効かせるためです。
実務での落とし穴
よくある間違いの1つ目は、用途と関数の取り違えです。パスワードに速い SHA-256 を使う(遅い Argon2 や bcrypt を使うべき)、逆に大量データの重複検知に遅い関数を使って性能を落とす、といったものです。ハッシュテーブル向けの速いハッシュ関数を、外部から入力を受け取る場所で使うと、わざと同じ番地に集まる入力を大量に送り込んで処理を詰まらせる攻撃(ハッシュフラッディング)を受けることもあります。多くの言語がハッシュテーブルに鍵付きの SipHash などを使うようになったのはこのためです。
2つ目は、ソルトなし・全員共通のソルトでのパスワード保存です。3つ目は比較の仕方です。ハッシュ値や MAC を先頭から1バイトずつ比べて、違った時点ですぐ返すと、応答時間の差から「何バイト目まで合っていたか」が漏れます(タイミング攻撃)。比較には、入力によらず同じ時間で終わる定数時間の比較関数を使います。4つ目は「ハッシュ化したから匿名化できた」という思い込みです。電話番号やメールアドレスのように候補の数が限られるデータは、全候補をハッシュ化して突き合わせれば元に戻せてしまいます。ハッシュ化は、何を守れて何を守れないかを分かったうえで使う道具です。
試問
あるサービスから、6人分のパスワード表が丸ごと漏れた場面です。攻撃者の道具は2つあり、「事前計算表」(よくあるパスワードのハッシュを前もって並べた一覧)と「GPU 総当たり」(候補をその場で片っ端からハッシュ化して試す。時計は1万年まで進む)を表へ運んで落とせます。保存方式は「SHA-256(ソルトなし)」「ソルト+SHA-256」「ソルト+bcrypt(わざと遅いハッシュ)」から選べます。
左は、あるサービスから丸ごと漏れたパスワード表です。右の攻撃者の道具「事前計算表」をつかんで、表の上に落としてください。
