30

I am presented with the following scenario:

I have a MySQL-Database with a table of users. The table has two fields: username and password. The password is stored as an unsalted hash.

An over 15 year old application uses this database to authenticate access to its services. It is only accessible internally.

Our team is tasked to modernize these services by offering a new application following best practices and lessons learned of the last 15 years (and potentially more that were ignored in the original application). This application shall be accessible from the wide open internet.

We are supposed to reuse the current database for authentication purposes to allow both applications to run in parallel on the underlying same authentication database.

I voiced several concerns regarding security as this application is about handling personal details of my coworkers and me.

We decided that part of our modernization is that password policies are put in place and that passwords are stored both salted and peppered.

This means our database table would get a third field salt and the password field now stores our salted and peppered hash.

The problem here is that this will break the authentication in our old application. As the code is very much legacy and we don't even have a working compiler for it anymore changing the code of the original application is out of question.

My question therefore is:

  • Is there a secure way to add salt (and pepper) to our authentication database while maintaining the old application's ability to authenticate users? Keep in mind that while the old application is inherently insecure it will not be accessible outside of our intranet, the new application however will be.
10
  • 2
    Can you edit your question and say how the other application is accessing the database to authenticate users? Is it via a direct query against the table, a query to a view, or executing a stored procedure?
    – Freiheit
    Commented Dec 2, 2019 at 21:39
  • 1
    @Freiheit I would love to but I honestly do not know that. I never got access to the application. I can try to ask around.
    – Ben
    Commented Dec 3, 2019 at 7:33
  • @Ben does it matter what flavour of db server you use or is it just the object name and schema that matters?
    – Frank
    Commented Dec 3, 2019 at 8:20
  • 2
    @Ben You shouldn't need access to the application to find out what queries it is running since you control the database. Review how to turn on query logging or auditing in MySQL and that will let you observe what the application does without having access to the application source.
    – Freiheit
    Commented Dec 3, 2019 at 14:12
  • 1
    Is your requirement to keep the old application working without any code changes? If that is the case, then no, you cannot change the user table without breaking the old application, as the old application requires an unsalted password. If you can change the old application and the requirement is for people to continue to use their passwords without having to change them, you can authenticate against Hash(salt+Hash(password)) instead of Hash(salt+password) as you these are no less secure and you can calculate them without knowing the original password.
    – Simon G.
    Commented Dec 4, 2019 at 14:50

9 Answers 9

50

You have conflicting requirements here. The compatibility requirements forces you to keep the old hashes. The security requirements forces you to drop them. You will have to make a choice here about what requirements to fulfill.

If you decide to keep the backwards compatibility, try making the best out of a bad situation:

  • The old hash and the new hash should be stored in different tables, and the database user that the web application uses should not have read access to the old hash. Use table and/or column permissions for this.
  • As soon as you no longer need the old application, drop the table with the old hash. Ashley Madison famously failed at this point - they upgraded to bcrypt, and then for some idiotic reason they left the old MD5 hashes lying around in the database. When the database was leaked, that fancy bcrypt did not help much...

Or, alternatively, if you are not afraid to create a bit of a mess:

  • Drop the old hashes. In the new application, add on option "Create temporary password for old application". It gives you a long, random password that is hashed in the old way and only kept in the database for X minutes. The user can then logg in to the old application, and the password is then automatically deleted.
5
  • 1
    I changed the accepted answer to this as it shines light on the thing that there are ways to work around the problem - just not a perfect one. But I guess there is always a compromise we have to live with. I would love to accept several answers that shined light on potential solutions (such as the answer by Brian) or showed that there is no "perfect secure way" such as the one by mti2935, but as I can't I think this one here is the most important takeaway - in combination with all the other great answers provided.
    – Ben
    Commented Dec 3, 2019 at 10:32
  • 5
    @Ben Disagree respectfully - you can easily solve this with hash(hash+salt) and a table view/triggers. Though I would add the problem is probably organizational. Not sure why you'd want the same auth db for an internal LoB app driving a public facing one.
    – Frank
    Commented Dec 3, 2019 at 10:42
  • An alternative way that I've done was to detect the first character of the stored password. If it was a "$", I knew that I was supposed to use password_verify(). Otherwise, it was an MD5 password, and I just had to re-hash the password after confirming the MD5 hash is the same, and store in the database. This gets rid of the MD5 and keeps your pristine new hash, with 0 usability issues. In my case, I did this re-hashing on login. Commented Dec 4, 2019 at 10:16
  • @IsmaelMiguel The old application would not be able to handle the new passwords, so once a password is upgraded that user would not be able to use the old application.
    – Anders
    Commented Dec 4, 2019 at 10:19
  • @Anders Oh, if compatibility with the old one must be kept, then yeah. Forcing the read-only on the old table and forcing users to use the new one for the increased security is a good idea. Commented Dec 4, 2019 at 10:35
