Convert C# PBKDF2 using Rfc2898DeriveBytes to PHP
Asked Answered
A

3

5

Long story short have a membership system built in .NET that we are porting to WordPress and need to replicate the PBKDF2 encryption so users don't need to reset their passwords.

Using a know hashed password I've been able to replicate this in .NET easily, with the following code:

static void Main(string[] args)
{
  var isValid = CheckPassword("#0zEZcD7uNmv", "5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ");
}

public static int PBKDF2IterCount = 10000;
public static int PBKDF2SubkeyLength = 256 / 8; // 256 bits
public static int SaltSize = 128 / 8; // 128 bits

private static bool CheckPassword(string Password, string ExistingHashedPassword)
{
  byte[] saltAndPassword = Convert.FromBase64String(ExistingHashedPassword);
  byte[] salt = new byte[SaltSize];

  Array.Copy(saltAndPassword, 0, salt, 0, SaltSize);

  Console.WriteLine("--Salt--");
  Console.WriteLine(Convert.ToBase64String(salt));

  string hashedPassword = HashPassword(Password, salt);

  Console.WriteLine("--HashedPassword--");
  Console.WriteLine(hashedPassword);

  return hashedPassword == ExistingHashedPassword;
}

private static string HashPassword(string Password, byte[] salt)
{
  byte[] hash = new byte[PBKDF2SubkeyLength];
  using (var pbkdf2 = new Rfc2898DeriveBytes(Password, salt, PBKDF2IterCount))
  {
    hash = pbkdf2.GetBytes(PBKDF2SubkeyLength);
  }

  byte[] hashBytes = new byte[PBKDF2SubkeyLength + SaltSize];
  Array.Copy(salt, 0, hashBytes, 0, SaltSize);
  Array.Copy(hash, 0, hashBytes, SaltSize, PBKDF2SubkeyLength);

  string hashedPassword = Convert.ToBase64String(hashBytes);
  return hashedPassword;
}

The console app will produce the following:

--Salt--
5SyOX+Rbclzvvit3MEM2nA==
--HashedPassword--
5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ
--IsValid--
True

However in the PHP side I can't get the same results. My code so far is below.

$mySalt = base64_decode('5SyOX+Rbclzvvit3MEM2nA==');
$dev = pbkdf2('sha1', '#0zEZcD7uNmv', $mySalt, 10000, 48, true);
$key = substr($dev, 0, 32); //Keylength: 32
$iv = substr($dev, 32, 16); // IV-length: 16

echo 'PHP<br/>';
echo 'PASS: '.base64_encode($dev).'<br/>';
echo 'SALT: '.base64_encode($iv).'<br/><br/>'; 

echo '.NET<br/>';
echo 'PASS: 5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ<br/>';
echo 'SALT: 5SyOX+Rbclzvvit3MEM2nA==<br/><br/>'; 

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;
    }
    return substr($output, 0, $key_length);
}

And the results are:

PHP
PASS: FFo9WjYztlOzuffOddN/Jbg6HBOUku+lxSUKvJuWCRCsYe+1Tgbb8Ob4FtxumMal
SALT: rGHvtU4G2/Dm+BbcbpjGpQ==

.NET
PASS: 5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ
SALT: 5SyOX+Rbclzvvit3MEM2nA==

Any help would be appreciated.

Analyzer answered 9/9, 2016 at 1:31 Comment(5)
you could try this perhaps, php.net/manual/en/function.hash-pbkdf2.phpDorothydorp
@Dorothydorp Thanks, switching it for the custom method resulted in the exact same result, which is great that it simplified the code, but still have the same problem unfortunately.Analyzer
Well, one problem is that you're printing $iv and labelling it SALT. Next I'll question of strlen is appropriate. But the harder to see one is that .NET turns the string password into bytes via UTF-8. If PHP uses UCS-2 or UTF-16 then your binary HMAC keys aren't the same.Nikolos
as @Nikolos - mentioned, I would add for charsets you may find this useful php.net/manual/en/function.mb-internal-encoding.phpDorothydorp
@Dorothydorp internal coding didn't help.Analyzer
A
4

