初めに
LaravelのHashingは公式のドキュメントで以下のような紹介をされています。
The Laravel Hash facade provides secure Bcrypt and Argon2 hashing for storing user passwords. If you are using one of the Laravel application starter kits, Bcrypt will be used for registration and authentication by default.
Bcrypt is a great choice for hashing passwords because its "work factor" is adjustable, which means that the time it takes to generate a hash can be increased as hardware power increases. When hashing passwords, slow is good. The longer an algorithm takes to hash a password, the longer it takes malicious users to generate "rainbow tables" of all possible string hash values that may be used in brute force attacks against applications.
Laravel Hashing Introduction (version 9.x)
この紹介文を要約すると、
- LaravelのHashingはパスワードをハッシュ値に変換する(以後この記事ではこれをハッシュ化とする)際にBcryptまたはArgon2を使用しており、デフォルトではBcryptを使用している。
- Bcryptはサーバーの性能によってハッシュ化にかかる時間を調節できるため、パスワードのハッシュ化には非常に良い手段である。
- ハッシュ化は遅い方が良く、ハッシュ化にかける時間が長ければ長いほど、悪意のあるユーザーによるブルートフォース攻撃で使用される「レインボーテーブル」の生成が長くなる
となり、LaravelのHashingを使用すれば安全なパスワードの保護ができることが分かります。
しかしそれは本当なのでしょうか? また、本当であればなぜそう言えるのでしょうか?
この記事では安全性の高いハッシュ化の要件と実際のLaravelのHashingの機能を比べたうえで、上記の疑問に対する答えを探していこうと思います。
この記事を読むにあたって
筆者はハッシュの分野について全くの初心者であり、間違いがないように様々な資料を参照しながらこの記事を書いていますがそれでも正しくない情報があると思います。もしおかしな部分を見つけましたら我が子に勉強を教えるようにやんわりと指摘していただけるとありがたいです(筆者のメンタルは豆腐です)
安全性の高いハッシュ化の要件
安全性の高いハッシュ化を考えるうえでまずはハッシュ化の脅威を考えていこうと思います。
暗号化とハッシュ化の違い
暗号化とハッシュ化はどちらもパスワードの保管方法として用いられることから同じものととらえてしまうかもしれませんが(最近まで自分は同じものと思っていた)、大きく異なります。その違いとは、暗号化は元のデータを復元できる一方でハッシュ化は復元できないことと、暗号化は暗号鍵の管理が必要である一方でハッシュ化はそれが必要ではないことです。つまりハッシュ化を選べぶことで、元のデータを復元できない一方で暗号鍵の保管がいらなくなり、また保管されるデータが元のデータではないためパスワードが流出した際の被害を暗号化よりも小さくできます。といってもハッシュ値のみから元データの解読ができることから、ハッシュ化が暗号化より優れているとは明確に言えないと思われます。
参考 EAGLYS コラム 暗号化とハッシュ化の違いとは。仕組みや活用例から読み解く
ハッシュ化の脅威と対応方法
ハッシュ化の脅威
- オンラインブルートフォース攻撃(総当たり攻撃)
- オンラインブルートフォース攻撃とは、性能の高いコンピューターを使って膨大な数のハッシュ計算を行うことでハッシュ値から元のデータを求めるというものです。オンラインブルートフォース攻撃は大抵の場合は後述するレインボーテーブルと組み合わせて行われ、現在ではMD5やSHA-1といったハッシュ計算に使用されてきたアルゴリズムは容易に解読されるようになりました。※1
- レインボーテーブル
- レインボーテーブルとはハッシュ値と元データの組み合わせが書かれた表であり、MD5やSHA-1などのアルゴリズム用の表が一般公開されていたり、販売されています。※2 レインボーテーブルのサイズは文字数や文字種の数が増えるほど大きくなります。
- ハッシュ値の衝突
- ハッシュ値の衝突とは異なる二つのデータが同じハッシュ値を持つことを指します。※3 パスワードのハッシュ計算でこれが発生した場合、正しいパスワードを知っていなくてもユーザーのなりすましができてしまいます。
- 同一のデータに対して同一のハッシュ値が出力される
- ハッシュ計算は基本的に同一のデータから同一のハッシュ値になります。この仕様はデータの改ざんチェックなどには良いですが、パスワードの保管にはあまり良いと言えません。例えばパスワードを含むユーザーデータが流出し、その中に同一のハッシュ値を持つユーザーが複数存在した場合、一つのハッシュ値を解読することで同じハッシュ値を持つ全てのパスワードも解読できてしまいまうからです。
参考資料
※1 とほほの暗号化入門 MD5、SHA-1の説明
※2 【bcrypt】ユーザーパスワードを本当に安全に保存する方法 saltによる強化
※3 FULL SUPORT MEDIA ITの知識不足による判決間違い?-MD5の脆弱性 ハッシュ関数暗号の改ざん可能性
ハッシュ化の脅威のまとめ
- ブルートフォース攻撃とレインボーテーブルによって容易にハッシュ値が解読されることがある
- パスワードのような単純になりがちなデータではハッシュ値が容易に解読される
- 二つのデータが同じハッシュ値を持つ可能性がある
- ハッシュ計算は同一のデータに対して同一のハッシュ値を出力する
安全性の高いハッシュ化の脅威が求まったところで、次にそれらに対応するための対応について考えていきます。
ハッシュ化の脅威に対する対応
- ソルト(salt)
- ソルトとは元データをハッシュ化する際にそのデータにランダムな文字列を追加することを指します。※1 ソルトによるパスワードの文字数の増加によって解読に使用するレインボーテーブルのサイズも増加することになり、ハッシュ値の解読が困難になります。また、パスワードなどのデータに加えてランダムな文字列データもハッシュ値となるので同一のデータであっても同一のハッシュ値になることはありません。(なったとしてもかなり低い確率です。)つまりソルトを用いることで、ユーザーに複雑なパスワードを要求せずにハッシュ化の脅威1、4を解決することができます。
- ストレッチング
- ストレッチングとはハッシュ化のためのハッシュ計算を膨大な回数行うことを指します。※2 ハッシュ計算の回数が多いほどハッシュ値の解読の時間が遅くなります。(なので解読のしにくいハッシュ化を遅いハッシュ化と呼びます。)つまりストレッチングによってハッシュ化の脅威1を解決することができます。
- 解読されにくいアルゴリズムを選ぶ
- ハッシュ値の解読が困難なアルゴリズムを選ぶことでハッシュ化の脅威1を解決することができます。アルゴリズムの選定は使用する言語・ライブラリを参照するのが良いと思われます。(アルゴリズムの正しい選定の仕方について筆者は分かりません。正しい選定の仕方については他の方の資料を参考にしていただきたいです)
- ハッシュ値の衝突の起きにくいアルゴリズムを選ぶ
- ハッシュ化に用いるアルゴリズムの中には異なるデータであっても同一のハッシュ値を生成する可能性が高いものがあります。※3 したがって、ハッシュ化に当たってはハッシュ値の衝突の可能性が低いアルゴリズムを使用することでハッシュ化の脅威3を解決することができます。
参考資料
※1 ITをわかりやすく解説 【パスワード】bcryptとは ソルトとは
※2 ITをわかりやすく解説 【パスワード】bcryptとは ストレッチングとは
※3 INTERNETWatch GoogleとCWI、SHA-1衝突に成功、ハッシュ値が同じ2つのPDFを公開
ハッシュ化の脅威に対する対応のまとめ
- ソルトを使用する
- ストレッチングを行う
- 解読されにくいアルゴリズムを使用する
- ハッシュ値の衝突が生じにくいアルゴリズムを使用する
これらの4つの対応方法を満たすことが安全なハッシュ化の要件と言えそうです。
ここまでハッシュ化の脅威とその対応方法について見ていきましたが、LaravelのHashingはどのように対応しているのでしょうか?
Laravel Hashing について
ここからはHashingの実装を見る中で対応方法について見ていこうと思います。
Hashing の実装コード
バージョン:9.48.0
/**
* Hash the given value.
*
* @param string $value
* @param array $options
* @return string
*
* @throws \RuntimeException
*/
public function make($value, array $options = [])
{
$hash = password_hash($value, PASSWORD_BCRYPT, [
'cost' => $this->cost($options),
]);
if ($hash === false) {
throw new RuntimeException('Bcrypt hashing not supported.');
}
return $hash;
}
Bcrypt(デフォルト)を使用したHash::make
の実装は./vendor/laravel/framework/src/Illuminate/Hashing/BcryptHasher.php
のmake
メソッドに書かれています。makeメソッドの内容はpassword_hash
メソッドの結果が正常であればそのメソッドの結果を返すこととなっています。
password_hash
メソッドとは何でしょうか?
password_hashメソッドについて
password_hash
メソッドは第一引数の値をもとに第二引数で指定されたアルゴリズムと第三引数のオプションによってハッシュ値を生成するメソッドです。BcryptHasher.php
のmake
メソッドによると、password_hash
メソッドはHash::make
の第一引数の値をCRYPT_BLOWFISH
アルゴリズム※(Bcryptアルゴリズムの一つ)と$this->cost($options)
で指定された負荷でハッシュ化しています。CRYPT_BLOWFISH
アルゴリズムは自動でランダムに生成されたソルトを使用して常に60文字のハッシュ値を生成します。$this->cost($options)
で指定された負荷はphpunit.xml
の
<env name="BCRYPT_ROUNDS" value="設定したい負荷"/>
を変更することなどで変更可能です。valueの値が大きくなればなるほどより遅いハッシュ化ができますが、その分ハッシュ化に時間がかかることとなります。
※デフォルトはBcryptアルゴリズムだが設定値を変更することでArgon2アルゴリズムを使用することもできる。
Laravel Hashing まとめ
Hashingの実装が分かったところで、最後に安全性の高いハッシュ化の要件とHashingを比較します。
- ソルトを使用しているか →
password_hash
メソッドを通じてソルトを使用。自動生成されたソルトはランダムなため同じハッシュ値になる可能性は低い - ストレッチングを使用しているか →
password_hash
メソッドを通じてストレッチングを使用。(ストレッチングによるハッシュ計算の回数は変更可能であるが多い方が遅いハッシュ化になる) - 解読しにくいアルゴリズムを使用しているか →
password_hash
メソッドを通じてデフォルトではBcrypt(CRYPT_BLOWFISH
アルゴリズム)というPHPが公式に使用を認めているアルゴリズムを使用。他のアルゴリズムに対してどれほど優位性があるのかは分からないが、PHPの公式が使用を認めているのである程度の安全性があると思われる。 - ハッシュ値の衝突の起きにくいアルゴリズムを使用しているか → デフォルトではBcryptを使用。このアルゴリズムが他に比べてどれだけハッシュ値の衝突が起きにくいかは調べていないが、PHPの公式が使用を認めているのであればハッシュ値の衝突に関して一定の耐性があると思われる。
つまり、安全性の高いハッシュ化の全ての要件を満たしているためLaravel Hashingはデフォルトの状態でもパスワードの保護方法として安全だといえます。
終わりに
就活でLaravelを利用した自分のアプリケーションの紹介をしていた際、面接官の方に「Hashingはパスワードの保護方法として適切なのか?」という質問に対して何も答えられなかったという悔しい思いから、その時の質問に答えるつもりで(もうそんなことありませんが)この記事を書きました。ハッシュ化は暗号化よりも開発者に対する実装の負担が低い一方で、脆弱性があるアルゴリズムを使っていたり、ハッシュ化を軽量にしたいがあまりハッシュ計算の回数を少なくするなどで安全性が低くなることもあります。アプリケーションの安全性に対する自分の知識はまだまだですが、アプリケーションを作る側のリテラシーとして今後もアプリケーションの安全性についての情報を知っていこうと思います。