How do I use Node.js Crypto to create a HMAC-SHA1 hash?

Javascriptnode.jsAlgorithmHashNode Crypto

Javascript Problem Overview


I want to create a hash of I love cupcakes (signed with the key abcdeg)

How can I create that hash, using Node.js Crypto?

Javascript Solutions


Solution 1 - Javascript

Documentation for crypto: http://nodejs.org/api/crypto.html

const crypto = require('crypto')
    
const text = 'I love cupcakes'
const key = 'abcdeg'
    
crypto.createHmac('sha1', key)
  .update(text)
  .digest('hex')

Solution 2 - Javascript

A few years ago it was said that update() and digest() were legacy methods and the new streaming API approach was introduced. Now the docs say that either method can be used. For example:

var crypto    = require('crypto');
var text      = 'I love cupcakes';
var secret    = 'abcdeg'; //make this your secret!!
var algorithm = 'sha1';   //consider using sha256
var hash, hmac;

// Method 1 - Writing to a stream
hmac = crypto.createHmac(algorithm, secret);    
hmac.write(text); // write in to the stream
hmac.end();       // can't read from the stream until you call end()
hash = hmac.read().toString('hex');    // read out hmac digest
console.log("Method 1: ", hash);

// Method 2 - Using update and digest:
hmac = crypto.createHmac(algorithm, secret);
hmac.update(text);
hash = hmac.digest('hex');
console.log("Method 2: ", hash);

Tested on node v6.2.2 and v7.7.2

See https://nodejs.org/api/crypto.html#crypto_class_hmac. Gives more examples for using the streaming approach.

Solution 3 - Javascript

Gwerder's solution wont work because hash = hmac.read(); happens before the stream is done being finalized. Thus AngraX's issues. Also the hmac.write statement is un-necessary in this example.

Instead do this:

var crypto    = require('crypto');
var hmac;
var algorithm = 'sha1';
var key       = 'abcdeg';
var text      = 'I love cupcakes';
var hash;

hmac = crypto.createHmac(algorithm, key);

// readout format:
hmac.setEncoding('hex');
//or also commonly: hmac.setEncoding('base64');

// callback is attached as listener to stream's finish event:
hmac.end(text, function () {
    hash = hmac.read();
    //...do something with the hash...
});

More formally, if you wish, the line

hmac.end(text, function () {

could be written

hmac.end(text, 'utf8', function () {

because in this example text is a utf string

Solution 4 - Javascript

Despite all the sample code for signing and verifying hashing algorithms, I still had experiment and tweak quite a bit to make it work. Here's my working sample which I believe has all edge cases covered.

It's URL safe (i.e. doesn't need to be encoded), it takes an expiry time, and will not unexpectedly throw an exception. There is a dependency on Day.js, but you can replace that with another date library or roll your own date comparison.

Written in TypeScript:

// signature.ts
import * as crypto from 'crypto';
import * as dayjs from 'dayjs';

const key = 'some-random-key-1234567890';

const replaceAll = (
  str: string,
  searchValue: string,
  replaceValue: string,
) => str.split(searchValue).join(replaceValue);

const swap = (str: string, input: string, output: string) => {
  for (let i = 0; i < input.length; i++)
    str = replaceAll(str, input[i], output[i]);

  return str;
};

const createBase64Hmac = (message: string, expiresAt: Date) =>
  swap(
    crypto
      .createHmac('sha1', key)
      .update(`${expiresAt.getTime()}${message}`)
      .digest('hex'),
    '+=/', // Used to avoid characters that aren't safe in URLs
    '-_,',
  );

export const sign = (message: string, expiresAt: Date) =>
  `${expiresAt.getTime()}-${createBase64Hmac(message, expiresAt)}`;

export const verify = (message: string, hash: string) => {
  const matches = hash.match(/(.+?)-(.+)/);
  if (!matches) return false;

  const expires = matches[1];
  const hmac = matches[2];

  if (!/^\d+$/.test(expires)) return false;

  const expiresAt = dayjs(parseInt(expires, 10));
  if (expiresAt.isBefore(dayjs())) return false;

  const expectedHmac = createBase64Hmac(message, expiresAt.toDate());
  // Byte lengths must equal, otherwise crypto.timingSafeEqual will throw an exception
  if (hmac.length !== expectedHmac.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(hmac),
    Buffer.from(expectedHmac),
  );
};

You can use it like this:

import { sign, verify } from './signature';

const message = 'foo-bar';
const expiresAt = dayjs().add(1, 'day').toDate();
const hash = sign(message, expiresAt);

const result = verify(message, hash);

expect(result).toBe(true);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser847495View Question on Stackoverflow
Solution 1 - JavascriptRicardo TomasiView Answer on Stackoverflow
Solution 2 - JavascriptAdam GriffithsView Answer on Stackoverflow
Solution 3 - JavascriptDaveView Answer on Stackoverflow
Solution 4 - JavascriptJohnny OshikaView Answer on Stackoverflow