These are just some quick notes after playing with the Ruby BCrypt gem. I had read this article and realized I wasn’t really sure how the implementation of password storage worked in some past projects since I have not had to store password salt separately.

BCrypt Password Basics

The following shows how BCrypt can neatly store all the information needed in a single field.

require 'bcrypt'

password = BCrypt::Password.create("lame_password")

## password.to_s contains salt + hashed password
password.to_s # => "$2a$10$NX8y4tG4RkfRFdbfAKUmIO/S3yY1Nn4Vgr6omFaUKhuBdeoX0GK5W"
password.salt # => "$2a$10$NX8y4tG4RkfRFdbfAKUmIO"
password.checksum                          # => "/S3yY1Nn4Vgr6omFaUKhuBdeoX0GK5W"

## Salt contains $version$cost$salt
password.salt   # => "$2a$10$NX8y4tG4RkfRFdbfAKUmIO"
password.version # => "2a"
password.cost        # => 10

The salt generated is different for each password created, even if the secret is the same.

p2 = BCrypt::Password.create("lame_password")
p2.salt == password.salt # => false

Because BCrypt::Password stores the salt together with the secured secret, it can be stored in a single field of a database and recalled to use the same salt when comparing a newly provided secret.

The bcrypt gem uses an == override to make this really simple:

class BCrypt::Password
  # <snip>
  def ==(secret)
    super(BCrypt::Engine.hash_secret(secret, @salt)
  alias_method :is_password?, :==
  # <snip>

password == "not_password"  # => false
password == "lame_password" # => true

This way the salt from the originally stored secret can be used for future comparisons.

Ruby on Rails Usage

ActiveModel::SecurePassword uses essentially this same code when you add a password to a model using the has_secure_password mechanism.

From the rails docs:

class User < ActiveRecord::Base
  has_secure_password validations: false

user = 'david', password: 'mUc3m00RsqyRe')
user.authenticate('notright')      # => false
user.authenticate('mUc3m00RsqyRe') # => user

The #authenticate method is basically what we showed above:

def authenticate(unencrypted_password) && self

where password_digest is the stored password on the User model.

Similarly, user.password = 'new_password' makes a familiar call to create a secure encrypted password with the new secret:

def password=(unencrypted_password)
  @password = unencrypted_password
  cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
  self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)