This is Anti-pattern—thoughts on programming and whatnot by Brandon Weiss.

Tales from the BCrypt

Update: About three weeks after writing this bcrypt-ruby was finally updated to support externally setting the cost!

# test/test_helper.rb
BCrypt::Engine.cost = BCrypt::Engine::MIN_COST

BCrypt is the gold standard for password hashing. It’s different from the algorithms that came before it in that it’s designed to be slow. Slow is not generally considered to be a feature of good algorithms, but as computing power increases, brute-forcing a hashing algorithm takes less time, so making the algorithm slower makes the hashes it generates more secure. A slower algorithm will impact normal application usage very little—like users signing up or signing in—but exponentially increases the time it takes a password to be brute-forced. BCrypt is the easiest and most secure way to hash a password.

One often-overlooked side-effect of using BCrypt is that its design inherently makes your test suite slow. Your test suite probably creates a lot of users, authenticates with your app, and exercises the parts of your code using BCrypt a fair amount. All those extra milliseconds add up.

Fortunately, BCrypt allows you to lower the cost (time) of hashing a password. The default cost is 10, but you can lower it as low as 4. The easiest way to do this and what everyone recommends is to override the BCrypt::Engine::DEFAULT_COST constant in your test helper.

# test/test_helper.rb
BCrypt::Engine::DEFAULT_COST = 4

BCrypt also has a BCrypt::Engine::MIN_COST constant that can be used instead of hardcoding the number.

# test/test_helper.rb
BCrypt::Engine::DEFAULT_COST = BCrypt::Engine::MIN_COST

Lowering the cost makes my tests run about 50% faster. Your mileage may vary, but you should see a marked improvement.

However, redefining constants is understandably a no-no and Ruby will complain.

/Users/brandon/Code/canary/api/test/test_helper.rb:8: warning: already initialized constant DEFAULT_COST

BCrypt allows you to specify the cost when hashing a password, so the fix is to define your own default cost within your application, set it to the value you want or let it fall back to BCrypt’s default cost, and then pass the cost in as an option when hashing the password.

# app/models/user.rb
require "bcrypt"

class User

  include BCrypt

  def password=(new_password)
    @password_hash = Password.create(new_password, cost: self.class.bcrypt_cost)
  end

  def self.bcrypt_cost
    @bcrypt_cost || BCrypt::Engine::DEFAULT_COST
  end

  def self.bcrypt_cost=(cost)
    @bcrypt_cost = cost
  end

end

# test/test_helper.rb
User.bcrypt_cost = BCrypt::Engine::MIN_COST
Avatar
Brandon Weiss
Programmer at Domus
Get new posts in your inbox
I post a few times a year about programming and whatnot. No spam.