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

# devise/bcrypt/encrypted_password
以下のバージョンで確認
devise v4.2.1
bcrypt-ruby v3.1.6
# encrypted_passwordの生成
## deviseからbcryptの呼び出し
https://github.com/plataformatec/devise/blob/v4.2.1/lib/devise/encryptor.rb#L5-L10

```
    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側の処理
https://github.com/codahale/bcrypt-ruby/blob/v3.1.6/lib/bcrypt/password.rb#L43-L47

```
      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"
```

## ハッシュ値の中身
https://github.com/codahale/bcrypt-ruby/blob/v3.1.6/lib/bcrypt/password.rb#L83

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