ssecutils
Security / Browser-native guide

パスワードハッシュの選び方

平文保存が危険な理由、salt、pepper、bcrypt / scrypt / Argon2 の位置づけを説明します。

7Zero tracking reading surface

「パスワードを保存する」とはどういうことか

サービスに新規登録する時、ユーザーが入力したパスワードはサーバーに送られます。サーバ側でそのまま DB に保存してはダメ、というのは現代では常識ですが、なぜダメなのか、ではどうすればいいのか、を順を追って整理します。

平文保存の問題はシンプルで、DB が漏洩した瞬間に全ユーザーのパスワードが攻撃者の手に渡るから。Yahoo!(30億件)、LinkedIn(1.6億件)、Adobe(1.5億件)など、過去にも DB 漏洩事件は枚挙にいとまがなく、「漏洩する前提で設計する」のが現代のスタンスです。

第1段階: ハッシュ関数で保存

パスワードを一方向ハッシュ関数(MD5 / SHA-1 / SHA-256 等)に通して、結果を保存する方式。

保存:    hash = SHA256(password)
検証:    SHA256(input) === stored_hash ?

ハッシュ関数は一方向(出力から入力を逆算するのが計算的に困難)なので、DB が漏れても元のパスワードは分からない…はずでした。

レインボーテーブル攻撃

単純なハッシュ保存には致命的な弱点があります。同じパスワードからは同じハッシュが生まれるため、攻撃者は事前に「よく使われるパスワード→ハッシュ」の対応表を作っておけば、漏洩 DB のハッシュを表で引くだけで一致が見つかります。

この対応表が レインボーテーブル。`password123` のハッシュは何回計算しても同じ値なので、世界中のサーバで同じハッシュ値が登録されています:

MD5("password123")    = 482c811da5d5b4bc6d497ffa98491e38
SHA-1("password123")  = cbfdac6008f9cab4083784cbd1874f76618d2a97
SHA-256("password123") = ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f

Google でef92b778bafe...を検索しても元のパスワードが分かるレベル。実際、crackstation.net のような無料サイトに 150 億件のレインボーテーブルが置かれています。

第2段階: ソルト(Salt)の追加

レインボーテーブル対策の基本がソルト。ユーザーごとにランダムな文字列を生成し、パスワードと連結してからハッシュします:

保存:
  salt = randomBytes(16)
  hash = SHA256(salt + password)
  DB に (salt, hash) を保存

検証:
  SHA256(stored_salt + input) === stored_hash ?

ソルトの効果:

  • 同じパスワードでも違うハッシュ: ユーザーAとBが同じ`password123`を使っていても、ソルトが違うので DB 上のハッシュは別の値
  • レインボーテーブルが無効化: 16バイトソルトの組み合わせ数(2^128)ぶんのテーブルが必要になり、事実上不可能

ソルトは秘密にする必要はない(DBに平文で並べてOK)。攻撃者はソルトを知ってもユーザーごとに個別に総当たりしないといけないので、レインボーテーブル方式が崩れます。

第3段階: それでも GPU で総当たりされる問題

ソルトを付けてもまだ問題があります。SHA-256 は速すぎるのです。

現代の GPU(NVIDIA RTX 4090)の SHA-256 計算速度はおよそ 20 GH/s(200億回/秒)。8文字英数字パスワード(62^8 ≈ 218兆通り)でも、ソルト込みで 3時間で全パターンを試せてしまいます。

62^8 通り / 20,000,000,000 通り/秒
≈ 218,000,000,000,000 / 20,000,000,000
≈ 10,900 秒
≈ 3 時間

ソルトはレインボーテーブル攻撃を防いだだけで、個別の総当たり攻撃には無力でした。これを防ぐには「ハッシュ計算自体を遅くする」必要があります。

第4段階: 「遅いハッシュ関数」 = bcrypt / scrypt / Argon2

正規ユーザーのログイン時間(数百ミリ秒なら気にならない)と、攻撃者の総当たり時間(1秒に何回試せるか)は、同じ計算量に対して大きな差を意図的に作れます。これがパスワードハッシュ専用関数の発想です。

計算を内部で何千〜何万回繰り返し、1ハッシュに 0.1 秒程度かけることで:

  • 正規ログイン: 0.1秒(人は気にならない)
  • 攻撃者の総当たり: 0.1秒/試行 → 10回/秒 → 元のSHA-256比で10億倍遅くなる

bcrypt(1999年〜)

Blowfish 暗号を改造したハッシュ関数。コストファクター(4〜31)でループ回数を 2^cost 倍します。

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
↑     ↑   ↑                       ↑
方式  cost ソルト(22文字)         ハッシュ(31文字)

cost=12 が現代の推奨値(2024年時点、約 0.3 秒)。マシンが速くなったら cost を上げて再ハッシュすることで、長期運用にも耐えます。

