記事一覧へ戻る
フォーマット比較

bcrypt vs Argon2 vs scrypt — パスワードハッシュ KDF をどれにすべきか

パスワード保存に SHA-256 を直接使ってはいけない理由と、bcrypt / Argon2 / scrypt の選び方を、計算コスト / メモリ硬さ / 並列攻撃耐性 / 実装の枯れ具合 で比較します。

なぜ SHA-256 でパスワードを保存してはいけないか

最初に確認しておきたいのは、SHA-256 や SHA-512 などの汎用ハッシュ関数でユーザーのパスワードを保存してはいけない という点です。理由は単純で、これらは「速さ」を目的に設計されているからです。最新の GPU 1 枚で SHA-256 を毎秒 100 億回以上計算できるため、8 文字程度のパスワードであれば事前計算済みのレインボーテーブルや総当たりで数時間〜数日で割られます。漏洩したハッシュをまとめてオフライン解析されると、ソルトを付けても焼け石に水です。

ここで登場するのが KDF (Key Derivation Function) です。bcrypt / scrypt / Argon2 はいずれも「意図的に遅くしてあるハッシュ」で、攻撃者の総当たり速度を直接抑え込みます。判断軸は 4 つあります。計算コストの調整しやすさ (将来 CPU が速くなったら強度を上げられるか)、メモリ硬さ (memory-hardness、GPU/ASIC が並列化しにくいか)、並列化耐性 (1 検証あたりに必要なリソースを攻撃者が安く済ませられないか)、実装の枯れ具合 (監査済みの言語標準ライブラリがあるか) です。

3 つの KDF を並べて比較する

項目bcryptscryptArgon2id
開発年199920092015
ベース暗号Blowfish 由来PBKDF2 + Salsa20/8BLAKE2b
コスト調整cost factor (work factor、対数)N (CPU/メモリ)t_cost (iteration)
メモリ硬さ弱い (4 KB 固定)強い (N r 128 バイト)強い (m_cost で MiB 指定)
並列パラメータなしp (並列度)p (lanes)
出力長60 文字 (固定)任意任意
入力長制限72 バイト制限なし制限なし
標準化de factoRFC 7914RFC 9106
推奨パラメータ目安cost=12 (約 250 ms)N=2^17, r=8, p=1m=64 MiB, t=3, p=4

bcrypt は 1999 年から動き続けており、最も枯れています。ただし「cost factor を上げても消費メモリは 4 KB 固定」という設計上の限界があり、GPU 攻撃に対する優位性が時代とともに薄れています。さらに 72 バイトを超える入力は切り詰められる という有名な落とし穴があり、長いパスフレーズをそのまま渡すと末尾が無視されます。scrypt は memory-hard を初めて実用化したもので、Bitcoin Core のウォレット暗号化や Litecoin の PoW で採用されています。Argon2 は 2015 年の Password Hashing Competition の勝者で、現代の新規実装での第一選択です。Argon2id は side-channel 耐性 (Argon2i 由来) と GPU 耐性 (Argon2d 由来) を両立したハイブリッド版で、OWASP も推奨しています。なお PBKDF2 は NIST SP 800-132 で標準化されていますが memory-hard ではないため、強度を上げるには iteration を 600,000 回以上に引き上げる必要があり、現代的な攻撃者の前では bcrypt よりも弱いと評価されることが増えています。

ユースケース別の推奨

既存システムで bcrypt が稼働中: 無理に Argon2 へ移行する必要はありません。cost factor を 12 以上に保ち、ユーザーが次にログインしたタイミングで再ハッシュする「opportunistic upgrade」で十分です。bcrypt は 25 年以上の実装監査の蓄積があり、Node.js なら bcrypt パッケージ、Python なら passlib、Go なら golang.org/x/crypto/bcrypt がすぐ使えます。

新規プロジェクトのパスワード保存: Argon2id 一択です。m_cost=64 MiB 程度から始めて、サーバーの応答時間を見ながら調整します。Node.js なら argon2、Python なら argon2-cffi、Rust なら argon2 クレートが代表的な実装です。

暗号通貨ウォレット / マスター鍵の導出: 鍵導出 (KDF) としての用途では scrypt が引き続き使われています (BIP38、Litecoin)。新規仕様であれば Argon2id でも問題ありません。一方で JWT の署名鍵やセッショントークン生成には KDF を使わない こと。これらは crypto.randomBytes(32) のような暗号学的乱数で十分で、用途を取り違えると遅いだけで意味のない処理になります。一般的なハッシュ計算 (ファイルの完全性チェックなど) には hash-generate を使い、KDF と通常ハッシュの責務を混同しないでください。

ブラウザだけで bcrypt 文字列を読み解く

実装側でやるべきは「自分で KDF を書かない」ことです。bcrypt の $2a$12$... のような出力をパースするのは枯れたライブラリに任せましょう。手元に流出した DB ダンプや Heroku の環境変数の bcrypt ハッシュがあるとき、それを社外サービスに貼り付けて解析するのは避けたいケースです。bcrypt-info はバージョンプレフィックス ($2a$ / $2b$ / $2y$)、cost factor、salt、ハッシュ本体をブラウザ内で分解して表示するため、本番ハッシュをそのまま貼り付けても外部へ送信されません。実装は GitHub で確認でき、DevTools の Network タブで送信ゼロを目視確認できます。

落とし穴として、bcrypt の $2y$ プレフィックスは PHP 由来の互換タグで、$2a$ と同じアルゴリズムです。$2b$ は OpenBSD 由来の修正版で、文字列長の取り扱いが厳密化されています。アプリ側で「$2a$ だけ受け付ける」というハードコードをすると Laravel や Symfony 由来のハッシュが弾かれるので、プレフィックスを問わず処理できる実装になっているか確認してください。