What is bcrypt gem?

Published: Jul 5, 2024

When a Ruby on Rails application is created by rails new command, we typically see bcrypt gem in Gemfile.

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> [VERSION]"

The Rails generator adds commonly used gems to Gemfile so that developers can add those by just removing # at the beginning of a line. That’s pretty handy.

Okay, then, what is bcrypt gem actually doing? The instruction at https://guides.rubyonrails.org/active_model_basics.html#securepassword says:

ActiveModel::SecurePassword depends on bcrypt, so include this gem in your Gemfile….”

We can understand how to setup and use the gem to make passwords secure, but still it’s not clear why the passwords become secure by bcrypt gem.

It’s time google it!

Hashing, not Encryption

If we see google search results about bcrypt, we notice many websites mention about hashing vs encryption. The difference is a one-way or two-way algorithm or function. The one-way function is able to generate a string of random characters based on the given string, but there’s no way to make the original string back. In another words, it is impossible to decrypt. The generated string is called a hash or digital fingerprint.

On the other hand, the two-way algorithm or function can do both: encrypt and decrypt. If the two-way function is used to generate a string of random characters based on the the given string, the given string can be recovered from the generated string. This process is called an encryption and decryption.

Bcrypt is the one-way function, so we will never ever know what is the password actually. The question now would be how to test a given password from a user A is correct. To test the password, exactly the same hashing process runs to generate a hashed string, then, compare the saved and generated hashed strings. If those matches, the given password is correct.

Since nobody can recover passwords from hashed string, the one-way function is commonly used to secure password storage.

What about the two-way function? The two-way function or encryption/decryption is used to secure communication including emails, store sensitive data such as PII (personal identifiable information), and etc.

In the area of two-way function, many online articles mention symmetric and asymmetric key encryption. The symmetric encryption uses the same key to both encryption and decryption. While asymmetric encryption uses a key pair, which is known as a public/private key pair. The private key is used to encrypt. The public key is used to decrypt. Well-known algorithms are:

  • Symmetric: Advanced Encryption Standard (AES), Data Encryption Standard (DES)
  • Asymmetric: Rivest-Shamir-Adleman (RSA), Digital Signature Algorithm (DSA)

If you are a software developer, you should have used one when you generated a ssh key pair. Then, you should have pasted a public key to the source code repository such as GitHub or GitLab.

Bcrypt algorithm – slow is good

Of course, bcrypt is not the only widely used hashing algorithm. Famous algorithms are Argon2id, scrypt, and PBKDF2.

Among those, bcrypt is said to be good since its computation is slow. SLOW?? WHAT? Yes, in the computing world, the faster, the better. To make it run faster, people use their brains and pay a lot of efforts. Even though, bcrypt is good because it runs slow.

One of the purposes of the slow computation is to stop attackers in an early stage. When a monitoring is in place, an administrator can spot a suspicious activity. Thanks for the slow bcrypt, the attackers’ progresses slow down, which effectively prevents further data breach.

Bcrypt has a cost parameter to control its slowness. The Ruby world has another gem called authlogic, which covers bcrypt, scrypt and a couple more hashing algorithm. Authlogic’s API document has interesting benchmarks at: https://www.rubydoc.info/github/binarylogic/authlogic/Authlogic/CryptoProviders/BCrypt

require "bcrypt"
require "digest"
require "benchmark"

Benchmark.bm(18) do |x|
  x.report("BCrypt (cost = 10:") {
    100.times { BCrypt::Password.create("mypass", :cost => 10) }
  }
  x.report("BCrypt (cost = 4:") {
    100.times { BCrypt::Password.create("mypass", :cost => 4) }
  }
  x.report("Sha512:") {
    100.times { Digest::SHA512.hexdigest("mypass") }
  }
  x.report("Sha1:") {
    100.times { Digest::SHA1.hexdigest("mypass") }
  }
end

