ハッシュルールをカスタマイズ
パスワードについての心構え
念のためDB内のユーザー情報を見てみましょう。
MySqlにアクセスし、以下SQLを発行してみてください。
| id | name | email | email_verified_at | password |
+----+-----------+---------------+-------------------+--------------------------------------------------------------+
| 12 | hoge_user | [email protected] | NULL | $2y$12$ceF2VFQhke6vkOQIg3IsR.CZPCwsVq00O4AmkJFvPKZ9lszlkNfTS |
| 21 | test_user | [email protected] | NULL | $2y$12$ingw2IDdwtPTZ5biI7a9wuRjzBFT/PQZwkRtIyv2fNCPVmn0qxpZK |
+----+-----------+---------------+-------------------+--------------------------------------------------------------+
パスワードが何か違うぞ。
確か「hoge0000」「foo0000」としてました。 まぁそりゃそうですよね。
そもそもパスワードを平文で管理してる企業のサービスを受けたいと思います?
答えは「Nooooooo!!」ですよね。
② ハッシュ化にソルト文字列をユーザー単位に付与する
③ 更にペッパー文字列を設定ファイルで管理する
答えは単純で、ランダムな文字列となり、この文字列と平文のパスワードを一緒にハッシュ化する事でより強固な仕組みとします。
一般的にソルト文字列はユーザー単位で別々な文字列にする事が推奨されます。 一緒に管理しては危険では?
と思うかもしれませんが、仮にソルト文字列をDBとは別に設定ファイル等で固定文字列として管理した場合を考えてみましょう。
DB情報が漏洩した場合でもソルト文字列が漏洩してなきゃ安心。。 ではありませんっ!!
ユーザー数が多ければ多いほど、安易なパスワードを設定する「おバカ」が存在します。
ありがちなのが「password」です。 攻撃者はこのありがちなパスワードは必ずあると仮定し、固定ソルト文字列を物量作戦で解読します。
そして数兆パターン目に「特定の文字列」+「password」でハッシュ化したところ、漏洩した情報の中の1つと完全一致します。
そうです。
固定ソルト文字列が解読された事を意味します。
固定の文字列が解読されれば、その漏洩したパスワードはただの平文をハッシュ化しただけのパスワードです。 それに比べ、②の様にユーザー単位でソルト文字列を変更する方法はどうでしょう。
こちらはDB情報が漏洩した瞬間に各ユーザーのソルト文字列も解読された状態となります(DBに登録されてるので)
一見こちらの方がザルの様に見えませんか? 答えは「No」です。
先ほどの説明の通り「おバカ」のせいで設定ファイルに管理したソルト文字列の解読は時間の問題です。
ではお互いにソルト文字列が解読された状態で100万人の情報が漏洩した場合を考えて見ましょう。 固定ソルト文字列の場合は、全てのユーザーが同じソルト文字列なので攻撃者が「hoge0000」ってパスワードはないかな?
と、ハッシュ化してみて100万データと照合する事ができます。
対してユーザー単位の動的ソルト文字列の場合は1回ハッシュ化したものは1データとしか照合できません。
(そりゃ残りの999,999件のソルト文字列が違うので照合するだけ無駄ですよね)
つまり攻撃者の労力が100万倍違うのです!! どんなに顧客情報を厳重に管理しても、情報漏えいは「起きる時には起きる」ものです。。
情報漏えいが起きてしまった時、企業側にできる事は当然迅速な対応と 「攻撃者に対していかに時間稼ぎをするか」に尽きます。 それでは改めてパスワード登録の要件を整理します。
○ {②ソルト文字列} + $ + {ハッシュ化した文字列}をDB登録する
○ パスワード情報の「$」の左側を[②ソルト文字列]とする
○ {③ペッパー文字列} + {②ソルト文字列} + {入力された平文のパスワード}をハッシュ化する
○ {②ソルト文字列} + $ + {ハッシュ化した文字列}とDB情報を比較する
○ 同じ内容ならログイン成功!!
③ペッパー文字列とは、設定ファイル等による固定文字列の事です。
つまり、固定文字列の「ペッパー」と、動的文字列の「ソルト」で強力な【味付け】をするわけです。
この重装備で、レインボーテーブル攻撃を迎え撃ちます。
※ ハッシュ化されたパスワードを効率的に逆引きする攻撃手法です
効率的なパスワード管理
処理は細分化させるのが基本ですので、便利クラスを作成して個々の役割を割り振りましょう。
「SystemUtil」クラスとし、「~\app\Utility\SystemUtil.php」へ新規作成します。
では一気にいきます!!
<?php
namespace App\Utility;
class SystemUtil{
// ハッシュ文字列作成
public static function getHash512($str, $flg = false) {
$hash = hash('sha512', $str, $flg);
return $hash;
}
// ランダムな文字列を生成する
public static function getRandomStr($length = 16) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$bytes = random_bytes($length);
$charLength = strlen($chars);
$result = '';
for ($i = 0; $i < $length; $i++) {
$result .= $chars[ord($bytes[$i]) % $charLength];
}
return $result;
}
// パスワード生成
public static function createHashPassword($password) {
// ペッパー文字列取得
$pepper_str = config('app.pepper_str');
// ソルト文字列取得
$salt_str = self::getRandomStr();
$plain_password = $pepper_str . '$' . $salt_str . '$' . $password;
// パスワードハッシュ化
$hash_password = self::getHash512($plain_password);
return $salt_str . '$' . $hash_password;
}
// パスワード確認
public static function checkPassword($inputPassword, $hashPassword) {
// ソルト文字列、パスワードを分解
list($salt_str, $check) = explode('$', $hashPassword);
// ペッパー文字列取得
$pepper_str = config('app.pepper_str');
$plain_password = $pepper_str . '$' . $salt_str . '$' . $inputPassword;
// パスワードハッシュ化
$hash_password = self::getHash512($plain_password);
return $hash_password == $check;
}
}
第2引数を[true]とするとバイナリで返却されますが、テキスト形式が良いので初期値を[false]としています。
文字数は何文字でも良いのですが、初期値を16文字としています。
ユーザーが入力したパスワードを引数とします。
第1引数をユーザーが入力したパスワード。
第2引数をDBに登録されているパスワードとします。
SystemUtilクラス内に以下コードがあります。
$pepper_str = config('app.pepper_str');
このままでは設定不足によりエラーとなってしまうので、上記コード内容に合わせた設定をしてください。 それでは動作確認をしてみましょう。
認証の影響下にない画面のコントローラに以下のコードを貼り付けてアクセスしてみてください。
use App\Utility\SystemUtil;
~ 省略 ~
public function index(Request $request){
$pass = SystemUtil::createHashPassword('input_password');
$check = SystemUtil::checkPassword('input_password', $pass);
var_dump($check);exit;
return view('hoge/index', ['title'=>'任意の画面']);
}
続いて入力されたパスワード「input_password」とDB値「$pass」をチェックしています。
認証成功なら変数「$check」は[true]となります。
独自パスワード処理を組込む
次は実際にユーザー登録の際に、このロジックを使う必要があります。
Laravelでは以下メソッドで認証処理をしています。
Auth::attempt({プレーンな[ID][パスワード]情報}); この関数に先ほど作成した独自ロジックを組み込む必要があります。
さてどうするか。。 まずは、認証処理をオーバーライドするクラスを作成しましょう。
「~\app\Services\Sha512Hasher.php」クラスを作成します。
<?php
namespace App\Services;
class Sha512Hasher {
// パスワード生成
public function make($value, array $options = []) {
var_dump('パスワード生成');exit;
}
// パスワードチェック
public function check($value, $hashedValue, array $options = []) {
var_dump('パスワードチェック', $value, $hashedValue);exit;
}
// パスワードリフレッシュ
public function needsRehash($hashedValue, array $options = []) {
return false;
}
}
.form-text {
border: solid 1px #AAA;
outline: none;
padding: 5px;
border-radius: 8px;
}
<?php
return [
'driver' => 'sha256',
];
試しに任意のユーザーでログインを試みてください。
以下のコードが発火して画面処理が止まると思います。
var_dump('パスワードチェック', $value, $hashedValue);exit; ダンプされた内容をよく見ると、「入力したパスワード」「DBに保存したパスワード」になっているのに気がつくと思います。
それではこのメソッドに先ほど作成した「SystemUtil」クラスの処理を追加しましょう。
<?php
namespace App\Services;
use App\Utility\SystemUtil;
class Sha512Hasher {
// パスワード生成
public function make($value, array $options = []) {
return SystemUtil::createHashPassword($value);
}
// パスワードチェック
public function check($value, $hashedValue, array $options = []) {
return SystemUtil::checkPassword($value, $hashedValue);
}
// パスワードリフレッシュ
public function needsRehash($hashedValue, array $options = []) {
return false;
}
}
無事ログインに失敗しましたね。。w
DBに登録されている内容がLaravel標準の内容でしたので当然失敗します。 それでは新たなユーザーを以下の様に登録しましょう。
パスワード:fuga0000
ログインできましたよね!?
これであなたのシステムは自由なルールでパスワードの管理をできる事になりました。
認証のカスタマイズにより、自動生成された以下内容は不要となります。
《WEB_ROOT》
・\Auth\ConfirmPasswordController.php
・\Auth\ForgotPasswordController.php
・\Auth\RegisterController.php
・\Auth\ResetPasswordController.php
・\resources\views\auth\_verify.blade.php
・\resources\views\auth\_register.blade.php
リダイレクト先について
そんな時は以下ファイルを修正しましょう。 「~\bootstrap\app.php」
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->redirectGuestsTo(function ($request) {
return route('index');
});
})
->create();





少し余談ですが、たまにパスワード忘れで、メールからパスワードを再設定するシステムがありますよね。
知らないのに何でログイン処理が出来るかって?あれ、「再設定じゃなくて登録してたパスワード教えてくれよ」って思った事ありませんか?
実はあなたのパスワードってサービスの運営側の誰一人として知らないんです。
それは運営側が知っているのは、あなたのパスワードをハッシュ化した後の文字列だからです。
決まったパスワードを決まった方法でハッシュ化するので認証処理が可能なわけです。
ハッシュ化は不可逆の性質を持っているためハッシュ文字列が分かっても、平文のパスワードは分からないって事です。