4

With 'portable_hashes' turned on. I've noticed that for whatever reason, the hashes it generates aren't always the same - but always return as valid when passed through 'CheckPassword'. I've also noticed that 'PHP_VERSION' is used in the generation of the hash - these two things combined have me worried... How portable is portable? Can I move the hashes (Saved in a user database) between servers, linux, windows, 64-bit, 32-bit, etc. - and still have them validate? What would I have to do to make the passwords not validate anymore?

The reason I ask is because I'm using phpass for passwords in my framework which will power several of my sites, many of which currently have several thousands of users - and there have been cases where I've had to move them onto different servers, and of course upgrade php. I also may switch one or two of them off of Apache to, say, lighthttpd or something similar. Needless to say I'm extremely paranoid I'm going to have a support nightmare someday and I won't be able to fix it in any other way than emailing new passwords to everyone (Which sounds really insecure).

If there's even the slightest chance that the passwords will ever be made invalid - what steps would I have to take to make my own password hash generator? I already use a 16-byte random salt (Per-user), and other than that the only other issue is stretching - right?

Jon
  • 305
  • 3
  • 20
  • 45

2 Answers2

8

Depending on the PHP version, you do not need to have portable hashes on. On PHP 5.3 and above, PHP supplies its own implementation of bcrypt if it isn't available on the system. If all your servers have PHP 5.3 and above, I highly recommend to turn portable hashes off. PHPass "portables hashes" exists because, depending of the version of PHP installed, bcrypt might not be available.

That said, PHPass portable hashes does store the salt in its hash. That's why every run on the same password is different.

Also, PHPass uses PHP_VERSION during the generation of those hashes* to check if the md5() function available with that version supports the $rawMode parameter. If it doesn't, pack() is use to transform the hexadecimal data into binary (note that this is considerably slower then simply using $rawMode, which is why the branch is made).

Again, if all your servers are running PHP 5.3 and above, I highly recommend to turn off portable mode and let PHPass use bcrypt instead. Since PHP 5.3+ provides its own implementation when the system one isn't available, your hash will be checkable across OSes. Even if you do turn off portable mode, PHPass will still be smart enough to check your old hashes the proper way.

I was in the same situation as you, using PHPass in my framework across multiple sites. Since I turned off portable mode, I've set my login script to progressively re-hash passwords which are not using bcrypt on login.

* Line 131


EDIT: For more explanation, here is how hashes in portable mode are generated (simplified, does not use actual variables found in PHPass, but accurate). Note that PHPass uses their own version of base64 encoding.

  1. $final = '$P$'

  2. $final .= encode64_int($rounds) (from constructor, minimum is 5 on PHP 5+, 3 other)

  3. $final .= genSalt() (Salt is 6 bytes... 8 bytes in "encode64" format).

  4. $hash = md5($salt . $password)

  5. For 2$rounds times, do $hash = md5($hash . $password)

  6. $final = encode64($hash)

So the final hash essentially is this:

$P$9IQRaTwmfeRo7ud9Fh4E2PdI0S3r.L0
\__________/\____________________/
  \                   \
   \                   \ Actual Hash
    \
     \  $P$   9   IQRaTwmf
        \_/   \   \______/
         \     \      \
          \     \      \ Salt
           \     \ 
            \     \ # Rounds (not decimal representation, 9 is actually 11)
             \
              \ Hash Header
Andrew Moore
  • 93,497
  • 30
  • 163
  • 175
  • Alright that clears a lot up, but let me run something by you... Right now I run PHP 5.3.3 on my personal testing machine (Win7, WAMP), and most of my servers are running CentOS with cPanel... They all have 5.2.x, but once again - they're CentOS. Knowing this - would it be okay to run exclusively in non-portable mode? Right now I move the entire database back and forth between my testing machine and my server, so some hashes will be generated on my CentOS php 5.2.x machine, and others will be generated on my Win7 PHP 5.3.3 machine. Eventually I may even test on a shared host - what then? – Jon Jan 25 '11 at 03:54
  • @Jon: I have the exact same setup (CentOS 5.2 on servers, Win7 on dev). If the constant `CRYPT_BLOWFISH == 1` **OR** the PHP version is 5.3+, you are fine. Just check `CRYPT_BLOWFISH` on your CentOS PHP 5.2.x servers (`var_dump(CRYPT_BLOWFISH)`). If the result is `1`, you are golden. Hashes generated using bcrypt on one machine or the other are the same... The problem is/was when bcrypt wasn't available. – Andrew Moore Jan 25 '11 at 04:02
  • Personally though, use WHM's EasyApache to upgrade your PHP version. – Andrew Moore Jan 25 '11 at 04:04
  • I really wish I could upgrade to 5.3, however there are still a lot of scripts that don't work in it on my server. I plan on upgrading them down the line but for now it's easier to just use 5.2.x. Especially since I didn't make any of them. I also just ran the var_dump on my server - it's 0. So I'm stuck with portable hashes? – Jon Jan 25 '11 at 04:09
  • Yes, right now you need to use portable hashes. However, when you upgrade, you can easily turn it off. Your new hashes will be generated using bcrypt and PHPass will still be smart enough to check your old hashes using the portable method and your new ones in bcrypt. With a small modification to your login script, you'll be able to upgrade the hashes in the database on user login. Bcrypt hashes will have the `$2a$` header while your portable hashes will have `$P$`. – Andrew Moore Jan 25 '11 at 04:13
  • Thank you so much, especially for that awesome diagram - that clears up a lot :) I wish I could upvote you a hundred times. – Jon Jan 25 '11 at 04:15
1

The only use that I can see of PHP_VERSION is in this line:

$output .= $this->itoa64[min($this->iteration_count_log2 +
    ((PHP_VERSION >= '5') ? 5 : 3), 30)];

Now, all that's saying is determining the maximum number of iterations. And it's in the gensalt_private method which generates salts. So this would only happen when storing a new password and generating the salt. So all previously generated salts are 100% portable. So there's no real portability issue with that at all...

As far as the rest, as long as you're using a reasonably recent version of php (5.0+), you shouldn't have any portability problem at all as far as I can tell (since the hash function is built in)...

ircmaxell
  • 163,128
  • 34
  • 264
  • 314
  • Thanks, but the salts aren't stored anywhere - from my understanding, it generates salts based on the settings each time it hashes, or checks against an existing hash. I know I'm not storing anything from phpass other than the resulting hash - my salt is my own creation. Knowing that, is there any issue switching versions? Like if it were on a server with PHP 4 and they upgraded to 5. Or any other issues that could cause the hashes to not line up. Speaking of which - how is it that it generates a new hash for the same password, each time you run it? That part really bugs me as well. – Jon Jan 25 '11 at 02:22
  • @Jon Are you sure it's not storing a salt inside of the hashed password? Is there a deliminator or a section of the output that stores it? I typically use a custom method like the one I submitted to Kohana: [patch file](http://dev.kohanaframework.org/attachments/1495/kohana_security_patch.patch). The only thing(s) you might want to change are the default hash method, and the number of rounds in `makeSaltedHash` from 20 to 500 (or some higher number)... – ircmaxell Jan 25 '11 at 02:29
  • It doesn't look like it is, there is, however, $P$ at the front... But that's it. – Jon Jan 25 '11 at 03:18
  • I just read though the (Kind of funny) comments on a drupal ticket regarding phpass - and it seems that the final hash is the result of encoded the REAL hash + salt... Or something like that. So you were right, that also explains why each hash generated is different but each can be compared against an earlier or later hash of the same string. – Jon Jan 25 '11 at 03:46