【Rails】deviseで応用するために知っておきたいBCrypt基礎

バックエンド

はじめに

deviseは認証機能を簡単に実装できるgemで、railsで認証機能を実装しようと思ったら、大体の人が選ぶgemじゃないでしょうか。
deviseではBCryptというハッシュ化のアルゴリズムが使われており、deviseをインストールしたら、bcryptというgemもインストールされます。

今回はdeviseを扱う上で知っておきたいBCryptの知識についてまとめようと思います。
軽く本当に軽くハッシュ化のロジックも載せてます。結論だけ見るでも役に立つようにしてますので、
その際は最後の方だけでも見て頂けると嬉しいです。

BCryptのロジック

簡単にBCryptの動作についてまとめときます。

BCryptの目的

BCryptの目的はパスワード平文をハッシュ値に変換することです。
パスワードをわざわざハッシュ値に変換することで、パスワードを平文でDBに保存しなくていいので、セキュリティ的に安心です。
また、ハッシュ値から平文を算出することは簡単にはできないようになってます。
なので、ログインする際は、平文同士が等しいか比較するのではなく、ハッシュ値同士が等しいかを比較することになります。

ハッシュ化の方法

BCryptがハッシュ値を生成する上で、必要となるものは、soltperpperstretchです。
soltpepperは文字列で、stretchは何回ハッシュ化を繰り返すかを決定する数字です。

config/initializers/devise.rbにconfig.stretchesconfig.pepperという項目があり、設定できることがわかるかと思います。
soltはランダム生成される文字列なので、設定不可です。

覚えておいた方が良いのはstretchsoltです。

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

正常に比較できる組み合わせは以下
( hashBCrypt::Passwordクラスのインスタンスで、strhashの元となった平文 )

左辺 右辺判定メソッド
hash== str=>TrueBCrypt::Password#==
hash== hash=>FalseBCrypt::Password#==
hash== hash.to_s=>FalseBCrypt::Password#==
hash.to_s== hash=>TrueString#==
hash.to_s== hash.to_s=>TrueString#==
比較可能な組み合わせ
hash = BCrypt::Password.create("password")
hash.to_s
=> "$2a$12$76aUsAYArn91T0DrYZA9wO7HPh6LdFYa5ChPhH2iTnusgPwxfU.Ii"

パスワードを比較する方法は少しややこしいので、実装の際は確認することをお勧めします!

タイトルとURLをコピーしました