35

You maintain an existing application with an established user base. Over time it is decided that the current password hashing technique is outdated and needs to be upgraded. Furthermore, for UX reasons, you don't want existing users to be forced to update their password. The whole password hashing update needs to happen behind the screen.

Assume a 'simplistic' database model for users that contains:

  1. ID
  2. Email
  3. Password

How does one go around to solving such a requirement?


My current thoughts are:

  • create a new hashing method in the appropriate class
  • update the user table in the database to hold an additional password field
  • Once a user successfully logs in using the outdated password hash, fill the second password field with the updated hash

This leaves me with the problem that I cannot reasonable differentiate between users who have and those who have not updated their password hash and thus will be forced to check both. This seems horribly flawed.

Furthermore this basically means that the old hashing technique could be forced to stay indefinitely until every single user has updated their password. Only at that moment could I start removing the old hashing check and remove the superfluous database field.

I'm mainly looking for some design tips here, since my current 'solution' is dirty, incomplete and what not, but if actual code is required to describe a possible solution, feel free to use any language.

6
  • 4
    Why would you be unable to distinguish between a set and an unset secondary hash? Just make the database column nullable and check for null. Nov 9, 2013 at 17:55
  • 6
    Go with a hash-type as your new column, instead of a field for the new hash. When you update the hash, change the type field. That way, when this happens again in the future, you'll already be in a position to deal, and there's no chance of you holding onto an old (presumably less secure) hash. Nov 9, 2013 at 18:54
  • 1
    I think you are on the right track. 6 months or a year from now, you can force any remaining users to change their password. A year later or whatever, you can get rid of the old field. Nov 10, 2013 at 3:22
  • 3
    Why not just hash the old hash?
    – Siyuan Ren
    Nov 10, 2013 at 5:19
  • because you want the password to be hashed (not the old hash) so that unhashing it (i.e. comparing it to a hashed one given by the user) gives you... the password - not another value that then needs to be 'dehashed' under the old method. Possible but not cleaner. Nov 12, 2013 at 22:23

6 Answers 6

26

I would suggest adding a new field, "hash_method", with perhaps a 1 to signify the old method and a 2 to signify the new method.

Reasonably speaking, if you care about this sort of thing and your application is relatively long-lived (which it apparently already is), this is probably going to happen again as cryptography and information security is such an evolving, rather unpredictable field. There was a time when a simple run through MD5 was standard, if hashing was used at all! Then one might think they should use SHA1, and now there's salting, global salt + individual random salt, SHA3, different methods of crypto-ready random number generation...this isn't going to just 'stop', so you might as well fix this in an extensible, repeatable way.

So, lets say now you have something like (in pseudo-javascript for simplicity, I hope):

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword());


if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword)
{
    // TODO: Learn what "hash" means
    return clearPassword + "H@$I-I";
}

Now realizing there is a better method, you just need to give a minor refactoring:

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword(), user.getHashingMethod());

if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword, hashMethod)
{
    // Note: Hash doesn't mean what we thought it did. Oops...

    var hash;
    if (hashMethod == 1)
    {
        hash = clearPassword + "H@$I-I";
    }
    else if (hashMethod == 2)
    {
        // Totally gonna get it right this time.
        hash = SuperMethodTheNSASaidWasAwesome(clearPassword);
    }
    return hash;
}

No secret agents or programmers were harmed in the production of this answer.