24

Is there a secure way to add salt (and pepper) to our authentication database while maintaining the old application's ability to authenticate users?

Yes, this can be done. Below are high-level implementation instructions. The basic technique is to hash all the passwords, then MITM the connection between the client and the legacy server in order to replace the unhashed password with a hashed password. Note that you'll need to come up with a roll-out plan; blindly running step 1 in production will break everything.

Step 1: Salt all existing passwords, then store the salt somewhere. Overwrite the password field of the legacy database with the salted password.

Step 2: Create a shim. The shim will accept identical parameters as the legacy APIs.
So, if the Legacy API is implemented as:

LegacyDoStuff(username,password,argument)
{
    if(!VerifyCredentials(username,password)) return AuthenticationError();
    result = DoStuff(argument);
    return result;
}

The new API is implemented as:

NewDoStuff(username,password,argument)
{
    hashedpassword = DoHash(password+getSalt(username))
    return LegacyDoStuff(username,hashedpassword,argument);
}

Step 3: Point the legacy client at the shim, instead of the main server (or equivalently, move the legacy server to new IP/DNS, then put the shim at the old IP/DNS).

This approach does allow you to treat the internals of the legacy code as a black box, but it requires you to be aware of the public surface area of the legacy code, since your shim will need to send requests/responses between the client and the legacy server.

This approach, unlike the approaches described in other answers, completely avoids storing the old password. However, this approach is much more difficult to do and is far more likely to introduce bugs.

10
  • 2
    If it's old quick hashing function they could likely crack them with not much effort. Alternatively they could require all users to re-set their passwords.
    – Dracs
    Commented Dec 3, 2019 at 2:50
  • 2
    From a purely technical standpoint I love this idea. And yes, indeed it does fulfill basically all the requirements we have. And still I doubt that my boss would let us ever do this when we explain to him that we want to MITM our own system... Really great answer though imho!
    – Ben
    Commented Dec 3, 2019 at 7:40
  • 13
    @Ben This is a really clever idea, indeed. If you call it "proxy" instead of "MITM" maybe your boss would be more inclined to agree?
    – Anders
    Commented Dec 3, 2019 at 10:35
  • 9
    @SawyerKnoblich Assuming the old hash function was Hash(password), then the new function can be pbkdf2(sha1, Hash(password), salt, 1000*1000) - ie make the final hashing function be a two stage function. Commented Dec 3, 2019 at 14:17
  • 2
    @Bergi I understood it to mean "somewhere between the user and the old application". For a web application, the shim would accept the input from the login form, manipulate the password value, and post it on to the real application.
    – IMSoP
    Commented Dec 4, 2019 at 6:24
11

Is there a secure way to add salt (and pepper) to our authentication database while maintaining the old application's ability to authenticate users?

No. The reason for salting and hashing passwords is so that if the user database is hacked/leaked/compromised, the users' passwords are not accessible to the attacker (see What is the point of hashing passwords?). In the solution that you describe, the users' passwords are still stored in the user database, in an adjacent column, in plaintext. This completely defeats the purpose of salting and hashing passwords.

8
  • Meaning that as we only hash to make a potential databreach less impactful we are simply opening ourselves up to a way bigger world of hurt by now also having the data accessible over the open internet instead of internally only, making the chance for such a breach to happen in the first place just way more likely, correct? I don't see how this is anything but a massive security disaster about to happen right now...
    – Ben
    Commented Dec 2, 2019 at 11:56
  • 9
    I agree. I think your organization needs to seriously think about updating or replacing the legacy application such that it follows modern security standards. If that is not possible in the near future, then you might want to consider using two separate databases (one for the internal legacy application, with the passwords stored in plaintext; and another separate database for the new public-facing application with the passwords salted and hashed), and running a process that keeps the data synced between the two databases.
    – mti2935
    Commented Dec 2, 2019 at 12:06
  • 8
    The op does not say there is any plaintext password.
    – Frank
    Commented Dec 2, 2019 at 23:52
  • 8
    @Frank: Unsalted with weak hash is, for all intents and purposes, plaintext. Commented Dec 3, 2019 at 1:06
  • @R.. agreed but it changes the parameters of the problem. We know now that the problem can be solved, because a password reset does not store plaintext. See my answer below for solution
    – Frank
    Commented Dec 3, 2019 at 8:00
7

Clearly the current table needs to remain so the old application can work.

Maybe add a new table with salted+peppered hashes for the new application to use, then:

  • Make the old table inaccessible to the new application!
  • Update both tables when updating a user's password
  • Require all users of the new system to update/reset their password
  • The new table can store a copy of the old hash initially, then update it to be properly hashed when the user updates their password.

This assumes that the password control system is separate from either of the content systems.

This means that any vulnerability in the new system can't access the old hashes (once migrated). The database could have a vulnerability that allows the new system's database credentials have access to a table it shouldn't, but that's much less likely than your new application having a vulnerability.

4
  • From a technological standpoint, when the table is inaccessible for the new application, is there any way I could leak the table content without someone getting access to my internal network?
    – Ben
    Commented Dec 2, 2019 at 11:50
  • Only if the database has a vulnerability that allows the new system's account to access tables it shouldn't. Commented Dec 2, 2019 at 11:54
  • You could use a completely separate database I suppose, but that makes the password update program more complicated. Commented Dec 2, 2019 at 11:55
  • Okay. So we could at least keep the potential vulnerability level on a comparably similar level it already was before the new application (which is probably way too high already) by restricting access of everything new to the security vulnerability. I guess that is something.
    – Ben
    Commented Dec 2, 2019 at 11:58
4
  1. Add two new columns, a salt column, and a new hash column, initially null.
  2. When an authentication request comes in, check the salt field.
  3. If the field has a value then there is a new-style hash that has pepper and salt added. Handle accordingly.
  4. If not, then this is an old-style hash. Verify using the old mechanism. Assuming it succeeds, you now have the plaintext password from the original request; generate and store a salt and you now have everything needed to compute and store a new-style hash. The legacy system won't know to do any of this, so only the new system can do it. (It would have been better if neither system had to deal with it but could delegate the job to the database itself. The new system can, the old system can't.)

Eventually either everyone has logged in at least once and had their hashes transparently updated (there are no NULL salts left in the table), or the only ones left are those who never log in at all.

Putting the unsalted and salted hashes right next to each other in the table just illustrates the fact that for as long as the legacy system continues to be used, the use of best practices in the new system will continue to be irrelevant from the standpoint of security.

3
  • why you need a new hash column? Commented Dec 3, 2019 at 9:53
  • @kevinSpaceyIsKeyserSöze We need to keep the unsalted hash to keep the old app running, and a salted hash (which will be different) for the new app to run securely. Commented Dec 3, 2019 at 18:41
  • While this is one way to generate the new salted hashes, another is simply to double-hash all the existing entries. Neither is particularly relevant to the main part of this question, which is how to keep the legacy app working; the only part of this answer that addresses that is "create a new column and leave the old one".
    – IMSoP
    Commented Dec 3, 2019 at 19:44
2

Yes this is possible.

In the new application, have a hash-of-hash double hash algorithm eg:step1 : hash the salted password , step2 : hash that hash (no salt)

Approach #1:

Develop a simple standalone app that takes a password and generates the salted hash of a user password and displays to screen or silently to clipboard.

Ask all internal users to reset their password in the old application to the output of the standalone app, and from now on when logging in use the process of entering the password into the standalone app to generate the password hash that will be entered into the login screen. The output could be generated to clipboard to avoid visual clues.

Approach #2

If you can somehow use the db, do the hash of hash in the database. Make step 2 the salted hash and only store that.

