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
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.
bcrypt gem uses an
== override to make this really simple:
class BCrypt::Password # <snip> def ==(secret) super(BCrypt::Engine.hash_secret(secret, @salt) end alias_method :is_password?, :== # <snip> end 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
From the rails docs:
class User < ActiveRecord::Base has_secure_password validations: false end user = User.new(name: 'david', password: 'mUc3m00RsqyRe') user.save user.authenticate('notright') # => false user.authenticate('mUc3m00RsqyRe') # => user
#authenticate method is basically what we showed above:
def authenticate(unencrypted_password) BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self end
password_digest is the stored password on the
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) end