3

I've been tasked with improving the security of the password storage of a site which currently uses Openwall's PHPass. All the hashes will be converted at once, i. e. we don't want to wait for the user to log in to re-hash their password.

To achieve this I thought we could use bcrypt over the existing PHPass hashes, this way: bcrypt(phpass(password))

But we need an important intermediate step: just like bcrypt, PHPass has its own salt embedded into the hash, so we have to "carry" that salt into bcrypt if we want the operation to be repeatable when verifying the hashes.

Luckily, PHPass' salt size is smaller (6 bytes) than bcrypt's (16 bytes), so we can "share" the first 6 bytes of the salt and adding 10 additional random bytes for bcrypt.

Imagine something like this in pseudo-PHP:

$password = 'somestring123';

$hash = phpass($password)

// $hash is now '$H$9Uvsrbh2Wxo3SebfGb4xVtODMmD2K70',
// where 'Uvsrbh2W' is an encoded, random salt of 6 'raw' bytes

$raw_salt = decode(substr($hash, 4, 8));

$hash = bcrypt_with_custom_salt($hash, $raw_salt . random_bytes(10));

// $hash is now '$2y$12$Uvsrbh2WzN9HrapVpnmu2OYOdJ2jnjHt2LTwIYQPJe.BUJQKezKuO'
// whose salt uses the first 6 bytes we had in phpass,
// so that we can repeat the process when veryfing the password
// since we have all the salts available in only one hash

Barring any implementation bugs and assuming the random sources are cryptographically secure, is this a theoretically secure way to use bcrypt in a legacy context? From my understanding the result should be as secure (if not more...?) as just doing bcrypt(password), but I'm not an expert and I might be missing some obscure detail.

MM.
  • 133
  • 3

4 Answers4

2

I was initially going to close this as a duplicate of "why-is-hashing-a-password-with-multiple-hash-functions-useless" however you have a slightly more complex case in that you already have a database of hashed passwords.

If you consider your existing mechanism of hashing to be insufficient than you need to migrate to a better solution. That means you need to be able to discriminate between the old and new representation, run the verification mechanisms in parallel and convert the clear text from the old representation to the new. And the only time you get the clear text is when somebody logs in.

Although PHPPass is rather long in the tooth, it does use blowfish (and bcrypt is based on blowfish). So maybe you're not improving the security quite as much as you think.

symcbean
  • 18,278
  • 39
  • 73
  • I'd phrase it as "there isn't all that much space for improvement as you think"; "you're not improving..." might be interpreted as bcrypt being not secure enough. But great answer even so. – LSerni Mar 23 '17 at 14:28
  • bcrypt is a significant improvement over phpass for bruteforce resistance. Assuming default work factors, hashcat can crack phpass about 500 times faster than bcrypt. See https://gist.github.com/roycewilliams/2d4a4bb05c045f226fe7a9433e68accc for an example. – Royce Williams Mar 23 '17 at 17:18
  • hmmm, just as fast as MD5(Wordpress), MD5(phpBB), MD5(Joomla) and with an accuracy quoted to 6 digits. I note that the PHPass website says "and a **last resort fallback to MD5-based** salted and variable iteration count password hashes" hence I would question your methodology. – symcbean Mar 23 '17 at 17:35
  • The main motivation for wanting to do this migration is that bcrypt(12) is much more costly to crack than phpass(11), which is what we're using here. We could argue if the improvement is worthy of the effort or not, sure. But my question is asking more about the theoretical security point of view: _am I creating a hole that somehow makes the hashes easier to crack in a way I dont understand?_ – MM. Mar 23 '17 at 18:03
  • symcbean, do you mean you're not sure whether the benchmarks that I posted are accurate? They're only mine in that I ran them on my own hardware; they're generated directly by hashcat's built-in benchmark mode. – Royce Williams Mar 23 '17 at 18:14
  • Ah, I see what you're saying. You're right - PHPass only falls back to native iterated md5crypt if bcrypt isn't available on that platform. We need more information from the original poster about why (and whether) their PHPass runs in fallback mode. I assumed that, but I might be mistaken. – Royce Williams Mar 23 '17 at 18:16
  • And since the salts are the wrong size for full bcrypt, MM.'s PHPass must indeed be running in fallback mode, so my benchmark data still applies. However, I recommend determining root cause of the fallback before making final architectural decisions. If bcrypt is available, PHPass should be able to use it. – Royce Williams Mar 23 '17 at 18:36
  • The current code seems to be forcing the portable mode, with 2^11 iterations. That's `md5(md5(... md5(salt + password)))` Pretty much worst case for PHPass (well, actually it could be worse since it accepts a minimum of 2^7 iterations). Why? I'm not sure. Portableness I guess. – MM. Mar 23 '17 at 18:44
  • That's your own native client code, not PHPass, right? http://www.openwall.com/phpass/ says "To ensure that the fallbacks will never occur, PHP 5.3.0+ or the Suhosin patch may be used. PHP 5.3.0+ and Suhosin integrate crypt_blowfish into the PHP interpreter such that bcrypt is available for use by PHP scripts even if the host system lacks support for it." Is the fact that your implementation isn't using bcrypt because of PHPass, or because PHPpass isn't being used to its potential? – Royce Williams Mar 23 '17 at 20:45
  • The latter. For some reason PHPass is being used that way on purpose, forcing it to generate portable, weaker hashes even if the environment had bcrypt available (which it does!). Sadly you can configure PHPass to do that. I'm sorry if I was confusing and I implied "phpass = md5 in a loop" when I should really have said "phpass = bcrypt most of the time but our code uses it wrong and forces it to be md5 in a loop". – MM. Mar 24 '17 at 10:12