Ended up getting it working using the https://github.com/defuse/password-hashing libraries, with some minor changes match the format of hashes I was working with database I'm importing.

But my main problem was with these lines where I'm trying to get a key out of a hash.

$dev = pbkdf2('sha1', '#0zEZcD7uNmv', $mySalt, 10000, 48, true);
$key = substr($dev, 0, 32); //Keylength: 32
$iv = substr($dev, 32, 16); // IV-length: 16

Changing it to the below, so that it is creating a hash hash that is 32 bits long and joining the returning hash to the salt fixed the issue.

$dev = pbkdf2('sha1', '#0zEZcD7uNmv', $mySalt, 10000, 32, true);
echo 'PASS: '.base64_encode($mySalt.$dev).'<br />';

With the output below now matching .NET:

PASS: 5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ
Analyzer answered 17/9, 2016 at 8:43 Comment(0)
A
1

I ran into this post while searching for a way to migrate passwords from a legacy Asp.Net MVC application to Laravel.

For those interested in just comparing the generated hash (ie. for authentication purpose), please consider the following:

function legacyHashCheck($hash, $password)
{
    $raw     = base64_decode($hash);
    $salt    = substr($raw, 1, 16);
    $payload = substr($raw, 17, 32);

    //new Rfc2898DeriveBytes(password, salt, 1000).GetBytes(32)
    $check   = hash_pbkdf2('sha1', $password, $salt, 1000, 32, true);

    return $payload === $check;
}
Actinoid answered 28/1, 2021 at 23:25 Comment(0)
S
0

It seems .NET core implements 2 formats now (2022).

Source https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs

I needed to implement both for Laravel, so here is my contribution:

private function dotNetVerifyHash($hash, $password) {
    $version = ord($hash[0]);
    if ($version !== 0 && $version !== 1) {
        throw new \Exception('wrong version header: ' . $version);
    }
    if ($version === 0) {
        // Format: { 0x00, salt, subkey }
        $iterations = 1000;
        $subKeyLength = 32;
        $saltSize = 16;
        $salt = substr($hash, 1, $saltSize);
        $derived = hash_pbkdf2('sha1', $password, $salt, $iterations, $subKeyLength, true);
        $newHash = chr(0x00) . $salt . $derived;
    } else if ($version === 1) {
        // Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
        $unp = unpack('N3', substr($hash, 1, 12));
        $prf = $unp[1];
        $algorithm = '';
        switch ($prf) {
            case 0: $algorithm = 'sha1'; break;
            case 1: $algorithm = 'sha256'; break;
            case 2: $algorithm = 'sha512'; break;
            default: throw new \Exception('invalid prf: ' . $prf);
        }
        $iterations = $unp[2];
        $saltLength = $unp[3];
        $subKeyLength = 32;
        $salt = substr($hash, 13, $saltLength);
        $derived = hash_pbkdf2($algorithm, $password, $salt, $iterations, $subKeyLength, true);
        $newHash = chr(0x01) . pack('N3', $prf, $iterations, $saltLength) . $salt . $derived;

    }
    return $hash === $newHash;
}
function dotNetCreateHash($password, $version = 1) {
    if ($version !== 0 && $version !== 1) {
        throw new \Exception('invalid version: ' . ord($hash[0]));
    }

    $salt = Str::random(16);
    if ($version === 0) {
        // Format: { 0x00, salt, subkey }
        $dev = hash_pbkdf2('sha1', $password, $salt, 1000, 32, true);
        return base64_encode(chr(0x00) . $salt . $dev);
    } else if ($version === 1) {
        // Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
        $algorithm = 'sha256';
        $prf = 1;
        $iterations = 10000;
        $saltLength = strlen($salt);
        $subKeyLength = 32;
        $derived = hash_pbkdf2($algorithm, $password, $salt, $iterations, $subKeyLength, true);
        return base64_encode(chr(0x01) . pack('N3', $prf, $iterations, $saltLength) . $salt . $derived);
    }
}

And you can also extend Laravel with custom hasher: https://gist.github.com/tonila/5719aea8ad57df6821d7acdd1ed4ef1a

Sotos answered 6/6, 2022 at 16:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.