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:
- A user (let’s call her Alice), creates an account on your site.
- Alice chooses a username and a password.
- The server hashes Alices password, keeps a plaintext of the ID and stores this in the database.
- Alice comes to the site at a later time; she enters her ID and password.
- Her login details are securely (over HTTPS hopefully) transferred to your site’s backend.
- 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.
- 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 has_secure_password end
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 end end
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")
Notice how we created the user with
password arguments, but we got back
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.
At this point, we can see how the plaintext value of “sekreet” is hashed by running
$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
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.
|Users||Salt||Input to be hashed||Hash 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
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:
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
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:
- The function accepts three parameters.
- A function called
EksBlowfishSetuptakes the cost, salt and key and sets up a state. It uses the password as a key.
- A 192-bit magic value “OrpheanBeholderScryDoubt” is encrypted sixty-four times in ECB mode with the state from the earlier step.
- 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
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 MAX_PASSWORD_LENGTH_ALLOWED = 72 class << self attr_accessor :min_cost # :nodoc: end self.min_cost = false module ClassMethods def has_secure_password(attribute = :password, validations: true) begin 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" raise end # ... end end # ... end end
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.