I am trying to implement HKDF with CryptoJS. (It's not allowed to use native crypto in my case.) The output of my implementation is different from the output from CyberChef, so I think my implementation is wrong somewhere. I cannot figure out what is wrong. Below is my code:
function toUint8Array(wordArray) {
// copy from: https://gist.github.com/lettergram/ba6733a854f835bca22b
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
var u8 = new Uint8Array(sigBytes);
for (var i = 0; i < sigBytes; i++) {
var byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
u8[i]=byte;
}
return u8;
}
/**
* HKDF (HMAC-based Key Derivation Function) implementation.
*
* @param {Uint8Array} salt - The salt value used in the extraction phase.
* @param {Uint8Array} ikm - The input key material.
* @param {Uint8Array} info - Contextual information for the extraction.
* @param {number} length - The desired length of the output key material in bytes.
* @returns {Uint8Array} The derived key material
*/
function hkdfSha256(salt, ikm, info, length) {
const prk = CryptoJS.HmacSHA256(CryptoJS.lib.WordArray.create(ikm), CryptoJS.lib.WordArray.create(salt)); // HKDF-Extract
let okm = CryptoJS.lib.WordArray.create();
let previousBlock = CryptoJS.lib.WordArray.create();
const wordArrayInfo = CryptoJS.lib.WordArray.create(info)
for (let i = 1; okm.sigBytes < length; i++) {
previousBlock = CryptoJS.HmacSHA256(previousBlock.concat(wordArrayInfo).concat(CryptoJS.lib.WordArray.create([i], 1)), prk);
okm.concat(previousBlock);
}
return toUint8Array(okm);
// return new Uint8Array(new Int32Array(okm.words).buffer, 0, length);
}
function main() {
const salt = CryptoJS.enc.Hex.parse('f339a9b6f339a9b6');
const ikm = CryptoJS.enc.Utf8.parse('Hello');
const info = CryptoJS.enc.Hex.parse('');
const length = 32;
let outputKeyMaterial = hkdfSha256(toUint8Array(salt), toUint8Array(ikm), toUint8Array(info), length);
console.log(CryptoJS.lib.WordArray.create(outputKeyMaterial).toString());
// output: 4ac2a7fe494a5920aaac2f4771ec73468a14f8af1d49bf3d11ce75c8fda8f4e1
// CyberChef: 3d18bc7eccb941ded1260bef702b94d899f4defa0365a49ee83543c59f336fe6
}
This is the recipe of the CyberChef
I haven't looked at the code in detail, but I suspect the problem is most likely somewhere in the data type conversions.
The following implementation is a CryptoJS port of the Python example implementation from Wikipedia and uses (as far as possible) the CryptoJS internal WordArray
type:
function hmac(digest, key, data) {
return CryptoJS.algo.HMAC.create(digest, key).finalize(data);
}
function hkdf_extract(digest, salt, ikm) {
if (!salt.sigBytes) {
var digestLen = digest.create().finalize('').sigBytes;
var salt = CryptoJS.enc.Latin1.parse("\0".repeat(digestLen));
}
return hmac(digest, salt, ikm)
}
function hkdf_expand(digest, prk, info, length) {
var okm = CryptoJS.lib.WordArray.create();
var t = CryptoJS.lib.WordArray.create();
var i = 0
while (okm.sigBytes < length) {
i++;
t = hmac(digest, prk, t.concat(info).concat(CryptoJS.lib.WordArray.create([i << 24], 1)));
okm = okm.concat(t);
}
okm.sigBytes = length;
okm.clamp();
return okm;
}
function hkdf(digest, salt, ikm, info, length){
var prk = hkdf_extract(digest, salt, ikm);
return hkdf_expand(digest, prk, info, length);
}
function main() {
// CyberChef test
const salt = CryptoJS.enc.Hex.parse('f339a9b6f339a9b6');
const ikm = CryptoJS.enc.Utf8.parse('Hello');
const info = CryptoJS.enc.Hex.parse('');
const length = 32;
const result = hkdf(CryptoJS.algo.SHA256, salt, ikm, info, length);
console.log(result.toString());
// Wikipedia test 1
const salt2 = CryptoJS.enc.Hex.parse('000102030405060708090a0b0c');
const ikm2 = CryptoJS.enc.Hex.parse('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
const info2 = CryptoJS.enc.Hex.parse('f0f1f2f3f4f5f6f7f8f9');
const length2 = 42;
const result2 = hkdf(CryptoJS.algo.SHA256, salt2, ikm2, info2, length2);
console.log(result2.toString());
// Wikipedia test 2
const salt3 = CryptoJS.enc.Hex.parse('');
const ikm3 = CryptoJS.enc.Hex.parse('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
const info3 = CryptoJS.enc.Hex.parse('');
const length3 = 42;
const result3 = hkdf(CryptoJS.algo.SHA256, salt3, ikm3, info3, length3);
console.log(result3.toString());
}
main();
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
This port provides the expected results regarding the CyberChef test and the Wikipedia tests.