A Closer Look at the BCrypt Gem
The standard way of avoiding storing user passwords in plaintext is password hashing: before storing a new password, we run it through a one-way function and then store the result, rather than the original password. Then, when we want to authenticate a user who is attempting to log in, we perform the following three steps:
- Retrieve stored hashed password for authenticating user
- Take the password the user who is attempting to log in entered, and run it through the hashing function
- Check whether the results of 1 and 2 are the same
Because a hashing function always returns the same output when given the same input,1 this will work. And because there is no way to compute the original password from the hash, this is secure.
A BCrypt Mystery
That’s the theory. If you’re a Rubyist, BCrypt is the implementation.2 But the documentation provides code snippets that seem to flip the theory on its head. Say pete69
registers for your site using the password password
. The first step is to convert that password to a hash:
BCrypt::Password.create('password')
# => "$2a$10$PAsLS8PqIZAeQ3r1pNTIIejtRkLh1w/6mkkU0ZL4NAeolBsFIiG5y"
We can then store that in our users database.
Later, suppose someone tries to log in as pete69
and enters the password password
. To verify this password is correct, you might expect to write this:
BCrypt::Password.create('password') == stored_hash
In fact, however, the documentation suggests we do exactly the opposite:
BCrypt::Password.new(stored_hash) == 'password'
Here it looks like we’re passing the hash through some kind of function and checking whether the result equals the entered password. And this makes it look like BCrypt::Password.new
can convert a hashed password back to plain text – violating the supposed one-way feature of a hashing function! What’s going on?
Digging Deeper
We can figure this out if we keep our wits about us, and remember our Ruby fundamentals. First, let’s see what happens if we do what we originally thought we’d do:
BCrypt::Password.create('password') == stored_hash # => false
What the heck? Didn’t we say at the outset that a hashing function will always return the same output for a given input? We did, but we also had a footnote noting that BCrypt adds a random salt to the password before running it through the hashing algorithm (which you can verify by checking the source code – it’s very readable). So each time we call create
with 'password'
as an argument, we’re going to get a different result.
So that’s why that doesn’t work. Let’s return to what the documentation tells us to do. We can get our feet wet by trying the left-hand side of that equality in irb:
BCrypt::Password.new(stored_hash)
# => "$2a$10$PAsLS8PqIZAeQ3r1pNTIIejtRkLh1w/6mkkU0ZL4NAeolBsFIiG5y"
That’s weird on two fronts: we seem to have gotten back the very hash string we put in, and it sure doesn’t look like that string double-equals 'password'
!
Let’s back up for a minute. That line of code is calling the new
class method on the BCrypt::Password
class. Clearly, then, that method call is going to return a new instance of the Password
class defined in the BCrypt
module.
Now remember that in Ruby, ==
is just a regular instance method. The default implentation is Object#==
, from which all objects inherit, and it returns true only if the receiver is numerically the same object as the argument. Our BCrypt::Password
instance is plainly not the same object as the String
instance 'password'
, and yet the call returns true
. We can infer that BCrypt::Password
must define its own ==
implementation that overrides the default. And if we look at the source, we’ll see that it does:
def ==(secret)
super(BCrypt::Engine.hash_secret(secret, @salt))
end
Not only does this confirm that the Password
class implements its own version of ==
, it gets to the heart of our mystery: apprently, this method converts its argument, a plaintext password, to a hash using the BCrypt::Engine
’s class method hash_secret
. And note that we’re passing the @salt
instance variable to ensure that we hash the entered password using the same salt we used when we originally hashed the password. (Because the salt used to create a hash is stored as a string in the hash itself, when we instantiate a new Password
instance from a hash string, the Password
class extracts the salt and assigns it to @salt
.)
There’s one more piece to the story. The call to super
here means we’re passing the resulting hashed password to the parent class’s ==
method. Scrolling up in the soure, we see that BCrypt::Password
actually subclasses our old friend String
!3 So we’re just calling String#==
on the string representation of our BCrypt::Password
instance, and passing in the hash string we got back from running the password through the hashing function.4
So, to recap, when we call ==
on a BCrypt::Password
instance with a string argument, the ==
method will salt that string and convert it to a hash behind the scenes, and then compare the result to the BCrypt::Password
instance using String#==
. And this, of course, conforms perfectly to our 3-step process above for authenticating users using hashed passwords.
Why?
You might be wondering why BCrypt
is set up this way. And it mostly has to do with the salt. If we want to hash a candidate password in the same way we did the original, we’ll need to use the same salt. While we could extract this manually from the stored hash string – the salt is just the first 29 characters of the hash string – and then call BCrypt::Engine#hash_secret
directly, this would be annoying and error-prone. Knowing what it’s salt is is the Password
object’s responsibility. And since it does know what it’s salt is, and how to generate a hash using that salt, we might as well let that object tell us whether a plain text password is the same as the hash that it represents.
In the end, this is really convenient – using BCrypt, you get the benefits of salting without ever having to think about salts.
Wrapping Up
So we’ve solved solved our mystery. Like any solution to hashing, BCrypt runs the candidate password through the same hashing function used to hash the user’s original password (including the same salt), and then compares the result to the stored hash. Through some clever use of Ruby’s expressive object model, we can verify a password with a deceptively simple line of code:
BCrypt::Password.new(stored_hash) == entered_password
-
Some hashing libraries add a salt to your password before executing the actual hashing algorithm. BCrypt does this, so the same password does not always generate the same hash using
BCrypt::Password#create
. But if you pass the same password and salt to the bcrypt algorith, you will always get the same output. ↩ -
The BCrypt gem uses the bcrypt hashing algorithm. This algorithm is intentionally slow, to make it difficult to use brute force methods to crack hashed password. See here for an excellent overview, and here for discussion of how astoundingly fast modern brute force attacks can be. ↩
-
This explains why
BCrypt::Password
’s#new
and#create
methods produce irb output that looks like a string:BCrypt::Password
is inheritingto_s
andinspect
methods from theString
class. ↩ -
Cleverly, the
Password
class’sinitialize
method uses the mutatingself.replace(hash_argument)
to ensure that the “value” of the new instance is the hash string argument its invoked with. This explains a bit more clearly why its==
method gives the results that it does. ↩