How does Devise keep your passwords safe?
I’ve always been curious about the
encrypted_password column that Devise stores on your database. Here’s an example:
But what does it 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.
$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
- Cost (
11) - the cost factor used to create the hash (stored after the second
- 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)
Let’s explore the last 3 parameteres:
- When using Devise, the
Costvalue 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 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 that computes the final hash to be stored using the generated salt and the password selected by the user. This final hash (ex.
$2a$11$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO) will in turn be stored in the
encrypted_password column of the database.
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:
- Fetch the input password (
- Fetch the salt of the stored password (
- Generate the hash from the password and salt using the same bcrypt version and cost factor (
- Check if the stored hash is the same one as the computed on step 3 (
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.
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.
More information about some of the topics.
Cost factor *
Rainbow Table Attacks **