by Tiago Alves

How Devise keeps your Rails app passwords safe

WsIjDqCJh9Mkz5YnCe8qO38OOfEevfkleLBm
Photo by James Sutton on Unsplash

Devise is an incredible authentication solution for Rails with more than 40 million downloads. However, since it abstracts most of the cryptographic operations, it’s not always easy to understand what’s happening behind the scenes.

One of those abstractions culminates in the persistence of an encrypted_password directly on the database. So I’ve always been curious about what it actually represents. Here’s an example:

$2a$11$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO

But what does that gibberish mean?

Devise uses Bcrypt to securely store information. On its website it mentions that it uses “OpenBSD bcrypt() password hashing algorithm, allowing you to easily store a secure hash of your users’ passwords”. But what exactly is this hash? How does it work and how does it keep stored passwords safe?

That’s what I want to show you today.

Let’s work backwards — from the stored hash on your database to the encryption and decryption process.

That hash $2a$11$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO is actually comprised of several components:

  • Bcrypt version (2a) - the version of the bcrypt() algorithm used to produce this hash (stored after the first $ sign)
  • Cost (11) - the cost factor used to create the hash (stored after the second $ sign)
  • Salt ($2a$11$yMMbLgN9uY6J3LhorfU9iu) - a random string that when combined with your password makes it unique (first 29 characters)
  • Checksum (LAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO) - the actual hash portion of the stored encrypted_password (remaining string after the 29 chars)
C1o8LTw8UCfepc7Tq3m5Yd1VGcMcT4XpRkBD

Let’s explore the last 3 parameters:

  • When using Devise, the Cost value is set by a class variable called stretches and the default value is 11. It specifies the number of times the password is hashed. (On your devise.rb initializer, you can configure this to a lower value for the test environment to make your test suite run faster.) *
  • The salt is the random string used to combine with the original password. This is what makes the same password have different values when stored encrypted. (See more below about why that matters and what are Rainbow Table Attacks.) **
  • The checksum is the actual generated hash of the password after being combined with the random salt.

When a user registers on your app, they must set a password. Before this password is stored in the database, a random salt is generated via BCrypt::Engine.generate_salt(cost) by taking into account the cost factor previously mentioned. (Note: if the pepper class variable value is set it will append its value to the password before salting it.)

With that salt (ex. $2a$11$yMMbLgN9uY6J3LhorfU9iu, which includes the cost factor) it will call BCrypt::Engine.hash_secret(password, salt) that computes the final hash to be stored using the generated salt and the password selected by the user. This final hash (for example, $2a$11$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO) will in turn be stored in the encrypted_password column of the database.

mKgk9fAildsnwkuXhmOU0SDIiflG-nI8FPUa

But if this hash is nonreversible and the salt is randomly generated on the BCrypt::Password.create call by BCrypt::Engine.generate_salt(cost), how can it be used to sign in the user?

That’s where those different hash components are useful. After finding the record that matches the email supplied by the user to sign in, the encrypted password is retrieved and broken down into the different components mentioned above (Bcrypt version, Cost, Salt and Checksum).

After this initial preparation, here’s what happens next:

  1. Fetch the input password (1234)
  2. Fetch the salt of the stored password ($2a$11$yMMbLgN9uY6J3LhorfU9iu)
  3. Generate the hash from the password and salt using the same bcrypt version and cost factor (BCrypt::Engine.hash_secret(“1234”, “$2a$11$yMMbLgN9uY6J3LhorfU9iu”))
  4. Check if the stored hash is the same one as the computed on step 3 ($2a$11$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO)
kJxy3TSK0VJ3fVqfFcVjmCCPCdgm1HyBb9C8

And that’s how Devise stores passwords securely and protects you from a range of attacks even if your database is compromised.

Get in touch on Twitter @alvesjtiago and let me know if you found this article interesting! Thank you for reading.

Lo-yU9BkzXFiqwgk5JS4RAKntUaE4KffvT5-
PS: I’m by no means a security or cryptography expert so please do reach out if you find something wrong. I’m hoping that by simplifying some of the concepts it will be easier to understand what’s happening.

Thank you @filipepina, @ivobenedito, @jackveiga, @joao_mags and @pedrosmmoreira for the reviews and suggestions. This article is also available at http://blog.tiagoalves.me/how-does-devise-keep-your-passwords-safe.

More information about some of the topics.

Cost factor *

Rainbow Table Attacks **