パスワードハッシュの進化(MD5からArgon2へ) —— なぜ「SHA-256で暗号化」したパスワードは数秒で蒸発するのか?
はじめに:ニュースの「パスワードはハッシュ化されていたため安全」は本当か?
企業の個人情報流出ニュースで、決まり文句のように目にする一節があります。
「流出したデータベース内のパスワードは、高度なアルゴリズムでハッシュ化(暗号化)されて保存されていたため、第三者に解読される恐れはありません」
この文言を見て胸をなでおろしたことがあるなら、残念ながら現実の物理法則を知る必要があります。
もしそのシステムが、プログラミング入門書によく書かれているような hash('sha256', $password) でハッシュ化されていた場合、攻撃者の手元に流出したパスワード群は数分から数時間のあいだに次々と平文へと復元され、蒸発していきます。
「SHA-256はビットコインでも使われている世界最高峰の安全なハッシュ関数ではなかったのか?」 「なぜSHA-256でハッシュ化して保存してはならないのか?」
その答えは、数学的な強度の問題ではありません。 CPUとGPUという計算プロセッサの物理構造の違い、そして**「ハッシュ関数が本来何のために作られたのか」という歴史的設計目的のズレ**にあります。
1. 速度のパラドックス —— ハッシュ関数は「速すぎる」から危険である
ハッシュ関数の最大の弱点は、**「計算が速すぎる」**ことです。
MD5、SHA-1、SHA-256といった汎用暗号学的ハッシュ関数は、元々パスワードを守るために作られたものではありません。 それらは**「ギガバイト単位の巨大なファイルやネットワークパケットが改ざんされていないかを瞬時に検証する(完全性保証)」**ために設計されました。
そのため、CPUのクロックサイクルを極限まで無駄にしないよう、極めて軽量かつ高速に計算できるようチューニングされています。
- ログイン処理(防御者):ユーザーがパスワードを入力したとき、サーバーはたった1回ハッシュを計算すれば十分です。計算時間が0.000001秒だろうが0.1秒だろうが、人間の体感には何も違いがありません。
- 総当たり攻撃(攻撃者):流出したハッシュ値を手に入れた攻撃者は、辞書にある単語やランダムな文字列を毎秒数千億回ハッシュ化して照合し続けます。
計算が高速であることは、防御者にとっては「0.0001秒の得」にしかなりませんが、**攻撃者にとっては「1秒間に試行できる回数が天文学的に跳ね上がる最強の追い風」になります。 パスワード保存に求められる真の要件は、「速い計算」ではなく「意図的に遅く、重く作られた計算(物理的遅延)」**なのです。
2. 黎明期の防壁 —— ソルト(Salt)によるレインボーテーブルの粉砕
初期のWebシステムでは、平文パスワードをただMD5やSHA-1に通して保存していました。
hash = MD5("password123") = "482c811da5d5b4bc6d497ffa98491e38"
これに対して攻撃者が開発したのが、**レインボーテーブル(事前計算対照表)**です。 「よく使われる数億個のパスワード」を事前にすべてハッシュ化し、ハードディスクにインデックス化して並べておくことで、計算時間ゼロ(検索計算量 $O(1)$)でハッシュ値から元のパスワードを逆引きできるようにしました。
この事前計算を物理的に無力化したのが ソルト(Salt) です。
パスワードをハッシュ化する際、ユーザーごとに暗号論的にランダムなバイト列(ソルト)を生成し、それをパスワードの前後に結合してからハッシュ化します。
ソルトが付与されると、同じ password という安易な文字列を使っていても、ユーザーごとに全く異なるハッシュ値が生成されます。
攻撃者は用意していたレインボーテーブルを一切使い回すことができなくなり、**「ユーザー1人ひとりに対して、ゼロから総当たりをやり直す」**ことを強制されました。
しかし、ソルトは「事前の辞書作り」を封じただけで、「漏洩後のリアルタイム総当たりの速度」を1ミリも落としてはいませんでした。
触って試す:ソルトと計算コストの体験
🧂 ソルトと計算コストの体験
パスワードは固定のまま、ソルト(ユーザーごとに付けるランダム値)を変えてみてください。同じパスワードでもハッシュ値がまったく別物になります。
さらに、ハッシュ計算の「反復回数(計算コスト)」を増やすと、攻撃者が候補を全部試す時間はどう伸びるでしょうか。
※ 前提:攻撃者は毎秒100億回ハッシュを計算でき、候補10億件をすべて試すものとして計算しています。ログインする正規ユーザーは1回だけ計算するので、反復を増やしても体感はほぼ変わりません。ハッシュ値はデモ用の簡易関数で、実際のArgon2はこれに加えて大量のメモリ消費も強制します。
3. ハードウェアの物理戦争 —— なぜGPUはSHA-256を嘲笑うのか?
ここで、コンピュータの物理アーキテクチャの話に移ります。 なぜ攻撃者は、普通のパソコンでは考えられない速度でパスワードを試行できるのでしょうか?
答えは、GPU(グラフィックス・プロセッサ)の並列処理物理にあります。
- CPU(中央演算処理装置): 8コア〜16コア程度の強力な頭脳を持ちます。複雑な条件分岐、深い投機的実行、大きなL1/L2/L3キャッシュメモリを備え、「複雑なプログラムを1つずつ超高速に順次処理する」のが得意です。
- GPU(画像処理装置): 3Dゲームのポリゴンやピクセルを描画するために設計されています。個々のコアは単純な計算しかできませんが、その演算器(ALU)の数は数千〜数万個(GeForce RTX 4090で16,384基)に達します。
そして、SHA-256のアルゴリズムをアセンブリレベルで分解すると何が見えるでしょうか。
そこにあるのは、AND、XOR、NOT、そして「ビットの右巡回シフト(ROTR)」という極めて単純な算術論理演算のループだけです。複雑な分岐など一切ありません。
これはGPUの演算器にとって、最も消化しやすい最高のご馳走です。 ハイエンドGPUを数枚並べたクラスタを使えば、**SHA-256の計算速度は毎秒数百億回〜数千億回(数百GH/s)**に達します。8桁の英数字パスワードなど、わずか数分で全通り試行されてしまいます。
4. 時間稼ぎから「メモリの壁」へ —— bcryptからscryptへの進化
GPUの暴力に対抗するため、暗号技術者たちは防御の哲学を転換しました。 **「計算回数を物理的に引き延ばす(ストレッチング)」**時代の幕開けです。
第1世代:CPUバウンドな遅延(PBKDF2、bcrypt)
- PBKDF2:ハッシュ化を例えば10万回〜100万回ループさせる。
- bcrypt:Blowfish暗号の鍵スケジュールを利用し、意図的に重いループ計算を回す。
これにより、CPUで1回計算するのに約0.1秒かかるように調整できるようになりました。 しかし、これらには物理的な弱点がありました。**「メモリをほとんど使わない(CPUバウンド)」**という点です。
bcryptが必要とする作業メモリはわずか4KB程度です。 最新のGPUは各ストリーミングプロセッサ内に超高速な共有メモリを持っています。4KB程度であればチップ内のローカルキャッシュにすっぽり収まるため、GPUは何千本ものスレッドで外部メモリとの通信待ちをすることなく、フルスピードで並列計算をぶん回すことができたのです。さらに、SHA-256専用の集積回路(ASIC)を作れば、圧倒的な電力効率でクラッキングが実行できました。
第2世代:メモリハードネスの覚醒(scrypt)
2009年、Colin Percivalが提唱した scrypt がゲームのルールを根底から変えました。 彼が持ち込んだ新概念が 「メモリハード(Memory-Hard)関数」 です。
「計算を遅くするだけでは足りない。大量のRAM(メモリ)を強制的に消費させろ」
GPUのコア数がいくら1万個あろうとも、搭載されているメモリ(VRAM)は高々数十GBです。 もし1回のハッシュ計算に「32MBのメモリ空間を行き来しながらランダムに読み書きする」という物理的制約を課したらどうなるでしょうか?
- 24GBのVRAMがあっても、同時に走らせられるスレッド数は $24\text{GB} \div 32\text{MB} = 750$ 個程度に制限されます。
- 1万個ある演算コアの90%以上が、**「メモリが足りない」「メモリからのデータ到着待ち」**によって完全に遊んでしまいます。
攻撃者は、GPUの並列性という最大の武器を物理的に封じ込められたのです。
5. 現代の最高到達点 —— Argon2の物理工学
scryptによってメモリハードの有効性は証明されましたが、まだ弱点がありました。 「内部のメモリアクセス順序が入力値に依存しているため、CPUのキャッシュアクセスの時間差からパスワードを推測される(サイドチャネル攻撃)」リスクがあったのです。
この問題を解決すべく、2013年から2015年にかけて開催された国際コンペティション「Password Hashing Competition (PHC)」で世界中の暗号学者たちが激突しました。 その頂点に立ち、RFC 9106として現代の標準となったのが Argon2 です。
3次元の防壁チューニング
Argon2は、防御側が自分のサーバーのハードウェア資源に合わせて、3つの物理パラメータを個別に調整できます。
| パラメータ | 意味 | 攻撃者に与える物理的制約 |
|---|---|---|
| 時間コスト ($t$) | ループ回数(イテレーション) | 単純な計算時間の引き延ばし |
| メモリコスト ($m$) | 使用するRAM容量(例: 64MB〜1GB) | GPU/ASICの並列実行数を物理的に圧殺 |
| 並列度 ($p$) | 同時に使用するCPUスレッド数 | マルチコアCPUの性能をフル活用 |
Argon2の3つのバリアント
- Argon2d(Data-dependent): パスワードの値そのものに依存してメモリの参照位置を決定します。GPUやASICに対する耐性は最も高いですが、サーバーと同じマシンで悪意あるコードが動いている場合、CPUキャッシュのアクセス速度からパスワードを推測される危険があります(暗号通貨のマイニング向け)。
- Argon2i(Data-independent): パスワード値とは無関係にあらかじめ決まった疑似乱数順序でメモリを巡回します。キャッシュ攻撃を完全に防御できます。
- Argon2id(Hybrid:現代の実務推奨): 最初の第1パケットはサイドチャネル耐性のある Argon2i でメモリを初期化し、残りの計算をGPU耐性の最も高い Argon2d で回します。二大脅威を同時に完封する現代の黄金律です。
6. 実務コードの真実 —— 自作を捨て、標準ライブラリに委ねる
現代のWeb開発において、パスワードのハッシュ化ロジックを自分で実装することは厳禁です。 メジャーな言語環境は、すでに内部で最適なソルト生成・ストレッチング・メモリ確保を自動処理する関数を備えています。
PHP 8.x における現代の実装
// パスワードのハッシュ化(Argon2id を明示指定)
$password = "UserSecretPassword#2026";
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64MB のRAMを強制確保
'time_cost' => 4, // 4回の反復計算
'threads' => 2 // 2スレッドを並列駆動
]);
// 生成される文字列の構造:
// $argon2id$v=19$m=65536,t=4,p=2$ランダムなソルト$計算されたハッシュ値
// ★ソルトもパラメータもすべてハッシュ文字列の中に安全に自己内包される!
// 認証チェック
if (password_verify($password,$hash)) {
// ログイン成功
}
このわずか数行の裏で、サーバーは指定された64MBのメモリブロックを確保し、複数スレッドでメモリ空間を複雑にシャッフルしながらハッシュを計算しています。
もし攻撃者がこのハッシュ文字列を手に入れても、1回の試行ごとに物理メモリを64MB占有されるため、数千台のGPU並列マシンは瞬時にメモリ不足に陥り、総当たり攻撃は物理的な終焉を迎えます。
📝 3行で頭に入る『試験・実務の要点まとめ』
- 速度のパラドックス:SHA-256などの汎用ハッシュは高速検証用に設計されているため、GPUの数万並列ALUによって毎秒数百億回の総当たりを許してしまい、パスワード保存には極めて不適である。
- メモリの物理壁:bcryptなどのCPUバウンドな遅延から進化し、scryptやArgon2は「大量のRAM消費」を強制することで、VRAM容量の限界とメモリバス帯域を突いてGPUの並列性を物理的に窒息させる。
- 現代のデファクト:実務ではサイドチャネル耐性とGPU耐性を兼ね備えた Argon2id(または広く普及しているbcrypt)を採用し、フレームワーク標準の検証APIを利用する。