Balancing CPU cost when using BCrypt

Manfred Stienstra

You need to take care when setting the cost parameter for BCrypt. Brute-force cracking of passwords becomes harder when the cost parameter is higher. Unfortunately it also makes checking passwords more expensive.

When choosing a cost parameter for your code, you need to realize how it impacts performance. This is directly related to how often you verify the password. Broadly speaking there are two situations:

Password is only checked when creating the authenticated session

A lot of authentication systems give you an authentication token in exchange for valid authentication credentials. The token is either stored in a cookie for further requests or it's returned for use in API access.

POST /sessions.json?username=jenny&password=secret

Set-Cookie: token=34ui87er98vb
An extremely simplified request with response headers

In this situation you can use a relatively high cost because the password only needs to be checked during authentication and possibly when changing the password. This is likely to only happen once every 100 requests.

Password is checked with every request

Some authentication systems need the user's credentials with every request (i.e. HTTP Basic Authentication). In this case the password is verified in every request. You will need to determine how much of a performance penalty you're willing to take with every request.

A workaround could be to cache the password value to speed up this check.

class Account
  class PasswordCache
    class << self
      attr_accessor :cache
    end
    self.cache = {}

    def self.find(params, account)
      cache[[params[:username], params[:password]]]
    end

    def self.set(params, account)
      cache[[params[:username], params[:password]]] = account
    end
  end

  def self.authenticate_with_crypt(params={})
      if account = find_by_username(params[:username]) and
         account.has_password?(params[:password])
        account
      end
    end

  def self.authenticate(params={})
    if account = PasswordCache.find(params)
      account
    else
      if account = authenticate_with_crypt(params)
        PasswordCache.set(params, account)
        account
      end
    end
  end
end
Untested proof of concept password cache

Benchmark!

Whatever you come up with, always benchmark your solution. You should start by getting an idea about how long it takes to create a crypted password on your production server. Add that to the current response time for an authentication request and figure out how far you're willing to go.

Benchmark.measure { BCrypt::Password.create('secret', :cost => 10) }
  #=> 0.252698 seconds
Benchmark.measure { BCrypt::Password.create('secret', :cost => 7) }
  #=> 0.032790 seconds
Benchmark.measure { BCrypt::Password.create('secret', :cost => 5) }
  #=> 0.008705 seconds
Benchmark.measure { BCrypt::Password.create('secret', :cost => 1) }
  #=> 0.005096 seconds

In this example 10 is probably a bit too high and 5 is too low. 7 is around 32 milliseconds, a penalty I'm willing to take when creating a session.

Slowing down test suites

Using a high cost parameter can be acceptable for your super-duper fast production servers, but it can definitely slow down your test suite. A solution is to configure the cost differently for various environments: 7 for production, 1 for development and test.

Some pitfalls

Note that the cost needs to be configured globally because you use the same cost parameter when creating and verifying the crypted password.

Computers become faster so to stay safe you will need to set higher costs in the future. To ease this transition you can decide to store the cost with the password so you can change the default in the future without breaking older crypted passwords.