Trying to get pbkdf2_hmac to produce consistent results

I am try to implement an authentication protocol (RFC 5802 - Salted Challenge Response Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms)

I am at the point where I have salt a password with sha256.

I have a third party javascript client that does the same thing ie it does

console.log("iterations: " + iterations);
console.log("salt: " + salt);
console.log("password: " + password);
console.log("keyBits / 8: " + keyBits / 8);

var saltedPassword = crypto.pbkdf2_hmac_sha256(password, salt, iterations, keyBits / 8);

console.log("saltedPassword: " + Buffer.from(saltedPassword, 'utf8').toString('hex'));

This produces the result

base64_salt: SHdiM1g3K0paRHZWSjlPT3JYMmh1ZlJBZm8wTzFB
iterations: 10000
salt: Hwb3X7+JZDvVJ9OOrX2hufRAfo0O1A
password: pencil
keyBits / 8: 32
saltedPassword: 6cc28d45c29e24c296c2a641c3a0c2bcc3bb28200cc2bf1b5fc3a008c28234c2a1c3863ec38355c286c3b2c39671c2bfc3b5

On my rust code I am using the openssl crate and have code like

 let digest: MessageDigest = MessageDigest::sha256();
 let mut salted_password: [u8; 32] = [0; 32];

 println!("password: {:?}", password);
 println!("salt: {:?}", str::from_utf8(&salt).unwrap());
 println!("salt: {}", str::from_utf8(&salt).unwrap());

 let enc_result = pbkdf2_hmac(password.as_bytes(), &salt, 10000, digest, &mut salted_password);

 println!("salted_password: {:x?}", salted_password);

This produces

password: "pencil"
salt: "Hwb3X7+JZDvVJ9OOrX2hufRAfo0O1A"
salted_password: [6c, 8d, 45, 9e, 24, 96, a6, 41, e0, bc, fb, 28, 20, c, bf, 1b, 5f, e0, 8, 82, 34, a1, c6, 3e, c3, 55, 86, f2, d6, 71, bf, f5]

The password, salt and iteration seem consistent.
The results have some similarity but don't match.
Can anyone offer a reason why ?

Thanks

There are a lot more 0xc2 and 0xc3 bytes in the JavaScript output than I would expect to see in something that should be random, which a bit of investigation points to being from UTF-8 encoding. I would expect the JavaScript to produce the same output if you call Buffer.from(saltedPassword, 'latin1') instead, if that's Node.js.

1 Like

You are right. Buffer.from(saltedPassword, 'latin1') does produce the same output.

I am assuming the client is wrong. I think it should be utf-8 encoded,

The JavaScript code is representing the HMAC output bytes as a string of those bytes represented as 16-bit characters, like with String.fromCharCode(byte). I guess this was the best option before there were Buffer and Uint8Array types, but it’s going to be confusing to treat the bytes as Unicode characters and encode them as UTF-8 bytes.

It's not precisely Latin 1, either, but that gets you a buffer with the right bytes in it.

You can get the same result from Rust if you need to match the client like this:

let bytes: [u8; 32] = [0x6c, 0x8d, 0x45, 0x9e, 0x24, 0x96, 0xa6, 0x41, 0xe0, 0xbc, 0xfb, 0x28, 0x20, 0xc, 0xbf, 0x1b, 0x5f, 0xe0, 0x8, 0x82, 0x34, 0xa1, 0xc6, 0x3e, 0xc3, 0x55, 0x86, 0xf2, 0xd6, 0x71, 0xbf, 0xf5];
let s: String = bytes.iter().map(|b| *b as char).collect();
println!("{:x?}", s.as_bytes());

Or you can get the result directly as a Buffer in Node.js with the built-in crypto module:

const bytes = crypto.pbkdf2Sync('pencil', 'Hwb3X7+JZDvVJ9OOrX2hufRAfo0O1A', 10000, 32, 'sha256');
console.log(bytes.toString('hex'));

That's interesting
In the client code I originally had console.log(bytes.toString('hex'));
but it was printing printing strange characters to the console (linux)
even though I expecting hex. I don't know why.

I changed it to console.log("saltedPassword: " + Buffer.from(saltedPassword, 'utf8').toString('hex'));
to actually get something hex to print.

So it may be that change that caused the issue.

saltedPassword must be a string here, which silently discards any parameters to toString(). Only Buffer.toString() knows how to encode the string as hex, and Buffer.from(s, 'latin1') is the right way to convert this kind of string to a buffer.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.