Reproducing JS PBKDF2 Hash in C#

感情迁移 提交于 2019-12-23 17:27:02

问题


I had to implement some new security features for an Existing Database. They used to hash passwords on the side of the client with a salt from the DB. They used this code to hash the passwords before sending them to the server:

var hash = CryptoJS.PBKDF2(password, USER_SALT, {
    keySize: 4,
    iterations: 1000
});

Now as you can already guess, this is highly insecure, cause an attacker gets the PW as if it was sent in plain text if the user gets the Salt from the server and the hasing is done client side. That's why I need to do it server side.

Now the DB has several thousand entries with hashed passwords of users, that use this DB. I tried implementing a similar method in C# with many references from the internet, but I couldn't get the same Hashes as stored in the DB.

    public static string HashPassword(string password, string salt)
    {
        byte[] saltBytes = Encoding.UTF8.GetBytes(salt);
        using (var rfc2898 = new Rfc2898DeriveBytes(password, saltBytes, 1000))
        {
            byte[] hash = rfc2898.GetBytes(16);
            string hashString = string.Empty;
            foreach (byte x in hash)
            {
                hashString += String.Format("{0:x2}", x);
            }
            return hashString;
        }
    }

This was the method I used. I wanted to reproduce the keysize 4 of CryptoJS so I'd get 4x = 32 digit long password hashes. I do get them, but they're different than the ones I would get in JS with CryptoJS.

Does anyone know how to get the same result as the CryptoJS version would? BEcause all passwords in the DB have been stored that way, it is crucial they are now generated the same way again only server side. (The Passwords are now encrypted against attacks).


回答1:


Using a JSFiddle for CryptoJS/PBKDF2 (though I had to correct the CDN links), whose relevant code is

var password = $("[name='password']").val();

var iterations = 1000;
// sizes must be a multiple of 32
var keySize = 128;
var ivSize = 128;
var salt = CryptoJS.lib.WordArray.random(128/8);

$("#salt").html(salt.toString(CryptoJS.enc.Base64));
$("#iter").html(iterations);
$("#keysize").html(keySize);
$("#ivsize").html(ivSize);

var output = CryptoJS.PBKDF2(password, salt, {
    keySize: (keySize+ivSize)/32,
    iterations: iterations
});

// the underlying words arrays might have more content than was asked: remove insignificant words
output.clamp();

var key = CryptoJS.lib.WordArray.create(output.words.slice(0, keySize/32));
var iv = CryptoJS.lib.WordArray.create(output.words.slice(keySize/32));

$("#hasher").html("SHA1");

$("#key").html(key.toString(CryptoJS.enc.Base64));
$("#iv").html(iv.toString(CryptoJS.enc.Base64));

I got as one sample input/output "hello" with a (base64) salt of 0CD1HGFdkclqcWG5aV+rvw== (and the default hash algorithm, with the specified 1000 iterations and 32+32 byte outputs); that produced tRczLRRuFy/zFiPn1PBKmQ== / dhyeE+0Dd9avSJbM/4TcNw==.

The following C# discovery code was then utilized:

string password = "hello";
byte[] salt = Convert.FromBase64String("0CD1HGFdkclqcWG5aV+rvw==");
int iterations = 1000;

using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA1))
{
    Console.WriteLine("UTF-8 / SHA-1");
    Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
    Console.Write(' ');
    Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}

using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
    Console.WriteLine("UTF-8 / SHA-2-256");
    Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
    Console.Write(' ');
    Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}

byte[] utf16 = Encoding.Unicode.GetBytes(password);
using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA1))
{
    Console.WriteLine("UTF-16LE / SHA-2-256");
    Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
    Console.Write(' ');
    Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}

using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA256))
{
    Console.WriteLine("UTF-16LE / SHA-2-256");
    Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
    Console.Write(' ');
    Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}

with an output of

UTF-8 / SHA-1
tRczLRRuFy/zFiPn1PBKmQ== dhyeE+0Dd9avSJbM/4TcNw==
UTF-8 / SHA-2-256
lkBtILt+xDNEQrX0aWUk3Q== ouOiijCw5sjfMcJo9YZ4Ug==
UTF-16LE / SHA-2-256
1T2gJFFECc5AnpvoiFrBwg== rmHsTuOQdM5YDsmzklMEUQ==
UTF-16LE / SHA-2-256
G4/Ik5vZAd2l8kwq45BKaw== Iqy61Eaf8jmoxO2TpA+rkg==

Conclusion: CryptoJS.PBKDF2 is using SHA-1 and UTF-8, which means that the most likely problem is that you're loading in the wrong salt if you're not getting the same answer. If it's not a UTF-8 string it's probably either hexadecimal data or base64 (but that depends completely on your usage, there's no "the right answer is" here, as the salt is "just bytes").



来源:https://stackoverflow.com/questions/53209486/reproducing-js-pbkdf2-hash-in-c-sharp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!