はじめに
Laravel のパスワードハッシュについて調べていたときに、bcrypt の rounds を 10 から 12 に変更したら、既存ユーザーはログインできるのかどうか気になったので、調べた内容を備忘録として残します。
bcrypt のハッシュ例
DB に保存されているパスワードハッシュは、以下のような形式になっています。
$2y$10$abc123...
この中の 10 の部分が bcrypt の cost です。
$2y$10$abc123...
↑
cost
Laravel の設定では rounds と呼ばれていますが、bcrypt のハッシュ文字列上では cost として含まれています。
例えば、Laravel の bcrypt 設定を 10 から 12 に変更したとしても、すでに DB に保存されている $2y$10$... のハッシュはそのまま残ります。
つまり、設定を変えたからといって、既存のハッシュが自動で $2y$12$... に変わるわけではありません。
Hash::check() の検証イメージ
Laravel では、パスワードの検証に Hash::check() を使います。
use Illuminate\Support\Facades\Hash;
if (Hash::check($plainPassword, $user->password)) {
// パスワード一致
}
このとき Hash::check() は、DB に保存されているハッシュ文字列の情報をもとに検証します。
例えば、DB に保存されているハッシュが以下のようなものだった場合、
$2y$10$abc123...
そのハッシュに含まれている情報を使って検証されます。
そのため、現在の設定が 12 になっていても、既存の $2y$10$... のハッシュでログインできなくなるわけではありません。
設定を 12 に変更した後の挙動
bcrypt の rounds を 12 に変更した後の挙動を整理すると、以下のようになります。
| 操作 | 処理 | DB |
|---|---|---|
| 新規登録 | 12 rounds でハッシュ生成 | $2y$12$... |
| パスワード変更 | 12 rounds でハッシュ生成 | $2y$12$... |
| 既存ユーザーのログイン | 既存の $2y$10$... を使って検証 |
変化なし |
設定の 12 は、あくまで「次にハッシュを作るときのデフォルト値」です。
既存のハッシュを書き換えるものではないため、既存ユーザーのログインには影響しません。
既存ユーザーのハッシュも 12 にしたい場合
既存ユーザーのハッシュも新しい rounds で更新したい場合は、ログイン成功時に再ハッシュする方法があります。
Laravel には Hash::needsRehash() が用意されています。
use Illuminate\Support\Facades\Hash;
if (Hash::check($plainPassword, $user->password)) {
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($plainPassword);
$user->save();
}
// ログイン成功
}
このようにすると、ログイン成功時に現在の設定と保存済みハッシュを比較して、再ハッシュが必要な場合だけ新しい設定で保存し直すことができます。
例えば、既存ユーザーのハッシュが $2y$10$... で、現在の設定が 12 の場合、ログイン成功後に $2y$12$... に更新できます。
ユーザーがログインするタイミングで少しずつ更新できるので、一括で全ユーザーのパスワードハッシュを変更する必要はありません。
実際に検証したこと
今回の内容について、素の PHP 関数と Laravel の Hash ファサードの両方で検証しました。
素の PHP 関数での検証
まず、Laravel を介さずに PHP の関数だけで確認しました。
確認した流れは以下です。
cost 10 で bcrypt ハッシュを生成
↓
そのハッシュを password_verify() で検証
↓
cost 12 を現在設定とみなして password_needs_rehash() を確認
↓
cost 12 で新しい bcrypt ハッシュを生成
この検証で、以下のことを確認できました。
$2y$10$... のハッシュは、ハッシュ文字列内の情報を使って検証できる
cost 12 に変更した想定でも、cost 10 の既存ハッシュは password_verify() で検証できる
cost 10 の既存ハッシュに対して password_needs_rehash() が true になる
cost 12 で新規生成したハッシュは $2y$12$... になる
つまり、bcrypt の cost はハッシュ文字列に含まれており、検証時にはその情報が使われることを確認できました。
Laravel の Hash ファサードでの検証
次に、ローカルの Laravel 環境でも Hash::make()、Hash::check()、Hash::needsRehash() を使って確認しました。
確認した内容は以下です。
use Illuminate\Support\Facades\Hash;
$password = 'password-test';
$oldHash = Hash::make($password, ['rounds' => 10]);
Hash::check($password, $oldHash);
// true
Hash::needsRehash($oldHash);
// 現在の bcrypt rounds 設定が 12 の場合 true
$newHash = Hash::make($password);
// 現在の bcrypt rounds 設定が 12 の場合、$2y$12$... が生成される
この検証で、Laravel 上でも以下のことを確認できました。
Hash::check() で $2y$10$... の既存ハッシュを検証できる
Hash::needsRehash() で現在設定との差分を判定できる
Hash::make() で新規生成されるハッシュは現在設定の rounds に従う
また、検証は一時的なスクリプトで行い、アプリケーションコードや DB は変更していません。
そのため、既存データに影響を与えずに、bcrypt の cost 変更時の挙動を確認できました。
注意点
今回の話は、bcrypt の rounds / cost を変更する場合の話です。
bcrypt から Argon2 など、別のハッシュアルゴリズムへ変更する場合は別途注意が必要です。
Laravel の Hash::check() は、保存されているハッシュが現在のアプリケーションで選択されているハッシュアルゴリズムで作られたものかも確認します。
そのため、単に bcrypt の rounds を変更する場合と、bcrypt から Argon2 へ移行する場合は分けて考えた方がよさそうです。
まとめ
整理すると、
- 既存の
$2y$10$...はそのまま検証できる - 新規登録やパスワード変更では
$2y$12$...が作られる - 設定の
12は「次にハッシュを作るときのデフォルト値」 - 既存ハッシュを更新したい場合は
Hash::needsRehash()を使う - 素の PHP 関数と Laravel の
Hashファサードの両方で、cost 10 の既存ハッシュが検証できることを確認した - bcrypt の rounds 変更と、ハッシュアルゴリズム変更は別物として考える
という感じです。
設定を変更すると既存ユーザーのログインに影響するのでは?と思っていましたが、bcrypt のハッシュ構造を知ると納得できました。