こんにちは!kossyです!
さて、今回は認証機能を提供するRubyのGemである「devise」を使っている際に、
パスワードが正しいか未定義かを確認する方法と、valid_password?メソッドのコードを読んでみたので、
ブログに残してみたいと思います。
deviseを使って認証機能を提供しているシステムの場合、
CSチームからこんな問い合わせが来たりしませんか?
「そのお客さん、このパスワードで設定したはずなんですけど、ログインできなくて、、、
パスワードがちゃんと設定されてるか試してもらえませんか?」
deviseを普通に使っていればDBにはハッシュ化されたパスワードが保存されるため、
DBを見てもパスワードはわかりません。
しかし、「このパスワード」が正しいかどうかは判定することができます。
valid_password?メソッドを使えば判定ができます。
# パスワードが正しい時 valid_password?("test1234") => true # パスワードが間違っている時 valid_password?("test4321") => false # パスワードが未定義の時 valid_password?("test4321") => nil
パスワードが正しければ true が、間違っていれば false が、パスワードが設定されていない(レアケースですが、、、)場合は、 nil が返却されます。
これだけで終わりにするには味気ないので、ソースコードを追ってみましょう。
Devise::Encryptor.compare
valid_password?メソッドの定義はこちらです。
# Verifies whether a password (ie from sign in) is the user password. def valid_password?(password) Devise::Encryptor.compare(self.class, encrypted_password, password) end
まずはDevise::Encryptor.compareメソッドを追ってみます。
Devise::Encryptor.compareメソッドの定義元はこちら
def self.compare(klass, hashed_password, password) return false if hashed_password.blank? bcrypt = ::BCrypt::Password.new(hashed_password) if klass.pepper.present? password = "#{password}#{klass.pepper}" end password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt) Devise.secure_compare(password, hashed_password) end end
早期リターン文は説明不要ですかね。
BCrypt::Passwordクラスの中身を見てみましょう。
と思ったら外部のGemの処理みたいです。
def initialize(raw_hash) if valid_hash?(raw_hash) self.replace(raw_hash) @version, @cost, @salt, @checksum = split_hash(self) else raise Errors::InvalidHash.new("invalid hash") end end
うーんいまいちわからんので動かしてみましょうか。
bcrypt = BCrypt::Password(User.first.encrypted_password) bcrypt.version => "2a" bcrypt.cost => 12 bcrypt.salt => "$2a$12$U5Yc/wGr2vQMBixITdTcxe" bcrypt.checksum => "LHVc35YygLEHakakiOmSOX.1h6zivK6"
この数字の意味するところは
のブログが詳しかったです。
要はBCrypt::Passwordのinitializeで渡されたパスワードをBCrypt暗号化しているようです。
klass.pepper
次はklass.pepper.present?のコードです。
if klass.pepper.present? password = "#{password}#{klass.pepper}" end
klass.pepperが存在していれば、引数で渡されたpaswordにklass.pepperを結合させて新たなpasswordとしています。
pepperとはなんでしょうか。
平文パスワードにあらかじめ設定されている文字列(pepper)をくっつけ、結合された文字列をハッシュ化する。
さらに、ランダムに生成されたハッシュ(salt)を追加して、encrypted_passwordとする。
平文パスワードに塩こしょうをして暗号化するという洒落。
とのことで、特に設定を行っていないとpepperはかかりません。
残りのコードを読んでみます。
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt) Devise.secure_compare(password, hashed_password)
BCrypt::Engine.hash_secretはパスワードのハッシュ化を行っているコードですね。
Devise.secure_compareはちゃんと中身みてみましょう。
# constant-time comparison algorithm to prevent timing attacks def self.secure_compare(a, b) return false if a.blank? || b.blank? || a.bytesize != b.bytesize l = a.unpack "C#{a.bytesize}" res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end
ゴリゴリにRubyのメソッドが使われてますね、、、
「タイミング攻撃を防ぐための一定時間比較アルゴリズム」とコメントアウトがあるので、
セキュアに比較を行うために必要な処理なのだと思います。
deviseの中身は追うのがとても大変ですね、、、見てもいまいちわからんし。
とはいえ、ブラックボックスのままツールを使い続けるのもあまりよろしくないので、
時間を見つけて理解を進めたいなと思います。
勉強になりました。
参考にさせていただいたサイト
この場を借りてお礼を申し上げます。