Hashing shows up everywhere in frontend work: cache keys, Subresource Integrity (SRI), file checksums, webhook signatures, and content fingerprints. The same word — "hash" — also appears in password storage docs, which leads teams to misuse SHA-256 for secrets. Browser APIs make digests easy; choosing the right algorithm and threat model is the hard part.
Bottom line: Use SHA-256 (or stronger) for integrity checksums and fingerprints. Use HMAC when you need a keyed integrity check. Never use a bare SHA digest as a password hash. Prefer Web Crypto over home-grown hex loops, and keep secrets out of client-side code when the security property depends on them.
Who this guide is for
Frontend and full-stack developers who need to verify downloads, build deterministic cache keys, debug webhook signatures, or understand what crypto.subtle.digest actually guarantees. If you are designing login or password storage, jump ahead to the password section — the answer is "not SHA alone."
Hash families you will see
| Algorithm |
Output size |
Browser Web Crypto |
Typical use today |
| SHA-1 |
160 bits |
Yes (legacy) |
Avoid for security; still seen in old Git / tooling |
| SHA-256 |
256 bits |
Yes |
Default integrity checksum, SRI, JWT at_hash style digests |
| SHA-384 |
384 bits |
Yes |
Higher-assurance digests, some JWT profiles |
| SHA-512 |
512 bits |
Yes |
Large fingerprints, some HMAC configs |
All of these are cryptographic hash functions: one-way, fixed-length digests of arbitrary input. Given the same bytes, you always get the same digest. Given only the digest, you should not recover the input.
SHA-1 is cryptographically broken for collision resistance. Do not use it for new security controls. Prefer SHA-256 as the default; use SHA-384/512 when a protocol or compliance profile requires them.
Checksums vs password hashing
These problems look related and are not the same.
| Goal |
Right tool |
Wrong tool |
| Detect accidental corruption |
SHA-256 checksum |
— |
| Detect tampering by an attacker who can modify the file but not a secret |
HMAC-SHA-256 (or signature) |
Bare SHA of the file alone if the attacker can also replace the checksum |
| Store user passwords |
Memory-hard KDF: Argon2id, scrypt, bcrypt |
SHA-256(password) or SHA-256(password + salt) alone |
| Fast cache key from URL + locale |
SHA-256 of canonical string |
Encryption |
A checksum answers: "Did these bytes change?" Password hashing answers: "Can I verify a secret without storing it, while slowing attackers who steal the database?" Password KDFs are deliberately slow and parameterized; SHA-256 is deliberately fast.
If an attacker gets your password database and you stored SHA-256(password), they will crack common passwords at GPU speed. That is why frameworks use bcrypt/Argon2 — not because SHA is "weak math," but because it is the wrong job.
Integrity checksums in practice
Typical frontend workflows:
- Download verification — vendor publishes
SHA256 (file) = …; you hash the file locally and compare.
- Build artifact fingerprint — CI hashes a bundle and stores the digest next to the release.
- Deduplication — hash file contents to skip re-uploads of identical blobs.
- SRI —
<script integrity="sha384-…"> lets the browser reject a tampered CDN script.
Generate digests quickly with the Hash Generator for small text payloads while debugging. For large binaries, prefer CLI tools (sha256sum, shasum -a 256) so you are not pasting multi‑megabyte files into a page.
Hex vs Base64
Digests are raw bytes. Humans usually see them as lowercase hex (e3b0c4…) or Base64. Always compare the same encoding. SRI uses Base64; many Linux tools print hex. Mismatched encoding is a common false "corruption" report.
Web Crypto digest API
Modern browsers expose hashing through crypto.subtle.digest — asynchronous, constant-time friendly implementations, and available in secure contexts (HTTPS or localhost).
async function sha256Hex(text) {
const data = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest("SHA-256", data);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
const fingerprint = await sha256Hex('{"locale":"en","v":2}');
Supported algorithm names include "SHA-1", "SHA-256", "SHA-384", and "SHA-512". Inputs must be binary (BufferSource). Strings must be encoded explicitly — UTF-8 via TextEncoder is the usual choice. Hashing a string with a different encoding than your backend produces a different digest.
What Web Crypto does not do for you
- It does not salt passwords.
- It does not turn a digest into encryption (use AES-GCM via Web Crypto for confidentiality — see the AES guide and AES Encrypt/Decrypt).
- It does not authenticate a message by itself. Anyone can recompute SHA-256 of public data.
When HMAC fits
HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function (usually SHA-256) so that only parties who know the key can produce a valid tag.
Use HMAC when:
- A webhook provider signs the body with a shared secret (
X-Hub-Signature-256, Stripe-style signatures, etc.).
- Two backend services share a secret and need to detect tampering.
- You need integrity and authenticity under a shared-secret model (not public verifiability — that needs signatures).
Do not put the HMAC secret in frontend bundles if end users would become verifiers and forgers. Browser-side HMAC is fine for demos, local experiments, or cases where the "secret" is actually a user-supplied passphrase they already know. Production webhook verification belongs on the server.
async function hmacSha256Hex(secret, message) {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(message),
);
return [...new Uint8Array(sig)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
Debug expected tags with the HMAC Generator. When verifying webhooks, hash the exact raw body bytes — pretty-printing JSON before HMAC will break verification even when the logical payload is unchanged.
JWT, cookies, and "hash" confusion
- JWT signatures often use HMAC-SHA-256 (
HS256) or asymmetric algorithms. Verifying them is not the same as digest(token).
- Session tokens should be random opaque values (or signed JWTs), not
SHA-256(userId).
- CSRF tokens should be unguessable random bytes; hashing a predictable value does not help if the input is guessable.
Practical debugging workflow
- Confirm the exact input bytes (charset, whitespace, trailing newline).
- Confirm algorithm and output encoding (hex vs Base64 vs Base64url).
- Reproduce with the Hash Generator or HMAC Generator on a non-secret sample.
- For production secrets, verify on a secure machine with CLI or server code — do not paste live signing keys into any website.
- If confidentiality is required as well as integrity, use authenticated encryption (AES Encrypt/Decrypt for demos; proper key management in production).
SHA-256 on kilobytes of text is negligible in the browser. Hashing multi‑MB files on the main thread can jank the UI — move work to a Worker or stream chunks if you build a client-side uploader. Web Crypto is faster and safer than pure-JS SHA implementations for the same algorithm.
Practical takeaway
Pick SHA-256 for checksums and fingerprints; prefer SHA-384/512 when a profile requires them; avoid SHA-1 for new security work. Use HMAC when a shared secret must authenticate a message. Never store passwords as bare SHA digests. Reach for Web Crypto's digest and sign APIs for correct browser-side hashing, and keep production secrets on the server. codefunc's Hash Generator, HMAC Generator, and AES Encrypt/Decrypt tools run locally so you can experiment without uploading payloads.
Sources