2

I would tend to say that if you are moving away from phpass that you would want to remove it from your code base as much as possible. I might suggest that you first replace all phpasses with the bcrypted version of them, then replace your phpass hash with the bcrypt hash the next time a user logs in. In order to support users who are logging in for the first time since the switch, you should also check to see if the phpass matches.

So something like the following. Obviously I'm using pseudo code as the function is not called "bcrypt".

if bcrypt(phpass($supplied_password)) == $storred_password;
    $storred_password = bcrypt($supplied_password);
    login();
if bcrypt($supplied_password) == $storred_password;
    login();

In this scenario, you are slowly migrating to the new password scheme, but the old one is still supported.

If there is any vulnerability in phpass like a memory leak or something crazy, you are not using it at all in most of your passwords as users login.

MikeSchem
  • 2,266
  • 1
  • 13
  • 33
  • As I said in the question, ideally all the hashes would be converted at once. I don't really want to wait for the user to log in: there are several hundred thousands of accounts accumulated for almost 2 decades and a huge percentage of them will probably never come back or will login only once a year. – MM. Mar 23 '17 at 17:48
  • Right, in this solution, you convert all the passwords to `bcrypt(phpass(password))` and you only convert to the raw `bcrypt(password)` when the user logs back in. Slight improvement over the other suggestions because you move away from the phpass over time. – MikeSchem Mar 24 '17 at 05:17
  • 1
    Ah, I had understood you incorrectly then, sorry. That actually might be a good idea that I could implement after the first conversion. Thanks. – MM. Mar 24 '17 at 10:14
1

The one-time wrapping of PHPass in bcrypt should indeed provide significant additional offline bruteforce resistance. As I commented above, using default work factors, hashcat can crack PHPass fallback md5crypt-style PHPass about 500 times faster than bcrypt.

Reusing the inner PHPass salt for use with the outer bcrypt seems reasonable. (It certainly seems better than a static salt!) Your legacy bcrypt salts will not be quite random -- but only non-random relative to the inner PHPass hash that is not yet known to the attacker.

Since each bcrypt and PHPass are still randomly salted relative to all other hashes of their type, the distribution of salts still provides the broad resistance to bruteforce that salts are supposed to.

I can't think of a way that knowing the PHPass salt in advance would provide any additional advantage to the attacker. And even if it does provide a slight advantage, it will almost certainly be outweighed by the higher cost of cracking bcrypt.

Royce Williams
  • 9,128
  • 1
  • 31
  • 55
0

What you are doing is essentially a key expansion. But phpPass already does this (as does bcrypt), so what you're really doing from a security standpoint is (slightly) increasing security by adding one more round, nothing more.

There might be some implementation vulnerabilities in phpPass that might lead to key disclosure (for example not sanitizing memory buffers), but chances of exploitation seem small.

All in all I'd say that you do not decrease security, even if you probably don't increase it significantly.

Even so, some way of migrating the database could be worth pursuing. For example adding a field to the user table to hold a pure bcrypt hash (or NULL). If it is NULL, the phpPass algorithm is used, and if the password validates, the bcrypt field is generated. You can monitor the percentage of bcrypt-filled hash fields to see how the migration is going on. And you can zero the phpPass hash of some test migrated users to verify that everything is working when standing on bcrypt alone (if the system is cleanly designed this is superfluous, but it's still good to to be able to show a Happy Day Scenario to the CEO - a table with code coverages doesn't cut it as much).

LSerni
  • 22,521
  • 4
  • 51
  • 60
  • Yeah, the first thing I thought was keeping the 2 hashes in parallel and let the migration progress "on its own", but there are *tons* of accounts and a big chunk will never login again and other big percentage will login once a year or less. I wanted to accelerate that process because otherwise it could last years and even then never be completed... – MM. Mar 23 '17 at 17:52