Three operations get confused constantly: encoding, hashing, and encryption. They transform data so it often looks the same in a log or a database, but they exist for completely different reasons. Mixing them up is how passwords end up Base64'd in a database and how "encrypted" tokens turn out to be readable by anyone.

This guide draws a hard line between the three, shows what each looks like in practice, and walks through the misuses that show up in real code reviews.

The one question that separates them

Before any definitions, ask: who is supposed to be able to reverse this, and do they need a secret to do it?

  • Encoding is reversible by anyone. No secret involved. Its job is representation, not protection.
  • Hashing is reversible by no one. It's a one-way function. Its job is verification and fingerprinting.
  • Encryption is reversible only by someone holding the key. Its job is confidentiality.

That single question — "is there a key, and who has it?" — resolves almost every real-world mix-up. Hold onto it.

Encoding: changing the representation

Encoding transforms data into a different format so it can survive a transport channel or fit a syntax. Base64 turns arbitrary bytes into a 64-character alphabet that's safe to drop into JSON, email, or a data URI. URL encoding (percent-encoding) escapes characters like spaces and

text
&
that would otherwise break a query string.

There is no security here. None. Anyone can decode it, and that's the entire point.

text
Base64 encode: "hello" -> "aGVsbG8=" Base64 decode: "aGVsbG8=" -> "hello"

In JavaScript:

js
const encoded = btoa("hello"); // "aGVsbG8=" const decoded = atob("aGVsbG8="); // "hello"

Legitimate uses of encoding:

  • Embedding a small image as a data URI.
  • Putting binary data (an image, a key blob) into a JSON field.
  • Safely placing user input into a URL with percent-encoding.
  • Wrapping cryptographic output (a ciphertext or a hash) so it's printable.

That last point is the source of endless confusion. A SHA-256 hash is raw bytes; you'll usually see it as hex or Base64 because it was encoded for display. The encoding is cosmetic. The hashing is the real operation underneath. If you want to inspect or convert encoded values, a Base64 encoder and decoder makes the round-trip obvious — and reinforces that anyone with the string can reverse it.

The classic encoding misuse

The canonical mistake: treating Base64 as a security measure.

js
// WRONG: this protects nothing const apiKey = btoa("sk_live_abc123"); localStorage.setItem("key", apiKey);

Anyone who opens dev tools runs

text
atob()
and reads the key. Base64 is not obfuscation, not encryption, not even a speed bump. If you catch yourself reaching for it to "hide" a value, you wanted encryption.

Hashing: one-way fingerprints

A cryptographic hash function takes any input and produces a fixed-size output (a digest) that is deterministic, fast to compute, and practically impossible to reverse. The same input always yields the same digest; changing a single bit of input changes the output completely.

text
SHA-256("hello") -> 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 SHA-256("hellp") -> ce8e... (entirely different)

Key properties:

  • One-way. You cannot derive the input from the digest.
  • Deterministic. Same input, same output, every time.
  • Collision-resistant. It's infeasible to find two inputs with the same digest.
  • Fixed length. SHA-256 is always 256 bits regardless of input size.

What hashing is for:

  • Integrity checks. Compare the hash of a downloaded file to a published one to confirm it wasn't tampered with.
  • Deduplication and content addressing. Git names objects by their hash.
  • Fingerprinting large data for quick comparison.
  • Password storage — but with critical caveats below.

Generating and comparing digests is a routine task; a SHA-256 hash generator is handy for verifying a checksum or sanity-checking that two strings really do produce the same digest.

Hashing passwords is a special case

Here's where developers get burned. A plain

text
SHA-256(password)
is not acceptable password storage, for two reasons:

  1. It's too fast. SHA-256 is designed to be fast, which is exactly wrong for passwords. An attacker with a leaked database can compute billions of guesses per second on a GPU.
  2. It's unsalted. Identical passwords produce identical hashes, so attackers precompute giant lookup tables (rainbow tables) once and crack everyone.

The fix is a slow, salted password hashing function built for this job — bcrypt, scrypt, or Argon2. They incorporate a per-user random salt and a tunable work factor so you can deliberately make each guess expensive.

