node.js – Is Node Crypto createHmac() output different to PHP hash_hmac() output with same inputs?-ThrowExceptions

Exception or error:

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

How to solve:

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

Leave a Reply

Your email address will not be published. Required fields are marked *