Help comparing HMAC

Hope someone can help me with this as I have been struggling with this for a few days. We are trying to run a SHA256 to create a base64 digest for a Shopify webhook to verify its authenticity. Basically, Shopify provides us with a x-shopify-hmac-sha256 header and response body.

We should derive our own HMAC and compare results. If our value is the same as the x-shopify-hmac-sha256 header then its authentic

The best article on how to handle this request if written for Node and can be found here Verifying Shopify webhooks with NodeJS & Express | by Scott Dixon | Medium

We will be happy to compensate/hire anyone that can assist us with this

Our current code looks like this (the response is incomplete as we are still working on it)

function byteStringToUint8Array(byteString) {
  const ui = new Uint8Array(byteString.length)
  for (let i = 0; i < byteString.length; ++i) {
    ui[i] = byteString.charCodeAt(i)
  }
  return ui
}

async function sha256(hmac, body) {
  const encoder = new TextEncoder()
  const secretKeyData = encoder.encode("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d11")
  const key = await crypto.subtle.importKey(
    "raw",
    secretKeyData,
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  )

  const receivedHmac = byteStringToUint8Array(atob(hmac))
  const dataToAuthenticate = body
  const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(dataToAuthenticate))
  const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)))

  console.log('Compare',{hmac, base64Mac})
}


async function handlePostRequest(event) {
  const request = event.request
  const shopifyBody = await request.clone().text()
  const shopifyHmac = await request.headers.get("x-shopify-hmac-sha256")
  //console.log('shopifyBody',shopifyBody)
  //console.log('shopifyHmac',shopifyHmac)

let isAuthentic = sha256(shopifyHmac, shopifyBody)
}

addEventListener("fetch", event => {
  try {
    const request = event.request
    const shopifyHeader = request.headers.get("x-shopify-hmac-sha256")
    if ((request.method.toUpperCase() === "POST") && shopifyHeader){
      return event.respondWith(handlePostRequest(event))
    }else{
    return event.respondWith(fetch(request))
    }
  } catch (e) {
    return event.respondWith(new Response("Error thrown " + e.message))
  }
})

Hey Man

Has you solved this yet? If not I can write up the solution for you.

No, I have not. Benn trying different things for days. Would love if you can assist

DO NOT USE IN PRODUCTION
The code shown below is just a rough demo. Read the code, understand what its doing, then write it robustly for production.

`

First setup an envar for your secret
So first of all we need to setup a envar for the SECRET so that your Shopify Sign Secret is not written in the code directly.

In my code below you’ll see “SECRET” which is an envar for this:

`

Second, create a new kv for testing
I created a new KV namespace for dumping output when Shopify makes a request. It’s only for testing and wouldn’t be kept in production code.

`

Third, the code:

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event))
})


function b64ToArrBuff(b64) {
    const byteString = atob(b64);
    let byteArray = new Uint8Array(byteString.length);

    for(let i=0; i < byteString.length; i++)
        byteArray[i] = byteString.charCodeAt(i);

    return byteArray;
}


async function handleRequest(event) {
  /* GET A CRYPTOKEY FROM THE SIGN SECRET PROVIDED BY SHOPIFY
   * You can find this sign secret at the bottom of the notification webhooks
   * from your stores settings notifications page. It's a 64 char long string.
   */
  let cryptoKey = await crypto.subtle.importKey(
    'raw',
    (new TextEncoder()).encode(SECRET),
    {
      name: 'HMAC',
      hash: {name: 'SHA-256'}
    },
    true,
    ['sign','verify']
  );


  /**
   * GET THE PAYLOAD AS AN ARRAY BUFFFER
   * This is the data which Shopify has sent you, its UTF-8 Stringified json
   * but at the moment we need it as an ArrayBuffer.
   */
  let requestPayload = await event.request.arrayBuffer();


  /**
   * CONVERT THE x-shopify-hmac-sha256 TO AN ARRAY BUFFER
   * It was sent to us as a base64 string, we need it to be an array buffer.
   */
  let signature = b64ToArrBuff( event.request.headers.get('X-Shopify-Hmac-Sha256') );


  /**
   * WE CAN NOW VERIFY
   * Now that we have the requestPayload, the Shopify Signature, and our
   * sign secret properly encoded, we can now verify if the request is legit.
   */
  let result = await crypto.subtle.verify(
    'HMAC',
    cryptoKey,
    signature,
    requestPayload
  );


  // CONVERT THE requestPayload ARRAY BUFFER TO TEXT
  let plaintext = (new TextDecoder()).decode(requestPayload);

  await KVDump.put('X-SHOPIFY-HMAC-SHA256', event.request.headers.get('X-Shopify-Hmac-Sha256'));
  await KVDump.put('THE REQUEST PAYLOAD', plaintext);
  await KVDump.put('VERIFY SUCCESS RESULT', result ? 'IS VALID' : 'IS NOT VALID');

  return new Response("Hello world")
}

At the end of my handleRequest function you can see I am doing some KV writing.

I’ll go into Shopify and press ‘Send test notification’

Here’s the results. You can see it passed.

I hope my solution has worked for you, I’m happy to provide further assistance if you need.

This is a community forum, no compensation is required.

Hey @user2765

I would like to thank you, your solution worked perfectly.

Much appreciated

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.