Moving to a safer password solution
In an application we wrote back in 2004 I found MD5 hashed passwords. We decided this was too weak for modern standards so we wanted to switch to bcrypt. During the move we wanted the user to be affected as little as possible.
In order to compute the crypted password we need the cleartext version. We only have a hashed version so the user has to type her password. Luckily they do this every time they authenticate, so that is a nice opportunity to upgrade their password.
First I added a crypted_password column to the accounts table. We now have two columns for storing the password: the old hashed_password and the new crypted_password.
add_column :accounts, :crypted_password, :string
After that we updated the password accessor methods; assignment and verification.
class Account
def password=(password)
if new_record? or !password.blank?
self.crypted_password = BCrypt::Password.create(password)
end
end
def has_password?(input)
BCrypt::Password.new(crypted_password) == input
rescue BCrypt::Errors::InvalidHash
false
end
end
Now need to make sure we can authenticate with both the hashed as well as the crypted password stored for an account.
class Account
def self.authenticate_with_crypt(params={})
if account = find_by_username(params[:username]) and
account.has_password?(params[:password])
account
end
end
def self.hash_password(password)
::Digest::MD5.hexdigest(password)
end
def self.authenticate_with_md5(params={})
find_by_username_and_hashed_password(
params[:username],
hash_password(params[:password])
)
end
end
Finally we need to make sure the password automatically updates. We try to authenticate using bcrypt. BCrypt raises an exception when the crypted_password is blank. This makes authentication fail and we fall back to trying the hashed password. When authentication with a hashed password succeeds we know the cleartext password and we can update it.
class Account
def self.authenticate(params={})
if account = authenticate_with_crypt(params)
account
elsif account = authenticate_with_md5(params)
account.password = params[:password]
account.hashed_password = nil
account.save!
account
else
account = Account.new(params.slice(:username, :password))
account.errors.add_to_base("The credentials you entered are"
"invalid. Please try again.")
account
end
end
end
This solution will leave a group of users with a hashed password indefinitely. After a few months we could decide to throw away the hashed passwords. This means that infrequent users will have to reset their password if they do decide to log in again. It could cause some support requests, but I think we can handle them.
Note that this change doesn’t make the application safer. In case we leak information or when the database is somehow stolen it will make it harder to recover passwords. A lot of people use the same password for multiple accounts so this will give them time to reset their other accounts in case it is compromised. The change only took 15 minutes in this application so it’s totally worth the time.
Comments
Add your comment
In order to fight spam on this blog, posting comments from a browser without javascript is currently not supported.
Subscribe
Jay 4 days later: (delete)
From a technical standpoint, this is an excellent article for people to read. From a business standpoint, I wonder if there is any real value for a company to make a switch like this. It seems that MD5 hash with a good salt will be plenty secure. Is there a particular scenario in which you believe that would not be the case? In what scenario would an MD5 hash on a password and salt not be good enough? ¶
Manfred Stienstra 11 days later: (delete)
It helps in the case an attacker gets total control over your server. In that case they will also have the salt and know your hashing algorithm.
More and more of our data is locked into online applications. I think companies should take responsibility for safely storing this data. Security will also be a major selling point when organizations and individuals become more aware of the value of their data. ¶