ruby-on-railsrubybcrypt-ruby

has_secure_password - only hashes or encrypts as well?


I'm not entirely sure if, when you add has_secure_password in a Rails model, there's any encryption involved. I know there's definitely hashing with a salt, but is there encrypting? bcrypt can use blowfish, but is it being used in bcrypt-ruby (the gem behind all of this)?


Solution

  • TL;DR : has_secure_password will make you use Bcrypt's hash function when using the self.password= method.


    Let's look at the code of has_secure_password :

        # File activemodel/lib/active_model/secure_password.rb, line 53
        def has_secure_password(options = {})
          # Load bcrypt gem only when has_secure_password is used.
          # This is to avoid ActiveModel (and by extension the entire framework)
          # being dependent on a binary library.
          begin
            require "bcrypt"
          rescue LoadError
            $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
            raise
          end
    
          include InstanceMethodsOnActivation
    
          if options.fetch(:validations, true)
            include ActiveModel::Validations
    
            # This ensures the model has a password by checking whether the password_digest
            # is present, so that this works with both new and existing records. However,
            # when there is an error, the message is added to the password attribute instead
            # so that the error message will make sense to the end-user.
            validate do |record|
              record.errors.add(:password, :blank) unless record.password_digest.present?
            end
    
            validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
            validates_confirmation_of :password, allow_blank: true
          end
        end
    

    We can see that it does NOT hash/encrypt anything. Nevertheless, we notice:

          include InstanceMethodsOnActivation
    

    If we go on the documentation of InstanceMethodsOnActivation we get the following code :

    def password=(unencrypted_password)
      if unencrypted_password.nil?
        self.password_digest = nil
      elsif !unencrypted_password.empty?
        @password = unencrypted_password
        cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
        self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
      end
    end
    

    Therefore, has_secure_password does not encrypt/hash anything BUT includes InstanceMethodsOnActivation module. This module defines the password= method. The important part of this method is:

    self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
    

    Let's now go see BCrypt::Password.create 's code:

      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
    
      def valid_hash?(h)
        h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/
      end
    end
    

    In this method, we notice in particular:

    Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)))
    

    So it seems to be a hash, which is logical (you don't want to decrypt a password anyway).