0

I am working with Payeezy API to handle payment on a webapp, their API for purchase requires a HMAC of payload signed using api secret. An excerpt from the docs:

Construct the data param by appending the parameters below in the same order as shown. a. apikey - API key of the developer. b. nonce - secure random number. c. timestamp - epoch timestamp in milliseconds. d. token - Merchant Token. e. payload - Actual body content passed as post request. Compute HMAC SHA256 hash on the above data param using the key below f. apiSecret - Consumer Secret token for the given api key Calculate the base64 of the hash which would be our required Authorization header value.

I found a library called jshashes on NPM and I tried to use their library to hash my header params, my code looks like this:

const payload = {
        "merchant_ref": "1-Sale",
        "transaction_type": "purchase",
        "method": "credit_card",
        "amount": amount * 100,
        "partial_redemption": "false",
        "currency_code": "USD",
        "credit_card": {
          "type": type,
          "cardholder_name": cardholder_name,
          "card_number": card_number,
          "exp_date": exp_date,
          "cvv": cvv
        }
      }
      const data = apikey + nounce + timestamp + token + JSON.stringify(payload)
      const sha256 = new Hashes.SHA256()
      const shaData = sha256.b64_hmac(apiSecret, data)

The outcome compared to the sample hashed value looks like this:

//mine
beWtpCGDv/iBoAUDAThGFXIge9eli/Xtl7JIBuR1bd4= 


//payeezy sample 
NmUzMTNmYWU0YjExM2UxMmM0NjllZGI1NThjY2M5MmUzMzE3NTFlZmQ1NDQxYzAzMTgwMmIwNDQ0MWVmYTdhMw== 

from the looks of the character counts I could tell that my hashing process is not correct but I can't figure out where went wrong.

I've seen similar questions being asked here but none answered, any help is appreciated.

ADDITION, I tried crypto library on Node.js:

const data = apikey + nounce + timestamp + token + JSON.stringify(payload)

  const hmac = crypto.createHmac('sha512', apiSecret)

  hmac.on('readable', () => {
    const data = hmac.read()
    if (data) {
      console.log(data.toString('base64'));
    }
  })

  hmac.write(data)
  hmac.end()

Same result, with only half the character length compared to the sample hashed value

UPDATE: After I used SHA512 on the data it finally returned a string that looks to have the same character length as the sample, but the validation is still not passing...

mxdi9i7
  • 677
  • 4
  • 13
  • Where are you converting to base64? – Maximilian Burszley May 31 '18 at 03:14
  • the method on the last line `b64_hmac()` I'm assuming it converts to base64 – mxdi9i7 May 31 '18 at 03:15
  • looks like you're doing SHA256 ... looks like they do SHA512 (based on the number of "bits" in the result) – Jaromanda X May 31 '18 at 03:17
  • https://developer.payeezy.com/payeezy-api/apis/post/transactions-3 The documentation said it's SHA256, click on header params, and the little '!' icon on Authorization header. – mxdi9i7 May 31 '18 at 03:19
  • I'm just counting bits – Jaromanda X May 31 '18 at 03:20
  • I've tried node.js's crypto library, same result, the hashed value is the same character length as the jshashes library I use... which is only half the length of their sample – mxdi9i7 May 31 '18 at 03:20
  • I don't see where you're getting sha256 then? unless the library is both hashing with sha256 at the same time it's converting to base64 – Maximilian Burszley May 31 '18 at 03:21
  • Based on the name of the method I assume that's what it's doing. I tried with crypto too, https://nodejs.org/api/crypto.html#crypto_hmac_digest_encoding, same result – mxdi9i7 May 31 '18 at 03:22
  • Oh ... I see what they are doing ... they get the HEX string HMAC (which is 64 characters long ... 4 bits per character = 256 bits) and THAT is what they convert to base64! – Jaromanda X May 31 '18 at 03:29
  • Thanks for pointing out the object object part, I actually fixed it in my code but havent edited the question. Could you clarify your comment a little? I'm not getting it – mxdi9i7 May 31 '18 at 03:31
  • Partial redemption is presented in the Purchase API, which is the one i'm working on, I'm not sure leaving it out would alter the auth result at all - but it is a optional parameter it seems – mxdi9i7 May 31 '18 at 05:21

1 Answers1

2

if you convert the base64 example from that site to a string

console.log(atob('NmUzMTNmYWU0YjExM2UxMmM0NjllZGI1NThjY2M5MmUzMzE3NTFlZmQ1NDQxYzAzMTgwMmIwNDQ0MWVmYTdhMw=='))

You get

6e313fae4b113e12c469edb558ccc92e331751efd5441c031802b04441efa7a3

