How To: Create a custom encryptor

Yoon Wai Yan edited this page Jul 13, 2015 · 15 revisions

When working with a legacy database where a non-supported password encryption method is used, you can write your own custom encryptor.

Devise version 2.1 and up

You have to add the devise-encryptable in addition to the devise gem.

# Gemfile
gem 'devise-encryptable'

Implement your own encryptor.

# config/initializers/md5.rb
require 'digest/md5'

module Devise
  module Encryptable
    module Encryptors
      class Md5 < Base
        def self.digest(password, stretches, salt, pepper)
          str = [password, salt].flatten.compact.join
          Digest::MD5.hexdigest(str)
        end
      end
    end
  end
end

Devise versions prior to 2.1

Implement your own encryptor.

# lib/devise/encryptors/md5.rb
require 'digest/md5'

module Devise
  module Encryptors
    class Md5 < Base
      def self.digest(password, stretches, salt, pepper)
        str = [password, salt].flatten.compact.join
        Digest::MD5.hexdigest(str)
      end
    end
  end
end

You can then set this as your encryptor in config/initializers/devise.rb:

config.encryptor = :md5

Don’t forget to enable the :encryptable in your User model.

Also, you should make sure that the new file is loaded, for instance by adding this to your users' class:

require Rails.root.join('lib', 'devise', 'encryptors', 'md5')

See also: http://www.markrichman.com/2010/11/22/rails-devise-datamapper-authentication/

Problems?

Additionally, I had to add the following to my user model in order to be able to log in. Supposedly there is a more elegant way, but I did not get devise_for … :encryptor => :md5 to work, nor devise :encryptor => :md5. Maybe this is because I do not have a password salt.

Try adding :encryptable to your devise statement as well. devise :encryptable, :encryptor => :md5.

# app/models/user.rb 
# debt: we should not need to do this, but seems like setting :encryptor => :md5 on the devise or devise_for
# does not do anything. Maybe because we don't use a salt. So this works for now.
def valid_password?(password)
  return false if encrypted_password.blank?
  Devise.secure_compare(Devise::Encryptable::Encryptors::Md5.digest(password, nil, nil, nil), self.encrypted_password)
end

And how I tested it.

# spec/models/user_spec.rb
describe "devise valid_password?" do
  it "should use our hashing mechanism, not the default bcrypt" do
    Factory(:member).valid_password?('blahblah').should be_true
  end
end

Workaround for empty salt

Encryptable expects a password_salt attribute in the model, else it won't even call the custom encryptor or even cause exceptions:

# app/models/user.rb
def password_salt
  'no salt'
end

def password_salt=(new_salt)
end

This advice of 'working around' the absence of a salt is not sound. Not only is MD5 no longer safe http://www.kb.cert.org/vuls/id/836068 (SHA2 > 256 key length is recommended see http://en.wikipedia.org/wiki/SHA-2) but even then hashes without a salt are open to rainbow table (brute force) attacks. Don't become another LinkedIn, use a strong hashing function with a salt.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.