0

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.

Steve Vinoski
  • 19,847
  • 3
  • 31
  • 46

1 Answers1

0

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);
Topaco
  • 40,594
  • 4
  • 35
  • 62