laravel5.1のパスワード・ハッシュ化にbcryptを使ってみたら、同じパスワードでもハッシュ値が毎回変わる!(レインボーテーブル対策)
echo bcrypt(‘a’);
1回目 $2y$10$MipvG1uobHQxtki2hG4tN.3YoB4E.xSKpnf15uSJCeNT4M1tuJuxe
2回目 $2y$10$QqMUZLgv4dkAg0.wHBPM5uaxIjyweOUi5gBDjZkXYNE7hBGssXNLe
3回目 $2y$10$ioTz3tEN.7Qy0Bjznr/3O.vE8RH0nSPgxkLXmtghsTVibpFQq.2Ey
参考URL:bcryptblowfish暗号について調べたので文書化してみました
bcryptで出力される文字列は、全部がハッシュ値では無く、4つのパーツに分かれているらしい
パート1(1-3文字) = bcryptのバージョン($2y)
パート2(4-6文字) = コスト、ストレッチ回数(ハッシュ化演算の繰り返しの回数) (2^10=1024回)
パート3(7-29文字) = ハッシュ化する度に生成されるソルト(乱数で自動生成)
パート4(30-60文字) = ハッシュ値
1 2 3 4 5 6 7 8 9 10 11 12 |
Route::get('/hash', function(){ $a1 = bcrypt('a'); $a2 = bcrypt('a'); // ハッシュ値同士を比較! if($a1== $a2){ return "same!" ."<br>". $a1 ."<br>". $a2;; }else{ return "Not same!"."<br>". $a1 ."<br>". $a2;; } }); |
同じ値をハッシュ化しても、出力されるハッシュ値(ソルト)が違うので、単純に文字列比較しても一致しない!!
Not same!
$2y$10$Blv3DYvxrIX/t8rRB/6Bh.q3YuIm2KX8XQtKtg2tr8osW2NrBVXTC
$2y$10$d0OxuzIwgy71waDayG4oIu4ytGOL.0Rcm0mylTKVYubObHgI4VnUy
じゃあ、どうするかというと専用の関数を使う。
Hash::check(‘平文’, bcrypt(‘平文’))で、自動生成されたソルトとコストでハッシュ化した後に比較してくれるので無問題!
1 2 3 4 5 6 7 8 9 10 11 12 |
Route::get('/hash', function(){ //$a1 = bcrypt('a'); $a2 = bcrypt('a'); // 平文とハッシュ値を比較! if (Hash::check('a', $a2)) { return "same!" ."<br>". $a2;; }else{ return "Not same!"."<br>". $a2;; } }); |
SQL文などでは、where句で直接条件に付け加えられないので、select句で取得後に除外するしかない?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function getUser($id, $password) { // IDとPASSWORDでチェック $query = User::query(); $query->where('username', '=', $id) // これは無理! // ->where('password', '=', bcrypt($password)) $users = $query->get(); // ハッシュ化済みパスワードのソルトを使って、受け取ったパスワードをハッシュ化後に比較 if(!Hash::check($password, $user['password'])){ return false; } // IDとパスワードは合っている! // userレコードの取得処理 } |
【総論】
なぜ、こんなに面倒な事をやっているかというと、パスワードが流出した時にレインボーテーブルなどで解析される時間稼ぎのためです。
※レインボーテーブルとは、ありがちなパスワード(passwordとかqwerty123とか)をハッシュ化した文字列群。ハッシュ関数と共通ソルトが分かれば作れる。
パスワード流出した場合は、固定の共通ソルトも流出しているとか考えられる。各ハッシュ値のソルトがバラバラなら、レインボーテーブル対策として効果的!
でも、パスワードそのものが推測しやすいと、こういった工夫も、あんまり役に立たないんだよな~。