The problem is that PasswordDeriveBytes
is only defined for the first 20 bytes - in that case it is PBKDF1 (not 2, as you are currently using in your Java code). Calling getBytes
multiple times may also change the result. The algorithm for one or more calls to getBytes
or for more than 20 bytes is Microsoft proprietary and doesn't seem to be described anywhere. In Mono it's even described as a non-fix as it may not be secure.
I would strongly suggest to use RFC2898DeriveBytes that does implement PBKDF2. Beware to only use it for ASCII input or it may not be compatible with the Java implementation.
The only other option is to figure out the proprietary extension of Microsoft PasswordDeriveBytes
to PBKDF1 (which only defines output up to the hash size of 20 bytes). I've reimplemented the version of Mono below.
Repeated request to Microsoft to update the API description of this function did not produce any results. You may want to read this bug report if your results differ.
This is the proprietary Microsoft extension. Basically it first calculates PBKDF-1 up to but not including the last hash iteration, call this HX. For the first 20 bytes it simply performs another hash, so it is compliant to PBKDF1. The next hashes are over the ASCII representation of a counter starting at 1 (so this is first converted to "1"
, then to 0x31
), followed by the bytes of HX.
What follows is a minimalistic, rather direct conversion from the Mono code:
public class PasswordDeriveBytes {
private final MessageDigest hash;
private final byte[] initial;
private final int iterations;
private byte[] output;
private int hashnumber = 0;
private int position = 0;
public PasswordDeriveBytes(String password, byte[] salt) {
try {
this.hash = MessageDigest.getInstance("SHA-1");
this.initial = new byte[hash.getDigestLength()];
this.hash.update(password.getBytes(UTF_8));
this.hash.update(salt);
this.hash.digest(this.initial, 0, this.initial.length);
this.iterations = 100;
} catch (NoSuchAlgorithmException | DigestException e) {
throw new IllegalStateException(e);
}
}
public byte[] getBytes(int cb) {
if (cb < 1)
throw new IndexOutOfBoundsException("cb");
byte[] result = new byte[cb];
int cpos = 0;
// the initial hash (in reset) + at least one iteration
int iter = Math.max(1, iterations - 1);
// start with the PKCS5 key
if (output == null) {
// calculate the PKCS5 key
output = initial;
// generate new key material
for (int i = 0; i < iter - 1; i++)
output = hash.digest(output);
}
while (cpos < cb) {
byte[] output2 = null;
if (hashnumber == 0) {
// last iteration on output
output2 = hash.digest(output);
} else if (hashnumber < 1000) {
String n = String.valueOf(hashnumber);
output2 = new byte[output.length + n.length()];
for (int j = 0; j < n.length(); j++)
output2[j] = (byte) (n.charAt(j));
System.arraycopy(output, 0, output2, n.length(), output.length);
// don't update output
output2 = hash.digest(output2);
} else {
throw new SecurityException();
}
int rem = output2.length - position;
int l = Math.min(cb - cpos, rem);
System.arraycopy(output2, position, result, cpos, l);
cpos += l;
position += l;
while (position >= output2.length) {
position -= output2.length;
hashnumber++;
}
}
return result;
}
}
Or, a bit more optimized and readable, leaving just the output buffer and position to be changed in between calls:
public class PasswordDeriveBytes {
private final MessageDigest hash;
private final byte[] firstToLastDigest;
private final byte[] outputBuffer;
private int position = 0;
public PasswordDeriveBytes(String password, byte[] salt) {
try {
this.hash = MessageDigest.getInstance("SHA-1");
this.hash.update(password.getBytes(UTF_8));
this.hash.update(salt);
this.firstToLastDigest = this.hash.digest();
final int iterations = 100;
for (int i = 1; i < iterations - 1; i++) {
hash.update(firstToLastDigest);
hash.digest(firstToLastDigest, 0, firstToLastDigest.length);
}
this.outputBuffer = hash.digest(firstToLastDigest);
} catch (NoSuchAlgorithmException | DigestException e) {
throw new IllegalStateException("SHA-1 digest should always be available", e);
}
}
public byte[] getBytes(int requested) {
if (requested < 1) {
throw new IllegalArgumentException(
"You should at least request 1 byte");
}
byte[] result = new byte[requested];
int generated = 0;
try {
while (generated < requested) {
final int outputOffset = position % outputBuffer.length;
if (outputOffset == 0 && position != 0) {
final String counter = String.valueOf(position / outputBuffer.length);
hash.update(counter.getBytes(US_ASCII));
hash.update(firstToLastDigest);
hash.digest(outputBuffer, 0, outputBuffer.length);
}
final int left = outputBuffer.length - outputOffset;
final int required = requested - generated;
final int copy = Math.min(left, required);
System.arraycopy(outputBuffer, outputOffset, result, generated, copy);
generated += copy;
position += copy;
}
} catch (final DigestException e) {
throw new IllegalStateException(e);
}
return result;
}
}
It's actually not all that bad security-wise as the bytes are separated from each other by the digest. So the key stretching is relatively OK. Note that there were Microsoft PasswordDeriveBytes
implementations out there that contained a bug and repeated bytes (see bug report above). This is not reproduced here.
Usage:
private static final String PASSWORD = "46dkaKLKKJLjdkdk;akdjafj";
private static final byte[] SALT = { 0x26, 0x19, (byte) 0x81, 0x4E,
(byte) 0xA0, 0x6D, (byte) 0x95, 0x34 };
public static void main(String[] args) throws Exception {
final Cipher desEDE = Cipher.getInstance("DESede/CBC/PKCS5Padding");
final PasswordDeriveBytes myPass = new PasswordDeriveBytes(PASSWORD, SALT);
final SecretKeyFactory kf = SecretKeyFactory.getInstance("DESede");
final byte[] key = myPass.getBytes(192 / Byte.SIZE);
final SecretKey desEDEKey = kf.generateSecret(new DESedeKeySpec(key));
final byte[] iv = myPass.getBytes(desEDE.getBlockSize());
desEDE.init(Cipher.ENCRYPT_MODE, desEDEKey, new IvParameterSpec(iv));
final byte[] ct = desEDE.doFinal("owlstead".getBytes(US_ASCII));
}
Side notes about the Java implementation:
- the iteration count is too low, check out what kind of iteration counts are required at the current date
- the key size is incorrect, you should create keys of 3 * 64 = 192 bits instead of 196 bits
- 3DES is getting old, use AES instead