パスワードの保存

Password Storage Cheat Sheet

原典
Password Storage Cheat Sheet(OWASP Cheat Sheet Series)
原文
GitHub 上の Markdown
底本
bac04fb5(2026-07-29 時点)
ライセンス
CC BY-SA 4.0(原典と同一。訳文も同ライセンスで再配布できます)

はじめに

このチートシートは、認証に用いるパスワードを保存する適切な方法を示す。 パスワードを保存するときは、アプリケーションやデータベースが侵害された場合でも攻撃者から保護されている必要がある。 幸い、現代の言語とフレームワークの多くは、パスワードを安全に保存する助けとなる機能を組み込みで備えている。

パスワードを平文で保存してはならない。 Argon2id、bcrypt、PBKDF2 のような、強固で遅いハッシュアルゴリズムで保護する。 攻撃者がレインボーテーブルのような事前計算済みの参照表を使えないようにするため、パスワードごとに一意なソルトを付与しなければならない。 SHA-256 のような高速なハッシュアルゴリズムはパスワードの保存に適さない。 短時間で大量の推測を試せてしまうからである。 遅く、メモリを多く要求するアルゴリズムを使えば、総当たり攻撃は著しく困難になり、費用も時間もかかるようになる。

推奨をまとめると次のようになる。

  • Argon2id を、最低でもメモリ 19 MiB、反復回数 2、並列度 1 の設定で使う。
  • Argon2id が使えない場合は、scrypt を CPU およびメモリのコストパラメータ最低 2^17、ブロックサイズ最低 8(1024 バイト)、並列化パラメータ 1 で使う。
  • bcrypt を使うレガシーなシステムでは、ワークファクタを 10 以上にし、パスワードの上限を 72 バイトとする。
  • FIPS-140 への準拠が必要な場合は、PBKDF2 をワークファクタ 600,000 以上、内部ハッシュ関数 HMAC-SHA-256 で使う。
  • 多層防御を追加するためにペッパーの使用を検討する(ただしそれ単体では、追加の安全性は得られない)。

背景

ハッシュと暗号化

ハッシュも暗号化も機微なデータを安全に保てるが、ほとんどすべての状況において、パスワードは暗号化や平文での保存ではなく、現代的で適応的なハッシュアルゴリズム(Argon2id、bcrypt、PBKDF2 など)によって安全にハッシュ化するべきである

ハッシュは一方向の関数であるため(すなわちハッシュを「復号」して元の平文の値を得ることは不可能である)、パスワードの検証には最も適した方法である。 攻撃者がハッシュ化されたパスワードを入手しても、それを使って被害者としてログインすることはできない。

暗号化は双方向の関数なので、攻撃者は暗号化されたデータから元の平文を復元できる。 暗号化は、利用者の住所のようなデータの保存に使える。 こうしたデータは利用者のプロフィール上で平文として表示されるからである。 住所をハッシュ化すれば、判読できない文字列になってしまう。

パスワードに暗号化を使うべきなのは、元の平文のパスワードを取得する必要がある例外的な場合に限られる。 OpenID Connect(OIDC)のような現代的な方法でプログラム的にアクセスを許可することに対応していない別のシステムに対して、アプリケーションがそのパスワードで認証する必要がある場合などである。 可能なかぎり、暗号化された形でパスワードを保存する必要が生じないような別のアーキテクチャを採るべきである。

暗号化についてさらに詳しくは暗号によるデータ保存を参照する。

パスワードのハッシュが破られる場合

現代的なハッシュアルゴリズムとハッシュの最良の慣行に従って保存された強固なパスワードは、攻撃者が実質的に破ることができないものになる。 現代的なハッシュアルゴリズムを選ぶことは、アプリケーションの所有者の責任である。

しかし、状況によっては攻撃者が次の手順でハッシュを「破る」ことができる。

  • 被害者が選んだと思われるパスワードを選ぶ(password1! など)
  • そのハッシュを計算する
  • 計算したハッシュと被害者のハッシュを比較する。一致すれば、ハッシュを正しく「破った」ことになり、そのパスワードの平文の値がわかる。

