パスワードの安全な保存(ハッシュとソルト)
パスワードは「平文で保存しない」のが大前提。だが単純なハッシュ化だけでは破られる。ソルトで使い回しを潰し、bcrypt/Argon2 でわざと遅くするのが正解。
- 1.パスワードは暗号化(復号できる)ではなく、ハッシュ化(元に戻せない一方向)で保存する。平文・可逆暗号はNG。
- 2.SHA-256 を1回かけるだけでは不十分。事前計算(レインボーテーブル)と高速総当たりで破られるため、ユーザーごとに違うソルトを付ける。
- 3.現代の正解は bcrypt / Argon2 など専用アルゴリズム。ソルト内蔵かつ「わざと遅く(ストレッチング)」して総当たりのコストを跳ね上げる。
なぜ平文保存がダメなのか
最悪なのは、パスワードを入力されたまま(平文)DBに保存することです。これは「いつか必ず漏れる」前提に立つと破綻します。
- DB漏洩で即終了:SQLインジェクションや内部不正、バックアップ流出で
usersテーブルが抜かれた瞬間、全員のパスワードがそのまま攻撃者の手に渡る。 - 使い回しの連鎖:多くの人は同じパスワードを複数サービスで使い回す。漏れた平文は、銀行やメールなど**他サービスへのログイン試行(クレデンシャルスタッフィング)**に直行する。
- 運営側も見えてはいけない:管理者がユーザーのパスワードを閲覧できる時点で設計が間違い。「パスワードを忘れた」で元のパスワードをメール送信してくるサービスは、平文保存している危険なサインです。
ではAES等で「暗号化」すればいいかというと、これも不適切です。暗号化は鍵があれば復号できる(可逆)ため、鍵が一緒に漏れれば平文と同じ。そもそもログイン認証に「元のパスワードを取り出す」必要はありません。だから戻せないこと(一方向性)こそが目的になります。
パスワード保存に求められるのは**復号できない一方向変換(ハッシュ)**であって、復号できる暗号化ではありません。AES等で暗号化する設計は「鍵管理に成否を全振り」する形になり、鍵漏洩で一発アウト。認証は「保存済みハッシュと、入力値のハッシュが一致するか」を照合するだけで成立し、元の文字列を復元する場面はありません。
ハッシュ化:戻せない一方向変換
ハッシュ関数は、入力を固定長の文字列に変換し、そこから元の入力を逆算できない性質を持ちます。同じ入力なら必ず同じ出力になるので、認証は「保存したハッシュ」と「入力をハッシュした値」を比べるだけで済みます。
// 保存時:パスワードそのものではなく、ハッシュを保存する
// 認証時:入力を同じ関数でハッシュ化し、保存値と一致するか比較
// → 一方向なので、DBが漏れても「元のパスワード」は復元されない(はず)
考え方は正しいのですが、「どのハッシュ関数を使うか」で安全性は天と地ほど変わります。ここで初学者がやりがちなのが、SHA-256 や MD5 を1回かけて満足してしまうパターン。次節のとおり、これは現代では“ほぼ平文”に近い危うさを持ちます。
なぜ単純な SHA だけでは不十分か
SHA-256 や MD5 は「速く計算できる」ことが取り柄のハッシュです。ファイルの改ざん検知には向きますが、パスワード保存ではその“速さ”が致命傷になります。攻撃は主に2方向から来ます。
1. レインボーテーブル / 事前計算攻撃 攻撃者は「よくあるパスワード → そのSHA-256値」の対応表をあらかじめ大量に作っておけます。漏れたDBのハッシュ値をこの表で逆引きするだけで、元のパスワードが判明する。ソルト無しのSHAは、この事前計算に丸腰です。
2. 高速な総当たり / 辞書攻撃 SHA-256 は最新GPUで1秒あたり数十億回計算できます。短いパスワードや辞書に載る単語は、漏れたハッシュに対して片っ端からハッシュを計算して照合され、現実的な時間で割れます。
さらに同じパスワードは同じハッシュになるため、ソルト無しだと「テーブル内で同じハッシュ値の人=同じパスワード」と一目で分かり、一人破れば芋づる式に複数アカウントが落ちます。
これらは高速ハッシュであり、パスワード保存には不適切です。「ハッシュ化してるから安全」は誤解で、ソルト無しの単純ハッシュはレインボーテーブルで逆引きされ、ソルトを足しても高速ゆえに総当たりで破られます。MD5・SHA-1 は衝突耐性も崩れており論外。パスワードには**専用に設計された低速アルゴリズム(bcrypt / Argon2 / scrypt / PBKDF2)**を使ってください。
ソルト:使い回しと事前計算を潰す
ソルト(salt)は、パスワードに連結するユーザーごとに異なるランダム値です。ハッシュ化の前にパスワードへ混ぜることで、出力を一人ひとりバラバラにします。
-- 脆弱な例:ソルト無しの単純ハッシュ(同じパスワード → 同じハッシュ)
-- alice と bob が偶然 "password123" だと、ハッシュ値が一致してバレる
INSERT INTO users (name, password_hash)
VALUES ('alice', SHA2('password123', 256)); -- ❌ レインボーテーブルで逆引き可能
ソルトを入れると、たとえ二人が同じパスワードでも保存されるハッシュは全く別物になります。これで次の2つが同時に潰せます。
- レインボーテーブルが無効化:ソルトはユーザーごとに違うので、汎用の事前計算表が使えなくなる。攻撃者はそのソルト専用の表を一から作り直す羽目になり、割に合わなくなる。
- 使い回しの可視化を防ぐ:同一パスワードでもハッシュが異なるため、「ハッシュが同じ=同じパスワード」という手掛かりを奪える。
ソルトはDBにハッシュと一緒に平文で保存してよい値です(漏れても問題ない)。重要なのは秘匿性ではなく、ユーザーごとに十分ランダムで一意であること。後述の bcrypt / Argon2 は、生成したソルトを出力文字列の中に自動で埋め込んでくれるので、開発者が別カラムで手管理する必要すらありません。なお「全員共通の固定ソルト(ペッパーと混同しがち)」はレインボーテーブルこそ防ぎますが、使い回しの可視化は防げず不十分です。
ストレッチング:わざと「遅く」する
ソルトで事前計算は潰せても、高速ハッシュなら総当たりはまだ速いまま。そこで効くのがストレッチング(key stretching)――ハッシュ計算を何千回〜何万回も繰り返し、1回の検証をわざと重くする手法です。
正規ユーザーのログインは1回だけなので、1回あたり 0.1〜0.5 秒かかっても体感できません。しかし攻撃者は何十億通りも試すので、1回が重いと総当たりのコストが指数的に膨らみ、現実的な時間で割れなくなります。bcrypt や Argon2 は、この反復回数(コストパラメータ)を後から引き上げられるよう設計されています。
ハードウェアは年々速くなるため、今日「十分遅い」設定も数年後には甘くなります。bcrypt の cost(コスト係数)や Argon2 のメモリ・反復パラメータは、運用しながら段階的に強められるのが利点です。逆に PBKDF2 を低すぎる反復回数で使うと「専用アルゴリズムを使っているのに実質高速ハッシュ」になり、意味が薄れます。パラメータの強さまで含めて初めて対策です。
結局どれを使えばいいか
「自前でソルトとストレッチングを組む」より、それらが最初から組み込まれた専用アルゴリズムを使うのが正解です。代表的な選択肢を比べます。
| アルゴリズム | 速度/コスト | ソルト | 評価・使いどころ |
|---|---|---|---|
| MD5 / SHA-1 / SHA-256 | 高速(数十億回/秒) | なし(自前で付ける必要) | パスワード保存には不適切。改ざん検知用 |
| PBKDF2 | 反復回数で調整 | あり | 規格準拠が要る場面で可。設定次第で弱くなる |
| bcrypt | cost係数で低速化 | 内蔵(自動生成) | 枯れていて実装が豊富。広く使える定番 |
| scrypt | CPU+メモリを消費 | 内蔵 | メモリも要求しGPU攻撃に強い |
| Argon2(id推奨) | メモリ+時間+並列度 | 内蔵 | 2015年のコンペ勝者。新規なら第一候補 |
bcrypt の出力は、アルゴリズム識別子・コスト・ソルト・ハッシュが1本の文字列に詰まっているのが特徴です。だから保存も照合も一行で済みます。
// 脆弱な例:高速ハッシュ+ソルト無し(やってはいけない)
const bad = sha256(password); // ❌ 逆引き・総当たりに弱い
// 安全な例:bcrypt(ソルト自動生成・ストレッチング内蔵)
const hash = await bcrypt.hash(password, 12); // 12 = cost。大きいほど遅く強い
// 保存される文字列の例(ソルトもハッシュに同梱されている):
// $2b$12$Nq9c....(22文字のソルト)....(31文字のハッシュ)
// 認証:保存ハッシュから cost とソルトを読み取り、自動で同条件で照合
const ok = await bcrypt.compare(inputPassword, hash); // true / false
sha256(salt + password) を自分で何度かループする…といった独自実装は避けてください。タイミング攻撃に弱い比較、パラメータの誤設定、ソルト生成の弱さなど、落とし穴が多すぎます。実績あるライブラリの bcrypt / Argon2 をそのまま使うのが、最も安全で確実です。新規開発なら Argon2id、枯れた安定実装が欲しければ bcrypt が現実的な第一候補です。
保存だけでは守り切れない
ハッシュ保存は「漏れたときの被害を抑える最後の砦」ですが、それ単体で認証が安全になるわけではありません。入口(ログイン)側の守りも合わせて必要です。
- 総当たり・スタッフィング対策:ログイン試行の回数制限(レートリミット)やアカウントロック、CAPTCHA で、オンラインでの推測を止める。
- 多要素認証(MFA):パスワードが漏れても二要素目で食い止める。最も効果が大きい上乗せ策。
- そもそも漏らさない:DB漏洩の主因であるSQLインジェクションを塞ぎ、通信はTLSで保護、認可は最小権限で絞る。
ハッシュとソルトの土台にあるハッシュ関数そのものの性質は 暗号の基礎 でより深く扱います。「戻せないからこそ守れる」という発想を押さえておけば、パスワード保存の設計判断はぶれません。
セキュリティ Article
パスワードの安全な保存(ハッシュとソルト)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
パスワード
比較で見る軸
難易度: intermediate / カテゴリ: セキュリティ / タグ数: 4
導入後に効く点
SHA-256 を1回かけるだけでは不十分。事前計算(レインボーテーブル)と高速総当たりで破られるため、ユーザーごとに違うソルトを付ける。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- セキュリティ
- タグ数
- 4
判断チェックリスト
- 自社の用途が「パスワード / ハッシュ」に近いか確認する。
- 強みである「パスワードは暗号化(復号できる)ではなく、ハッシュ化(元に戻せない一方向)で保存する。平文・可逆暗号はNG。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。