javascriptcryptojshmachkdf

How to implement HKDF with CryptoJS?


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


Solution

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