通常、攻撃者はこの処理を、次のような大量の候補パスワードの一覧に対して繰り返す。

  • 他の侵害されたサイトから得たパスワードの一覧
  • 総当たり(可能なすべての候補を試す)
  • よく使われるパスワードの辞書や単語リスト

組み合わせの数は膨大になりうるが、GPU のような高速なハードウェアや、多数のサーバーを借りられるクラウドサービスがあるため、パスワードの解読を成功させるための攻撃者側の費用は比較的小さい。 ハッシュに関する最良の慣行に従っていない場合はとくにそうである。

パスワード保存を強化する方法

ソルト付与

ソルトは、ハッシュ処理の一部として各パスワードに付与される、一意でランダムに生成された文字列である。 ソルトが利用者ごとに一意であるため、攻撃者はハッシュを一度計算して保存済みのすべてのハッシュと比較するのではなく、それぞれのソルトを用いて一つずつ破らなければならない。 これにより大量のハッシュを破ることが著しく難しくなる。 必要な時間がハッシュの個数に正比例して増えるからである。

ソルト付与は、レインボーテーブルやデータベースを用いた参照によって攻撃者がハッシュを事前計算することも防ぐ。 さらにソルト付与は、二人の利用者が同じパスワードを使っているかどうかを、ハッシュを破らずに判定することを不可能にする。 パスワードが同じでもソルトが異なれば、ハッシュは異なるものになる。

アルゴリズムと仕様のレベルでは、Argon2idbcryptPBKDF2 といった現代的なパスワードハッシュ関数は、呼び出し側がソルトを与えることを要求する。 ただし広く使われている実装やライブラリのほとんどは、内部でソルトを自動生成して管理する。 そのため、これらのライブラリを正しく使っていれば、アプリケーションの開発者が手作業でソルトの生成を扱う必要は通常ない。

ペッパー付与

ペッパー付与は、ソルト付与に加えて保護の層を追加するための一連の戦略である。 SQL インジェクションの脆弱性を悪用された場合やデータベースのバックアップを入手された場合など、攻撃者がデータベースにしかアクセスできないときに、ハッシュを一つも破れないようにする。

ペッパー付与の戦略に共通する要件

  • ペッパーは、パスワードのソルトのように個々のパスワードに一意なものではなく、保存されたパスワード全体で共有される
  • パスワードのソルトと違い、ペッパーは公開すべきではなく、生成されたハッシュと一緒に保存すべきではない。ペッパーはパスワードのデータベースとは別に保存する。
  • ペッパーはシークレットであり、シークレットの保管庫や HSM(ハードウェアセキュリティモジュール)に保存するべきである。シークレットの安全な保存についてはシークレット管理を参照する。
  • ペッパーが漏洩した場合、ペッパーを変更しなければならない。ペッパーは利用者のパスワードを知らなければ変更できない。したがってペッパーの変更には、以前のペッパーで保護されていたすべての利用者にパスワードの再設定を強制する必要がある。

ハッシュ前のペッパー付与

この戦略では、パスワードハッシュアルゴリズムでハッシュ化する前にパスワードへペッパーを付与する。 計算されたハッシュをデータベースに保存する。 この場合、ペッパーは安全に生成されたランダムな値であるべきである。 ランダムな値の安全な生成については暗号によるデータ保存を参照する。

ハッシュ後のペッパー付与

この戦略では、パスワードを通常どおりパスワードハッシュアルゴリズムでハッシュ化する。 得られたパスワードのハッシュを HMAC(求める出力長に応じて HMAC-SHA256、HMAC-SHA512 など)でもう一度ハッシュ化し、その結果をデータベースに保存する。 この場合ペッパーは HMAC の鍵として働くので、HMAC アルゴリズムの要件に従って生成する。

ワークファクタの利用

ワークファクタは、パスワードごとに実行されるハッシュアルゴリズムの反復回数である(通常は実際には 2^work 回の反復になる)。 ワークファクタは通常、ハッシュの出力に含めて保存される。 これはハッシュの計算に必要な計算量を増やし、それによって攻撃者がパスワードのハッシュを破ろうとする速度を落とすか、費用を増やす。

