How I Built a Free Hash Generator That Runs 100% in Your Browser
Every week I add a free tool to toolsofweb.com, and the newest one is a hash generator. It computes MD5, SHA-1, SHA-256, SHA-384 and SHA-512 — plus HMAC for all of them — without a server, an account, or a single byte of your data leaving the tab.
This isn't a launch post. It's a build log: the decisions I made, the crypto API quirks I hit, and the code that shipped.
Why client-side matters for this one
Most free hash tools online paste your input into a form, POST it to a server, and return the digest. For a hash generator, that's backwards: people hash things precisely because the data is sensitive — passwords before storing them, file checksums before trusting a download, API secrets when debugging HMAC signatures.
The site's rule is simple: anything you hash stays on your machine. The whole tool is a React component plus one TypeScript module, and the only "backend" is crypto.subtle — the WebCrypto API already living in your browser.
MD5: hand-rolled, RFC 1321, streaming
Here's the surprise: browsers no longer ship MD5 in WebCrypto. crypto.subtle.digest() supports SHA-1, SHA-256, SHA-384 and SHA-512, but MD5 was left behind because it's not collision-resistant anymore.
Still, people need MD5 constantly — legacy database fields, Gravatar URLs, htpasswd files (our htpasswd generator uses it). So I wrote an incremental MD5 implementation straight from RFC 1321:
export class Md5 {
private a = 0x67452301; // Initial state, straight from the RFC
private b = 0xefcdab89;
private c = 0x98badcfe;
private d = 0x10325476;
private buf = new Uint8Array(64); // one 512-bit block
...
}
The round constants are derived the way the spec demands — floor(abs(sin(i+1)) * 2^32) — no magic-number tables copied from the internet. And because the class is incremental (update() keeps 64 bytes of internal buffering and processes full blocks as they arrive), I can hash a 100 MB file in chunks without loading it all into memory. That matters on phones.
One detail I was careful about: I verified my implementation byte-for-byte against the MD5 function already powering the site's htpasswd generator. Two implementations agreeing on every test vector means a bug would have to exist identically in both — unlikely. md5("hello") gives the famous 5d41402abc4b2a76b9719d911017c592, as expected.
SHA family: let the browser do the heavy lifting
For SHA-1 through SHA-512, there was no reason to hand-roll anything. WebCrypto's digest() is implemented in the browser's native crypto layer — it's faster and better reviewed than anything I'd write:
const digest = await crypto.subtle.digest('SHA-256', data);
One gotcha worth knowing: crypto.subtle.digest() is async and takes an ArrayBuffer (a bare Uint8Array needs a cast). The tool computes all five digests in parallel with Promise.all for text input, so the UI updates essentially instantly — no loading spinner for normal text.
HMAC: half native, half by hand
The tool has an HMAC tab — you supply a key and a message, it signs it. For HMAC-SHA-*, WebCrypto has you covered:
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, data);
But HMAC-MD5 needs the hand-rolled MD5 again, built exactly per RFC 2104:
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
with ipad = 0x36 and opad = 0x5c repeated over a 64-byte block-sized key (hash the key first if it's longer than 64 bytes). Nothing exotic, but getting the padding exactly right is the whole game with HMAC, so I tested it against Python's hmac module output before shipping.
Four tabs, each answering one real question
The UI is four tabs, one per question people actually ask:
- Text — "What are the hashes of this string?" All five digests compute live as you type.
-
File checksum — "Is this download intact?" Drop a file (up to 100 MB) and it gets hashed locally —
FileReaderreads the bytes, WebCrypto/MD5 digests them, nothing uploads. Then paste the checksum the publisher listed and use… - Compare — "Does this match?" Paste the expected hash and your input; the tool auto-detects the algorithm from the hex length (32 chars → MD5, 40 → SHA-1, 64 → SHA-256, 96 → SHA-384, 128 → SHA-512) and reports match/mismatch.
- HMAC — "What's the signature for this webhook payload?" Key + message → signature.
Real example: after downloading a Linux ISO, the mirror usually publishes a SHA-256 checksum. Open the File checksum tab, drop the ISO, compare against the published value. If it matches, the file wasn't corrupted or tampered with in transit. Doing this without uploading a 3 GB file to someone's server is the entire point of building it client-side.
What I learned shipping this
-
WebCrypto is async everywhere. Mixing it with a synchronous hand-rolled MD5 meant making the MD5 path synchronous and the SHA path async, then joining results with
Promise.all. Fine, but plan the API boundary early. -
Verify against independent implementations. My MD5 cross-checks against the htpasswd generator's MD5; my HMAC-MD5 cross-checks against Python's
hmacmodule. One self-consistent implementation can be self-consistently wrong. - Streaming isn't optional for files. A naive implementation reads the whole file into memory. The incremental MD5 processes 64-byte blocks as they arrive; WebCrypto handles its own chunking internally. Keep the memory footprint flat.
- MD5 is still everywhere. People reach for it for checksums, cache keys, deduplication — non-security uses where collision resistance doesn't matter. A "SHA-only" tool would have missed half the use cases.
Try it
The tool is free, no signup, no ads: toolsofweb.com/hash-generator. Paste some text, drop a file, or check an HMAC — everything happens in your browser, and I never see any of it.
If there's a hash-related feature you'd want (bcrypt/argon2 wrappers, keccak, bulk file hashing), the site has a request-a-tool form — every request gets read.
Top comments (0)