JavaScript Basics: Calculate SHA-256 Digests
A digest is a fixed-size representation of bytes. Use Node.js to hash text, combine chunks, and detect a changed input. These examples do not encrypt data or authenticate its sender.
These examples use built-in features and were tested on Linux with Node.js v24.19.0. No packages are needed. Assertions check the results before the program finishes.
Prepare a scratch directory
You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. These commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Hash exact text bytes
Save this as hash-text.mjs:
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
const digest = createHash('sha256').update('abc', 'utf8').digest('hex');
assert.equal(digest, 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
console.log(`SHA-256: ${digest}`);
console.log(`Hex characters: ${digest.length}`);Run it:
node hash-text.mjsOutput:
SHA-256: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Hex characters: 64createHash() starts a hash calculation. update() supplies bytes, using UTF-8 here. digest("hex") finalizes it and returns hexadecimal text. SHA-256 produces 32 bytes, represented by 64 hex characters.
2. Feed more than one chunk
Save this as hash-chunks.mjs:
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
const chunkedHash = createHash('sha256');
chunkedHash.update('a', 'utf8');
chunkedHash.update('bc', 'utf8');
const chunkedDigest = chunkedHash.digest('hex');
const wholeDigest = createHash('sha256').update('abc', 'utf8').digest('hex');
assert.equal(chunkedDigest, wholeDigest);
assert.throws(() => chunkedHash.update('more'));
console.log(`Chunked equals whole: ${chunkedDigest === wholeDigest}`);
console.log('Update after digest: rejected');Run it:
node hash-chunks.mjsOutput:
Chunked equals whole: true
Update after digest: rejectedChunked and whole-input hashes match when the chunks supply exactly the same bytes in the same order. Each string chunk is encoded separately, so splitting a UTF-16 surrogate pair can change the bytes. For Unicode text, encode the whole string to a Buffer before splitting it into chunks. Chunk boundaries are not included in the digest. After finalization, start a new hash object for another calculation. For large files, feed a stream rather than loading the entire file into memory.
3. Detect a changed input
Save this as compare-digests.mjs:
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
function digestText(text) {
return createHash('sha256').update(text, 'utf8').digest('hex');
}
const original = digestText('report');
const unchanged = digestText('report');
const withNewline = digestText('report\n');
assert.equal(original, unchanged);
assert.notEqual(original, withNewline);
console.log(`Same bytes match: ${original === unchanged}`);
console.log(`Added newline matches: ${original === withNewline}`);Run it:
node compare-digests.mjsOutput:
Same bytes match: true
Added newline matches: falseA newline changes the bytes and this digest. Matching hashes are useful for integrity checks, but the expected hash must come from a trusted source to detect malicious replacement. An attacker who can replace both data and digest defeats this comparison. Plain SHA-256 is not password storage, encryption, or a message-authentication mechanism; use purpose-built password hashing or authentication protocols for those jobs.
Clean up
In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:
rm -- hash-text.mjs hash-chunks.mjs compare-digests.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.