How to properly store a PBKDF2 password hash

Alex picture Alex · Jul 31, 2012 · Viewed 11.5k times · Source

I have been doing some research for proper ways to hash/encrypt a password and store it in a database. I knew about Salt and Hashing so I looked around and PBKDF2 seemed to be a good choice. So I've found this website that gave a good tutorial on it as well as an adaptation of PBKDF2 for PHP (which is what I am using for my website).

So I've set up my website to use these functions to generate/create passwords, but as you can see in the following code:

function create_hash($password) {
// format: algorithm:iterations:salt:hash
$salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM));
return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" .  $salt . ":" .
    base64_encode(pbkdf2(
        PBKDF2_HASH_ALGORITHM,
        $password,
        $salt,
        PBKDF2_ITERATIONS,
        PBKDF2_HASH_BYTES,
        true
    )); }

The salt is generated in the create_hash function, and is stored in the resulting hash which ends up looking like sha256:1000:salt:hashed_password. This is what I had to store in my database, and since the salt was included in the resulting hash, I didn't need to add it to my database. However, after generating a few test users with this, I wondered if having the PBKDF2 settings inside my hashed password in the database was actually a good thing. They way my newbie self see it is a hacker, after cracking my database, would see these bunch of sha256:1000:salt:password things, and figure out what each part would stand for, which would help him greatly in his attempts, no?

So I modified it a bit to have an external salt that I generate and store in my database, and include the salt in the password before running it through PBKDF2. I then do the same thing to compare the given password with what I have in my database for the login, and it works. My only concern is that a with 128bit salt, the resulting password hash is barely 50 characters long, that doesn't seem right to me.

Here is my current code:

define("PBKDF2_HASH_ALGORITHM", "sha256");
define("PBKDF2_ITERATIONS", 10000);
define("PBKDF2_SALT_BYTES", 128);
define("PBKDF2_HASH_BYTES", 24);

define("HASH_SECTIONS", 4);
define("HASH_ALGORITHM_INDEX", 0);
define("HASH_ITERATION_INDEX", 1);
define("HASH_SALT_INDEX", 2);
define("HASH_PBKDF2_INDEX", 3);

function create_hash($password, $salt)
{
    // format: salthash
    return  
        base64_encode(pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTES,
            true
        ));
}

function validate_password($password, $salt, $good_hash)
{
    $pbkdf2 = base64_decode($good_hash);
    return slow_equals(
        $pbkdf2,
        pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTES,
            true
        )
    );
}

// Compares two strings $a and $b in length-constant time.
function slow_equals($a, $b)
{
    $diff = strlen($a) ^ strlen($b);
    for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
    {
        $diff |= ord($a[$i]) ^ ord($b[$i]);
    }
    return $diff === 0; 
}

function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
    $algorithm = strtolower($algorithm);
    if(!in_array($algorithm, hash_algos(), true))
        die('PBKDF2 ERROR: Invalid hash algorithm.');
    if($count <= 0 || $key_length <= 0)
        die('PBKDF2 ERROR: Invalid parameters.');

    $hash_length = strlen(hash($algorithm, "", true));
    $block_count = ceil($key_length / $hash_length);

    $output = "";
    for($i = 1; $i <= $block_count; $i++) {
        // $i encoded as 4 bytes, big endian.
        $last = $salt . pack("N", $i);
        // first iteration
        $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
        // perform the other $count - 1 iterations
        for ($j = 1; $j < $count; $j++) {
            $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
        }
        $output .= $xorsum;
    }

    if($raw_output)
        return substr($output, 0, $key_length);
    else
        return bin2hex(substr($output, 0, $key_length));
}

Is my concern for that format of password saving makes sense? Or will it just amount to the same thing anyway, since by having my salt in my database, once it's cracked, the hacker can still brute-force his way through?

Thanks, and sorry for the long question.

Answer

Slartibartfast picture Slartibartfast · Aug 1, 2012

My only concern is that a with 128bit salt, the resulting password hash is barely 50 characters long, that doesn't seem right to me.

The size of the resulting hash is completely unrelated to the size of the salt, the password, and the number of iterations. The output of a modern secure hash algorithm (like sha256) is always the same length regardless of the input. A zero length input has the same length output as a 25TB input.

Or will it just amount to the same thing anyway, since by having my salt in my database, once it's cracked, the hacker can still brute-force his way through?

By separating the salt into two pieces, you cost yourself increased code complexity (generally a bad thing). Depending on how you store the salt pieces, you may gain a little benefit under some circumstances. As an example, if the static salt fragment is stored outside of the database, then a dump of the database will not give an attacker enough information to perform an offline attack on the password hashes in the database.

The gain if the salt fragments are stored separately from one-another is a small amount of defence in depth. Whether it outweighs the complexity cost is a judgement call, but I'd say the chances are good that the time would be better spent looking for XSS and SQL injection vulnerabilities (the way that attackers often get the database dumps mentioned above), and securing connections between the various components of your system with SSL and certificates or strong passwords.