7

I am attempting to replicate a PHP hash generation function in Node. This hash is used as part of an API. The PHP version creates the correct output that is accepted by the system. The Node version creates a different output despite what I believe to be the same inputs on the functions.

Is this because there is some fundamentally different way the PHP and Node HMAC functions work? Or is it because of some quirk with character encoding that I am just missing? Or have I just simply messed up something else?


PHP Code

$url = 'https://example.com/api/endpoint';
$user = 'apiuser';

// Example key
$key = '+raC8YR2F+fZypNJ5q+CAlqLFqNN1AlAfWwkwJLcI7jrAvppjRPikWp523G/u0BLSpN9+2LusJvpSwrfU9X2uA==';

$timestamp = gmdate('D, d M Y H:i:s T', 1543554184); // gmdate('D, d M Y H:i:s T');

$hashdata = "GET\n$url\n$user\n$timestamp\n";

print_r($hashdata);
/*
GET
https://example.com/api/endpoint
apiuser
Fri, 30 Nov 2018 05:03:04 GMT
*/

$decoded_key = base64_decode($key);

print_r(unpack('H*', $decoded_key));
// Array ( [1] => fab682f1847617e7d9ca9349e6af82025a8b16a34dd409407d6c24c092dc23b8eb02fa698d13e2916a79db71bfbb404b4a937dfb62eeb09be94b0adf53d5f6b8 )

$generated_hash = hash_hmac('sha256', $hashdata, $decoded_key, true);

$encoded_hash = base64_encode($generated_hash);

print_r($encoded_hash);
// vwdT8XhtSA1q+JvAfsRpJumfI4pemoaNFbjjc5JFsvw=

Node.js Code

crypto = require('crypto');
moment = require('moment-timezone');

let url = 'https://example.com/api/endpoint';
let api_user = 'apiuser';

// Example key
let api_key = '+raC8YR2F+fZypNJ5q+CAlqLFqNN1AlAfWwkwJLcI7jrAvppjRPikWp523G/u0BLSpN9+2LusJvpSwrfU9X2uA==';

let timestamp = moment.tz(1543554184 * 1000, 'GMT').format('ddd, DD MMM YYYY HH:mm:ss z'); // moment.tz(new Date(), 'GMT').format('ddd, DD MMM YYYY HH:mm:ss z');

let hash_data = 'GET\n' + url + '\n' + api_user + '\n' + timestamp + '\n';

console.log($hashdata);
/*
GET
https://example.com/api/endpoint
apiuser
Fri, 30 Nov 2018 05:03:04 GMT
*/

let decoded_key = Buffer.from(api_key, 'base64').toString('utf8');

console.log(Buffer.from(api_key, 'base64'));
// <Buffer fa b6 82 f1 84 76 17 e7 d9 ca 93 49 e6 af 82 02 5a 8b 16 a3 4d d4 09 40 7d 6c 24 c0 92 dc 23 b8 eb 02 fa 69 8d 13 e2 91 6a 79 db 71 bf bb 40 4b 4a 93 ... >

const hmac = crypto.createHmac('sha256', decoded_key);
hmac.update(hash_data);

// Not sure which should be closest to PHP
// Or if there is a difference
let encoded_hash = hmac.digest('base64');
// let encoded_hash = Buffer(hmac.digest('binary')).toString('base64');

console.log(encoded_hash);
// hmac.digest('base64') == eLLVC9cUvq6Ber6t9TBTihSoq+2VWIMUJKiL4/fIj3s=
// Buffer(hmac.digest('binary')).toString('base64') == eLLVC9cUvq6Ber6t9TBTihSoq+2VWIMUJKiL4/fIj3s=

Everything besides the HMAC functions output seems to be the same.

OS: Windows 10 - 64 Bit

Node.js Version: v10.13.0

PHP Version: 7.2.7

Verpos
  • 75
  • 1
  • 5
  • 3
    I've found this uqestion: [Different HMACs generated by nodejs and php](https://stackoverflow.com/q/43063221/7586), does that help? There is an `unpack` in the PHP code. – Kobi Dec 10 '18 at 05:25
  • 1
    @Kobi I found that in my searches too, that looks like the inputs where actually different though. In mine I only use an unpack to show that the byte[] are the same for the decoded key. The key for the HMAC should just be the base64 to utf8 decode of the api_key. If the byte[] is the same why would the output be different? – Verpos Dec 10 '18 at 05:38

1 Answers1

3

I can get the correct result in Node.js by keeping decoded_key a Buffer, and sending it directly as a Buffer to crypto.createHmac:

let decoded_key = Buffer.from(api_key, 'base64');
const hmac = crypto.createHmac('sha256', decoded_key);

This is supported , see crypto.createHmac:

key <string> | <Buffer> | <TypedArray> | <DataView>

Result is vwdT8XhtSA1q+JvAfsRpJumfI4pemoaNFbjjc5JFsvw= - same as PHP.
Working example: https://repl.it/repls/DisguisedBlankTechnologies

The problem must be with .toString('utf8'). I didn't find another encoding the works as a string, but it works just as well as a Buffer.

For completeness, another option supported by the Crypto module:

const hmac = crypto.createHmac('sha256', decoded_key);
hmac.write(hash_data);
hmac.end();
let encoded_hash = hmac.read().toString('base64');

Working example: https://repl.it/repls/LightcoralUnwelcomeProfessionals

Kobi
  • 125,267
  • 41
  • 244
  • 277
  • Wow, ok that works. That is really strange that the toString causes an entirely different output even though the byte[] is exactly the same. I knew the Buffer was an acceptable entry for the hmac function but the PHP hash_hmac() takes a string and AFAIK base64_decode() spits out utf8 so I thought it would be good. So is this a problem with the toString function then? – Verpos Dec 10 '18 at 06:37
  • 1
    @Verpos - I don't know *exactly* what causes the problem. I saw that `.toString('utf8')` doesn't exists in PHP so I tried to remove it from Node as well. Your code made it pretty clear the values were close, but there could be hidden changes - for example, there could be a change in endianness, and the buffer `console.log` is truncated. – Kobi Dec 10 '18 at 06:43
  • 1
    I have been playing with this a little and strange things happen when you "double" buffer strings / other buffers. [Check this out](https://repl.it/repls/SunnyGrandioseEmulators). With this in mind when we look at the [Crypto source](https://github.com/nodejs/node/blob/b010c8716498dca398e61c388859fea92296feb3/lib/crypto.js) we can see for the Hmac function (line 82) the key is passed to the toBuf function (line 32), where it is buffered again if it is a string causing the weird outcome. I am pretty new to Node and programming in general but is this expected? This seems like a bug to me. – Verpos Dec 10 '18 at 07:23
  • @Verpos - I saw [this answer](https://stackoverflow.com/a/18813806/7586) that is suggesting we're using the old API, with the new API being [`write`/`end`/`read`](https://repl.it/repls/LightcoralUnwelcomeProfessionals), and the comments on `toBuf` say it is meant to support older versions - maybe that's a clue. I'll add the current way because it is recommended, but I admit I'm a little confused at this point. – Kobi Dec 10 '18 at 08:08
  • @Verpos - OK, I read that wrong - `update`/`digest` *were* deprecated, but are now restored to former glory. – Kobi Dec 10 '18 at 08:13