PHP hash_hmac to JS Hmac

616 Views Asked by At

Im trying to verify a webhook signature, I have this PHP code from the documentation of api2cart but I need it in Javascript.I tried however I couldn't match the signature and the HMAC generated value, more details here

steps to follow:

  1. Gather all headers starting with “X-Webhook-” // I received it as x-webhook- in my header, don't know if it affects the encryption

  2. Remove “X-Webhook-Signature” from the array.

  3. Sort the headers alphabetically by the key

  4. Encode the headers into JSON.

  5. Concatenate the strings, JSON headers string should come first

  6. Hash the resulting string with HMAC SHA256 function with outputs raw binary data set true (raw_output = true)

Use your store_key as a secret key to generate a binary signature

  1. Encode the string in Base64

$headersForJson = [
'X-Webhook-Error-Code' => '0',
'X-Webhook-Action' => 'update',

'X-Webhook-Timestamp' => '1516291592',

'X-Webhook-Entity' => 'product',

'X-Webhook-Store-Id' => '1',

'X-Webhook-Signature' => 'SGVsbG8gd2l0aCBBUEkyQ2FydA==',

];

$signatureFromRequest = $headersForJson['X-Webhook-Signature'];

unset($headersForJson['X-Webhook-Signature']);

ksort($headersForJson);

$headers = json_encode($headersForJson);

$data = $headers . $params['raw_body'];

$generatedSignature = base64_encode(hash_hmac('sha256', $data, $storeKey, true));

 if (hash_equals($signatureFromRequest, $generatedSignature)) {
   return true;
 }

Here is what I did:

const signature = headers['x-webhook-signature'];
delete headers['x-webhook-signature'];
    // the header contained other keys I had to get keys starting with x-webhooks
    let xkeys = Object.keys(headers).filter(key => key.includes('x-webhook-')).sort();
    let xheaders = JSON.stringify(xkeys.reduce((res, key) => Object.assign(res, { [key]: headers[key] }), {}));
    let data = xheaders + rawBody


const generatedHash = createHmac('SHA256', "SecretKey")
            .update(data, 'utf-8')
            .digest('base64');


return generatedHash === signature

what am I missing here ?

2

There are 2 best solutions below

0
Taras Kubiv On

You need to convert header array keys from, for example, x-webhook-entity to X-Webhook-Entity

for example:

   let xheaders = JSON.stringify(
      xkeys.reduce((res, key) => Object.assign(
        res,
        { [key.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('-')]: headers[key] }),//format header key from x-webhook-entity to X-Webhook-Entity
        {}
      )
    );

Full code with Pipedream:

const crypto = require('crypto');
const storeKey = '5d780e682fbdbd4d04411be86ccd4b30';
const signature = event.headers['x-webhook-signature'];

const headers = event.headers;
delete headers['x-webhook-signature'];

// the header contained other keys I had to get keys starting with x-webhooks
let xkeys = Object.keys(headers).filter(key => key.includes('x-webhook-')).sort();
let xheaders = JSON.stringify(
  xkeys.reduce((res, key) => Object.assign(
    res,
    { [key.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('-')]: headers[key] }),//format header key from x-webhook-entity to X-Webhook-Entity
    {}
  )
);

const data = xheaders + JSON.stringify(event.body);
const generatedHash = crypto.createHmac('SHA256', storeKey)
            .update(data, 'utf-8')
            .digest('base64');

console.log(signature);
console.log(generatedHash);

let status = 403;

if (signature === generatedHash) {
  status = 200;
}

const response = await $respond({
  status: status,
  immediate: true,
  headers: {},
  body: {"signature":signature, "generatedHash":generatedHash} 
})

return JSON.parse(response.config.data).response
0
Jan Jarčík On

I had the same issue and I solved it with:

  1. headers must be in capital letters
  2. body must be JSON.stringify (req.body) and req.body must be JSON object, not text (not raw_body)

This works for me:

const validateRequest = (headers, body, storeKey) => {
  const webhookHeaders = {
    "X-Webhook-Action": headers["x-webhook-action"],
    "X-Webhook-Entity": headers["x-webhook-entity"],
    "X-Webhook-Error-Code": headers["x-webhook-error-code"],
    "X-Webhook-Store-Id": headers["x-webhook-store-id"],
    "X-Webhook-Timestamp": headers["x-webhook-timestamp"]
  };

  const signatureFromRequest = headers["x-webhook-signature"];
  const data = JSON.stringify(webhookHeaders) + JSON.stringify(body);
  const generatedSignature = crypto
    .createHmac("sha256", storeKey)
    .update(data)
    .digest("base64");

  return !!crypto.timingSafeEqual(
    Buffer.from(signatureFromRequest),
    Buffer.from(generatedSignature)
  );
};

you can call it with this:

const headers = {
  "x-webhook-entity": "product",
  "x-webhook-action": "update",
  "x-webhook-store-id": "0",
  "x-webhook-error-code": "0",
  "x-webhook-timestamp": "1635502511",
  "x-webhook-signature": "Ua1dtsDBi+37fEGr3mTHN7laZqLQpl+tEK02RDAg++0="
};
const body = { id: "28" };
const storeKey = "ed58a22dfecb405a50ea3ea56979360d";
const isValid = validateRequest(headers, body, storeKey);

console.log(isValid ? "success" : "failed"); // success