Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
194 lines (161 sloc) 5.95 KB
title
Upgrading to bcrypt
Update: Immediately Migrating Existing Passwords to bcrypt

Crypt

Every so often, someone hacks a company and steals their database. Usually the database contains a bunch of email addresses and passwords. Two weeks ago, LivingSocial was hacked, leaking 50 million users' data. Even companies as big as Sony aren't immune; they were hacked in 2011 and had 77 million users' data stolen.

As a developer, it's your responsibility to protect your users' data should this happen to you. Depending on how you store passwords, it can either be trivial or impossible for attackers to compromise your users' accounts. From least to most secure, these six formats cover the majority of password storage techniques:

  1. Plain text
  2. Obfuscated (Caesar cipher)
  3. Encrypted (Triple DES)
  4. Hashed (MD5)
  5. Salted and hashed
  6. Computed from a key derivation function (bcrypt)

It's unequivocally better to store passwords securely. There are no downsides and brute-force attacks become orders of magnitude harder. Any greenfield project should use bcrypt (or another key derivation function like PBKDF2 or scrypt).

But what about existing projects? Upgrading to a new scheme isn't trivial because current passwords need to be migrated. In fact, the user shouldn't be able to tell that anything changed. The old system should be silently deprecated and replaced with the new one.

Here's how to do exactly that with Rails 3.2.13 and bcrypt-ruby 3.0.1 on Ruby 1.9.3-p392.

Before

Let's say you're in the worst possible scenario: you store passwords in plain text. Hopefully you don't actually do this, but it makes this example a lot simpler. The same principles work regardless of how you store your passwords.

Assuming a straightforward user model, you might authenticate users with a class method. All it does is try to find the user, then compare the passwords. If everything checks out, it returns the user. In all other cases, it returns nil.

class User < ActiveRecord::Base
  def self.authenticate(username, password)
    user = find_by_username(username)
    user if user && password == user.password
  end
end

During

We want to jump straight to the best case scenario and start using bcrypt. Three things are necessary to get that done: add another field to the user model; add a handful of new methods; and modify the authenticate method.

Up first is adding a new field to the user model. We need to store the derived key bcrypt generates. A simple migration takes care of this step:

class AddBcryptHashToUser < ActiveRecord::Migration
  def change
    add_column :users, :bcrypt_hash, :string
  end
end

Now we need a couple utility functions. They'll allow us to see which users use bcrypt, set the password, and compare strings against it. These all require the bcrypt-ruby gem, so add gem 'bcrypt-ruby' to your Gemfile.

require 'bcrypt'
class User < ActiveRecord::Base
  include BCrypt
  def bcrypt?
    bcrypt_hash.present?
  end
  def bcrypt
    @bcrypt ||= Password.new(bcrypt_hash) if bcrypt?
  end
  def bcrypt=(new_password)
    @bcrypt = Password.create(new_password)
    self.bcrypt_hash = @bcrypt
  end
end

Lastly, the authenticate function needs to be modified. It should compare using bcrypt if the user has been updated. If they haven't, it should compare using the old method.

Once a user authenticates using the old method, it should generate a bcrypt hash for them so it'll use that next time. In addition, it needs to delete data stored by the old method. If it doesn't, an attacker could just focus their efforts on the legacy data.

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

After

At some point you'll want to remove everything that's still stored in the old format. For users that haven't updated yet, a new password must be generated. You can either email it to them or they can rely on your password recovery service.

require 'bcrypt'
class RemovePasswordFromUser < ActiveRecord::Migration
  def up
    user_ids = ActiveRecord::Base.connection.select_all(
        'SELECT id FROM users WHERE bcrypt_hash IS NULL').
      map { |e| e['id'] }
    remove_column :users, :password
    return if user_ids.blank?

    passwords = user_ids.length.times.
      map { SecureRandom.hex }
    bcrypt_hashes = passwords.
      map { |e| BCrypt::Password.create(e) }
    cases = user_ids.zip(bcrypt_hashes).
      map { |a, b| "WHEN #{a} THEN '#{b}'" }
    update_sql <<-SQL
      UPDATE users
      SET bcrypt_hash = CASE id #{cases.join(' ')} END
      WHERE id IN (#{user_ids.join(', ')})
    SQL

    user_ids.zip(passwords).each do |user_id, password|
      # Send an email, generate a notification, ...
    end
  end
end

Testing

Depending on how your tests are set up, switching to bcrypt could slow them down. Changing the work factor is the easiest way to avoid this slowdown. The next version of bcrypt-ruby will support setting the cost with BCrypt::Engine.cost = x. For the time being, monkey patching is the way to go. Drop this into spec/support/bcrypt.rb:

require 'bcrypt'
module BCrypt
  class Engine
    Kernel.silence_warnings do
      DEFAULT_COST = 1
    end
  end
end

Conclusion

Upgrading a legacy system to use bcrypt isn't that hard. You should do it sooner rather than later. In the unlikely (but entirely possible) event of a database leak, your users' passwords will be protected.