You may have implemented authentication in one of your apps, either directly with Rails’s has_secure_password or indirectly through a gem like Devise. Whichever way, you persisted user passwords in some way.

How are passwords securely stored in a database then? Let’s examine the underlining technology that safely keeps user passwords in a framework like Ruby on Rails.

In a Ruby on Rails console, if you’re using say Devise and you query a user with a password, you’ll see something the looks like gibberish.


The above is the password hash of a user’s plaintext password in the database. This value results from a password-hashing function; it’s how some frameworks securely store users’ passwords in the databases.

The value you see in the preceding screenshot is not an encrypted password. It’s a hash value of a plaintext a user chose as a password.

The Difference Between Encryption And Hashing

With passwords being the sole means of authentication within apps, applications need some means to store passwords to allow users into their accounts. Encryption is a way to store passwords but not secure enough. Hashing is a safer way.

The difference between encryption and hashing is:

  • Encryption is a two-way mathematical function.
  • Hashing is a one-way mathematical function.

With encryption, if one gets a key, they can decrypt the encrypted content. With hashing the value that comes out of a hash function cannot be reversed to reveal the plain text, it’s like converting a cow to steak.

How Password-Hashing Helps With Authentication

The idea of hashing passwords instead of encrypting them makes more sense when you think of it this way: anything man can encrypt… man can decrypt, with the right key. The key has to be stored somewhere on the server-side.

If an attacker gets your database with encrypted passwords, they will try to get hold of the key. If you can somehow get rid of the extra attack vector of storing a key in the first place, you give the attacker a more challenging time figuring out what your users’ passwords are.

The preceding statement is not to say password-hashing is flawless. Discussing the ways attackers can mitigate hash functions is beyond our scope today.

Here’s how hash-functions make authentication possible:

  1. A user (let’s call her Alice), creates an account on your site.
  2. Alice chooses a username and a password.
  3. The server hashes Alice’s password, keeps a plaintext of the ID and stores this in the database.
  4. Alice comes to the site at a later time; she enters her ID and password.
  5. Her login details are securely (over HTTPS hopefully) transferred to your site’s backend.
  6. On receiving the login details, the backend server looks up Alice in the database and passes her password through the same hash function that hashed her password when she signed up.
  7. If the password hash of the password Alice entered matches the one stored in the database, Alice is granted access. Otherwise, the site denies Alice access.

Let’s see how this plays out in code.

bcrypt In Rails

When you spin up a new application with Rails, you might notice that it comes with bcrypt commented out. It’s there for you to create password digests with when you roll out your authentication logic.

To leverage bcrypt, you comment out bcrypt and re-run bundler and add has_secure_password to the ActiveRecord model for which you want to enable authentication.

When you add has_secure_password to your model, you get a method like authenticate which is an alias to authenticate_password that matches up plaintext you send to it to a hash value.

Let the following be the User model:

  class User < ActiveRecord::Base

We can add a migration that adds password_digest string to our database schema.

ActiveRecord::Schema.define(version: 2021_01_30_173626) do
  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "password_digest"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false

With the above in place, we can leverage bcrypt to store users’ passwords. All we need to do now is to create a user with a password like so:

  User.create(name: "Alice", password: "sekreet")

This returns:

bcrypt user hash

Notice how we created the user with name and password arguments, but we got back name and password_digest: this is the magic of has_secure_password storing a plaintext password as a password_digest. We are not storing a password; we’re holding a digest of a password, so it makes sense that we created a string column called a password_digest to keep the hash value in our migration.

A “digest”, “hash value”, “hash code” or just “hash” are all different names to the same thing. I’ll use it interchangeably.

bcrypt user hash

At this point, we can see how the plaintext value of “sekreet” is hashed by running User.last.password_digest.

The value $2a$12$Uu744tDT28y8Zc5.bTxgUetv4oqAKzoihNjnMvendmt5xbNBuarcK here is the result of passing “sekreet” through the bcrypt hashing function. This digest is not reversible. There’s no key to “decrypt” this hash.

In our backend we can check if a user is who they claim to be by asking for their password, hashing them with bcrypt to see if the digest matches the one stored. In other words, we’re checking if the passwords they registered with matches. If it matches we return the user (or whatever model invokes has_secure_password), otherwise Rails returns false.