#                          user     system      total        real
# BCrypt (cost = 10):  37.360000   0.020000  37.380000 ( 37.558943)
# BCrypt (cost = 4):    0.680000   0.000000   0.680000 (  0.677460)
# Sha512:               0.000000   0.000000   0.000000 (  0.000672)
# Sha1:                 0.000000   0.000000   0.000000 (  0.000454)

As the result shows, the cost 10 bcrypt runs very slow.

Bcrypt gem’s default cost is 12 as explained at https://github.com/bcrypt-ruby/bcrypt-ruby. So, by default, bcrypt-ruby runs much slower.

It’s salted

Another goodness of bcrypt is, it is salted. The salt is a some length of random characters used by hashing functions. As for bcrypt, the salt is randomly generated 16 byte value, which will be 22 characters after base 64 encoded. Using salt, bcrypt generate a hash (or checksum) from salt + password. This makes hashed strings really unique and almost impossible to find passwords.

When the hashing is done, the generated string will have a format blow:

$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]

$2a$12$K0ByB.6YI2/OYrB4fQOYLe6Tv0datUVf6VZ/2Jzwm879BW5K1cHey
\__/\/ \____________________/\_____________________________/
Alg Cost      Salt                  Hash (Checksum)

Since every password has a different salt, bcrypt makes guessing the passwords really hard.

Devise and bcrypt gem

Bcrypt-ruby gem or simply bcrypt gem is a widely used hashing function in the Rails ecosystem. For example, famous devise gem uses bcrypt as a default hashing algorithm.

Let’s try what are going on by getting hands dirty.

Create a sample Rails app

As always, the first command is rails new. This can be very simple app, so –minimal option is added. Also, -d postgresql option is added to see what are actually in the database. Once the app is created, setup the database, install devise gem, and finally create a devise User model.

$ bundle exec rails new devise-sample --minimal -d postgresql
$ rake db:setup
$ bundle add devise
$ bundle exec rails g devise:install
$ bundle exec rails g devise User
$ bundle exec rails db:migrate

At this point, sign up, sign in, sign out and some more password related paths are already created.

$ bundle exec rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
      rails_health_check GET    /up(.:format)                  rails/health#show

For a convenience, let’s create a simple page which has buttons of sign up/in/out.

$ bundle exec rails g controller home index

Add below to app/views/home/index.html.erb.

<% if notice %>
  <p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
  <p class="alert alert-danger"><%= alert %></p>
<% end %>

<%= button_to(
        "Sign Up",
        new_user_registration_path,
        method: :get
      ) %>
<br/>
<%= button_to(
        "Sign In",
        new_user_session_path,
        method: :get
      ) %>
<br/>
<%= button_to(
        "Log Out",
        destroy_user_session_path,
        method: :delete
      ) %>

Update config/routes.rb as in blow:

Rails.application.routes.draw do
  root 'home#index'
  #...
  # ...
end

All are ready. It’s time to start a Rails server and create users.

$ bundle exec rails s

If the server starts, go to http://localhost:3000 on a browser.

img: devise sample buttons

Sign up the first user with:

  • email: foo@example.com
  • password: Foo’sPassw0rd!

img: devise sample sign up foo

Click the “Log Out” button and sign up the second user with exactly the same password as the first user:

  • email: bar@example.com
  • password: Foo’sPassw0rd!

img: devise sample sign up bar

What have been created

Now we had two devise users successfully signed up. Two users can be verified on a Rails console, but let’s see what are in PostgreSQL first.

# In this case, PostgreSQL was installed by brew on MacOS.
# On other OS, installation, or setup, psql command may start with different arguments. 
$ psql postgres
postgres=# \c devise_sample_development
devise_sample_development=# select * from users;

 id |      email      |                      encrypted_password                      | reset_password_token |...
----+-----------------+--------------------------------------------------------------+----------------------+...
  2 | foo@example.com | $2a$12$biaK7edYPkOMEKUFjt9rCucUrine6wP.La20blTv7.bvpxtPv/dYi |                      |...
  3 | bar@example.com | $2a$12$.e3uUveScoJJmjBg9FNozeU9G.knZVODPmmk6xCVU0Amwmk4Pg316 |                      |...