ワークファクタを選ぶときは、安全性と性能の釣り合いをとる。 ワークファクタを大きくすればハッシュを破ることは難しくなるが、ログイン試行の検証も遅くなる。 ワークファクタが高すぎるとアプリケーションの性能が落ち、大量のログイン試行でサーバーの CPU を枯渇させるサービス妨害攻撃に利用されうる。

理想的なワークファクタに絶対の規則はない。 サーバーの性能とアプリケーションの利用者数によって変わる。 最適なワークファクタの決定には、そのアプリケーションが使う具体的なサーバー上での実験が必要になる。 一般的な目安として、ハッシュの計算は 1 秒未満で終わるべきである。

ワークファクタの引き上げ

ワークファクタを持つことの主要な利点の一つは、ハードウェアが高性能かつ安価になるにつれて、それを引き上げられることである。

ワークファクタを引き上げる最も一般的な方法は、利用者が次に認証するのを待ち、そのときに新しいワークファクタでパスワードを再ハッシュすることである。 この場合、ハッシュごとにワークファクタが異なることになり、利用者がアプリケーションにログインし直さなければハッシュは更新されないままになる。 アプリケーションによっては、古く安全性の低いハッシュを保存し続けるのを避けるため、古いパスワードのハッシュを削除し、次のログイン時に利用者へパスワードの再設定を求めるのが適切な場合もある。

パスワードハッシュアルゴリズム

現代的なハッシュアルゴリズムの一部は、パスワードを安全に保存するために特別に設計されている。 つまりそれらは遅くあるべきであり(高速であるよう設計された MD5 や SHA-1 とは異なる)、ワークファクタを変えることで遅さを調整できる。

アプリケーションがどのパスワードハッシュアルゴリズムを使っているかを隠す必要はない。 現代的なパスワードハッシュアルゴリズムを適切な設定パラメータで使っているなら、どのアルゴリズムを使っているかを公に述べても安全であり、この一覧に掲載してもよい。

パスワードハッシュアルゴリズムを選ぶときは、GPU による攻撃とメモリを狙った攻撃の両方に耐えるよう設計された現代的なアルゴリズムを優先するべきである。 利用できる場合、新しいアプリケーションには新しいアルゴリズムを選ぶ。 レガシーなシステムでは、適切な設定のもとで古いアルゴリズムがなお許容されることもある。

検討すべきハッシュアルゴリズムは次の三つである。

Argon2id

Argon2 は 2015 年の Password Hashing Competition の優勝者である。 三つの Argon2 の変種のうち、Argon2id を使う。 サイドチャネル攻撃と GPU による攻撃の両方に対して均衡のとれた耐性を持つからである。

Argon2id は他のアルゴリズムのような単一のワークファクタではなく、設定できる三つのパラメータを持つ。 最小メモリサイズの下限(m)、最小反復回数(t)、並列度(p)である。 推奨する設定を以下に示す。

これらのパラメータは、パスワードのハッシュ計算にかかる計算量を制御する。 メモリ使用量、反復回数、並列度を増やすと、パスワード解読の試行は攻撃者にとって著しく遅く、費用のかかるものになる。 適切に調整すれば、正当な認証リクエストにとっては実用的な速度が保たれる。

  • m=47104(46 MiB)、t=1、p=1(Argon2i では使わない)
  • m=19456(19 MiB)、t=2、p=1(Argon2i では使わない)
  • m=12288(12 MiB)、t=3、p=1
  • m=9216(9 MiB)、t=4、p=1
  • m=7168(7 MiB)、t=5、p=1

これらの設定は同等の防御水準を与える。 違いは CPU とメモリの使用量の釣り合いだけである。

scrypt

scrypt は Colin Percival が作ったパスワードベースの鍵導出関数である。 パスワードのハッシュには Argon2id が最良の選択であるが、それが使えない場合は scrypt を使う。

