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 createHash from node: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:

hash-data.ts
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 a Uint8Array for 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, use secureCompare.

#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:

hash-data.ts
/** 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:

SituationWhat happens
data is not text or bytes, such as a number or a parsed objectThrows INVALID_TYPE. Hash JSON.stringify(value) or the bytes instead.
algorithm or returnAs is a name the library does not knowThrows UNSUPPORTED before any work. Algorithm names ignore case, so "sha-256" works.
The page runs over plain HTTP, or the runtime refuses the digestThrows PLATFORM, with the runtime's error as cause.
Two objects with the same content give different hashesNot 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):

OptionDefaultNotes
algorithm"SHA-256"Also "SHA-384" and "SHA-512". "SHA-1" exists only for older systems that require it.
returnAsmirrors 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, so hash(text, { returnAs: "bytes" }) comes back typed as Uint8Array and 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.