password authentication

Now that we know what bcrypt does in Rails let’s zoom in a bit and take a closer look at what it is and how it works.

What is bcrypt?

If you read up to this point, you may know that bcrypt is a password-hashing function. Niels Provos and David Mazières designed this function. It incorporates a salt for protection against rainbow tables.

New terms. Let’s define them.

What Are Rainbow Tables?

If hash functions like bcrypt take a cleartext input and produces a hash code, an attacker knows that for a cleartext password like “sekreet” will always return the same hash code. An attacker that knows your backend authentication system uses bcrypt stands to gain a lot if they get hold of the database.

A rainbow table is a list of commonly used passwords and their hash codes that a known hashing algorithm has produced.

If the hash code from bcrypt of “password13” is $2a$12$Uu744tDT28y8Zc5.bTxgUetv4oqAKzoihNjnMvendmt5xbNBuarcK, all an attacker needs to do is to iterate your database and find $2a$12$Uu744tDT28y8Zc5.bTxgUetv4oqAKzoihNjnMvendmt5xbNBuarcK, then the password of the user on that row is “password13”.

What Is A Salt?

To combat rainbow table attacks, hash functions like bcrypt incorporate a salt. A salt is a random data we add to input data to a hash function. A salt reinforces security by making every hash code different even for the same data input.

UsersSaltInput to be hashedHash code

The table above illustrates how bcrypt produces a different hash for all three users, even though their plaintext passwords are the same. An attacker who ran “password” through bcrypt will get a different hash value, so there’s no way for him to know the passwords of Alice, Bob and Emmanuel are “password”.

We know what a salt is, now is an excellent time to examine the structure of a password hash from bcrypt.

One property worth noting is the length of the hash codes, they are fixed-length; whatever the length of your password (capped at 72 bytes). A typical password hash looks like this:

bcrypt structure

The algorithm identifier shows what crypt (a UNIX utility program used for encryption) generated the hash. The cost factor specifies a key expansion iteration count as a power of two, which is an input to the crypt algorithm.

An Overview Of How The bcrypt Algorithm Works

There are several password-hashing functions. The one Rails and Devise use is bcrypt via the bcrypt-ruby gem. It is based on the Blowfish cipher, giving us the “b” in bcrypt. The crypt comes from the hashing function used by the UNIX password system.

The implementation details of bcrypt is beyond the scope of this post. Here’s a bird’s eye view of how the function works:

bcrypt algorithm
  1. The function accepts three parameters.
  2. A function called EksBlowfishSetup takes the cost, salt and key and sets up a state. It uses the password as a key.
  3. A 192-bit magic value “OrpheanBeholderScryDoubt” is encrypted sixty-four times in ECB mode with the state from the earlier step.
  4. The cost, salt and encrypted value are then concatenated and prefixed with the crypt and its version.

The result of this complex process is what is stored in a password digest, then used for authentication and other purposes. bcrypt-ruby does all of this, so we don’t have to.

How Rails Employs bcrypt

We stated earlier that Ruby on Rails uses the bcrypt hashing function via the bcrypt-ruby gem. Rails requires bcrypt in ActiveModelSecurePassword. We saw how adding has_secure_password gives you all the magic. This is where the magic happens in Rails:

  module ActiveModel
    module SecurePassword
      extend ActiveSupport::Concern


      class << self
        attr_accessor :min_cost # :nodoc:
      self.min_cost = false

      module ClassMethods
        def has_secure_password(attribute = :password, validations: true)
            require "bcrypt"
          rescue LoadError
            $stderr.puts "You don't have bcrypt installed in your application. " \
              "Please add it to your Gemfile and run bundle install"
          # ...
      # ...

We’ve come to the end of learning about bcrypt and what it gives us in our Ruby applications. There’s a whole lot we could have discussed; we could have talked about security, Argon2 comparison, how bcrypt adapts with increasing hardware power against brute-force attacks, key expansion et cetera; but then we’d be writing a mini-book.

Encryption and hashing are complex subjects. Thankfully, we don’t have to be experts in these computer security areas to leverage cryptography.

It does help in some cases though to understand a bit of what goes on under the hood. I hope this article gave you a little understanding of the subject of hashing for authentication.

Last Update: January 09, 2024