bcrypt vs Argon2: 2026年のパスワードハッシュ
正しいパスワードハッシュアルゴリズムを選び、プロダクションパラメータを調整。
パスワードハッシュの仕事はただ一つ。すでにデータベースを盗んだ攻撃者にとって、一回一回の推測を高くつくものにすることです。汎用ハッシュは設計上この仕事に失敗します。コンシューマー向けGPU一枚が毎秒数百億のSHA-256ハッシュを計算するため、SHA-256でハッシュされた8文字のパスワードは数分で陥落します。bcryptとArgon2はこの経済方程式を変えるために存在し、両者の選択は主に、それぞれが何を高価にするのかを理解する問題です。
脅威モデル: オフラインクラッキング
侵害後、攻撃者はハッシュ値を手にし、ハードウェア速度でローカルに推測を走らせます。ソルト(両アルゴリズムとも自動処理し、ソルトを出力文字列に埋め込む)はレインボーテーブルを防ぎ、ユーザーごとのクラッキングを強制しますが、推測レートには何の影響もありません。推測レートは2つのレバーで決まります。
- 時間コスト — ハッシュ1回に必要な計算量
- メモリコスト — ハッシュ1回に必要なRAM
より興味深いのはメモリのレバーです。GPUは数千のコアを持ちますが、コアあたりの高速メモリは限られています。ハッシュあたり64MBを要求するアルゴリズムは、GPUの並列性を数千の同時推測から一握りにまで削ります。
bcrypt: 1999年の設計、いまだ健在
bcrypt(ProvosとMazières)は、意図的に高価なBlowfish鍵スケジュールの上に構築されています。コストパラメータは対数的で、cost 12は4,096回の反復を意味し、1増えるごとに作業量が倍になります。
import bcrypt from 'bcrypt';const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(password, hash);
4KBの内部状態はSHA系よりGPUに強いものの、現代のメモリハード関数の要求には遠く及びません。知っておくべき鋭いエッジが2つ:
- 72バイト切り詰め。 bcryptは72バイトを超える入力を黙って無視するため、72バイト以降だけが異なる2つの文字列は同一にハッシュされます。回避のために事前ハッシュするなら、ダイジェストを先にbase64かhexでエンコードすること。生のSHA-256出力はNULバイトを含み得て、一部のbcrypt実装は最初のNULで切り詰め、多数のパスワードを一つに潰してしまいます。
- 変種の動物園。
$2a$、$2b$、$2y$のプレフィックスは歴史的な実装バグの名残です。現代のライブラリは$2b$を出力します。解釈の異なるライブラリを混ぜないこと。
Argon2: メモリハードの勝者
Argon2は2015年のPassword Hashing Competitionで優勝し、OWASPの推奨デフォルトです。Argon2id 変種を使いましょう。Argon2iのサイドチャネル攻撃への防御とArgon2dのより強いGPU耐性を組み合わせたものです。
import argon2 from 'argon2';const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // KiB単位、つまり64 MiB
timeCost: 3,
parallelism: 1
});
const ok = await argon2.verify(hash, password);
3つのパラメータは独立です。memoryCostはハッシュあたりのRAM、timeCostはそのメモリを走査する回数、parallelismはレーン数を決めます。OWASPの現行最小値は19 MiB / t=2 / p=1、64 MiB / t=3 / p=1ならサーバーグレードで余裕のある設定です。出力は自己記述的なPHC文字列($argon2id$v=19$m=65536,t=3,p=1$...)なので、パラメータはハッシュと共に移動し、設定を引き上げた後も古いハッシュの検証は動き続けます。
チューニングの方法論
パラメータはハードウェア依存なので、数字をコピーせず経験的にチューニングを。
- 本番クラスのハードウェアでベンチマークし、対話的ログインならハッシュあたり200〜500msを目標に。
- 並行性の下でテスト。64 MiBのログインが50件同時なら3.2GBのRAMです。メモリハードなハッシュは、無制限なら自ら招いたDoSベクトルになります。ハッシュ処理の周りにセマフォか専用キューを。
- 毎年再評価。今日のbcrypt cost 12は、ハードウェアが追いつけば13にすべきです。
パスワードリセットなしの移行
ハッシュ方式のアップグレードに強制リセットは不要です。
1. 既存ハッシュを今すぐラップ: argon2(legacy_hash) を保存して行にラップ済みの印を付け、保存状態の弱いハッシュを即座に排除します。
2. ログイン時に再ハッシュ: ユーザーの認証時には一時的に平文を握っています。旧方式で検証した後、新しいArgon2idハッシュを保存しましょう。
3. 多くのライブラリは、保存されたハッシュのパラメータを現行ポリシーと比較する needsRehash チェックを提供しています。ログイン成功のたびに呼べば、アップグレードは自動化されます。
よくある間違い
- パスワードに高速ハッシュ(MD5、SHA系、ソルト付きでも)を使うこと自体
- 定数時間でない文字列比較で検証を自作する
- ペッパーをハッシュと同じデータベースに保存する(KMSや環境シークレットに保管し、ハッシュ前にパスワードへHMACとして適用)
- 忙しいWebサーバーでparallelismを高く設定し、リクエストスレッドを飢えさせる
- パスワードハッシュとセッションセキュリティの混同 — セッショントークンが推測可能なら完璧なArgon2設定も無意味
結論
新規システム: OWASPパラメータ以上のArgon2id。cost 12以上の既存bcrypt: 緊急の移行は不要ですが、ログイン時再ハッシュを組み込み、システムが自己アップグレードするようにしましょう。FIPS制約環境: 600,000回以上の反復のPBKDF2-HMAC-SHA256が準拠のフォールバックです。いかなる場合も自作しないこと。
scryptの立ち位置
scrypt(2009)はArgon2が存在する前にメモリハードネスを開拓し、今でも堅実な選択肢です。特にライブラリのサポートが成熟している環境では。Node組み込みの crypto.scrypt、Goのx/crypto。パラメータの推論はより厄介です。N、r、pが非自明に相互作用し、よく推奨されるN=2^17、r=8、p=1はハッシュあたり約128MBを消費します。しかし正しくチューニングされたscryptは、Argon2idより意味のあるほど弱くはありません。プラットフォームがscryptを無償で提供し、Argon2がネイティブ依存を引き込むことになるなら、scryptを選ぶのは擁護できるエンジニアリング判断です。
パスワードの品質は依然としてハッシュより重要です。sdk.is/password-generator のパスワードジェネレーターで強力なランダムパスワードを生成しましょう。