Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Symfony's PBKDF2 polyfill benchmark (TL;DR it's slow) for the thread here https://twitter.com/spazef0rze/status/1050436425559302147
<?php
function hashPbkdf2($algorithm, $password, $salt, $iterations, $length = 0)
{
// Number of blocks needed to create the derived key
$blocks = ceil($length / strlen(hash($algorithm, null, true)));
$digest = '';
$length = strlen(hash($algorithm, '', true));
if (strlen($password) > $length) {
$password = hash($algorithm, $password, true);
}
for ($i = 1; $i <= $blocks; $i++) {
$ib = $block = hash_hmac($algorithm, $salt . pack('N', $i), $password, true);
// Iterations
for ($j = 1; $j < $iterations; $j++) {
$ib ^= ($block = hash_hmac($algorithm, $block, $password, true));
}
$digest .= $ib;
}
return substr($digest, 0, 40);
}
function hashPbkdf2Original($algorithm, $password, $salt, $iterations, $length = 0)
{
// Number of blocks needed to create the derived key
$blocks = ceil($length / strlen(hash($algorithm, null, true)));
$digest = '';
for ($i = 1; $i <= $blocks; $i++) {
$ib = $block = hash_hmac($algorithm, $salt . pack('N', $i), $password, true);
// Iterations
for ($j = 1; $j < $iterations; $j++) {
$ib ^= ($block = hash_hmac($algorithm, $block, $password, true));
}
$digest .= $ib;
}
return substr($digest, 0, 40);
}
function bench($p, $s, $i)
{
echo 'pw length: ' . strlen($p) . "\n";
echo 'iterations: ' . $i . "\n";
$start = microtime(true);
$h1 = bin2hex(hashPbkdf2('sha256', $p, $s, $i, 32));
echo 'polyfill: ' . (microtime(true) - $start) . "s\n";
$start = microtime(true);
$h2 = hash_pbkdf2('sha256', $p, $s, $i);
echo 'native: ' . (microtime(true) - $start) . "s\n";
echo 'h1 === h2: ' . ($h1 === $h2 ? 'true' : 'false') . "\n\n";
}
$iter = 1e4;
bench(str_repeat('*', 1e2), 'somerandombytes', $iter);
bench(str_repeat('*', 1e3), 'somerandombytes', $iter);
bench(str_repeat('*', 1e4), 'somerandombytes', $iter);
bench(str_repeat('*', 1e5), 'somerandombytes', $iter);
pw length: 100
iterations: 10000
polyfill: 0.039463996887207s
native: 0.032675981521606s
h1 === h2: true
pw length: 1000
iterations: 10000
polyfill: 0.044183969497681s
native: 0.030115127563477s
h1 === h2: true
pw length: 10000
iterations: 10000
polyfill: 0.037073850631714s
native: 0.032072067260742s
h1 === h2: true
pw length: 100000
iterations: 10000
polyfill: 0.044167041778564s
native: 0.047009944915771s
h1 === h2: true
@Sc00bz
Copy link

Sc00bz commented Oct 12, 2018

This is broken. Test:

$pw1 = 'password';
$pw2 = str_repeat('a', 33);
$salt = 'salt';
$iterations = 10;

echo "Password 1:\n";
echo "hashPbkdf2:         " . bin2hex(hashPbkdf2        ('sha256', $pw1, $salt, $iterations,  1)) . "\n";
echo "hashPbkdf2Original: " . bin2hex(hashPbkdf2Original('sha256', $pw1, $salt, $iterations,  1)) . "\n";
echo "hash_pbkdf2:        " . bin2hex(hash_pbkdf2       ('sha256', $pw1, $salt, $iterations, 32, true)) . "\n";
echo "\nPassword 2:\n";
echo "hashPbkdf2:         " . bin2hex(hashPbkdf2        ('sha256', $pw2, $salt, $iterations,  1)) . "\n";
echo "hashPbkdf2Original: " . bin2hex(hashPbkdf2Original('sha256', $pw2, $salt, $iterations,  1)) . "\n";
echo "hash_pbkdf2:        " . bin2hex(hash_pbkdf2       ('sha256', $pw2, $salt, $iterations, 32, true)) . "\n";

Note length for hashPbkdf2 and hashPbkdf2Original:

Password 1:
hashPbkdf2:         653cc888d937efe22810a5cbdb25a5bd82e2ebb27a800f85cfa360a6d925198e
hashPbkdf2Original: 653cc888d937efe22810a5cbdb25a5bd82e2ebb27a800f85cfa360a6d925198e
hash_pbkdf2:        653cc888d937efe22810a5cbdb25a5bd82e2ebb27a800f85cfa360a6d925198e

Password 2:
hashPbkdf2:         53976e1f88dfe9094244261d8d4c58a12b4eb5ee6ec351f76b918d29b91d44b9
hashPbkdf2Original: f7f031ec49d4df55b530f2a2c0cb4fcefd3079b508e726b448ee48f1b31af9ab
hash_pbkdf2:        f7f031ec49d4df55b530f2a2c0cb4fcefd3079b508e726b448ee48f1b31af9ab

Fixes:

-    $length = strlen(hash($algorithm, '', true));
-    if (strlen($password) > $length) {
+    $blockLength = mhash_get_block_size(constant('MHASH_' . strtoupper($algorithm)))
+    if (strlen($password) > $blockLength) {

...

-    return substr($digest, 0, 40);
+    return substr($digest, 0, $length);

Umm yeah so it uses mcrypt which you got removed :P... so I guess you need to do it the hard way:

$blockLengths = array('md5'=>64, ...);
$algorithm = strtolower($algorithm);
if (!isset($blockLengths[$algorithm]))
    die('Scott removed mcrypt sorry');
$blockLength = $blockLengths[$algorithm]);

One more thing:

function hashPbkdf2($algorithm, $password, $salt, $iterations, $length = 0)

The $length = 0 suggests this does something better than returning an empty string.

+    $hashLength = strlen(hash($algorithm, null, true));
+    if ($length <= 0) $length = $hashLength;
-    $blocks = ceil($length / strlen(hash($algorithm, null, true)));
+    $blocks = ceil($length / $hashLength);

But really you should add this if hash_copy() is available:

function hash_hmac_cached($ctxCached, $data)
{
    $ctx = hash_copy($ctxCached);
    hash_update($ctx, $data);
    return hash_final($ctx, true);
}

...

    $ctxCached = hash_init($algorithm, HASH_HMAC, $password);

And change these:

-    $ib = $block = hash_hmac($algorithm, $salt . pack('N', $i), $password, true);
+    $ib = $block = hash_hmac_cached($ctxCached, $salt . pack('N', $i));

...

-    $ib ^= ($block = hash_hmac($algorithm, $block, $password, true));
+    $ib ^= ($block = hash_hmac_cached($ctxCached, $block));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment