Hashes, passwords, and MACs
Use hashes for fingerprints
Use a modern cryptographic hash to identify bytes and detect accidental or adversarial changes when the expected digest is trusted.
A cryptographic hash turns any input into a fixed-size digest. Change one bit of the input and the digest looks completely different. That makes the digest a reliable fingerprint for a set of bytes.
SHA-256 is the standard choice today. It gives you a 32-byte digest, usually written as 64 hex characters:
shasum -a 256 app.zip
# 7c2df1a9e3b0f4d6... app.zip
Hashes identify files, content, and structured messages. Compute the digest on both ends of a transfer, compare, and you know the bytes match. Git is built on exactly this idea: every commit and every blob is addressed by its hash.
Verifying a download
The common workflow ships a checksum file next to the artifact. You run the check against that file:
shasum -a 256 -c app.zip.sha256
# app.zip: OK
OK means the file matches the digest in app.zip.sha256. Change one byte of the file and the same command prints FAILED.
Inside your own code you get the same digest with node:crypto:
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
createHash('sha256').update(readFileSync('app.zip')).digest('hex')
The trust problem
A downloaded checksum does not authenticate a file if the attacker can replace both the file and the checksum. This is the part most people skip.
Picture a download page that serves app.zip and its SHA-256 checksum from the same server. The server gets compromised. The attacker replaces both files, and every user sees a successful comparison. The math worked perfectly. The trust assumption was wrong.
A digest detects changes only when the expected value comes through a path the attacker does not control. That path can be a different server, a value you recorded earlier, or a digest published on a channel the attacker cannot touch. A signature can provide that path too, as long as you already trust the verification key. This is why serious projects sign their release checksums.
So every time you verify a fingerprint, ask where the expected digest came from. If the answer is “the same place as the file”, you verified transfer integrity and nothing more. That still catches corruption, which is useful. It does not catch an attacker.
Try this on your own: hash a small file, send the digest to yourself through a separate channel, and verify it from a clean copy. Change one byte and watch the check fail. Then replace both the file and the checksum sitting next to it, and explain why the command succeeds while authenticity still fails.
Lesson completed