Professional systems fail in predictable places: someone stores passwords the wrong way, treats Base64 like encryption, or ships a JWT and calls it “secure by default.” None of that is magic—it is just mixing up encoding, hashing, and encryption. This note is the mental model senior engineers expect you to have in design reviews, incident postmortems, and interviews.
The one-sentence map
| Idea | Reversible? | Typical goal | Common mistake |
|---|---|---|---|
| Encoding | Yes (by design) | Represent data safely for a channel or format | Confusing “not human readable” with “secret” |
| Hashing | No (for cryptographic hashes) | Fingerprint, verify integrity, store password verifiers | Using fast hashes (MD5/SHA-256) for passwords without a KDF |
| Encryption | Yes (with the right key) | Confidentiality (and sometimes authenticity with AEAD) | “Encrypting” passwords instead of hashing them |
If you only remember one thing: encoding is not a security control, hashing is not encryption, and encryption is not a drop-in replacement for hashing passwords.
Encoding: making bytes portable
Encoding answers: how do I represent this data so it survives transport, filing, or text protocols?
- Examples: UTF‑8 (text ↔ bytes), ASCII, hexadecimal, Base64, percent-encoding in URLs.
- Reversibility: intentionally trivial if you follow the scheme.
- Secrecy: none. Anyone can decode Base64. It obfuscates the way turning a page upside down obfuscates text.
The Base64 myth (and PEM files)
People say “it's encrypted because it's Base64.” No. Base64 expands binary into a printable alphabet so JSON, email bodies, or .pem wrappers can carry it. PEM is just labeled armor around a binary key or certificate—not a cryptographic boundary.
Encoding can pair with encryption (encrypt first, Base64 afterward for transport), but the encoding layer adds zero secrecy.
When encoding is exactly right
- Putting a ciphertext or signature in JSON.
- Embedding binary payloads in XML or HTML attributes.
- Displaying fingerprints as hex—because humans read hex, machines compare bytes.
When encoding is dangerously misleading
- Hiding payment IDs or user IDs “because they’re Base64.”
- Treating JWTs as encrypted payloads (most JWTs are merely signed, often with
HS256—see below).
Hashing: fingerprints and one-way verifiers
A cryptographic hash maps arbitrary input to a fixed-size digest. Good properties (for SHA‑256, BLAKE2, etc.):
- Pre-image resistance: hard to find an input that produces a given digest.
- Second pre-image / collision resistance: hard to find two inputs with the same digest (exact properties depend on the threat model and algorithm age).
Fast hashes vs password hashes
SHA‑256 is fast on purpose. That makes it great for integrity (git objects, download checksums, Merkle trees) and terrible for password storage, because attackers can guess billions of passwords per second with GPUs.
Password storage wants slow, memory-hard derivation with per-user randomness:
- Argon2id (modern default when available)
- scrypt
- bcrypt (still common; watch the 72-byte input limit)
- PBKDF2 (older, fine with high iteration counts and HMAC-SHA256)
These are often called password hashing functions or KDFs—they are not “just SHA‑256 with extra steps” in the same performance class.
Salts: defeating rainbow tables
A salt is a public random per-password value stored next to the verifier. It ensures two users with the same password do not share a hash, which collapses precomputed rainbow table attacks.
Rules of thumb:
- Use a CSPRNG salt per password (16+ bytes is typical).
- Salts do not need to be secret; they need to be unique.
- Peppers (a server-side secret mixed into the hash) can add defense-in-depth but complicate key rotation—treat as optional and operational, not a substitute for Argon2/bcrypt.
Practical password flow (register + login)
// Registration — only store Argon2id/bcrypt verifier + algorithm metadata.
// const verifier = await argon2.hash(password); // bindings differ; never roll your own
// Login — use the binding’s constant-time verify primitive.
// const ok = await argon2.verify(verifierFromDb, candidatePassword);If your ORM lets you accidentally log request bodies—stop that first. No algorithm saves you from plaintext retention.
Hashing for integrity—not secrecy
SHA‑256 over a release artifact proves “did this blob change?” It does not prove “did the right actor produce it?” For that you need digital signatures (asymmetric cryptography) or MACs (symmetric keyed integrity).
Interview tip: HMAC is not “hashing twice.” It builds a keyed hash using a nested structure that survives length-extension issues that naive secret + message hashing does not.
Encryption: secrecy with keys
Encryption provides confidentiality (and sometimes authenticated encryption combining confidentiality + integrity with AEAD modes like AES‑GCM and ChaCha20‑Poly1305).
-
Symmetric: one shared key (AES, ChaCha20). Fast for bulk data at rest (
LUKS, database TDE file layers) or in transit (TLS session keys—but TLS also authenticates endpoints with certificates). -
Asymmetric: key pairs (RSA, ECDH). Often used for key exchange/signing—not for hashing arbitrary gigabytes directly.
Encryption is reversible only with the keys you designed for decryption. Operational reality: plaintext exists at runtime in memory—it is not magically safe just because ciphertext is stored.
Important nuance—encryption preserves structure risk
Poor encryption preserves patterns (historic ECB mode meme blocks). Modern systems pick modes and constructions carefully—or use audited libraries exclusively.
“When NOT to encrypt passwords”
If you encrypt passwords with a server key you can decrypt them. Anyone with key material (operators, SSRF, insider, leaked env) suddenly has plaintext passwords.
We store verification codes derived by password hashing (Argon2id/bcrypt) so even a DB leak does not directly yield reusable passwords everywhere.
If you genuinely need escrow (rare!), that is exception territory with legal and threat modeling—not a bootstrap CRUD tutorial.
JWT misconception (interview fodder)
A JWT commonly has three Base64-encoded segments assembled with dots—a header, payload, signature.
- Signing proves tamper-evidence, not secrecy. With
HS256, anyone who knows the symmetric secret can mint tokens—which is OK if only your API knows that secret and you treat leakage as catastrophe. none/algconfusion histories exist because teams mixed verification policies. Libraries have hardened defaults, but misuse remains.
If you put PII inside a JWT and assume “because it looks random,” you've treated encoding + signing as encryption.
To carry secrets safely for third parties you'd want encryption constructs (often nested JWE), but the far better default for server sessions is opaque server-side identifiers with solid transport security.
Putting it together: three vignettes from production
1. File upload pipeline
Compute SHA‑256 of the inbound stream to dedupe/inventory; encrypt at rest with KMS-managed keys per tenant; optionally Base64 the ciphertext only because your metadata database is allergic to NUL bytes.
Three layers—integrity, confidentiality, transport-safe representation.
2. Forgot-password tokens
Issue a random token; store only a hash of that token plus expiry; verify with constant-time compare. Tokens are disposable secrets—like passwords but shorter-lived—and should not be reversible from DB leakage.
Some teams misuse JWTs here; ephemeral signed strings can work—but don’t reinvent timing attack resistance.
3. Ledger + audit hashes
Blockchain-like structures use hashes to bind history; privacy comes from other layers (commitments, ZK constructions, selective disclosure). Trying to encrypt inside naive hash chaining without thinking about payloads is mixing layers.
Common mistakes reviewers flag
| Mistake | Why it hurts | Prefer |
|---|---|---|
| “We Base64-encoded the API keys” | Cosmetic | Vault/secret stores, KMS, env injection—not encoding |
md5(password) | Collisions/instant guesses | Delete; rewrite with Argon2id migration path |
“We encrypted (AES) passwords” | Recoverable plaintext path | Dedicated password verifier with unique salts |
| Shipping JWT secrets to clients | Signing key becomes public | Audience separation, JWKS rotations, asymmetric keys for public verifiers |
| Logging request bodies with tokens | Game over | Central log redaction + structured logging defaults |
Interview answers you can reuse verbatim
-
“Why not SHA‑256 for passwords?” — Because defender and attacker both get a cheap primitive; we need asymmetric work factors favoring defenders via slow KDFs and unique salts.
-
“What’s the difference between signing and encryption?” — Signing proves origin/integrity; encryption hides content. JWTs are usually signed JSON, not encrypted secrets.
-
“Is Base64 encryption?” — No; it’s reversible encoding for representation. Secrecy requires keys and cryptographic goals beyond printable text.
-
“When is hashing enough?” — When you need fingerprints, integrity, or password verifiers—not when you need to recover the original message.
Closing
Encoding, hashing, and encryption solve different problems. Mixing them up doesn’t make you a bad person—it means you haven’t yet internalized the security contract each primitive offers. The teams that ship boring, correct systems are the ones that name the contract out loud: what are we protecting, from whom, for how long, and what breaks operationally when keys rotate?
Get that clarity, choose primitives with sober defaults (Argon2id, AEAD at rest/in transit), and reserve cleverness for the product—not for password storage schemes.