05.PasswordEncoder
概要
SpringSecurity5から、PasswordEncoderの設定が必須になりました。
PasswordEncoderが何なのか?
なぜ必須になったのか?
設定例を簡単に見ていきたいと思います。
PasswordEncoderとは
なじみのない方に、まず、PasswordEncoderについて簡単に書きます。
PasswordEncoderはSpring Securityが提供するクラスで、DBなどにログインパスワードを保存するときの暗号化に関わるクラスです
主に以下の機能があります。
・パスワードを暗号化(ハッシュ化)をする
・認証時に入力のパスワードと、保存した暗号化パスワードが一致するかを確認する
パスワードの暗号化と言っても「一方向暗号化」といって、MD5やSHA2などのように元に戻すことができないものです。
MD5やSHA2といったハッシュ関数って何?(この記事を読むにあたって覚えておくこと)
MD5、SHA2のようなハッシュ関数になじみがない方のために簡単に説明します。
ご存知の方は読み飛ばしてください。
この記事を読むにあたっては、ハッシュ関数については以下の点を覚えておいていただければよいかと思います。
<ハッシュ関数の例>
ハッシュ関数は、文字列全体を考慮した一方向の暗号化をする関数です。
イメージを持ってもらうため、具体的にSHA2で「taro」という文字列ハッシュ化した例を見てみましょう。
具体例:
B496F86E5975E1FDCADE1081CF75BEED264B1CF724CE87C797B82393
<ハッシュ関数の特徴>
この記事を読むにあたっては、以下の特徴だけ覚えておいていただければ大丈夫です。
・ハッシュ後はもとの値を推測しにくい(元に戻せない)。
・入力文字列が1文字でも変わると大きくハッシュ文字列が変わる
・同じ入力にはいつも同じ文字列を返す。
弱点は以下です。
・元の値とハッシュ後の値をマッピングしたテーブル表を作られると、元の値がほぼ分かってしまう。
ちなみに、「taro」を1文字変えた「uaro」をSHA2にかけると以下になります。
1文字変えただけで大きく変わりますよね。
D8922D6E838B5870FDF2D8755FB6B656E3547834CC28AB1DF4017282
PasswordEncoderはハッシュ化をどう使うの?
ハッシュ化は、パスワードをハッシュ化してDBに保存する目的で使います。
DBのパスワード(ハッシュ値)を何かしらの方法で盗み出したとしても、元の値は分からないので悪さをしずらくなります。
ただ、元に戻せなかったら、パスワードとしてDBに保存しても、ログイン認証で正しいPWかをチェックできないのでは?という疑問が浮かびます。
しかし、問題ありません。
なぜなら、まず、ログイン入力画面で入力した平文のPWをハッシュ化します。
次に、DBに保存したハッシュ後の文字列と一致するかを比較するのです。ハッシュ化した値どおしで比較すれば問題なくチェックできますね。
PasswordEncoderの利用が必須になった理由
Spring Securityではver.5.0.0からPasswordEncoderが必須になったことを冒頭で書きました。
SpringSecurity公式リファレンスのPasswordEncoderの章の内容を踏まえて、必須になった理由を見ていきましょう。
リファレンスによると、昔に比べて、SQLインジェクションや、管理不備などによるDB情報の漏えいが増え、また事件が多くなっているので
DBの値を盗まれた場合の対策を取るべきという考えが見えます。
DBの値を盗まれたとき、パスワードが平文で保存されていると、ログインがすぐにできてしまい、お客様の情報が盗まれます。
また、同じPWを他のサイトにも利用していれば、そちらのサイトもログインされてしまいます。
そこで、パスワードの「ハッシュ化」です。
「うちはSHA2でハッシュ化しているので安全だ」と思っているなら間違いです。
今は、GPUなどの高速CPUや、レインボーテーブルを使うという手法があり、安全ではないというのが一般的な見かたのようです。
ちなみに、レインボーテーブルは、元の値とハッシュ後の値をマッピングした特殊なテーブル表です。
SpringSecurityのリファレンスでも、GPUとレインボーテーブルの2つをかなり気にしているような書き方でした。
ましてや、平文でDBに保存するのは愚かすぎるので、PasswordEncoderを必須にしよう、ということのようです。
つまり、SpringSecurityがPasswordEncoderを必須にしたのは、世の中の技術革新などにより、必須にせざるを得ない状況にあると言えそうです。
是非とも、PasswordEncoderを使っていきましょう。
PasswordEncoderの設定例
【PasswordEncoderFactoriesを使用する方法】
<bean id="passwordEncoder"
class="org.springframework.security.crypto.factory.PasswordEncoderFactories"
factory-method="createDelegatingPasswordEncoder"/>
公式リファレンスでも紹介されている、よく使う設定です。
このファクトリPasswordEncoderFactoriesは、DelegatingPasswordEncoderを生成するファクトリーです。
DelegatingPasswordEncoderは、内部に以下ようなクラスを持ち、保存されたパスワードの形式に応じて、適切に使用するPasswordEncoderを選択するクラスです。
・NoOpPasswordEncoder
・BCryptPasswordEncoder
・StandardPasswordEncoder
など
<DelegatingPasswordEncoderの動作>
DelegatingPasswordEncoderで暗号化を行うと、{noop}などのプレフィックスがついた状態で返却されます。
例えば、以下の表の右側のような文字列が生成され、これをDBに保存します。
NoOpPasswordEncoderは平文のまま何も暗号化しない文字列を返すPasswordEncoderです。入力のパスワードとマッチするかを検証するときは、DelegatingPasswordEncoderが、「{noop}suzuki」の先頭のnoopを見て、
「NoOpPasswordEncoderで暗号化されているので、NoOpPasswordEncoderクラスでマッチするかを検証しよう」、と考えて検証します。
このDelegatingPasswordEncoderを使用するメリットは以下です。
<DelegatingPasswordEncoderのメリット>
・暗号化方式はすぐに廃れるが、新しい暗号化方式への変更が簡単にできる※。
・古い方式と新しい方式が同時にDBに存在しても対応できる。
※元の平文のPWが分からないので一気に新しい暗号化方式で暗号化するということはできないですが、
例えば、ユーザがログインした時に平文が分かるので、そのタイミングで置き変えるなどが考えられます。
【PasswordEncoder単体で使用する方法】
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
PasswordEncoderFactoriesの場合は、内部で複数のPasswordEncoderクラスを設定したDelegatingPasswordEncoderを生成していました。
設定されたクラスは、それぞれ個別でも使用できます。個別で使用する場合は、上記のように設定します。
上記の例では、Bcryptを使用するPasswordEncoderを設定しています。
特筆することはありませんが、DelegatingPasswordEncoderと違って複数の暗号化方式を混在して使用できなくなります。
【既存システムにDelegatingPasswordEncoderを導入したい場合】
<bean id="passwordEncoder" class="org.springframework.security.crypto.password.DelegatingPasswordEncoder">
<constructor-arg index="0" value="bcrypt"/>
<constructor-arg index="1" >
<map>
<entry key="bcrypt"><bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/></entry>
</map>
</constructor-arg>
<property name="defaultPasswordEncoderForMatches">
<bean class="org.springframework.security.crypto.password.NoOpPasswordEncoder"/>
</property>
</bean>
DelegatingPasswordEncoderを既存システムに導入したい場合に役に立ちそうな設定例を記述しました。
新規でシステム構築する場合は何も考えなくても導入可能かと思います。
既存システムに導入したい場合、すでにDBには平文のパスワードが保存されていますので、どうしたらよいでしょうか?
方法は以下が考えられます。
①DBのパスワードを一括置換で「{noop}」を先頭につける
②プレフィックスがない場合はNoOpPasswordEncoderを使用するように設定する。
(設定例:デフォルトをBcrypt、プレフィックスがない場合をNoOpで設定する例)
①のような方法を行いたい場合、DBの値を書き替えれば前節のPasswordEncoderファクトリの設定がそのまま使えます。
②のような方法を行いたい場合の設定方法が上記の設定例になります。
【注意点】
NoOpPasswordEncoderクラスは既にDeprecatedされています。
まだクラスは削除されていませんが、いつ削除されるか分かりませんのでお気を付けください。
参考:推奨の一方向暗号化
現状では、Bcryptという方式を推奨しているそうです。
内容的にはソルト(Salt)、ストレッチという手法を組み合わせた方式です。
Bcryptの原理
ソルトは、ランダムな文字列を平文PWに連結したうえで、ハッシュ化する方式です。
例えば、ランダム文字列を「aaaaaaa」で、ユーザパスワードを「suzuki」とすると、「aaaaaaa_suzuki」のような文字列にハッシュ関数をかけます。
ストレッチは、ハッシュ後の文字列を何度か再度ハッシュ化することで、元の文字列を分かりにくくする方式です。
Bcryptでは、ソルトのランダム文字列もストレッチの回数も、暗号化後に平文で付与され、回数+ランダム文字列+ハッシュ後の文字列の
ような並びで返ってきます。この文字列をDBなどに保存することになります。
<Bcryptの文字列に関する補足>
「ランダム文字列や回数をDBに保存したら意味ないでしょ?」と思いますか?
実際には問題ありません。
なぜなら、ランダム文字列が分かったところで、PWの文字列を知るためには、ランダム文字列を付与したハッシュ表を作らないといけないからです。
SHA2のみのハッシュ化がなぜ危険だったかというと、例えば、「pass」「taro」「admin」など、PWに使われる文字列とハッシュ化後の文字列の
汎用的なマッピング表を作ることができるからです。
しかしソルトの場合は、ランダム文字列がユーザ毎に違うので、ランダム文字列ごとにマッピング表を作らないといけません。
例えば、ユーザ1のランダム文字列が「aaaaaaaa」、ユーザ2のランダム文字列が「bbbbbbbb」だとすると、一般的なハッシュのみのマッピング表は使えないので
ユーザ1のPWを解析するためだけに、「aaaaaaaa」のマッピング表を作らないといけません。
また、それはユーザ2などの他のユーザには使えないので、1つ1つユーザパスワードの解析にとんでもない時間がかかります。
ですので、DBの値を盗まれても短時間にすべてのユーザのパスワードを解析するのは困難になります。
<Bcryptの疑問点>
ここまでお読みくださった方は少し「あれっ?」と思いませんでしたか?
私も調べきれていませんが、基本的には時間稼ぎにしかならないということが言えるのではないかと思います。
原理的には、例えば使用できるパスワード文字列が英大文字、英小文字、数字、記号の4種類で8文字の場合は74種類の文字があり、
全ての8文字の組合せを解析することを考えると、
仮に1秒間に10億個のハッシュを作れるとすると、8日以上の解析時間、10回のストレッチをする場合は80日以上の解析時間がかかることになります。
ただし、パスワードに使う文字列は偏りがあり、よく使う文字列をパスワードに設定していた場合、解析する文字列を絞れるのでそんなに時間はかかりません。
つまり、時間稼ぎにしかならず、時間稼ぎをしている間に「パスワードを変更してください」と案内することを考えるべきかなという気がします。
しかし、Bcryptなどを使用しないと、時間稼ぎする時間もありません。
DBの値が漏れてからすぐに気づくとは限らないので、時間稼ぎできる時間があることが重要だと思います。
参考:パスワードのハッシュ化の一般的なデメリット
念のため、パスワードをハッシュ化(一方向暗号化)することのデメリットも記述しておきます。
<導入の一般的なデメリット>
①秘密の質問に答えたらメールでPWを送る、などができない。ハッシュ化したPWは復号化できないので。
②将来古くなったハッシュの方式を変更したいとき、生の値が分からないので新しい方式に変更できない。
仮にログイン時に入力のPWを使用して新方式のハッシュに変更する場合でも、DB内で新旧の方式が混在する問題がある。
<デメリットに対する対策>
①PWを忘れた場合は、初期化する方式にする。
②Spring Securityでは対策がある。DelegatingPasswordEncoderを使用する。
⇒上記のPasswordEncoderFactoriesの設定例の説明を参照
最後に
長くなってしまいましたが、このお話は重要かと思っています。
DBのPW文字列が漏えいした時の課題は以下かと思います。
・自サイトのログインができてしまう
・多サイトのPWも同じにしていた場合、そちらのサイトもログインできる確率が上がる。
「PWはサイト毎に別々にしなさい」とはよく言われることで、別々にすれば例えDBのPWが漏えいしても、少なくとも他のサイトまでログインされる危険性は低いです。
そういう意味では、少なくとも他のサイトのログインまで可能になってしまうお話は、PWを別々にしていない個人の責任かもしれません。
しかし、WEBサイトがたくさん存在し、使用せざるを得ない現代ではその考えは時代に即しません。
つまり、上記の対策は、ユーザ個人の問題だけではなく、システム設計者の責任になりつつあるように感じます。
ですので是非ともPasswordEncoderか、同等の対策を検討されることをお勧めいたします。
Created Date: 2018/08/18