PHP 5.3+ Storing Password Hashes Instead Of Plain Text Passwords

If you’re storing user passwords in your database as plain text and your list is stollen, then the hackers can get the login details for all of your users. Even if there is no sensitive data associated directy with their account on your website, a lot of people use the same username and password combinations across many different websites so once a hacker can read your database, they can use the same details to breach other websites such as Facebook, Hotmail and so on.

This won’t do. We have an obligation to keep users login details secret and secure.

One further step is to use a simple hash function such as MD5 or SHA1 to convert the plain text passwords into a seemingly random set of characters. However, hackers use a giant lookup table of MD5 to password mappings (known as a Rainbow table). For short passwords, it takes only seconds to convert each MD5 hash back to a useable plain text password.

To increase security further, a per user random ‘salt’ can be added to the equation to prevent the Rainbow tables from easily being used and create unique hashes even for users with the same passwords.

For adaquate security, create a hash field e.g. passwd_hash varchar(60) and store only the calculated hash of the users password at the time they register, or change their password. Never store the plain text password, simply calculate the hash and throw away the password.

The following code is a modified condensed version of the Portable PHP password hashing framework but made to work using PHP5.3+ as a component for my Zend Framework project

This version enforces Blowfish one-way hash creation or raises an exception. The original framework was written to work with PHP3, 4 and 5 and attempts to use the most secure algorithm first, falling back silently if the installed version of PHP isn’t capable of handling the encryption method. Personally, I didn’t like the idea of the hashing framework silently falling back to some less secure method, so I spliced the this together into a smaller PHP5 Object-Oriented Style component.

I recommend reading the article above and also Secure Passwords with Phpass for more background.

The calling code

<?php
require_once 'BlowfishHasher.php';
 
 
// create a new hasher instance with
// an iteration count of 8
$hasher = new Vfr_BlowfishHasher(8);
 
// method function hash converts the plain text
// password into a hash using OpenBSD-style Blowfish-based bcrypt
// throws a Vfr_Exception_BlowfishUnsupported if PHP's
// CRYPT_BLOWFISH isn't available
try {
    $hash = $hasher->hash('somepassword');
} catch (Vfr_Exception_BlowfishUnsupported $e) {
    die("No blowfish, no dinner");
}
 
// use the checkPassword method function to valid passwords
if ($hasher->checkPassword('somepassword', $hash)) {
    var_dump("MATCH");
}

Library Code

class Vfr_BlowfishHasher
{
    private $_iterationCountLog2;
    private $_randomState;
 
    public function __construct($iterationCountLog2)
    {
        if ($iterationCountLog2 < 4 || $iterationCountLog2 > 31)
            $iterationCountLog2 = 8;
 
        $this->_iterationCountLog2 = $iterationCountLog2;
 
        $this->_randomState = microtime();
        if (function_exists('getmypid'))
            $this->_randomState .= getmypid();
    }
 
    private function getRandomBytes($count)
    {
        $output = '';
        if (is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) {
            $output = fread($fh, $count);
            fclose($fh);
        }
 
        if (strlen($output) < $count) {
            $output = '';
            for ($i = 0; $i < $count; $i += 16) {
                $this->_randomState = md5(microtime() . $this->_randomState);
                $output .= pack('H*', md5($this->_randomState));
            }
            $output = substr($output, 0, $count);
        }
 
        return $output;
    }
 
    private function generateBlowfishSalt($input)
    {
        $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 
        $output = '$2a$';
        $output .= chr(ord('0') + $this->_iterationCountLog2 / 10);
        $output .= chr(ord('0') + $this->_iterationCountLog2 % 10);
        $output .= '$';
 
        $i = 0;
        do {
            $c1 = ord($input[$i++]);
            $output .= $itoa64[$c1 >> 2];
            $c1 = ($c1 & 0x03) << 4;
            if ($i >= 16) {
                $output .= $itoa64[$c1];
                break;
            }
 
            $c2 = ord($input[$i++]);
            $c1 |= $c2 >> 4;
            $output .= $itoa64[$c1];
            $c1 = ($c2 & 0x0f) << 2;
 
            $c2 = ord($input[$i++]);
            $c1 |= $c2 >> 6;
            $output .= $itoa64[$c1];
            $output .= $itoa64[$c2 & 0x3f];
        } while (1);
 
        return $output;
    }
 
    public function hash($passwd)
    {
        if (CRYPT_BLOWFISH != 1) {
            require_once 'Vfr/Exception/BlowfishUnsupported.php';
            throw new Vfr_Exception_BlowfishUnsupported();
        }
        $random = $this->getRandomBytes(16);    
 
        $hash = crypt($passwd, $this->generateBlowfishSalt($random));
 
        if (strlen($hash) != 60) {
            require_once 'Vfr/Exception/BlowfishInvalidHash.php';
            throw Vfr_Exception_BlowfishInvalidHash();
        }
        return $hash;
    }
 
    public function checkPassword($passwd, $hash)
    {
        if (strlen($hash) != 60) {
            require_once 'Vfr/Exception/BlowfishInvalidHash.php';
            throw new Vfr_Exception_BlowfishInvalidHash();
        }
 
        $checkHash = crypt($passwd, $hash);
 
        return ($checkHash == $hash);
    }
}