This is a 64 character (256 bit) hex string

So my guess is that they get the hex string HMAC, and base64 encode that - which seems awfully stupid, hex is safe to send as is, why make it 4/3rds larger!!

if they simply used the base64 of the HMAC, it'd only be 45 characters long!!

Instead they get the 64 character hex string and base64 encode that to get 88 characters!! strange design decision!!

So, your code should do the same

like

Data = Buffer.from(sha256.hex_hmac(apiSecret, data), 'utf-8').toString('base64');

not sure if there's a better way in node to convert hex encoded string to base64, but that works

And finally (actually this bit is just to "match" how the authorization is calculated on the example page as linked by the OP https://developer.payeezy.com/payeezy-api/apis/post/transactions-3) So, it's not necessary to make the payload larger for no reason)

Another point you need to know, the payload JSON needs to be in a specific format it seems ... 2 space indented ... again, this is a damned stupid waste of bandwidth .. {"key":1234} takes 12 characters

{
  "key": 1234
}

takes 17

So, anyway, you need to do this:

JSON.stringify(payload,null, 2)

This last piece of the puzzle should make your code as follows

const data = apikey + nonce + timestamp + token + JSON.stringify(payload,null, 2)
const sha256 = new Hashes.SHA256()
const shaData = Buffer.from(sha256.hex_hmac(secret, data), 'utf-8').toString('base64');
Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • see this [pastebin](https://pastebin.com/vHwFbA7w) for the code that succeeded :p – Jaromanda X May 31 '18 at 04:28
  • I compared every single line and it just doesn't pass the HMAC validation check on their API :( – mxdi9i7 May 31 '18 at 04:35
  • I just used the API on the page you posted, copied all the relevant bits of information ... and got a match – Jaromanda X May 31 '18 at 04:37
  • perhaps you'd need to test it with my credentials – mxdi9i7 May 31 '18 at 04:37
  • I don't think so, that would be difficult, because of the whole timeout issue ... note: NONE of those values in the pastebin are "random" ... as much as, I've copied them from the request headers or "parameters" in the top part of that page where you can set these things up – Jaromanda X May 31 '18 at 04:39
  • I'm going to do the same in my code and compare their test values generated HMAC with the ones I generate with the same values – mxdi9i7 May 31 '18 at 04:41
  • how are you creating the timestamp and the nonce? are you sending both in the request headers as well as apikey and token? – Jaromanda X May 31 '18 at 04:44
  • Yes, all 5 of them: `headers: { 'apikey': apikey, 'token': token, 'Authorization': shaData, 'timestamp': timestamp, 'nounce': nounce },` – mxdi9i7 May 31 '18 at 04:49
  • I created a HMAC on their sandbox using my api keys and everything, then copied over the nounce and timestamp in my code to do the same operation, the result is still different, I even copied their request body – mxdi9i7 May 31 '18 at 04:50
  • Check this, https://pastebin.com/FqXXpap2, basically the same implementation as your code, but the sha value still dont match – mxdi9i7 May 31 '18 at 05:05
  • not sure why the previous comment has disappeared - you edited the payload `"partial_redemption": "false",` doesn't appear in there sample test data ... so I assume you must've added that - and if you do edit the sample payload, you have to create HMAC on that page – Jaromanda X May 31 '18 at 05:21
  • I think you commented on the question instead of the answer, my response was: Partial redemption is presented in the Purchase API, which is the one i'm working on, I'm not sure leaving it out would alter the auth result at all - but it is a optional parameter it seems – mxdi9i7 May 31 '18 at 05:22
  • try the purchase API instead of the authorize one, the example you did in the pastebin was on authorize api. Probably not going to make a difference because mine isn't matching – mxdi9i7 May 31 '18 at 05:23
  • do you run the example ... then copy all the values from the headers etc? because it always matches for me - but as I can't see the values you see when you try, I can only guess that you're missing something or doing something in the wrong sequence – Jaromanda X May 31 '18 at 05:29
  • I was able to get matching values for authorize api, but when I tested purchase api by changing only a couple things, it never matches, https://pastebin.com/PtvdJV6T – mxdi9i7 May 31 '18 at 05:38
  • well - I can get them to match as long as apikey, token, timestamp, apiSecret and nonce are all the same as in the test request - it's like you're missing some important step, but I can't tell which – Jaromanda X May 31 '18 at 05:48
  • one of their forum's question suggests that they don't really check for the HMAC key when it's sent with their sample api key – mxdi9i7 May 31 '18 at 14:35
  • Sure. But if your code doesn't create the same hmac then your code may be doing something wrong – Jaromanda X May 31 '18 at 21:30