Argon2id と同様に、scrypt にも設定できる三つのパラメータがある。 最小のメモリコストパラメータ(N)、ブロックサイズ(r)、並列度(p)である。 次のいずれかの設定を使う。

  • N=2^17(128 MiB)、r=8(1024 バイト)、p=1
  • N=2^16(64 MiB)、r=8(1024 バイト)、p=2
  • N=2^15(32 MiB)、r=8(1024 バイト)、p=3
  • N=2^14(16 MiB)、r=8(1024 バイト)、p=5
  • N=2^13(8 MiB)、r=8(1024 バイト)、p=10

これらの設定は同程度の最低限の防御水準を与える。 主な釣り合いは並列度とメモリ使用量のあいだにある。

bcrypt

bcrypt のパスワードハッシュ関数は、Argon2 と scrypt が使えないレガシーなシステムにおけるパスワード保存に限って使うべきである。

ワークファクタは、検証を行うサーバーの性能が許すかぎり大きくし、最低でも 10 とする。

bcrypt の入力長の制限

bcrypt はほとんどの実装において入力長の上限が 72 バイトである。 したがってパスワードの最大長を 72 バイト(使用する bcrypt の実装がより小さい上限を持つ場合はそれ以下)に制限するべきである。

bcrypt におけるパスワードの事前ハッシュ

別の方法として、利用者が入力したパスワードを SHA-2、HMAC、BLAKE3 のような高速なアルゴリズムで事前にハッシュ化し、その結果のハッシュ値を bcrypt でハッシュ化する(すなわち bcrypt(H($password)), $salt, $cost))というものがある。 これはハッシュ出力値に含まれるヌルバイトと、パスワードシャッキングのために危険になりうる。

元来の bcrypt はヌル終端されたパスワード文字列を前提としており、これはハッシュ値のうち最初のヌルバイトまでしか使われないことを意味する(H($password)[0] == 0 であれば bcrypt(H($password)), $salt, $cost) == bcrypt("", $salt, $cost) となる)。 これは bcrypt を他のハッシュ関数と組み合わせたときに衝突が見つかる可能性を高めるが、ハッシュ値を base64 などで印字可能な文字列にエンコードすれば避けられる。 base64 はハッシュ値の長さを 72 文字を超えて増やしうるので、SHA-512 のような大きなハッシュ値では多少の切り捨てが生じるが、これは無視できる程度である。

パスワードシャッキングは、bcrypt(base64(H($password))), $salt, $cost) == bcrypt(base64($leaked_hash), $salt, $cost) かどうかを容易に確認できるという事実を利用する。 内側のハッシュ関数 H が同じパスワードに対して別の場所でも使われており、それが攻撃者に知られている場合、パスワードの解読はハッシュ関数 H を破ることに帰着させられる。 素の SHA-512 をそのまま使うこと(すなわち bcrypt(base64(sha512($password))), $salt, $cost))は危険な慣行であり、素の SHA-512 を使うのと同程度の安全性しかない。 パスワードシャッキングが成立するのは、漏洩したハッシュが侵害データベースやレインボーテーブルを通じて攻撃者に知られている場合だけである。 パスワードシャッキングを緩和するにはペッパーを使える。

要するに、bcrypt を使わなければならず、かつパスワードを事前にハッシュ化する必要がある場合は、bcrypt(base64(hmac-sha384(data:$password, key:$pepper)), $salt, $cost) とし、ペッパーはデータベースに保存しない。

PBKDF2

PBKDF2NIST が推奨しており、FIPS-140 の検証を受けた実装が存在する。 そのためこれらが要求される場合には、PBKDF2 が優先すべきアルゴリズムになる。

PBKDF2 のアルゴリズムは、HMAC やその他各種のハッシュアルゴリズムなど、内部のハッシュアルゴリズムを選ぶことを要求する。 HMAC-SHA-256 は広く対応されており、NIST が推奨している。

PBKDF2 のワークファクタは反復回数として実装される。 これは使用する内部ハッシュアルゴリズムに応じて異なる値を設定するべきである。

  • PBKDF2-HMAC-SHA256:600,000 回(推奨)
  • PBKDF2-HMAC-SHA512:220,000 回
  • PBKDF2-HMAC-SHA1:1,400,000 回。レガシー専用であり、新しいシステムでは選ばない。NIST SP 800-131A Rev. 2 は 2030 年以降の新規用途での SHA-1 を禁じている。

