Three operations get described with the same word, and the confusion is not academic. It produces systems where sensitive data is stored in a form anyone can read in one step, and it produces security reviews where somebody says “it’s encrypted” and everybody nods.
The distinction is simple once stated. Encoding changes the representation of data and is trivially reversible by anyone. Encryption changes the content of data and is reversible only with a key. Hashing destroys the data and is not reversible by anyone, ever, which is exactly what makes it useful.
Encoding: a change of alphabet, not a lock
Base64 is the encoding people most often mistake for security, so it is worth being blunt: it is a way of writing bytes using a restricted set of printable characters, and that is the whole of it. There is no key. There is no secret. Reversing it requires no information beyond the encoded text, and every programming language, every browser console, and a great many web pages will do it instantly.
Encoding exists to solve a transport problem. Some channels only carry text reliably: an email body, a JSON string, a URL, a config file. Arbitrary binary data put through one of those can be corrupted by anything that interprets bytes, so it is re-expressed in characters that survive the trip. That is a genuine and important job, and our own Base64 encoder exists because doing it correctly is slightly harder than it looks: the platform’s built-in function treats each string code unit as a byte, so text outside the single-byte range has to be converted to real UTF-8 bytes first or it mangles.
The rule to carry away is that if something can be reversed with no secret, it is encoding, and it protects nothing. Base64 in a cookie is not a security measure. Base64 in a configuration file is not hidden. A token that is “obfuscated” with Base64 is a plaintext token with extra steps.
Hashing: a one-way fingerprint
A cryptographic hash takes input of any size and produces a fixed-size output. The same input always produces the same output, a tiny change to the input produces a completely different output, and, critically, there is no way to work backwards from the output to the input.
That last property is not a matter of the reversing algorithm not having been invented yet. Hashing throws information away. A hash of a one-gigabyte file and a hash of a three-letter word are both the same length, so the function is not a mapping that could be inverted even in principle.
What people mean when they say a hash was “cracked” is that somebody guessed. Given a candidate input, hashing it and comparing is cheap, so an attacker with a list of likely inputs can find matches. That is why the choice of hash function matters so much for passwords, and why speed, normally a virtue, is a defect there.
Two properties define whether a hash function is still safe. Collision resistance means it should be infeasible to find two different inputs with the same output. Preimage resistance means it should be infeasible to find any input producing a given output. MD5 has lost the first comprehensively: collisions can be generated cheaply and deliberately. Our MD5 hash generator exists for the legitimate remaining use, which is verifying a file against a published checksum where the threat is accidental corruption rather than an adversary, and for working with legacy systems that still demand it. For anything where an attacker might be choosing the input, SHA-256 is the sensible default: it is the workhorse of modern verification, it is what Web Crypto exposes natively in every browser, and nothing about it is currently in doubt.
Note what neither is suitable for: storing passwords. Both are designed to be fast, and fast is precisely wrong when the attack is guessing. Password storage wants a function that is deliberately slow and memory-hungry, which is a different family of algorithm entirely.
Encryption: reversible, with a key
Encryption transforms data so that it can be recovered, but only by someone holding the key. That reversibility is the point, and it is also the thing that makes it the wrong tool for passwords: a system that can decrypt is a system that can be made to decrypt.
Doing it correctly involves more moving parts than people expect, and our text encryption tool is a reasonable illustration of what a defensible configuration looks like in practice.
A password is not a key. Human passwords are short, low-entropy and structured, and feeding one directly into a cipher would be catastrophic. It has to be stretched into a key by a key derivation function, and the stretching has to be slow enough to make guessing expensive. That tool uses PBKDF2 with HMAC-SHA-256 at 600,000 iterations, a figure taken from OWASP’s own password storage guidance rather than chosen for feel. The iteration count is the security parameter: it is what turns a guess from free into costly, multiplied across every guess an attacker makes.
The cipher itself is AES-256 in GCM mode. The mode matters as much as the cipher, because GCM is authenticated: it does not merely conceal the content, it detects tampering. Without authentication, an attacker who cannot read your ciphertext may still be able to modify it in ways that produce predictable changes in the decrypted output, and the recipient has no way to notice.
Finally, a fresh random salt and initialisation vector are generated for every single call. Reusing either is one of the classic ways to destroy an otherwise sound scheme: a reused salt makes precomputation viable again, and a reused IV in GCM is catastrophic rather than merely weakening.
HMAC: proving who sent it
There is a fourth thing that sits between hashing and encryption and answers a question neither does alone: how do you know a message came from who you think, and was not altered on the way?
A plain hash cannot answer that. If an attacker can modify the message, they can also recompute the hash, and the recipient sees a perfectly consistent pair. What is needed is a fingerprint that cannot be computed without a shared secret, which is what an HMAC is.
The construction hashes the message together with the key twice, using two different padding constants, in the specific nested arrangement the standard specifies. That nesting is not decoration; the obvious approaches of simply prepending or appending the key to the message have known weaknesses against the way hash functions are built, and HMAC’s structure exists to avoid them.
Our HMAC generator uses the browser’s native Web Crypto implementation for SHA-1, SHA-256 and SHA-512. MD5 is a special case worth mentioning because it shows the seam: Web Crypto’s digest interface offers only the SHA family, so HMAC-MD5, which some legacy systems still require, is implemented explicitly against the standard construction and checked against published test vectors. That is the right way round. A test vector from the specification is evidence; an implementation that merely looks correct is not.
HMACs are what sit under webhook signatures, API request signing, and session tokens that must not be forged. If you are verifying that a payload really came from a payment provider, an HMAC is almost certainly the mechanism.
Choosing, in one pass
Ask what you need to be able to do later.
If you need to get the original back and anyone should be able to, that is encoding. If you need to get the original back and only key-holders should be able to, that is encryption. If you never need the original back and only need to check whether a candidate matches, that is hashing. If you need to prove a message is unaltered and came from a specific party, that is an HMAC.
The commonest real-world mistake is reaching for encryption where hashing was correct, usually for passwords, and the second commonest is reaching for encoding where either was needed. Both come from the same root: treating three different operations as one blurry idea called making it secure.

