Immediately Migrating Existing Passwords to bcrypt

Posted June 18, 2013 by Taylor Fausak

Security cannot afford to be "eventually consistent".

That's Geoffroy Couprie's response to my last post about upgrading to bcrypt. He's right, of course. The solution he proposed is the same one kcen suggested on Reddit:

I solved this problem at a company I joined a couple years ago. [...] I created a new database column, bcrypted all of those hashes, then dropped the original password column from the database.

At first, and for no good reason, I disliked the idea of bcrypting a password hash. It just felt weird. I asked around and everyone agreed: weird, but no real objections.

So I decided to do a little research. After all, combining cryptographic primitives in the wrong way is an easy way to do cryptography wrong. I eventually found a question on the cryptography Stack Exchange that assuaged my fears. It said that "the overall idea is a sound migration strategy", which was good enough for me.

Setup

Assume that everything's the same as before, except that the passwords are hashed. It doesn't matter how they're hashed or if a salt is used. Let's keep it simple by calling the hash function digest and storing the result in password_hash.

def digest(password)
    password.hash.to_s
  end
  # user.password_hash = digest(password)

Setting the password is now slightly more complicated than before. Instead of simply using the plain text password as the input to bcrypt, we have to use the password hash. This adds a layer of indirection but allows us to migrate without knowing the original passwords.

def bcrypt=(new_password)
    @bcrypt = self.bcrypt_hash =
      Password.create(digest(new_password))
  end

Similarly, checking passwords now requires comparing against the hashed password.

def self.authenticate(username, password)
    return unless user = find_by_username(username)
    password_hash = digest(password)
    if user.bcrypt?
      user if user.bcrypt == password_hash
    elsif user.password_hash == password_hash
      user.bcrypt = password
      user.save!
      user
    end
  end

Migrate

After doing that, the only thing left to do is the actual migration. Be warned: this will take a long time. Although the exact time depends on your machine, you can get an estimate using the benchmark module.

Benchmark.measure do
    100.times do
      BCrypt::Password.create('secret')
    end
  end.total
  # => 7.45

The migration itself is pretty straightforward. It has three moving parts:

  1. Grab unmigrated users in chunks until none are left. This allows the migration to pick up from where it left off it it gets interrupted. In addition, users migrated through the authenticate method won't throw a wrench in the works.

  2. Calculate a bcrypt hash for each user using the password hash as input. This part will take a while, since bcrypt is designed to be slow.

  3. Save the bcrypt hash to the database. Using update_column avoids triggering callbacks or running validators.

class BcryptMigration < ActiveRecord::Migration
    class User < ActiveRecord::Base; end
    def up
      loop do
        users = User.select([:id, :password_hash]).
          where(:bcrypt_hash => nil).order(:id).limit(100)
        break if users.empty?
        users.each do |user|
          bcrypt_hash =
            BCrypt::Password.create(user['password_hash'])
          user.update_column(:bcrypt_hash, bcrypt_hash)
        end
      end
    end
  end

Although you could remove password_hash entirely in this migration, it's better to do that as a separate migration after this one finishes. That way if anything goes wrong with the switch to bcrypt you can fall back to the old method.

[Originally posted to my blog.]


Taylor Fausak was born in California, but he got to Texas as soon as he could. He studied Computer Science at the University of Texas at Austin before entering the wild world of software development. After a brief stint at Cisco, he started his career at Famigo working on all aspects of web development. Then he swapped his Django experience for a chunky slice of Rails bacon and joined OrgSync in the fall of 2012. When he's not slinging code around, he likes riding bikes, playing Magic, and throwing frisbees.


comments powered by Disqus