弱点:

  • 72バイト制限: 入力パスワードを 72 バイトで切り捨てる仕様。長いパスフレーズで「73バイト目以降が無視される」事故が起きうる
  • メモリ使用量が小さい: GPU/ASIC で並列化しやすく、超大規模攻撃には弱い

とはいえ枯れていて実装が安定しており、「現代でも合格点」の選択肢。本サイトの Bcrypt Hasher で実際に生成・検証できます。

scrypt(2009年〜)

Litecoin の PoW 関数として有名。bcrypt の「並列化耐性が弱い」問題に対応するため、大量のメモリを使わせる設計(メモリハード関数)。

パラメータ:

  • N: CPU/メモリコスト(典型 16384〜1048576)
  • r: ブロックサイズ(典型 8)
  • p: 並列度(典型 1)

ASIC を作ろうとしても大容量 RAM が必要になり、コスト効率が悪化する。GPU でも VRAM 帯域がボトルネック化します。

Argon2(2015年〜、現代の推奨)

2013年からの Password Hashing Competition の優勝アルゴリズム。bcrypt / scrypt の弱点を一通り解消した「現時点のベストプラクティス」。

3つのバリアント:

種類用途
Argon2dGPU 攻撃耐性最大、サイドチャネル攻撃には注意
Argon2iサイドチャネル耐性、GPU 攻撃にやや弱い
Argon2id両者のハイブリッド(OWASP 推奨、現代の標準)

OWASP 2024 推奨パラメータ:

Argon2id:
  memory cost (m) = 19 MiB 以上
  time cost (t)   = 2 以上
  parallelism (p) = 1

なお、Argon2 は標準ライブラリでサポートされる言語が増えてきており、Python(argon2-cffi)、Node(argon2)、Go(golang.org/x/crypto/argon2)等で利用可能です。

結局どれを使えばいい?

2024〜2026年の OWASP 推奨:

  1. Argon2id(一番おすすめ) - 新規プロジェクトはこれ
  2. scrypt - Argon2 が使えない時の次善
  3. bcrypt(cost=12以上) - レガシー互換が必要な時

絶対に避ける

  • 素のMD5 / SHA-1 / SHA-256 / SHA-512(速すぎる、レインボーテーブル攻撃に弱い)
  • ソルトなしハッシュ
  • 独自ハッシュアルゴリズム(ほぼ確実に弱い)
  • 双方向暗号化(AES等)でパスワード保存(鍵が漏れたら全部復号される)

おまけ: ペッパー(Pepper)

さらに防御を厚くするならペッパーを追加する手があります。ソルトと違ってサーバー側に固定の秘密値(環境変数等)として持つ追加要素:

hash = Argon2id(salt + password + pepper)

DB が漏洩しても、ペッパー(コード/環境変数側)が漏れていなければ攻撃者は総当たりすらできない。ソルト = ユーザーごと・公開、ペッパー = アプリ全体・秘匿の使い分け。

運用面では「ペッパーをローテーションする時に全パスワードを再ハッシュできない」問題があるため、必須ではなく「上級向けオプション」位置付けです。

HMAC との関係

ペッパーは「ハッシュに秘密鍵を混ぜる」設計なので、本質的には HMAC と同じ発想です。実装上、HMAC-SHA256(pepper, password) を Argon2 に通す、というパターンも取られます。HMAC については別記事 HTTPS と TLS の仕組み でも触れています。

正規ユーザーへの影響: コストの調整

遅いハッシュは攻撃者を遅くしますが、正規ユーザーのログインも遅くなります。実用的な目安:

用途1回あたりのコスト
通常Webサービス0.2〜0.5 秒
低レイテンシ要求(ゲーム等)0.1 秒以下
銀行/医療システム1 秒以上でもOK

サーバ CPU 負荷との兼ね合いで、ログインが集中する瞬間の同時接続数も計算に入れる必要があります。「高負荷時にログインができなくなる」サービス停止リスクのほうが、ハッシュ強度を1段階下げる害より大きいこともあるので、本番投入前に必ず負荷試験を。

おわりに

パスワード保存は技術的には「Argon2id でハッシュ化」と一行で書けるほどシンプルですが、その一行に至るまでには「平文 → ハッシュ → ソルト付きハッシュ → 遅いハッシュ」と何度も失敗してきた歴史があります。

新規プロジェクトでは Argon2id(or 言語ライブラリの最新推奨)一択でOK。本サイトの Bcrypt Hasher でコストファクターを変えながら計算時間を体感したり、Hash Generator で SHA-256 と比較して「速さの違い」を実機で確認すると、なぜパスワード専用ハッシュが必要なのか直感的に掴めます。

パスワード保存は OWASP Top 10 の A02(Cryptographic Failures) に分類される重要分野。ユーザー側の対策としての「強いパスワード作成」は パスワード強度はどう決まるか で、認証強化の観点では 多要素認証 も合わせて読んでみてください。

Tool companion

この記事と一緒に使えるツール