Hashing vs encryption vs signing
Hashing, encryption, and signing are three different primitives engineers constantly confuse. Each guarantees something distinct — and using the wrong one is how passwords leak, sessions get forged, and "it's encrypted" turns out to mean nothing.
A junior on your team ships a “secure” password store: AES-256 over each password, key in the config. Code review nods — AES-256, sounds strong. Six months later a backup leaks, the key leaks with it, and every password in the database is recoverable in plaintext. The bug wasn’t a weak algorithm. It was the wrong primitive entirely: passwords must be hashed, not encrypted, precisely because hashing is the one operation you can’t reverse. The same week, another engineer “signs” an API payload by SHA-256-ing it — proving nothing, because anyone can recompute that hash. Three primitives, three different guarantees, and reaching for the wrong one is one of the most common ways otherwise-careful systems get breached.
By the end of this lesson you’ll be able to say, for hashing, encryption, and signing, exactly what each one guarantees, what it does not, and which one a given problem actually needs.
The three primitives, and the one property that separates them
The fastest way to keep these straight is to ask one question of each: can you get the original back?
- Hashing is a one-way function.
SHA-256("hello")always yields the same 256-bit digest, but there is nounhash()— the input is destroyed. You verify by recomputing the hash of a candidate and comparing. Its job is integrity and fingerprinting: did this bytes-for-bytes match? It says nothing about confidentiality (the digest is public) and nothing about who produced it. - Encryption is reversible with a key.
encrypt(plaintext, key)produces ciphertext thatdecrypt(ciphertext, key)turns back into plaintext. Its job is confidentiality: keep the content secret from anyone without the key. By itself, classic encryption does not prove the message wasn’t tampered with — which is why modern systems use authenticated encryption (AES-GCM, ChaCha20-Poly1305) that bolts an integrity tag on. - Signing is the asymmetric one. You sign with a private key and anyone verifies with the matching public key. Its job is authenticity (this came from the holder of the private key) plus integrity (and wasn’t changed since). It deliberately does not hide the content — a signed JWT is readable by anyone.
What each one does not guarantee — the failure modes
The breaches come from the negative space: assuming a primitive gives you a property it never promised.
Hashing is not encryption. A hash is public-safe but not secret-safe. “We hash the SSNs” protects nothing if the input space is small — an attacker hashes every possible SSN (there are only ~10⁹) and matches them in seconds. Hashing only hides what is high-entropy and unguessable. This is also why a plain hash is wrong for passwords: passwords are low-entropy, so you need a slow, salted password hash (bcrypt, scrypt, Argon2id) whose entire point is to be expensive to compute, defeating offline brute-force. SHA-256(password) is billions of guesses per second on a GPU; Argon2id with proper parameters is a handful.
Encryption is not integrity, and not authentication. Classic CBC-mode ciphertext can be modified by an attacker who can’t read it — flip bits in the ciphertext, flip bits in the decrypted plaintext (the padding-oracle and bit-flipping attack families). “It’s encrypted” does not mean “it’s untampered” unless you use an AEAD mode that authenticates. And encryption says nothing about who sent it: a shared key means anyone with the key could have produced the message.
Signing is not encryption. A signature is attached to data that stays in the clear. People routinely think a signed JWT is “secure” and put secrets in its payload — but the payload is just base64, readable by anyone who intercepts it. Signing protects against forgery and tampering, never against eavesdropping.
▸Why this works
Why is signing asymmetric (private to sign, public to verify) when a shared-key MAC like HMAC also proves integrity? Because of who can do what. With HMAC, the verifier holds the same secret as the signer — so the verifier could have forged the message too; it proves integrity only between mutually trusting parties. A digital signature splits the key: only the private-key holder can produce a valid signature, and the whole world can verify it without being able to forge. That’s why TLS certificates, software releases, and JWTs signed with RS256/ES256 use asymmetric signatures — you need third parties to verify origin without trusting them with forging power. HMAC (HS256) is the right tool only when one party both issues and checks the token.
Choosing the right primitive per problem
The senior move is to map the requirement to the primitive, not the algorithm name to a vibe of “strong.” Walk the requirement: do I need to keep this secret (confidentiality → encryption), prove it wasn’t changed (integrity → hash/MAC/signature), prove who produced it (authenticity → signature or MAC), or store a secret I only ever need to check, never read back (one-way → password hash)?
| Requirement | Primitive | Concrete choice | The trap to avoid |
|---|---|---|---|
| Store a password | Slow password hash | Argon2id (or bcrypt/scrypt) + per-user salt | Encrypting it (key leak = plaintext) or plain SHA-256 (GPU brute-force) |
| Keep data secret at rest / in transit | Authenticated encryption | AES-256-GCM or ChaCha20-Poly1305 | Unauthenticated CBC (tamperable); reusing a nonce |
| Verify a file/download is intact | Hash (fingerprint) | SHA-256 digest, compared constant-time | MD5/SHA-1 (collisions); trusting a hash from the same channel |
| Prove origin to third parties | Asymmetric signature | Ed25519 / ECDSA (RS256/ES256 for JWT) | Putting secrets in a signed-but-unencrypted payload |
| Tamper-proof a token between two trusting services | MAC (shared-key) | HMAC-SHA-256 | Treating HMAC as a third-party-verifiable signature |
A real system usually composes them rather than picking one. TLS 1.3 — the protocol behind every https:// — uses all three in one handshake: an asymmetric signature to authenticate the server’s certificate, asymmetric key exchange to agree on a secret, that secret feeding authenticated encryption for the session, and hashing (HKDF) woven through key derivation and the transcript that proves the handshake itself wasn’t tampered with. Knowing which primitive does which job inside that stack is the difference between configuring TLS and cargo-culting it.
You're storing user passwords for a login system. Pick the correct approach.
A teammate says 'the JWT is signed with our key, so it's safe to put the user's API token inside the payload.' What's wrong?
Why is encrypting data with AES-256-CBC alone (no MAC, no AEAD) insufficient when an attacker can modify the ciphertext in transit?
Match each requirement to its primitive — order these requirements from 'keep it secret' to 'prove who sent it':
- 1 Keep the content unreadable to anyone without the key → encryption
- 2 Detect any change to a downloaded file → hash (fingerprint)
- 3 Store a password you only ever check, never read back → slow salted password hash
- 4 Prove a message came from a specific party, verifiable by anyone → asymmetric signature
- 01For hashing, encryption, and signing, state what each one guarantees and the single property that distinguishes them.
- 02Why must passwords be hashed with a slow salted hash rather than encrypted or plain-SHA-256'd, and why is a signed JWT not safe for secrets?
Hashing, encryption, and signing are three distinct primitives, separated by one question: can you reverse it, and with which key? Hashing is one-way and keyless — it buys integrity and fingerprinting, never confidentiality or origin. Encryption is reversible with a key — it buys confidentiality, but classic modes give no integrity, so reach for authenticated encryption (AES-GCM, ChaCha20-Poly1305). Signing is asymmetric — private key to sign, public key to verify — buying third-party-verifiable authenticity plus integrity, while leaving the data readable. The breaches live in the negative space: encrypting passwords instead of hashing them (key leak = plaintext), plain-SHA-256-ing low-entropy data (GPU brute-force), trusting unauthenticated CBC for integrity (bit-flipping), or stuffing secrets into a signed-but-unencrypted JWT. The senior reflex is to name the requirement first — secret, intact, attributable, or check-only-never-read — and only then pick the primitive. Real systems like TLS 1.3 compose all three; knowing which does which is what separates configuring crypto from cargo-culting it.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.