Edit to clarify: example: if using a DB that supports custom operators then declare a custom equality operator on a column storing the newsaltedhash(oldhash) as a custom type. When the app runs a query for row with matching username and oldhash, the custom operator will execute newsaltedhash on the incoming hash and do the comparison. Password reset is handled by insert triggers.

Summary: In both scenarios your apps function as originally designed and the stored password hash is a hash+salt double hash.(Pepper optionally added in either implementation)

17
  • While this works, it's replacing a bad system with a better, but still a bit hackish and very non-standard one. IMHO it's better to do just a clean break, and use standardized components like bcrypt rather than trying to roll your own, yet again. Commented Dec 3, 2019 at 16:35
  • @SteveSether I would wholeheartedly agree. But that does run contrary to the spirit of the question. The whole premise of the question is strange. More importantly is the issue of why an internal line-of-business authentication database will be driving a public facing one, and the risk of exposing internet DDoS attacks on internal auth dbs. Yes. a clean break is essential, but the OP has not made clear why s/he is in this very unorthodox situation.
    – Frank
    Commented Dec 3, 2019 at 16:37
  • Note to others: "using 'standard' components like bcrypt" should not be taken to mean "use bcrypt". For example, in .NET there is no audited safe implementation of bcrypt AFAIK, and it is better to use the library hash functions from a sec pov.
    – Frank
    Commented Dec 3, 2019 at 17:02
  • 1
    This sounds like a variant of Brian's answer, but using the users to do some of the work. Note that you can't just have a tool to generate "a salted hash", it needs to generate the correct salted hash using the stored salt for that user. Also, you need people to be able to log into the new system anyway, so it's probably easier to bulk generate hashes of the form NewHashFunction($salt + $oldHashValue); you can then bulk update the old table with OldHashFunction($newHashValue) rather than making users reset their passwords.
    – IMSoP
    Commented Dec 3, 2019 at 19:54
  • 1
    @Frank No, the database could not do this for you, it sits at the wrong end of the legacy hashing function.
    – Bergi
    Commented Dec 4, 2019 at 1:47
1

This answer is inspired by Brian's answer and Frank's Approach #1 which both essentially use the new salted hash as the password for the old application. Since that requires an application to retrieve the salt from the database to generate the hash, it would be simpler and more secure to have that application authenticate the user, and use a completely separate random token as the password for the old system.

Step 1: Choose a modern password hashing solution

Pick a well-supported password hashing library for your language of choice, which should handle generating secure salt for you. It will probably output a single string containing the hash, salt, and other parameters needed to verify the password, allowing you to gradually roll out a stronger algorithm in future. For instance, in the case of PHP, use the built-in password hashing functions.

Create a new column in the database to store this single string. One way to populate this initially is to calculate NewHashFunction(OldStoredHash), and then run NewVerifyFunction(NewStoredHash, OldHashFunction(UserEnteredPassword)). There are plenty of existing discussions of this, and it doesn't make a difference to the rest of this answer.

Step 2: Blank the old password column

Permanently wipe all the old insecure hashes, but don't drop the column. At this point, nobody can log into the old application, so we need to fix that...

Step 3: Build a tool to log into the old application

Build a page in your new application (where the user is already authenticated) or a standalone page (which authenticates the user using the new password library) which does the following:

  • Generates a long random password
  • Stores the unsalted hash of this random password into the old password column for the authenticated user
  • Either displays the password to the user to enter into the old application, or directly submits it to log the user in automatically

Note that this random password doesn't need to be stored anywhere, because if the user wants to log in again, they can simply generate a new one.

Step 4: Make it more secure by timing out the tokens

The random passwords are bound to be much stronger than most users would have set unless using a password manager, but they will still be stored unsalted, and probably using a fast hash. There is also a risk that the password will be copied somewhere when shown to the user. We can make both attacks much harder by only having the random passwords valid for a short period of time.

Add an extra column to the DB for "random password expiry date". This can be extremely short if logging in automatically, and a few minutes if the user has to enter the password themselves. A scheduled job should then run once a minute, and blank the old password field for all rows which have expired.

1
  • Not only is this an excellent answer, it should be used as a good example of how seemingly insurmountable problems often have unobvious solutions
    – Frank
    Commented Dec 4, 2019 at 22:48