(2 rows)

The encrypted_password column has hashed values generated by bcrypt. Although two users used the exactly the same password, salt strings (22 characters after “12$”) are different. As a result, hash values (last 31 characters) are different.

Open up the Rails console and test some bcrypt APIs.

$ bundle exec rails c
# get bcrypt generated hash values
irb(main):001> hashes = User.all.map {|u| u.encrypted_password }
  User Load (0.9ms)  SELECT "users".* FROM "users"
=>
["$2a$12$biaK7edYPkOMEKUFjt9rCucUrine6wP.La20blTv7.bvpxtPv/dYi",
...
irb(main):002> hashes
=>
["$2a$12$biaK7edYPkOMEKUFjt9rCucUrine6wP.La20blTv7.bvpxtPv/dYi",
 "$2a$12$.e3uUveScoJJmjBg9FNozeU9G.knZVODPmmk6xCVU0Amwmk4Pg316"]

# try some bcrypt APIs
irb(main):004> require 'bcrypt'
=> true
irb(main):005> foo = BCrypt::Password.new(hashes[0])
=> "$2a$12$biaK7edYPkOMEKUFjt9rCucUrine6wP.La20blTv7.bvpxtPv/dYi"

# yes! the password test passes 
irb(main):006> foo == "Foo'sPassw0rd!"
=> true

# the second user's password test passes as well
irb(main):010> BCrypt::Password.new(hashes[1]) == "Foo'sPassw0rd!"
=> true

# get parameters from a generated hash value
# bcrypt version
irb(main):011> foo.version
=> "2a"
# cost 
irb(main):012> foo.cost
=> 12
# salt -- bcrypt gem's salt method returns "version + cost + salt"
irb(main):013> foo.salt
=> "$2a$12$biaK7edYPkOMEKUFjt9rCu"
# checksum or hash of 31 characters
irb(main):014> foo.checksum
=> "cUrine6wP.La20blTv7.bvpxtPv/dYi"
irb(main):015> foo.checksum.length
=> 31

As in above, we can manually test the password’s validity.

What can be prevented by password hashing?

At this point, we know what is bcrypt (bcrypt gem) and how to use it. Bcrypt is there to secure a password storage. The question is from what the password storage will be secured.

In this world, two major password storage attacks are there: brute force and rainbow table attack. The brute force attack takes trial and error approach guessing every possible password string. Nothing can prevent the brute force attack 100%. However, because of the bcrypt’s slow computing process, the attack can be detected in the early stage. That way, a damage can be possibly minimized.

Considering the slow computing process, evil hackers invented rainbow table attack. The table has already generated hashing values using really many combinations of salt and possible password string. The rainbow attack can bypass the slow bcrypt computation time. Bcrypt is strong enough to be cracked, but there’s no guarantee to make it 100% secure. A good news is, the rainbow table attack happens when the password storage is compromised.

As a Rails developer, what we can do would be to put an additional layer of password security. Two factor authentication or CAPTCHA might be good options.

Other than that, Rails devs might just pray for administrators or DevOps people’s relentless work to save the password storage.

References

Latest Posts

Application Development by Rails Action Cable

The previous two blog posts introduced WebSocket and how to implement a WebSocket application on Ruby on Rails. This blog post digs deeper. It is a memo on creating a more realistic application by Action Cable.

Real-time App on Rails by Action Cable

The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.

WebSocket on Rails by Action Cable

In the web application domain, we hear some protocol names. Absolutely, HTTP or HTTPS is the most famous protocol that all web developers know. Although there’s a mechanism of Keep-Alive, a single request/response sequence with a single client/server is all done by HTTP. The client initiates the HTTP request to the server. Once the client receives the HTTP response from the server, communication finishes. As far as HTTP is used, the server just waits and waits. Only when the request comes in, the server can send back some data to the client. This communication style is surprisingly capable of doing many things, so most web applications are satisfied with HTTP.