JavaScript: SHA-256 hashing and HMAC with node:crypto
Hash a message, authenticate it with a secret key, and compare tags in constant time.
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));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
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));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
| Step / line | What happens | State after step | Output 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.