Hash data
Compute a SHA-256 fingerprint of text or bytes, so you can tell whether content changed or matches a published checksum.
Think of hash as a fingerprint for data. Feed it the same bytes and you get the same short value every time; change one byte and you get a completely different one. Anyone can take a fingerprint, no secret needed, so a hash tells you data is unchanged but not who sent it. Concepts shows where it sits next to the other three tools.
#Before you start
- Node.js 22.13 or later (24 or later on the 24 line), or any runtime with the Web Crypto API. Browsers expose it only on HTTPS pages and
localhost. - The whole input must be in memory. Web Crypto has no streaming digest, so for a multi-gigabyte file use your runtime's streaming API, such as
createHashfromnode:crypto. - A hash is the wrong tool for two jobs. To store passwords, use Argon2. To prove who sent a message, use an HMAC.
#Fingerprint a response
An ETag is how a browser asks "has this changed since last time?" and gets an empty 304 back when it hasn't:
import { hash } from "unsecure/hash";
/** The same body always gets the same tag, and any change gives a new one. */
export async function etag(body: string): Promise<string> {
return `"${await hash(body, { returnAs: "base64url" })}"`;
}
export async function serve(request: Request, body: string): Promise<Response> {
const tag = await etag(body);
if (request.headers.get("if-none-match") === tag) {
return new Response(null, { status: 304, headers: { etag: tag } });
}
return new Response(body, {
headers: { "etag": tag, "content-type": "application/json" },
});
}- Output shape:
returnAs: "base64url"gives you 43 characters that sit safely in a header. Leave it out and you get hex for a string, or aUint8Arrayfor bytes. - Text is UTF-8: the same string gives the same hash in every runtime. For an object, hash
JSON.stringify(value)and keep the key order stable, since{ a, b }and{ b, a }turn into different text. - Plain
===: neither tag is a secret, so an ordinary comparison is fine here. When the comparison decides who gets in, as with an API token, usesecureCompare.
#Check a download against its checksum
Release pages list a SHA-256 checksum next to each download. If your copy hashes to the same value, you got exactly the file they published:
/** Checks a download against the SHA-256 checksum its publisher lists. */
export async function matchesChecksum(file: Uint8Array, published: string): Promise<boolean> {
const digest = await hash(file, { returnAs: "hex" });
return digest === published.trim().toLowerCase();
}A listed checksum may be uppercase or come with a trailing newline, so the function trims and lowercases it before comparing.
#When it fails
Three of these throw, and the last one is a surprise rather than an error:
| Situation | What happens |
|---|---|
data is not text or bytes, such as a number or a parsed object | Throws INVALID_TYPE. Hash JSON.stringify(value) or the bytes instead. |
algorithm or returnAs is a name the library does not know | Throws UNSUPPORTED before any work. Algorithm names ignore case, so "sha-256" works. |
| The page runs over plain HTTP, or the runtime refuses the digest | Throws PLATFORM, with the runtime's error as cause. |
| Two objects with the same content give different hashes | Not an error: their serialized text differs. Serialize in a fixed key order. |
Each one is an UnsecureError with the code shown.
#Options
Both options are optional for hash(data, options):
| Option | Default | Notes |
|---|---|---|
algorithm | "SHA-256" | Also "SHA-384" and "SHA-512". "SHA-1" exists only for older systems that require it. |
returnAs | mirrors the input | "hex", "base64", "base64url" or "bytes". A string gives hex, bytes give a Uint8Array. |
#Going further
Tokens, aliases, types and SHA-1 come up once hashing is part of a larger design:
- API tokens: a token from a random generator is too unpredictable to guess, so storing its SHA-256 is safe, unlike a password. The API tokens recipe shows the whole flow.
- Aliases:
"b64","b64url"and"uint8array"mean the same as"base64","base64url"and"bytes". - Types: the return type follows
returnAs, sohash(text, { returnAs: "bytes" })comes back typed asUint8Arrayand you don't need a cast. - SHA-1: in 2017, researchers published two different PDFs with the same SHA-1. Don't pick it for anything where someone might try to forge the input.