Search code examples
cryptographyhashjava.netpbkdf2

Working Rfc2898DeriveBytes.Pbkdf2 in Java


I have some passwords hashed with C# using Rfc2898DeriveBytes.Pbkdf2(bytes,src,5000,HashAlgorithmName.SHA1,24) (an older implementation) and would like to port this code to java.

However I don't seem to get the two hashes to be the same. I have no idea why and how. I tried multiple implementations and they produce the same results in Java, different from C# .

I also posted on https://github.com/skradel/Zetetic.Security/issues/1 since the original code is using Zetetic.Security package.

C# code:

using System.Security.Cryptography;
using System;
using System.Text;

namespace MyApp
{
    class Program
    {
        private const string _ALGORITHM = "pbkdf2";

        public static string PrintBytes(byte[] byteArray)
        {
            var sb = new StringBuilder("new byte[] { ");
            for (var i = 0; i < byteArray.Length; i++)
            {
                var b = byteArray[i];
                sb.Append(b);
                if (i < byteArray.Length - 1)
                {
                    sb.Append(", ");
                }
            }
            sb.Append(" }");
            return sb.ToString();
        }

        static Program()
        {
            CryptoConfig.AddAlgorithm(typeof(Zetetic.Security.Pbkdf2Hash), _ALGORITHM);
        }



        public static string EncodePassword(string pass, int passwordFormat, string salt)
        {
            var bytes = Encoding.Unicode.GetBytes(pass);
            var src = Convert.FromBase64String(salt);
            byte[] inArray;
               var hashAlgorithm = HashAlgorithm.Create(_ALGORITHM);
                    Console.WriteLine("hashAlgorithm is KeyedHashAlgorithm");
                    var algorithm2 = (KeyedHashAlgorithm)hashAlgorithm;
                    Console.WriteLine("algorithm2.Key.Length == src.Length " + src.Length);
                    algorithm2.Key = src;
                    Console.WriteLine("Compute hash" + algorithm2.HashSize);
                    inArray = algorithm2.ComputeHash(bytes);
            Console.WriteLine("Bytes " + inArray.Length + PrintBytes(inArray));
            Console.WriteLine("Salt " + PrintBytes(src));


            var pass2 = Rfc2898DeriveBytes.Pbkdf2(bytes,src,5000,HashAlgorithmName.SHA1,24);
            Console.WriteLine(Convert.ToBase64String(pass2)); 
            return Convert.ToBase64String(inArray);
        }

        static void Main(string[] args)
        {
            var password = args[0];
            var salt = args[1];
            var format = Int32.Parse(args[2]);

            Console.WriteLine("==Convert password '" + password + "' using salt '" + salt + "' and format '" + format + "' ==");
            Console.WriteLine(EncodePassword(password, format, salt));

        }
    }
}

And the Java code:

byte[] saltBytes = saltStr.getBytes(UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKeySpec pbeKeySpec = new PBEKeySpec("my-secret".toCharArray(), saltBytes, 5000, 192);
Key secretKey = factory.generateSecret(pbeKeySpec);
java.util.Base64.getEncoder().encodeToString(key.getEncoded())

Besides the official Java implementation, I also tried 2 more: https://rtner.de/software/PBKDF2.html and the one from https://stackoverflow.com/a/1360237/638331 . I get the same hash in Java.

I have tried Zetetic implementation and the one from C# standard library and I am getting the same result there, but not as in Java.

UPDATE: The Java version is compatible with https://8gwifi.org/pbkdf.jsp . I could generate the same hash in Java as in the above JS app using the same params: 5000 iterations, PBKDF2WithHmacSHA1, 192 bits and my salt.

(BTW, the website sends data to a server, so don't use any real passwords / data)

The hash is different than the one in C# .

UPDATE: Example run for C#

$ ./bin/Debug/net7.0/pw-encoder mama dNuhxK7K5SaJk2M5HqwyNA== 1 
==Convert password 'mama' using salt 'dNuhxK7K5SaJk2M5HqwyNA==' and format '1' ==
hashAlgorithm is KeyedHashAlgorithm
algorithm2.Key.Length == src.Length 16
Compute hash192
Bytes 24new byte[] { 25, 111, 18, 46, 51, 211, 183, 94, 103, 93, 54, 65, 64, 46, 0, 105, 76, 70, 33, 212, 150, 24, 185, 168 }
Salt new byte[] { 116, 219, 161, 196, 174, 202, 229, 38, 137, 147, 99, 57, 30, 172, 50, 52 }
GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo
GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo

Using the same password and values with Java local code and https://8gwifi.org/pbkdf.jsp using parameters: PBKDF2WithHmacSHA1, 16 bit Initial Vector[ dNuhxK7K5SaJk2M5HqwyNA==] , 5000 iteration and 192 bits for dkLen gives me the hash bellow:

pR/jfNDZoVqCiXQ7YTGR3g/h7O3J/sMR

Solution

  • The encodings differ: In the C# code the salt is Base64 decoded, in the Java code it is UTF-8 encoded. In the C# code the password is Unicode encoded specifying UTF16-LE in .NET, in the Java code it is UTF-8 encoded.

    Fix: Use the same encodings in the Java code as in the C# code.

    However, there is a small problem: The JCA/JCE implementation of PBKDF2 performs a UTF-8 encoding internally, s. getPasswordBytes(). There is neither a possibility to change the encoding nor one that directly process a byte sequence instead of a char[], s. PBEKeySpec. Workaround: Apply another library, e.g. PKCS5S2ParametersGenerator from BouncyCastle. Here is a sample implementation:

    import java.nio.charset.StandardCharsets;
    import java.util.Base64;
    import org.bouncycastle.crypto.PBEParametersGenerator;
    import org.bouncycastle.crypto.digests.SHA1Digest;
    import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
    import org.bouncycastle.crypto.params.KeyParameter;
    ...
    String saltStr = "dNuhxK7K5SaJk2M5HqwyNA==";
    String passwordStr = "mama";
    byte[] salt = Base64.getDecoder().decode(saltStr);                                      // Fix: Base64 decode salt
    byte[] password = passwordStr.getBytes(StandardCharsets.UTF_16LE);                      // Fix: UTF-16LE encode password
    PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA1Digest());    // Fix: Apply a PBKDF2 implementation that can handle byte sequences
    generator.init(password, salt, 5000);
    byte[] key = ((KeyParameter)generator.generateDerivedParameters(192)).getKey(); 
    System.out.println(Base64.getEncoder().encodeToString(key)); // GW8SLjPTt15nXTZBQC4AaUxGIdSWGLmo
    

    This code provides the same result as the C# code.


    Remark regarding the UTF-16LE encoding: One could come up with the idea of converting the initial string from which the C# code generates the UTF-16LE byte sequence in such a way that a UTF-8 encoding generates the same byte sequence:

    String passwordStr = new String("<your password>".getBytes(StandardCharsets.UTF_16LE), StandardCharsets.UTF_8);     
    

    However, this does not work in general, but only in some cases, as not every UTF-16LE byte sequence is UTF-8 decodable.
    For instance, it is possible for characters with Unicode code points < 128 (ASCII characters), so it would work for mama, but not for mama§ (which would be corrupted during conversion). Also, the converted password contains whitespaces (which is not critical here, as the password is not printed out).