-1

As pat of the upgrade, add a column to the user table indicating whether or not the account has been upgraded. If it hasn't then use the old code to generate and check the hash. If that's OK then generate the salt and the new hash and update the user record to the new hash, salt, and set the Upgraded flag.

If the Upgraded flag is set on login, the use the new code to read the salt, make the hash and check that.

8
  • 1
    While this obviously works for upgrading the current solution towards a new one this will still break the old login functionality as soon as an account is marked as Upgraded correct? The importance is that an account can still be accessed using the old login method even after changes have been made. Both applications have to run in parallel to support old legacy clients where the new application can't be run (no modern OS e.g.)
    – Ben
    Commented Dec 2, 2019 at 11:30
  • 1
    If I assume that the old site is separate from new, then don't upgrade the old - especially if you don't want to change it. If they share a common database, then instead of an Upgrade flag, add an AccountVersion flag and set the existing accounts to 1. Added new accounts would be version 2 and have a salt and hash in the new way. Any competent developer should be able to do this, or variations on it. Commented Dec 2, 2019 at 11:42
  • The problem is that any users should be enabled to login both using the old legacy application and the new application. There is no "either or", it's always "both". Even new users added shall be able to login both ways. In my thoughts that means the only way is to store both the unsalted and the salted hash - and that defeats the purpose completely and I can directly just scrap the solution. If we were simply writing a new application you could trust me that I would never setup on a 15y old database and make something new... It's sadly the constraints I am left with.
    – Ben
    Commented Dec 2, 2019 at 11:45
  • How does the user specify which way they want to logon? This is a very unclear situation: you are in control of the hashing process not the user; you need to ensure that the hashing process matches that of the stored hash not the user. You are in control. These are fairly elementary ideas of how to pick the right process and/or convert from one to the other. If this doesn't give a direction for a solution then I have completely failed to understand the problem - sorry! Commented Dec 2, 2019 at 11:51
  • 1
    That sounds like you should go with version 1 and 2 accounts and hash accordingly. This would require no change to the old app as it would just be doing v1 hashes anyway. Commented Dec 2, 2019 at 11:58
-4

Storing salted passwords is soooooooo 1995. Therefore you should (if nothing better is possible) at least store a salted hash with many iterations of the hash. This is actually less troublesome than you think.

One concern about this scheme is that you don't know how many iterations are enough, and as computers get faster, you will eventually need more. Over-hashing too much is very expensive, though... especially for a large user database. How to solve this?

The commonly applied solution to that question is at the same time the solution to your primary problem.

In order to account for growing CPU (or GPU, really) power, you add another column with the number of iterations. Every now and then, you add one iteration to a user (for example, 5% random chance at every login). This is very little work because you only need to do one extra hash. Update both the count, and the stored hash.

Now, one needs to realize that a hashed password is still only a password. It's kinda "unreadable", but it is still only a password, nothing else. Nobody prevents you from salting that and hashing it again. Even hash it many times. No harm done.

So, whenever your authentication layer pulls a row from the database where the hash count is zero, it knows that this is an "old" entry where the password has not been salted (it has zero extra hash rounds). So... salt it, hash it a thousand times, and store 1000 for the hash count as well as the salted hash. Done.

The login procedure would thus be something like (in pseudocode):

hash_entered = H(password_entered);

[hash, count] = sql_query(SELECT WHERE username=blah);

if(count == 0)
{
    login_success = compare(hash_entered, hash);
    for(i = 0; i < 1000; ++i) hash = H(hash);
    sql_query(UPDATE hash = hash, count = 1000);
    return login_success;
}
else
{
    for(i = 0; i < count; ++i) hash_entered = H(hash_entered);
    login_success = compare(hash_entered, hash);

    if(random(100) <= 5)
    {
        hash = H(hash);
        sql_query(UPDATE hash = hash, count = count + 1);
    }
    return login_success;
}
2
  • 2
    While some of this answer is reasonable general advice, it doesn't address the specific situation in this question, which is how to keep the old application working.
    – IMSoP
    Commented Dec 3, 2019 at 19:57
  • 1
    What's better than a salted password? I don't think you mention that.
    – schroeder
    Commented Dec 4, 2019 at 9:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .