zOMG My Passwords are Insecure, now What?
08 Jun 2012What happens if your passwords get breached like LinkedIn? Are your users safe? Will they trust you? Had LinkedIn taken a few more security precautions they could have greatly mitigated the security threat. Simply stretching and salting passwords would have made the break in much less valuable to the thieves.
These practices are normally discussed in even the most basic web programming books. As Dan Morrison over at Collective Idea mentions in a great Password Rant; no web app in this day and age should be committing these types of simple security mistakes. But what if they are? What if you are? If you start from scratch the solutions are pretty easy, but how do you migrate an existing system with real users and pre-existing password hashes?
As luck would have it I’ve spent some time moving authentication backends before, so I can help shed some insight into this problem.
If you were really naive and stored passwords in plain text, first shame on you, and second…life will be really easy. You just have to add an auth library such as Devise to your app and re-save all users while setting :password to the stored password. If you’ve been smart enough to hash your passwords, things will be a little harder, but not by much.
Options
Depending on the level of pain you want to go though, and the number of users in your app, you could send reset password instructions to all of your users and delete their old password hash. This process can be a bit jarring and users might wonder why they have to reset in the first case. You could also set temporary passwords for all users and have them reset on login. If you have a ton of users and don’t want to do that you can roll users to from their old passwords to use devise gradually. This is the process I’ll walk you through.
Road Blocks
The biggest problem to overcome is that you don’t know your user’s password (this is actually a good thing in the security world). So the only time we should have the user’s password is on login when they send it to be secure compared to the saved hash. First you can move existing password hashes and salts (if you have any) to a :legacy_password_hash
and a :legacy_password_salt
field in the user table. Once you’ve done this wait for the user to log in again and you can build a secure password hash for them using devise. So lets write some code.
The Code
This process isn’t just for migrating insecure systems, but can be useful if you want to change authentication schemes.
We’ll want to transition the old password checking code to a method called valid_legacy_password?
making sure to use a secure comparison. In this example the client application was using BCrypt wtih a salt but wanted to migrate to Devise and use stretching. So we’ll first have to generate the password hash and then compare against the legacy password hash.
In app/models/user.rb
def valid_legacy_password?(password)
calculated_hash = BCrypt::Engine.hash_secret(password, legacy_password_salt)
Devise.secure_compare(legacy_password_hash, calculated_hash)
end
Now that we can tell if the user is who they claim to be, using the legacy system. Then we will want to convert the password from a legacy one to one using devise.
def convert_legacy_password!(password)
return false if legacy_password_hash.blank? || !valid_legacy_password?(password)
self.password = password
if encrypted_password.present?
self.legacy_password_hash = nil
self.legacy_password_salt = nil
self.save
end
end
Here we check to see if the password they’re providing matches the legacy system, then we set the password. Note that devise uses a field called encrypted_password
to store the new secure hash. Devise will automatically hash the password on save. So if devise correctly converts the password to a hash we remove the legacy hash and salt and save the fields.
Now that we’ve got our model code in place we’ll need to wire up some logic in our sessions controller. You could put this in the model but that would require before callbacks or over-writing a Devise method. To do this you’ll need to tell devise that you want to implement your own session controller.
config/routes.rb
devise_for :users, :controllers => { :sessions => 'sessions' }
Now we want to implement the create action making sure to call super at the end to preserve the default devise functionality.
app/controllers/sesssions_controller.rb
class SessionsController < Devise::SessionsController
def create
user = User.where(:email => params[:user][:email]).first
user.convert_legacy_password!(params[:user][:password]) if user.present? && user.has_legacy_password?
super
end
end
First we are grabbing the user based on their email, checking if they have a legacy password, and if so converting it using the password they just provided. So as users log into our system, their old insecure passwords are flushed out and only the new passwords are saved. To help things along you could add something like this to your ApplicationController to speed up the process.
class ApplicationController < ActionController::Base
before_filter :check_legacy_password
private
def check_legacy_password
sign_out(current_user) if current_user && current_user.legacy_password_salt.present?
true
end
end
This can take some time depending on the number of users you have in your system, but it’s worth it for the piece of mind. At some point (after a number of months) you should consider notifying users they must sign in or their accounts will be deleted. You should also consider your backup strategy, if you have insecure passwords storage, you have insecure password storage backups. Either sanitize them or delete them.
Fin
Security shouldn’t be taken lightly especially as Dan pointed out, the hard work is already done for us, and there is no excuse for using an insecure system. While migrating authentication solutions seems like a pain, it only took maybe half a day at most to implement, test and verify the system worked in staging. Security is like air, you don’t miss it until it’s gone. So do the right thing, friends don’t let friends store insecure passwords.
Update
If you want a more secure version that doesn’t involve waiting try out this post https://blog.jgc.org/2012/06/one-way-to-fix-your-rubbish-password.html though you’ll still have to incorporate your own password hashing algorithm in-front of the auth solution. You could also add a salt to the existing hashes, re-hash them store them and update the code in valid_legacy_password?
and then your legacy passwords are secure, and you’re still using the off the shelf auth code.