Menu

PHPのpassword_hash()とpassword_verify():パスワードの保存

password_hash($password, PASSWORD_DEFAULT)でパスワードをハッシュ化し、60文字の結果を保存し、password_verify($password, $hash)でログインを確認します。ハッシュが毎回変わる理由、コスト、password_needs_rehash、Argon2id、md5やsha1がパスワードに不適切な理由を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

パスワードはpassword_hash($password, PASSWORD_DEFAULT)で保存し、ログインはtrueかfalseを返すpassword_verify($password, $hash)で確認します。パスワードそのものを保存してはいけませんし、それにmd5()やsha1()を使ってもいけません。

2回実行してみてください。ハッシュは毎回違いますが、どちらも検証に通ります。このページのすべてのハッシュについて同じことが言えるので、あなたの出力はほかの誰のものとも一致しません。

password_verifyでログインする

ログインでは、送信されたパスワードを、アカウント作成時に保存したハッシュと照合します。このブロックはユーザーを1人「登録」してから、ログインフォームを表示します。正しいパスワード(hunter2)を試してから、間違ったものを試してください。

エラーメッセージは、存在しないメールアドレスでも間違ったパスワードでも同じです。どちらが間違っていたかを伝えると、どのメールアドレスにアカウントがあるかを攻撃者に教えてしまいます。ログインに成功したら、本物のサイトではセッションのページで示しているように、ユーザーIDをセッションに保存します。

ハッシュの中身

bcryptのハッシュは、password_verify()が必要とするものをすべて持つ1つの文字列です。アルゴリズム、コスト、ソルト、そのあとにハッシュそのものが続きます。password_get_info()は先頭の部分を読み戻します。

ソルトがハッシュの中に保存されているので、データベースのカラムは1つで済み、同じパスワードの2人のユーザーも違うハッシュになります。そのカラムはVARCHAR(255)にしましょう。bcryptは60文字ですが、PASSWORD_DEFAULTはより長い出力のアルゴリズムに変わることが許されています。

コストを選ぶ

コストは、各ハッシュをわざとどれだけ遅くするかで、1段上げるごとに処理量が2倍になります。PHP 8.3はbcryptにコスト10を使います(PHP 8.4でデフォルトが12に上がりました)。ログインのハッシュ化にはおよそ100から300ミリ秒かかるのが目安で、自分のサーバーで計測しましょう。

数値はマシンによって違いますが、各行は前の行のおよそ2倍になるはずです。高いほど解析に強く、すべてのログインが遅くなるので、ログインページが許容できるいちばん高いコストを選びましょう。

password_needs_rehashで古いハッシュを更新する

コストを上げたりPHPがPASSWORD_DEFAULTを変えたりしても、既存のハッシュは動き続け、password_needs_rehash()がどれが古くなったかを教えてくれます。平文のパスワードが手元にあるのはログインの瞬間だけなので、そこで更新します。

接頭辞の$2y$10$が$2y$12$になり、次のログインではpassword_needs_rehash()がfalseを返します。

パスワードに決してmd5やsha1を使わない

md5()とsha1()は、ソルトのない高速な汎用ハッシュです。パスワードにとって高速であることはまさに間違いです。漏えいしたテーブルを持つ攻撃者はGPUで1秒間に何十億回も推測でき、よくあるパスワードのソルトなしのハッシュはすでに公開された照合表に載っています。速さを比べてみてください。

password_hash()が1つのハッシュを作る間に、md5は何十万個も作るので、解析する側のすべての推測がそれだけ安くなります。5f4dcc3b5aa765d61d8327deb882cf99で検索すると、すぐに「password」という単語が見つかります。

md5のハッシュのテーブルを引き継いだら、直接は変換できません。代わりに包みましょう。password_hash($oldMd5)を保存してpassword_verify(md5($input), $stored)で検証し、各ユーザーが次にログインしたときに普通のハッシュに置き換えます。

Argon2idと72バイトの制限

PASSWORD_ARGON2IDはハッシュごとにより多くのメモリを使うので、GPUによる解析にずっとコストがかかるようになります。PHPがlibargon2付きでビルドされていれば使え(ほとんどのLinuxのパッケージはそうです)、defined('PASSWORD_ARGON2ID')で確認できます。

最後の行はbool(true)と出力します。bcryptは72バイト目より後ろをすべて無視するので、2つの違うパスワードが一致してしまうのです。実際のパスワードがそこまで届くことはまれですが、日本語などのマルチバイト文字のパスフレーズは約24文字で72バイトに達します。とても長いパスワードを許すなら、そのような制限のないArgon2idを使いましょう。password_verify()はどちらのアルゴリズムでも動くので、上のようにpassword_needs_rehash()で切り替えられます。

よくある質問

password_hashが毎回違う結果を返すのはなぜですか?

呼び出しのたびに新しいランダムなソルトを生成してハッシュの中に保存するので、同じパスワードでも毎回違う文字列になります。これは意図どおりの動きで、password_verify()は保存されたハッシュからソルトを読み戻すので、正しいパスワードなら今でもtrueを返します。

password_hash用のパスワードのカラムはどれくらいの長さにすべきですか?

PASSWORD_DEFAULTによるbcryptのハッシュは今は60文字ですが、デフォルトのアルゴリズムは将来のPHPのバージョンで変わる可能性があります。マイグレーションなしでより長いハッシュが収まるようVARCHAR(255)を使いましょう。

password_verifyが常にfalseを返すのはなぜですか?

よくある原因は、ハッシュが切り詰められている(VARCHAR(50)のカラムは60文字のハッシュを切ってしまう)、2つ目の引数に保存したハッシュではなく何かを新しくpassword_hash()したものを渡している、md5()で作ったハッシュと比較している、保存した値に余計な空白がある、のいずれかです。strlen($hash)が60であることを確認し、1つ目の引数に平文のパスワードを渡しましょう。

PHPでpassword_hashの値を復号できますか?

できません。ハッシュは一方向で、パスワードに戻す関数はありません。パスワードを確認するには、入力されたものをハッシュ化して比較します。これがpassword_verify()のしていることです。パスワードを忘れた人には、古いパスワードではなくリセット用のリンクを送ります。

PASSWORD_DEFAULT、PASSWORD_BCRYPT、PASSWORD_ARGON2IDのどれを使うべきですか?

ふつうはPASSWORD_DEFAULTです。今はbcryptで、PHPはあとでより強いアルゴリズムに変えるかもしれず、password_needs_rehash()でそれに少しずつ移行できます。PASSWORD_ARGON2IDはGPUによる解析に強く、使っているPHPのビルドに含まれていれば使ってかまいません。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める