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?
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
'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
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
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
'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
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.
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.
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
#createmethods produce irb output that looks like a string:
inspectmethods from the
initializemethod uses the mutating
self.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. ↩