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