はじめに
deviseは認証機能を簡単に実装できるgemで、railsで認証機能を実装しようと思ったら、大体の人が選ぶgemじゃないでしょうか。
deviseではBCryptというハッシュ化のアルゴリズムが使われており、deviseをインストールしたら、bcryptというgemもインストールされます。
今回はdeviseを扱う上で知っておきたいBCryptの知識についてまとめようと思います。
軽く本当に軽くハッシュ化のロジックも載せてます。結論だけ見るでも役に立つようにしてますので、
その際は最後の方だけでも見て頂けると嬉しいです。
BCryptのロジック
簡単にBCryptの動作についてまとめときます。
BCryptの目的
BCryptの目的はパスワード平文をハッシュ値に変換することです。
パスワードをわざわざハッシュ値に変換することで、パスワードを平文でDBに保存しなくていいので、セキュリティ的に安心です。
また、ハッシュ値から平文を算出することは簡単にはできないようになってます。
なので、ログインする際は、平文同士が等しいか比較するのではなく、ハッシュ値同士が等しいかを比較することになります。
ハッシュ化の方法
BCryptがハッシュ値を生成する上で、必要となるものは、solt
とperpper
とstretch
です。solt
とpepper
は文字列で、stretch
は何回ハッシュ化を繰り返すかを決定する数字です。
config/initializers/devise.rbにconfig.stretches
やconfig.pepper
という項目があり、設定できることがわかるかと思います。solt
はランダム生成される文字列なので、設定不可です。
覚えておいた方が良いのはstretch
とsolt
です。
stretchについて
stretch
はハッシュ化を繰り返す回数を決定するための項目です。
ハッシュ化を繰り返す回数は2のstretch
乗で求められます。デフォルトは12なので、2^12 = 4096回ハッシュ化が行われます。
stretchが大きくなるほどセキュリティが高まる代わりに、演算に時間がかかるようになると認識していただければひとまずOKです。
stretchはconfigで変更できますが、いじる必要もないかと思います。
ただ、ハッシュ化にはある程度時間がかかることは覚えておいてください。
僕はこれを知らずにユーザー一覧取得APIでBCryptのハッシュ生成メソッドを呼び出していて、不具合を出した経験があります。
数秒遅くなるとかではなく、nginxのタイムアウト設定を大きくはみ出す遅さになるので要注意です。
soltについて
solt
はランダムな文字列です。ハッシュを生成する際に、ランダムな文字列を材料としているということは、同じパスワードでもハッシュ値は生成するたびに異なるということになります。
例えばdeviseではpassword_digest
としてハッシュ値を保存しています。
とあるユーザーがパスワードを"password"
と設定しているかどうかを調べる時に、以下のように記述するのは間違いです。
# 間違ってる例
user.password_digest == BCrypt::Password.create("password")
BCrypt::Password#create
はパスワードをハッシュ化してくれるメソッドです。
この時、password_digest
が仮に"password"
のハッシュ値だったとしても、solt
によって、ランダム要素が加えられ、左辺と右辺は別々の文字列になります。
僕が参画していたプロジェクトでは、初期パスワードは自動生成していました。ユーザーが初期パスワードからパスワードを変更しているかの判定をする際に上記のように記述し、判定がうまくいってないケースが実際にありました。
こういう場合、以下のように記述すればOKです。
# 正しい例
BCrypt::Password.create("password") == user.password_digest
右辺と左辺を入れ替えただけです。
間違いの例では、String
クラスの==
メソッドが呼ばれており、正解の例ではBCrypt::Password
クラスの==
メソッドが呼ばれます。
rubyは==を演算子としてではなく、メソッドとして各クラスに定義しているので、少しややこしいですね。
パスワードのハッシュ値を比較する際は、BCrypt::Password#==
を利用することで、solt
とかをよしなに考慮してくれるくらいの理解があれば困ることはないです。
コード
よく使うメソッドをコードベースで紹介します。
Create
BCryptを用いて文字列をハッシュ化するメソッド。
戻り値はBCrypt::Password
クラスです。
BCrypt::Password.create("password")
=> "$2a$12$vzFjfYzMCBkiy6pfYd0Aker4Xhx.Icr2Z6z3JhLChBCaT7Xt9xf7."
BCrypt::Password.create("password").class
=> BCrypt::Password
New
ハッシュ値を元にBCrypt::Password
クラスを生成するメソッド。
BCrypt::Password.new("$2a$12$vzFjfYzMCBkiy6pfYd0Aker4Xhx.Icr2Z6z3JhLChBCaT7Xt9xf7.")
=> "$2a$12$vzFjfYzMCBkiy6pfYd0Aker4Xhx.Icr2Z6z3JhLChBCaT7Xt9xf7."
BCrypt::Password.new("$2a$12$vzFjfYzMCBkiy6pfYd0Aker4Xhx.Icr2Z6z3JhLChBCaT7Xt9xf7.").class
=> BCrypt::Password
==
ハッシュ値同士や、ハッシュ値と平文を比較するメソッド。
生成元の文字列が同じでもハッシュ値は生成の度に異なるので、単純な文字列比較ではないことに注意。
hash = BCrypt::Password.create("password")
hash == "password"
=> true
正常に比較できる組み合わせは以下
( hash
はBCrypt::Password
クラスのインスタンスで、str
はhash
の元となった平文 )
左辺 | 右辺 | 判定 | メソッド | ||
---|---|---|---|---|---|
hash | == | str | => | True | BCrypt::Password#== |
hash | == | hash | => | False | BCrypt::Password#== |
hash | == | hash.to_s | => | False | BCrypt::Password#== |
hash.to_s | == | hash | => | True | String#== |
hash.to_s | == | hash.to_s | => | True | String#== |
hash = BCrypt::Password.create("password")
hash.to_s
=> "$2a$12$76aUsAYArn91T0DrYZA9wO7HPh6LdFYa5ChPhH2iTnusgPwxfU.Ii"
パスワードを比較する方法は少しややこしいので、実装の際は確認することをお勧めします!