JavaScript: SHA-256 hashing and HMAC with node:crypto

Hash a message, authenticate it with a secret key, and compare tags in constant time.

JavaScript
const crypto = require("node:crypto");

const sha = (text) => crypto.createHash("sha256").update(text).digest("hex");
console.log(sha("abc"));
console.log(sha("abd"));

const message = "what do ya want for nothing?";
const tag = crypto.createHmac("sha256", "Jefe").update(message).digest();
console.log(tag.toString("hex"));

const again = crypto.createHmac("sha256", "Jefe").update(message).digest();
const forged = crypto.createHmac("sha256", "Jefe").update("what do ya want for something?").digest();
console.log("same message valid: " + crypto.timingSafeEqual(tag, again));
console.log("tampered message valid: " + crypto.timingSafeEqual(tag, forged));
Output
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
same message valid: true
tampered message valid: false

Explanation

A cryptographic hash such as SHA-256 maps any input to a fixed 32-byte digest. Changing one character of the input changes the digest completely, so hashes are used to check integrity. Anyone can compute a plain hash, though, so it does not prove who produced the message.

An HMAC mixes a secret key into the hash. Only a holder of the key can create a valid tag, so a matching tag proves both integrity and authenticity. The tag below is the RFC 4231 test value for key Jefe. crypto.timingSafeEqual compares two buffers in time that does not depend on their contents, which defeats timing attacks.

Explore the dry run
Guided walkthroughReady
1const crypto = require("node:crypto");
2
3const sha = (text) => crypto.createHash("sha256").update(text).digest("hex");
4console.log(sha("abc"));
5console.log(sha("abd"));
6
7const message = "what do ya want for nothing?";
8const tag = crypto.createHmac("sha256", "Jefe").update(message).digest();
9console.log(tag.toString("hex"));
10
11const again = crypto.createHmac("sha256", "Jefe").update(message).digest();
12const forged = crypto.createHmac("sha256", "Jefe").update("what do ya want for something?").digest();
13console.log("same message valid: " + crypto.timingSafeEqual(tag, again));
14console.log("tampered message valid: " + crypto.timingSafeEqual(tag, forged));

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The SHA-256 digest of abc is a standard test vector.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
One changed letter gives an unrelated digest.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
The tag depends on both the message and the key.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
This is the published RFC 4231 value.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
The receiver recomputes the tag, and the changed message gives a different one.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
same message valid: true
tampered message valid: false

Cost and common mistakes

O(m) time for an m-byte message.

  • timingSafeEqual throws if the two buffers have different lengths. Check the lengths first.
  • Never use a plain hash like SHA-256 to store passwords. It is far too fast; use a password hashing function.
  • Keep secret keys out of source code. Load them from the environment or a secrets manager.

Try it yourself

Does an HMAC hide the message?

Reveal answer and reasoning

No. It proves who wrote the message and that it was not changed, but the message itself is still readable.

Further reading