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
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).