0

I have 2 existing implementations of an HMAC hash for a 3rd party API. Java and Ruby match perfectly when doing SHA1 or SHA256, but Node doesn't match either. The code to implement in Node seems simple and straight forward, so I'm not sure where the difference is.

In Ruby:

def calculateRFC2104HMAC(canonicalizedData, accessKey, algorithm)
  digest = OpenSSL::Digest.new(algorithm)
  hmac = OpenSSL::HMAC.digest(digest, accessKey, canonicalizedData)

  return Base64.encode64(hmac)
end
# SHA1: SCN+L/M/nwwbk90VXBmEPe+18RU=
# SHA256: hgY38OlBKRsFlcBYAiX94blJPisXTIr08rvZDc7Ljhk=

In Java:

private static String calculateRFC2104HMAC(String data,String 
    accessKey,String algorithm) {

   SecretKeySpec signingKey=null;
   byte[]rawHmac=null;
   // get an hmac_sha256 key from the raw key bytes
   signingKey=new SecretKeySpec(accessKey.getBytes(), 
     algorithm);

   // get an hmac_sha256 Mac instance and initialize with the signing key
   Mac mac=Mac.getInstance(algorithm);
   mac.init(signingKey);
   // compute the hmac on input data bytes
   rawHmac=mac.doFinal(data.getBytes());

   // base64-encode the HMAC
   return new String(Base64.encodeBase64(rawHmac));
}
# SHA1: SCN+L/M/nwwbk90VXBmEPe+18RU=
# SHA256: hgY38OlBKRsFlcBYAiX94blJPisXTIr08rvZDc7Ljhk=

In Node:

_calculateRFC2104HMAC = ({canonicalizedData, accessKey, algorithm}) => {
    const hmac = crypto.createHmac(algorithm, accessKey);
    hmac.update(canonicalizedData);

    const hash = hmac.digest('base64');

    return hash;
};
# SHA1: GspTWly+Ezh2aW/QkKZA1o+qHRg=
# SHA256: FjVQ1Uj7866QZUv+jgLz/OahPbtPGEXpGwBbioqtBec=

I've verified the key and data is identical.

Edit: Looks like the issue is line endings. The data we build has to be separated by \n line return. This is done in both Java and Ruby just fine. But the same \n in Node makes the hash different and the request fail.

Soabirw
  • 43
  • 9
  • 1
    In 99% of cases, when people ask here why their identical bytestream is not compressed / encrypted / hashed identically, it turns out that their "identical" bytestream was not, in fact, identical. In the other 1% of cases, it turns out that the key / salt / parameters weren't identical. So, first check that you are *actually* hashing the same bytestream. *Not* the same string, the same *bytestream*. The "same" string (for various definitions of "same") can be encoded as many different bytestreams, depending on encoding or newlines, for example, so the string being the same is not enough. – Jörg W Mittag Aug 08 '18 at 13:50
  • @JörgWMittag I agree with this. If I removed the newlines everything matches. What I'm not sure how to resolve is that difference. Because the 3rd party requires newline separation. If their system uses Java and mine is Node and even locally Java and Node don't produce the same results when using newlines.... – Soabirw Aug 08 '18 at 14:47
  • I'm taking a wild guess just looking at the names, and would take a hard look at whatever is "canonicalizing" that `canonicalizedData`, since (I assume) the *whole point* of that canonicalizing is to make the data the *same*. – Jörg W Mittag Aug 08 '18 at 15:16
  • It's pretty simple bit of code that each one does. Basically they iterate over a map and create a string of the following format: "#{key}=#{value}\n". If I do anything other than \n they all match. Well, maybe not anything, but if I were to separate based on comma or semi-colon, for example, then they match perfectly. It is only an issue with \n (an maybe \t and other similar, I haven't verified that). – Soabirw Aug 08 '18 at 18:33

1 Answers1

0

Looks like the issue was a conflict in requirements. They wanted a \n to separate parameters and to have it included on the last pair as well. But they also wanted all white space trimmed.

If the trim was done at the end, it was removing that last \n. The trim needed to be done while building the pairs, while leaving \n on all pairs, including the last one.

Soabirw
  • 43
  • 9