将使用Rfc2898DeriveBytes的C# PBKDF2转换为PHP

5

简而言之,我们有一个使用.NET构建的会员系统,现在需要将其移植到WordPress,并且需要复制PBKDF2加密方式,以便用户无需重置密码。

使用已知的哈希密码,我已经能够轻松地在.NET中复制它,以下是代码:

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;
}

控制台应用程序将生成以下内容:
--Salt--
5SyOX+Rbclzvvit3MEM2nA==
--HashedPassword--
5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ
--IsValid--
True

然而在PHP方面,我无法得到相同的结果。目前我的代码如下:

$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);
}

结果如下:

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

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

非常感谢您的帮助。


你可以尝试这个,http://php.net/manual/zh/function.hash-pbkdf2.php - ArtisticPhoenix
@ArtisticPhoenix 谢谢,将其切换为自定义方法后得到了完全相同的结果,这简化了代码,这很好,但不幸的是仍然存在同样的问题。 - philba888
一个问题是你打印了 $iv 并将其标记为 SALT。接下来我会质疑 strlen 是否合适。但更难看出的问题是,.NET 通过 UTF-8 将字符串密码转换为字节。如果 PHP 使用 UCS-2 或 UTF-16,则二进制 HMAC 密钥不同。 - bartonjs
正如@bartonjs所提到的,对于字符集,您可能会发现这个链接很有用http://php.net/manual/en/function.mb-internal-encoding.php - ArtisticPhoenix
@ArtisticPhoenix 内部编码没有帮助。 - philba888
3个回答

4

最终我使用了https://github.com/defuse/password-hashing库,经过一些微小的更改使其匹配我正在导入的数据库中的哈希格式。

但我的主要问题在于这些行代码,我试图从哈希中获取一个密钥。

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

将其更改为以下内容,以便创建一个32位长的哈希散列,并将返回的哈希与盐连接起来,从而解决了该问题。
$dev = pbkdf2('sha1', '#0zEZcD7uNmv', $mySalt, 10000, 32, true);
echo 'PASS: '.base64_encode($mySalt.$dev).'<br />';

现在的输出结果与.NET相匹配:

PASS: 5SyOX+Rbclzvvit3MEM2nBRaPVo2M7ZTs7n3znXTfyW4OhwTlJLvpcUlCryblgkQ

1
我在搜索将遗留的Asp.Net MVC应用程序中的密码迁移到Laravel的方法时遇到了这篇文章。
对于那些只想比较生成的哈希值(即用于身份验证目的)的人,请考虑以下内容:
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;
}

0

看起来.NET Core现在(2022年)实现了两种格式。

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

我需要为 Laravel 实现两者,所以这是我的贡献:
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);
    }
}

你还可以使用自定义哈希器扩展Laravel: https://gist.github.com/tonila/5719aea8ad57df6821d7acdd1ed4ef1a


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接