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:

  1. Retrieve stored hashed password for authenticating user
  2. Take the password the user who is attempting to log in entered, and run it through the hashing function
  3. 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
  1. 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. 

  2. 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. 

  3. This explains why BCrypt::Password’s #new and #create methods produce irb output that looks like a string: BCrypt::Password is inheriting to_s and inspect methods from the String class. 

  4. Cleverly, the Password class’s initialize method 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.