text
// Conceptually: stored = argon2(password, salt, cost_parameters) // salt is random per user and stored alongside the hash

Use a vetted library for this. Don't assemble it from a general-purpose hash. "I salted my SHA-256" is better than nothing but still far too fast — the speed problem remains.

The classic hashing misuse

Beyond fast password hashes, the other common error is using a hash where you needed encryption. Hashing is one-way: if you ever need to get the original value back, hashing was the wrong tool. You can't "un-hash" a credit card number to charge it later. If you need to recover the plaintext, you need encryption.

Also retire MD5 and SHA-1 for any security purpose. Both have practical collision attacks. They're fine as non-security checksums; they are not fine for signatures, integrity guarantees against an adversary, or anything trust-bearing.

Encryption: reversible with a key

Encryption transforms plaintext into ciphertext using a key. Given the key, you can reverse it to recover the original. Without the key, the ciphertext is meaningless. This is the only one of the three that provides confidentiality you can undo.

There are two families:

  • Symmetric encryption uses the same key to encrypt and decrypt. AES is the workhorse. It's fast and ideal for encrypting data at rest or large payloads.
  • Asymmetric encryption uses a key pair: a public key to encrypt, a private key to decrypt. RSA and elliptic-curve schemes power TLS handshakes, signing, and key exchange.

A conceptual symmetric example:

text
ciphertext = AES_encrypt(plaintext, key, iv) plaintext = AES_decrypt(ciphertext, key, iv)

What encryption is for:

  • Protecting data in transit (TLS).
  • Encrypting sensitive fields or files at rest.
  • Anything you must read back later but must keep secret in the meantime — session data, stored tokens, personal information.

Encryption has sharp edges

Encryption is the easiest of the three to get subtly, dangerously wrong:

  • Use authenticated encryption. Prefer an AEAD mode like AES-GCM, which detects tampering. Plain CBC without a separate integrity check lets attackers modify ciphertext in ways that aren't caught.
  • Never reuse a nonce or IV with the same key. Reusing a nonce in GCM can leak the key stream and is catastrophic. Generate a fresh random IV for every message.
  • Key management is the hard part. The encryption is only as safe as the key. A key hardcoded in source, committed to a repo, or shipped in a mobile app's binary is not a secret.
  • Don't invent your own scheme. Use established libraries and standard modes.

The classic encryption misuse

The headline misuse is calling something "encrypted" when it's only encoded. If there's no key, it isn't encryption. Base64 is the perennial impostor here.

The second is storing data encrypted when it should have been hashed. Passwords are the textbook example. If your system can decrypt user passwords, so can an attacker who reaches your keys — and you've now made a breach far worse. Passwords should be hashed (with a slow KDF), never encrypted, because you never need the original back. You only ever need to check whether a submitted password produces the same digest.

A quick decision guide

Match the goal to the tool:

  • "I need to put binary or special characters somewhere text-only." → Encoding (Base64, URL encoding). No security expected.
  • "I need to verify data wasn't changed" or "check a password without storing it." → Hashing. Use SHA-256 for integrity; use Argon2/bcrypt/scrypt for passwords.
  • "I need to keep something secret but read it back later." → Encryption. Symmetric (AES-GCM) for data at rest; asymmetric for key exchange and signing.

And three red flags to watch for in code review:

  • Base64 used to "hide" or "protect" a value.
  • A bare, fast hash (
    text
    SHA-256
    , MD5) used for passwords.
  • Reversible encryption used for passwords, or a hardcoded encryption key.

The takeaway

Encoding, hashing, and encryption are not three strengths of the same thing — they answer three different questions. Encoding changes how data looks; anyone can reverse it. Hashing produces a one-way fingerprint nobody can reverse. Encryption locks data behind a key so only the key-holder can read it.

Get the question right — who needs to reverse this, and with what secret? — and the right tool falls out on its own. Get it wrong, and you ship a vulnerability that looks, in the database, exactly like the secure version.