deviseの暗号化アルゴリズムの変更方法

認証を、Rails組込みのhas_secure_passwordからdeviseに変更した時に、なにか変更があるかと思ったが、 どちらもbcryptを使っているのでそのまま移行できた(bcryptはダイジェストにストレッチの情報を含むため)。 ただ、せっかく調べたのでメモとして残しておく。

deviseの暗号化処理を変更する

暗号化処理を変更する対象はなんでも良いが、bcryptを使って実装してみる。(実際、deviseはもともとbcryptを使って実装してあるのでbcryptで再実装する意味は無い。)
また、SHA1等の処理は用意されているためこちらを参考。

まず、devise-encryptable のgemをインストールする。

READMEの通りにする。

# deviseの引数に :encryptable を追加する。
class User < ActiveRecord::Base
  devise :database_authenticatable, :encryptable
end
# password_saltカラムを追加するため、マイグレーションファイルを作成
class DeviseCreateUsers < ActiveRecord::Migration
  def change
    add_column :users, :password_salt, :string
  end
end

rake db:migrate を実行する。

devise-encryptableをカスタマイズする

devise-encryptableで定義しているメソッドを再定義する。
まず、暗号化の処理をdigestメソッドに書く。このメソッドは暗号化後の文字列を返す必要がある。また、引数については以下のようになっている。

  • password : 入力されたパスワード
  • stretches : stretchesに設定された値、設定するにはdeviseをインクルードしたクラスにdevise :database_authenticatable, stretches: 20 などとして設定する。stretchesには通常、ハッシュ関数をパスワードに適用する回数が設定される。 (bcryptではcostとなっているが、2のcost乗の回数分だけハッシュ関数を適用しているだけなのでstretchesをcostに設定すれば良い。)
  • salt : 暗号の強度を増すためにパスワードに付加する文字列。
  • pepper : saltと同じ役割

例としてbcryptを使った物を書いておく。

# config/initializers/devise_encryptor.rb
module Devise
  module Encryptable
    module Encryptors
      class PasswordAuthentification < Base
        def self.digest(password, stretches, salt, pepper)
          # has_secure_passwordからそのまま持ってきただけなのでpasswordしか使っていない...
          cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
          BCrypt::Password.create(password, cost: cost)
        end
      end
    end
  end
end

また、サインインの時にはself.compareが呼ばれる。このメソッドはdevise-encryptableで以下のように定義されている。

def self.compare(encrypted_password, password, stretches, salt, pepper)
  Devise.secure_compare(encrypted_password, digest(password, stretches, salt, pepper))
end

Devise.secure_compareは2つの引数(文字列)が等しい場合にtrueを返す。 SHA1等の場合はdigestメソッドの戻り値とencrypted_passwordが等しくなるのでこれで問題ないが、 今回使うbcryptは暗号化時にランダムな文字列を内部で生成し、それをソルトとして使うので暗号化処理行う度に暗号化後の文字列が変わる。 そのため既存のcompareでは比較ができないため、compareも再定義する。

# config/initializers/devise_encryptor.rb
module Devise
  module Encryptable
    module Encryptors
      class PasswordAuthentification < Base
        def self.digest(password, stretches, salt, pepper)
          cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
          BCrypt::Password.create(password, cost: cost)
        end

        def self.compare(encrypted_password, password, stretches, salt, pepper)
          # BCrypt::Passwordでは 「==」メソッドが再定義されているので、これでパスワードの確認ができる
          BCrypt::Password.new(encrypted_password) == password
        end
      end
    end
  end
end

最後に、config/initializers/devise.rb について

  # config.encryptor = :sha512

となっている部分を変更する。

config.encryptor = :password_authentification

bcryptのメモ

bcryptにはcostを設定できるが、有効な値は4〜30までで、2のcost乗だけハッシュ関数の適用が繰り返される。
なお、costを4未満に設定すると強制的に4が設定され、costを31以上に設定すると例外が発生する。