Search code examples
hashsha256pbkdf2

Calculate PBKDF2 Sha256 in C# - Same results, different data


I've tried to calculate a hashed password.

The mechanism is decribed here:

German: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf

English: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf

First step, to get the challenge from an HTTP-Request with follwing result:

<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
    <SID>0000000000000000</SID>
    <Challenge>2$60000$f56cbe583d35a155cfbdf24c8d6bfa45$6000$99a2a02c9bd6989e52307a6e83bab646</Challenge>
    <BlockTime>0</BlockTime>
    <Rights></Rights>
    <Users>
        <User last="1">root</User>
    </Users>
</SessionInfo>

The challenge is build as following data:

<type>$<ter1>$<salt1>$<ter2>$<salt2>

<iter1> = 60000
<salt1> = f56cbe583d35a155cfbdf24c8d6bfa45
<iter2> = 6000
<salt2> = 99a2a02c9bd6989e52307a6e83bab646

The password will hashed as following (source from code in PDF):

# Hash twice, once with static salt...
hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1)

# Once with dynamic salt.
hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2)

And here is my C# Code to try the hashing:

private void CreateChallenge(String challenge, LoginData data) {
    // PBKDF2 (ab FRITZ!OS 7.24)
    if(challenge.Contains("$")) {
        String[] parts  = challenge.Split('$');
        String type     = parts[0];                             // 2
        int iter1       = int.Parse(parts[1]);                  // 600000
        byte[] salt1    = Encoding.UTF8.GetBytes(parts[2]);     // HEX
        int iter2       = int.Parse(parts[3]);                  // 600000
        byte[] salt2    = Encoding.UTF8.GetBytes(parts[4]);     // HEX

        System.Diagnostics.Debug.Print("Salt 1:  " + parts[2] + " ~> " + iter1);
        System.Diagnostics.Debug.Print("Salt 2:  " + parts[4] + " ~> " + iter2);

        // Calculate
        byte[] hash1 = HMAC(Encoding.UTF8.GetBytes(data.Password), salt1, iter1);
        System.Diagnostics.Debug.Print("Hashed PW: " + BitConverter.ToString(hash1).Replace("-", ""));

        byte[] hash2 = HMAC(hash1, salt2, iter2);
        System.Diagnostics.Debug.Print("Hashed Request: " + BitConverter.ToString(hash2).Replace("-", ""));

        this.CallSignin(parts[4] + "$" + BitConverter.ToString(hash2).Replace("-", "").ToLower(), data);
        // MD5-Fallback
    } else {
        this.CallSignin(challenge + "-" + MD5(challenge + "-" + data.Password), data);
    }
}

private byte[] HMAC(byte[] input, byte[] salt, int iterations) {

    using (var pbkdf2 = new Rfc2898DeriveBytes(input, salt, iterations, HashAlgorithmName.SHA256)) {

        return Encoding.UTF8.GetBytes(pbkdf2.ToString());
        //return pbkdf2.GetBytes(length);
    }
}

private String MD5(String input) {
    MD5 hasher              = System.Security.Cryptography.MD5.Create();
    byte[] data             = hasher.ComputeHash(Encoding.UTF8.GetBytes(input));
    StringBuilder output    = new StringBuilder();

    for(int index = 0; index < data.Length; index++) {
        output.Append(data[index].ToString("x2"));
    }

    return output.ToString().ToLower();
}

And here is a sample Call:

CreateChallenge("2$60000$f56cbe583d35a155cfbdf24c8d6bfa45$6000$99a2a02c9bd6989e52307a6e83bab646", new LoginData() {
    Username = "root",
    Password = "123Test"
});

But curiously i've get follwing output:

Salt 1:  f56cbe583d35a155cfbdf24c8d6bfa45 ~> 60000
Salt 2:  99a2a02c9bd6989e52307a6e83bab646 ~> 6000
Hashed PW: 53797374656D2E53656375726974792E43727970746F6772617068792E526663323839384465726976654279746573
Hashed Request: 53797374656D2E53656375726974792E43727970746F6772617068792E526663323839384465726976654279746573
Result: 99a2a02c9bd6989e52307a6e83bab646$53797374656d2e53656375726974792e43727970746f6772617068792e526663323839384465726976654279746573

Question

WHY is the Hashed PW complete the same as Hashed Request?

Both using other values..

Can anyone help me?

Change the HMAC Method and tried to use other mechanics for Rfc2898DeriveBytes


Solution

  • WHY is the Hashed PW complete the same as Hashed Request?

    Your HMAC() method, which is supposed to perform a key derivation with PBKDF2, returns pbkdf2.ToString(), which always is System.Security.Cryptography.Rfc2898DeriveBytes regardless of the input data.
    Correct would be to return pbkdf2.GetBytes(32) instead (this line is present in your code, but commented out for some reason).
    Note that PBKDF2 uses an HMAC internally, but is not identical to it, so the name of the method is misleading and it should be renamed.

    Fix of the method HMAC():

    private static byte[] HMAC(byte[] input, byte[] salt, int iterations)
    {
        using (var pbkdf2 = new Rfc2898DeriveBytes(input, salt, iterations, HashAlgorithmName.SHA256))
        {
            return pbkdf2.GetBytes(32); // apply GetBytes()
        }
    }
    

    A second problem is that the salt is hex encoded, so it must be hex decoded with Convert.FromHexString() (and not UTF-8 encoded with Encoding.UTF8.GetBytes()).

    Fix in CreateChallenge():

    ...
    byte[] salt1 = Convert.FromHexString(parts[2]); // hex decode...
    ...
    byte[] salt2 = Convert.FromHexString(parts[4]); // hex decode...
    ...
    

    The linked documentation describes a test case (here, sec. Calculating the Response Value):

    The example challenge “2$10000$5A1711$2000$5A1722” and the password “1example!“ (utf8-encoded) results in:

    hash1 = pbdkf2_hmac_sha256(“1example!”, 5A1711, 10000)
    => 0x23428e9dec39d95ac7a40514062df0f9e94f996e17c398c79898d0403b332d3b (hex)

    response = 5A1722$ + pbdkf2_hmac_sha256(hash1, 5A1722, 2000).hex()
    => 5A1722$1798a1672bca7c6463d6b245f82b53703b0f50813401b03e4045a5861e689adb

    When the fixed code for this test case is executed:

    CreateChallenge("2$10000$5A1711$2000$5A1722", new LoginData()
    {
        Username = "root",
        Password = "1example!"
    });
    

    the expected results are returned:

    Hashed PW: 23428E9DEC39D95AC7A40514062DF0F9E94F996E17C398C79898D0403B332D3B
    Hashed Request: 1798A1672BCA7C6463D6B245F82B53703B0F50813401B03E4045A5861E689ADB
    

    Note: In .NET 5 Rfc2898DeriveBytes() requires a salt which is at least 8 bytes long, in higher versions of .NET this constraint does not exist. Since in the test case only a 3 bytes salt is applied Rfc2898DeriveBytes() cannot be used with .NET 5. Instead C#/BouncyCastle can be applied:

    using Org.BouncyCastle.Crypto.Generators;
    using Org.BouncyCastle.Crypto.Parameters;
    ...
    private static byte[] HMAC(byte[] input, byte[] salt, int iterations)
    {
        var pbkdf2 = new Pkcs5S2ParametersGenerator(new Org.BouncyCastle.Crypto.Digests.Sha256Digest());
        pbkdf2.Init(input, salt, iterations);
        var pbkdf2KeyParam = (KeyParameter)pbkdf2.GenerateDerivedMacParameters(32 * 8);
        return pbkdf2KeyParam.GetKey();
    }