4
  • +1 This seems like quite a sensible abstracting, thanks. Though I may end up moving the hash and hashmethod fields to a separate table when I implement this.
    – Willem
    Nov 9, 2013 at 21:50
  • 1
    The problem with this technique is that for accounts don't log in, you can't update the hashes. In my experience most users won't login for years if ever (unless you delete inactive users). Your abstraction doesn't really work either, since it doesn't account for salts. The standard abstraction is having two functions, one for verification and one for creation. Nov 10, 2013 at 12:07
  • Password hashing has barely evolved over the last 15 years. Bcrypt has been published in 1999 and is still of of the recommended hashes. Only one significant improvement has happened since then: sequentially memory hard hashes, pioneered by scrypt. Nov 10, 2013 at 12:11
  • This leaves the old hashes vulnerable (e.g. if someone steals the DB). Hashing each old hash with the new more secure method mitigates this to some degree (you'll still need the hash type to distinguish between newHash(oldHash, salt) or newHash(password, salt)
    – dbkk
    Nov 18, 2013 at 20:54
48

If you care enough about rolling out the new hashing scheme to all users as quickly as possible (e.g. because the old one is really insecure), there is actually a way for instantaneous "migration" of every password.

The idea is basically to hash the hash. Rather than waiting for users provide their existing password (p) upon next login, you immediately use the new hashing algorithm (H2) on the existing hash already produced by the old algorithm (H1):

hash = H2(hash)  # where hash was previously equal to H1(p) 

After doing that conversion, you are still perfectly able to perform password verification; you just need to compute H2(H1(p')) instead of the previous H1(p') whenever user tries to log in with password p'.

In theory, this technique could be applied to multiple migrations (H3, H4, etc.). In practice, you would want to get rid of the old hash function, both for performance and readability reasons. Fortunately, this is quite easy: upon the next successful login, simply compute the new hash of user's password and replace the existing hash-of-a-hash with it:

hash = H2(p)

You will also need additional column to remember what hash you're storing: the one of the password or of the the old hash. In the unfortunate event of database leak, this column shouldn't make the cracker's job any easier; regardless of its value, the attacker would still have to reverse the secure H2 algorithm rather than the old H1.

4
  • 3
    A giant +1: H2(H1(p)) is usually as secure as using H2(p) directly, even if H1 is terrible, because (1) salt and stretching are taken care of by H2, and (2) the problems with "broken" hashes like MD5 don't affect password hashing. Related: crypto.stackexchange.com/questions/2945/…
    – orip
    Nov 11, 2013 at 19:38
  • Interesting, I would've figured hashing the old hash would create some kind of security 'issue'. Seems I was mistaken.
    – Willem
    Nov 11, 2013 at 21:54
  • 4
    if H1 is really terrible this won't be secure. Being terrible in this context is mostly about losing a lot of entropy from the input. Even MD4 and MD5 are nearly perfect in this regard, so this approach is only insecure for some homebrew hashes or hashes with really short output (below 80 bits). Nov 13, 2013 at 11:05
  • 1
    In that case, probably your only option is admit that you screwed up hard and just go ahead and reset password for all users. It's less about migration at that point and more about damage control.
    – Xion
    Nov 14, 2013 at 1:54
8

Your solution (additional column in the database) is perfectly acceptable. The only issue with it is the one you already mentioned: that the old hashing is still used for users who haven't authenticated since the change.

In order to avoid this situation, you may wait for your most active users to switch to the new hashing algorithm, then:

  1. Remove the accounts of users who haven't authenticate for too long. There is nothing wrong in removing an account of a user who never came to the website for three years.

  2. Send an email to the remaining users among those who haven't authenticated for a while, telling that they may be interested in something new. It will help to further reduce the number of accounts which use the older hashing.

    Be careful: if you have a no-spam option users can check on your website, don't send the email to users who checked it.

  3. Finally, a few weeks after the step 2, destroy the password for the users who are still using the old technique. When and if they try to authenticate, they'll see that their password is invalid; if they are still interested in your service, they could reset it.

1

You will probably never have more than a couple hash types, so you can solve this without extending your database.

If the methods have different hash lengths, and each method's hash length are constant (say, transitioning from md5 to hmac-sha1), you can tell the method from the hash length. If they have the same length, you can calculate the hash using the new method first, then the old method if the first test failed. If you have a field (not shown) telling when the user was last updated/created, you can use that as an indicator for which method to use.

1

I did something like this and there is a way to tell if it's the new hash AND make it even more secure. I'm guessing more secure is a good thing for you if you are taking the time to update your hash ;-)

Add a new column to your DB table called "salt" and well, use it as a per user random generated salt. (pretty much prevents rainbow table attacks)

That way, if my password is "pass123" and random salt is 3333 my new password would be the hash of "pass1233333".

If the user has a salt you know it's the new hash. If the user's salt is null you know it's the old hash.

0

No matter how good your migration strategy is, if the database has already been stolen (and you cannot negatively prove that this has never happened), then passwords stored with insecure hash functions may already be compromised. The only mitigation for this is requiring users to change their passwords.

This is not a problem that can be solved (only) by writing code. The solution is to inform users and customers about the risks they have been exposed to. Once you are willing to be open about it, you can do the migration by simply resetting every user's password, because it is the easiest and most secure solution. All the alternatives are really about hiding the fact that you screwed up.

1
  • 1
    One compromise that I have done in the past is to keep the old passwords for a period of time, rehashing them as people log in, but then after that time has passed you then force a password reset on anyone who didn't log in during that time. So you do have a period where you still have the less secure passwords, but it's time limited and you also minimise disruption for the users who do log in regularly. Jun 21, 2018 at 16:30

Not the answer you're looking for? Browse other questions tagged or ask your own question.