「ユーザーのパスワード、データベースにどう保存するのが正解?」は、サービスを作る人が必ず一度ぶつかる問いです。答えははっきりしています。攻撃手順には触れず、安全な保存のしかただけを順番に解説します。
なぜ平文・暗号化・素のハッシュがダメなのか
データベースはいつか漏れる前提で考えます。そのとき、保存方式ごとに被害がまるで違います。
- 平文:漏れた瞬間に全パスワードが読まれる。さらに使い回しにより、利用者の他サービスまで連鎖的に乗っ取られる。最悪。
- 暗号化(可逆):鍵があれば元に戻せる=鍵が一緒に漏れれば平文と同じ。パスワードは読み返す必要がないので、そもそも可逆である意味がない。
- 素のハッシュ(MD5/SHA-256):速いハッシュは、攻撃者が大量の候補を高速に試せる。よくあるパスワードはレインボーテーブルや総当たりで戻されてしまう。
安全な保存方法までの4段階
考え方は、弱い方式に一手ずつ対策を足していく流れで理解すると早いです。
ポイントは、ソルトと低速ハッシュは役割が違うことです。ソルトは「事前計算(レインボーテーブル)と使い回し一括解析」を無効にする。低速ハッシュは「総当たりの速度を現実的でないレベルに落とす」。両方そろって初めて安全になります(→ ハッシュ化とは)。
実践:今やること
Argon2id(または bcrypt)を使う
新規なら Argon2id を第一候補に。メモリ消費も要求できるためGPU総当たりに強い。既存の実績ある実装を重視するなら bcrypt でも実用上十分。どちらも標準ライブラリの実装をそのまま使う。
ソルトはライブラリに自動生成させる
bcrypt/Argon2 は、利用者ごとのソルトを自動生成し、ハッシュ文字列の中に同梱する。自分でソルト列を持つ必要はない。手で MD5(salt + password) を組むのは、やってはいけない典型。
コスト(強度)を環境に合わせて設定する
bcrypt のコスト係数、Argon2 のメモリ・反復・並列度を、正規ログインが体感で気にならない範囲で最大になるよう設定する。サーバーが速くなるほど攻撃者も速くなるので、年単位で見直して引き上げる。
検証は標準の照合関数で
ログイン時は、保存済みハッシュと入力をライブラリの照合関数で突き合わせる(多くがタイミング差に配慮した定数時間比較になっている)。自前で文字列一致を書かない。
弱いハッシュは“ログイン時に再ハッシュ”で移行
既に MD5/SHA-256 で保存済みなら、ユーザーがログイン成功した瞬間に新方式で再ハッシュして保存し直す。未ログイン分は、既存ハッシュをさらに bcrypt 等でハッシュ化する二重ハッシュで、暫定的に強度を上げられる。
やりがちな失敗 vs 正しい実装
やりがち
- パスワードを暗号化して保存(鍵が漏れたら終わり)
MD5(password)やSHA-256(password)をそのまま保存- 全ユーザー共通のソルトを1個だけ使う
- 自前で
hash(salt + password)を手組み
正しい実装
- Argon2id / bcrypt で一方向ハッシュ
- ソルトは利用者ごと(ライブラリが自動付与)
- コストは体感OKな範囲で最大、定期的に引き上げ
- 標準のハッシュ/照合関数をそのまま使う
ハッシュ化・暗号化・エンコードの違い
3つは混同されやすいですが、元に戻せるかどうかがまったく違います。パスワードの保存に使ってよいのはハッシュ化だけです。
| ハッシュ化 | 暗号化 | エンコード(Base64など) | |
|---|---|---|---|
| 元に戻せるか | 戻せない(一方向) | 鍵があれば戻せる | 誰でも戻せる |
| 鍵 | 不要 | 必要 | 不要 |
| 主な用途 | パスワードの保存、改ざん検知 | 通信やファイルの秘匿 | データの形式変換 |
| パスワード保存に | 使う(ソルト+低速ハッシュ) | 使わない | 使わない(平文と同じ) |
推奨される設定値(OWASP)
OWASP Password Storage Cheat Sheet が示す最低限の設定です。これより弱くしないことを目安に、サーバーの性能に合わせて引き上げます。
| 方式 | 最低限の設定 | 向いている場面 |
|---|---|---|
| Argon2id | メモリ19MiB・反復2回・並列度1 | 新規開発の第一候補 |
| scrypt | N=2^17(128MiB)・r=8・p=1 | Argon2idが使えないとき |
| bcrypt | コスト10以上。入力は72バイトまで | 既存システム・実績重視 |
| PBKDF2-HMAC-SHA256 | 反復60万回以上 | FIPS 140の準拠が必要なとき |
主な言語・フレームワークの標準機能も確認しておきます。PHPの password_hash() は既定(PASSWORD_DEFAULT)でbcryptを使い、PHP 8.4から既定のコストが10から12に上がりました。Djangoは既定でPBKDF2を使い、django[argon2] を入れて PASSWORD_HASHERS の先頭をArgon2にすると切り替えられます。Djangoは、ログイン時に古い方式のハッシュを自動で新しい方式に作り直します。
自分のサービスで確認する方法
保存済みのハッシュは、文字列の先頭を見れば方式がおおよそ分かります。本番データを外に持ち出さず、自分の管理する環境で数件だけ確認してください。
| ハッシュの見た目 | 方式 | 対応 |
|---|---|---|
$argon2id$v=19$m=…,t=…,p=…$ | Argon2id | m・t・pがOWASPの最低限以上か確認 |
$2y$12$… / $2b$10$… | bcrypt(数字がコスト) | 10未満なら引き上げ |
pbkdf2_sha256$1000000$… | PBKDF2(数字が反復回数) | 60万回以上か確認 |
| 16進数32文字 | MD5の可能性が高い | ログイン時の再ハッシュで移行 |
| 16進数40文字/64文字 | SHA-1/SHA-256の可能性が高い | 同上 |
| ログインに使う文字列そのまま | 平文 | 最優先で移行。漏えい時の影響を確認 |
あわせて、次の点も確かめます。
- アプリやWebサーバーのログ、エラー通知、分析ツールに、入力されたパスワードが記録されていないか
- bcryptを使っている場合、72バイトを超える入力をどう扱うか決めてあるか
- バックアップやテスト用に複製したデータベースにも、同じ方式のハッシュしか入っていないか
具体例:平文保管が被害を広げた事件
宅ふぁいる便(2019)では、サーバーへの不正アクセスで約481万件の個人情報が流出し、そのなかのログインパスワードは平文で保管されていました。ソルト付きの低速ハッシュで保存していれば、流出したパスワードをそのまま他サービスで使われる危険は大きく下がっていたはずです。侵入を完全に防げなくても、保存方法で被害の大きさは変えられることを示す例です。
当サイトの視点:パスワード保存は実績ある標準実装をそのまま使う
パスワード保存は、独自性を出してよい領域ではありません。世界中で検証された Argon2/bcrypt の標準実装をそのまま使うのが、最も安全で、運用も楽です。当サイトの立場は「ここで創意工夫しない」。むしろ力を入れるべきは、そもそもパスワード依存を減らすことです。具体的には多要素認証(MFA)の徹底や、将来的なパスキー(パスワードレス)への移行です。最強のハッシュでも、弱いパスワードと使い回しは守り切れません。
次に読む
- 用語:パスワードのハッシュ化とは / ソルトとは
- 入門:パスワードの正しい保管(利用者側) / パスワードマネージャーの選び方
- 対策:二要素認証(MFA)の選び方(パスワード依存を減らす)
- 用語:パスキーとは(パスワードレスへの移行)
- 事例:宅ふぁいる便 情報漏洩事件(2019)(パスワード平文保管が被害を広げた実例)
出典
- OWASP「Password Storage Cheat Sheet」: cheatsheetseries.owasp.org
- NIST「SP 800-63B-4(2025年8月)」パスワードの保存(ソルトとハッシュ): pages.nist.gov
- RFC 9106「Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications」: rfc-editor.org
- PHP マニュアル「password_hash」: php.net
- Django ドキュメント「Password management in Django」(5.2): docs.djangoproject.com