並列 PBKDF2

  • PPBKDF2-SHA512:コスト 2
  • PPBKDF2-SHA256:コスト 5
  • PPBKDF2-SHA1:コスト 10

これらの設定は与える防御において同等である(2022 年 12 月時点の数値。RTX 4000 系 GPU での測定に基づく)。

PBKDF2 における事前ハッシュ

PBKDF2 を HMAC とともに使い、パスワードがハッシュ関数のブロックサイズ(SHA-256 では 64 バイト)より長い場合、パスワードは自動的に事前ハッシュされる。 たとえば "This is a password longer than 512 bits which is the block size of SHA-256" というパスワードは、16 進表記で fa91498c139805af73f7ba275cca071e78d78675027000c99a9925e2ec92eedd というハッシュ値に変換される。

PBKDF2 の良い実装は、計算量の大きい反復ハッシュの段階の前に事前ハッシュを行う。 しかし実装によっては反復ごとに変換を行うものがあり、長いパスワードのハッシュ計算が短いものより著しく高くつくことになる。 利用者が非常に長いパスワードを与えた場合、2013 年に Django で公表されたようなサービス妨害の脆弱性が生じうる。 手作業での事前ハッシュはこのリスクを下げられるが、事前ハッシュの段階にソルトを加える必要がある。

レガシーなハッシュの移行

MD5 や SHA-1 のような安全性の低いハッシュアルゴリズムを使っている古いアプリケーションは、上述の現代的なパスワードハッシュアルゴリズムへ移行できる。 利用者がパスワードを入力したとき(通常はアプリケーションでの認証時)、その入力を新しいアルゴリズムで再ハッシュする。 防御側は利用者の現在のパスワードを失効させ、新しいものを入力させるべきである。 そうすれば、そのパスワードの古い(安全性の低い)ハッシュは攻撃者にとって役に立たなくなる。

ただしこれは、利用者がログインするまで古い(安全性の低い)パスワードのハッシュがデータベースに残ることを意味する。 この難点を避けるには、二つの方法のいずれかを採れる。

移行方法その一。長期間活動のない利用者のパスワードのハッシュを失効させて削除し、再度ログインするにはパスワードの再設定を要求する。 これは安全ではあるが、利用者にとってとくに親切とはいえない。 多数の利用者のパスワードを失効させると、サポート担当者に問題を生じさせたり、利用者に侵害の兆候と受け取られたりする可能性がある。

移行方法その二。既存のパスワードのハッシュを、より安全なアルゴリズムの入力として使う。 たとえばアプリケーションが元々 md5($password) としてパスワードを保存していたなら、これは bcrypt(md5($password)) に容易に移行できる。 ハッシュを重ねれば元のパスワードを知る必要はなくなるが、ハッシュを破りやすくしてしまう可能性がある。 これらのハッシュは、利用者が次にログインしたときに、パスワードを直接ハッシュ化したものへ置き換えるべきである。

パスワードのハッシュ方式は一度選んだあとも将来的に更新しなければならないことを忘れず、ハッシュアルゴリズムの更新ができるだけ容易になるようにしておく。 移行期間中は、古いハッシュアルゴリズムと新しいものの混在を許す。 パスワードハッシュアルゴリズムとワークファクタを、modular PHC string format のような標準の形式でパスワードとともに保存しておけば、ハッシュアルゴリズムの混在は扱いやすくなる。

各国語の文字

ハッシュのライブラリは幅広い文字を受け付けられなければならず、すべての Unicode コードポイントに対応しているべきである。 そうすれば利用者は、現代の機器、とくにモバイルのキーボードで使える文字をすべて使える。 利用者は各種の言語からパスワードを選び、絵文字を含めることもできるべきである。 ハッシュ化の前に利用者の入力のエントロピーを減らしてはならず、パスワードハッシュのライブラリはヌルバイトを含みうる入力を扱える必要がある。