LoginSignup
60
44

More than 5 years have passed since last update.

deviseで使用しているbcryptによるencrypted_passwordの挙動

Last updated at Posted at 2017-04-27

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から取得するので特に気にする必要は無い様な気はする。

60
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
44