devise/bcrypt/encrypted_password
以下のバージョンで確認
devise v4.2.1
bcrypt-ruby v3.1.6
encrypted_passwordの生成
deviseからbcryptの呼び出し
def self.digest(klass, password)
if klass.pepper.present?
password = "#{password}#{klass.pepper}"
end
::BCrypt::Password.create(password, cost: klass.stretches).to_s
end
self.digestに平文パスワードを渡して、暗号化されたパスワードを生成している。
ソルトアンドペッパー (salt & pepper)
平文パスワードにあらかじめ設定されている文字列(pepper)をくっつけ、結合された文字列をハッシュ化する。
さらに、ランダムに生成されたハッシュ(salt)を追加して、encrypted_passwordとする。
平文パスワードに塩こしょうをして暗号化するという洒落。
https://github.com/plataformatec/devise/blob/v4.2.1/README.md#configuring-models
READMEによると、deviseを利用しているmodelにpepperを設定出来る様になっている。特に設定がなければ、pepperは付加されない。
bcrypt側の処理
def create(secret, options = {})
cost = options[:cost] || BCrypt::Engine.cost
raise ArgumentError if cost > 31
Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)))
end
costは4~31で設定可能。ハッシュ化処理の繰り返し回数で、数字が高い方が暗号強度は高いが、計算リソースを食う。デフォルトは10。
saltを何度か生成すると、その度にランダムに文字列が作成される事が分かる。
pry(main)> BCrypt::Engine.generate_salt(10)
=> "$2a$10$ei3qRUGN7BpBV7Y4MrvMvu"
pry(main)> BCrypt::Engine.generate_salt(10)
=> "$2a$10$gEQOmrnPdAzf2QYAtoq6ye"
pry(main)> BCrypt::Engine.generate_salt(10)
=> "$2a$10$v3KliXylcyE0uHWOy4WvMu"
上の3つのsaltを使って、hash_secretに平文のパスワードを渡してハッシュ化してみる。
3回ともバラバラなハッシュが生成され、0~29文字目まではsaltがそのまま使われている事が分かる。30文字目からはランダムに変化している。
つまり、
ランダムに生成したsalt + 生成されたsaltによって変化するパスワードのハッシュ値
が生成されている、という事になる。
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$ei3qRUGN7BpBV7Y4MrvMvu')
=> "$2a$10$ei3qRUGN7BpBV7Y4MrvMvuk7yDRNVPT/c.ZNQHMXYx47FyJ4sinSO"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$gEQOmrnPdAzf2QYAtoq6ye')
=> "$2a$10$gEQOmrnPdAzf2QYAtoq6yeyh19ReHlj1Ca1kpOWIb4iU150yGDdm2"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
saltが一緒の場合は何度やっても同じハッシュが生成される。
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
当たり前だが、パスワードだけを変えるとsaltは同じで、パスワード部分だけが変化していく。
pry(main)> BCrypt::Engine.hash_secret('PASSWORD0', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMuaC1SYeccEI1vnZeRJa3M.xVZx2tYaCO"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD1', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMugmN.mRm/mPQbFmhzfnu.HN/zZ7zYkt2"
pry(main)> BCrypt::Engine.hash_secret('PASSWORD2', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMuUOgLYcUISiFlWy7809cjZwtwbiHJneC"
ハッシュ値の中身
"$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
$でsplitしている。それぞれ以下の意味を持つ。
2a : バージョン
10 : コスト
v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO : パスワード部分(前述の通り、前半部分はsaltの一部)
encrypted_passwordの比較
パスワードの比較
ログイン時にデータベースに保存されているencrypted_passwordと、ユーザーが入力して来た平文のパスワードの比較をする為に以下のメソッドが定義されている。
https://github.com/plataformatec/devise/blob/v4.2.1/lib/devise/encryptor.rb#L12-L20
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
データベースに保存されているencrypted_passwordのsaltを取得する
https://github.com/codahale/bcrypt-ruby/blob/v3.1.6/lib/bcrypt/password.rb#L27
これは単純に、encrypted_passwordの0~29文字目を取るだけ。
取得したsaltを使って平文パスワードをハッシュ化する
encrypted_passwordの生成部分で書いた様に、saltが同じであればencrypted_passwordは一致するので、これによって比較が可能になっている。
実際に試してみるとこんな感じ。Userはdeviseを利用しているClass。
pry(main)> BCrypt::Engine.hash_secret('PASSWORD', '$2a$10$v3KliXylcyE0uHWOy4WvMu')
=> "$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO"
pry(main)> Devise::Encryptor.compare(User, '$2a$10$v3KliXylcyE0uHWOy4WvMupgumCudZe6wVxSmV/WeqgVWM54HjZmO', 'PASSWORD')
=> true
注意した方がいい点?
encrypted_passwordを決定するポイント
cost, pepper, salt
複数のアプリケーションがユーザーアカウントを共有しているような場合
encrypted_passwordを生成する場所は一カ所にまとめるか、cost, peeperが必ず一致する様に注意しておく必要がある。
saltは保存されているencrypted_passwordから取得するので特に気にする必要は無い様な気はする。