Search code examples
javascriptcryptographyhmacone-time-passwordtotp

Translate createHmac from crypto to crypto-js with Buffers


I am trying to translate a given Code using crypto to a version using crypto-js without luck so far.

I am not even able to get the same hash values. The final goal is to generate TOTP tokens and I need to do it with crypto-js, because I want to generate them in a webbrowser.

The critical part is "Step 1: Generate an HMAC-SHA-1 value", I am not able to get a identical result as in const hmacResult = hmac.digest(); in crypto-js...

const crypto = require('crypto');
const cryptojs = require('crypto-js');
const base32 = require('hi-base32');

function generateHOTP(secret, counter) {
   const decodedSecret = base32.decode.asBytes(secret);
   const buffer = Buffer.alloc(8);
   for (let i = 0; i < 8; i++) {
      buffer[7 - i] = counter & 0xff;
      counter = counter >> 8;
   }

   // Step 1: Generate an HMAC-SHA-1 value
   let hmacjs = cryptojs.algo.HMAC.create(cryptojs.algo.SHA1, cryptojs.enc.Utf8.stringify(decodedSecret) );
   hmacjs.update(buffer);
   console.log(hmacjs.finalize().toString());
   
   const hmac = crypto.createHmac('sha1', Buffer.from(decodedSecret)); 
   hmac.update(buffer);
   const hmacResult = hmac.digest();
   console.log(hmacResult.toString('hex'));
   
   // Step 2: Generate a 4-byte string (Dynamic Truncation)
   const code = dynamicTruncationFn(hmacResult);

   // Step 3: Compute an HOTP value
   return code % 10 ** 6;
}

function dynamicTruncationFn(hmacValue) {
   const offset = hmacValue[hmacValue.length - 1] & 0xf;

   return (
      ((hmacValue[offset] & 0x7f) << 24) |
      ((hmacValue[offset + 1] & 0xff) << 16) |
      ((hmacValue[offset + 2] & 0xff) << 8) |
      (hmacValue[offset + 3] & 0xff)
   );
}

function generateTOTP(secret, window = 0) {
   const counter = Math.floor(Date.now() / 30000);
   return generateHOTP(secret, counter + window);
}

console.log(generateTOTP('GEZDGNBVGY3TQOJQGEZDG', 0));

I tried diverse variation and consulted the documentations, but couldn't figure out a solution.


Solution

  • CryptoJS uses the WordArray data type internally, i.e. you have to convert decodedSecret (JavaScript array) and buffer (NodeJS buffer) into a WordArray and the resulting HMAC as WordArray into a NodeJS buffer.

    There are several approaches to this. One possibility is the conversion via latin1:

    // Step 1: Generate an HMAC-SHA-1 value
    let decodedSecretWA = cryptojs.enc.Latin1.parse(Buffer.from(decodedSecret).toString('latin1')); // JS array -> WordArray 
    let bufferWA = cryptojs.enc.Latin1.parse(buffer.toString('latin1'));                            // NodeJS Buffer -> WordArray
    let hmacjsWA = cryptojs.algo.HMAC.create(cryptojs.algo.SHA1, decodedSecretWA);
    hmacjsWA.update(bufferWA);
    let hmacResultjsWA = hmacjsWA.finalize();
    const hmacResultjs = Buffer.from(hmacResultjsWA.toString(cryptojs.enc.Latin1), 'latin1'); // WordArray -> NodeJS Buffer
    console.log(hmacResultjs);
    

    Another approach is to convert directly to a WordArray with cryptojs.lib.WordArray.create(), which can handle JavaScript arrays (via typed arrays) and NodeJS buffers. There is no CryptoJS support for the reverse direction, but e.g. CryptJsWordArrayToUint8Array() from here can be applied:

    // Step 1: Generate an HMAC-SHA-1 value
    let decodedSecretWA = cryptojs.lib.WordArray.create(new Uint8Array(decodedSecret)); // JS array -> WordArray 
    let bufferWA = cryptojs.lib.WordArray.create(buffer);                               // NodeJS Buffer -> WordArray
    let hmacjsWA = cryptojs.algo.HMAC.create(cryptojs.algo.SHA1, decodedSecretWA);
    hmacjsWA.update(bufferWA);
    let hmacResultjsWA = hmacjsWA.finalize();
    const hmacResultjs = Buffer.from(CryptJsWordArrayToUint8Array(hmacResultjsWA)); // WordArray -> NodeJS Buffer
    console.log(hmacResultjs);