Cyber security · Cryptography

Hashing vs Encryption vs Signing: Developer Cheat Sheet

Pick the right crypto tool without mixing concepts.

Reading time: ~8–12 min
Level: All levels
Updated:

Hashing, encryption, and signing get mixed up constantly—often in code reviews, API designs, and incident postmortems. This cheat sheet gives you a clean mental model: hashing for fingerprints, encryption for secrecy, and signing for authenticity. You’ll also learn where HMAC fits, why “base64 isn’t encryption,” and which defaults keep you out of trouble.


Quickstart

Use this when you’re in the middle of building an API, storing secrets, or shipping files—and you just need the right crypto tool without a rabbit hole.

Decision in 30 seconds

Your goal Use Typical choice What you get
Detect accidental changes (integrity fingerprint) Hash SHA-256 / SHA-512 Same input → same digest
Prove a message came from “someone with a shared secret” HMAC (keyed hash) HMAC-SHA-256 Integrity + message authentication
Hide contents from everyone except recipients Encrypt AES-GCM / ChaCha20-Poly1305 Confidentiality + integrity (AEAD)
Prove who authored data (public verification) Digital signature Ed25519 / ECDSA / RSA-PSS Authenticity + non-repudiation properties
Store passwords safely Password hashing (slow KDF) Argon2id / bcrypt / scrypt Resistance to offline cracking

Fast wins to prevent “crypto bugs”

  • Prefer AEAD modes (AES-GCM / ChaCha20-Poly1305) for encryption
  • Never roll your own: use well-reviewed libraries and safe defaults
  • Don’t hash passwords with SHA-256 (use Argon2id/bcrypt/scrypt)
  • Don’t confuse encoding with encryption (base64 is just encoding)

Quick sanity check before shipping

  • Do we need confidentiality, integrity, authenticity, or all three?
  • Where do keys live? (env vars, KMS, HSM, secret manager)
  • How do we rotate keys and invalidate old ones?
  • Do we verify before trusting? (signature/MAC verification, auth tag validation)
If you remember one rule

Encryption hides data. Hashing fingerprints data. Signing proves who wrote data. Mixing these concepts is how systems “look secure” while being exploitable.

Overview

Crypto bugs often come from correct code built with the wrong primitive. A few real-world examples: teams “encrypt” passwords by hashing them (can’t be verified properly), teams “sign” webhooks by hashing with no secret (anyone can forge), or teams encrypt but forget integrity (bit-flips become exploits).

What you’ll learn

  • The exact job of hashing vs encryption vs signing (and where HMAC fits)
  • Which algorithms are “safe defaults” for most apps
  • How to pick the primitive based on your security goal
  • Common developer mistakes and how to avoid them

What this post won’t do

  • It won’t teach cryptanalysis or deep math
  • It won’t recommend custom crypto protocols
  • It won’t replace threat modeling or security review
Most systems need more than one primitive

A typical “secure delivery” path uses multiple tools: a hash for integrity checks, encryption for storage-at-rest, and signatures for release authenticity. The trick is using each for the job it’s meant to do.

Core concepts

Think in terms of properties. Most product requirements translate to: confidentiality, integrity, and authenticity. Different crypto primitives provide different properties.

Hashing (unkeyed): “fingerprint this data”

A cryptographic hash function takes input of any size and produces a fixed-size digest. Good hashes are designed so that: it’s infeasible to find two different inputs with the same digest (collision resistance) and infeasible to reverse the digest to recover the original input (preimage resistance).

  • Use when: you need a stable identifier or integrity check (file fingerprinting, caching keys, deduping)
  • Don’t use when: you need secrecy (hashing does not hide data)
  • Common safe default: SHA-256

HMAC (keyed hash): “prove the sender knows a secret”

HMAC combines a secret key with a hash function to produce a message authentication code (MAC). It gives you integrity + authenticity between parties that share the same secret.

  • Use when: signing webhooks, internal service-to-service message authentication, API request signing with a shared secret
  • Key insight: without the key, anyone can compute a plain hash and forge it
  • Common safe default: HMAC-SHA-256

Encryption: “hide this data”

Encryption transforms plaintext into ciphertext using a key. Decryption reverses it with the appropriate key. Modern application encryption should almost always use AEAD (Authenticated Encryption with Associated Data) so you get integrity too.

Type Key model Typical use Reality check
Symmetric Same key encrypts/decrypts Data at rest, session encryption, app-level encryption Fast and common; key distribution is the hard part
Asymmetric Public key encrypts, private key decrypts Key exchange, small payload encryption, bootstrapping trust Slower; usually used to exchange a symmetric key (hybrid)

Digital signatures: “prove who authored this”

A digital signature uses a private key to sign data so that anyone with the corresponding public key can verify it. This gives you authenticity (and integrity), with a public-verification model—great for releases, tokens, and distributed systems.

  • Use when: software updates, artifact signing, JWT signatures, verifying data from an untrusted network
  • Key insight: verification does not require sharing a secret with verifiers
  • Common safe defaults: Ed25519 or RSA-PSS (depending on ecosystem requirements)

One more concept: encoding vs encryption

Encoding (like base64, hex, URL encoding) changes representation for transport/storage. It provides zero security properties. If you can decode it without a key, it’s not encryption.

Password storage is its own category

Password hashing is not “regular hashing.” Use a slow, memory-hard password hash (Argon2id/bcrypt/scrypt) with a unique salt. Fast hashes (SHA-256) make offline attacks dramatically easier.

Step-by-step

This is a practical workflow you can reuse in designs, code reviews, and incident fixes. The goal is to choose the right primitive, pick safe defaults, and avoid subtle footguns (nonces, integrity, key handling).

Step 1 — Write the requirement as properties

Don’t start with “we should encrypt it.” Start with: confidentiality, integrity, authenticity, replay protection, and who verifies.

Questions to ask

  • Do we need to keep data secret from the server, or only from outsiders?
  • Do we need to detect tampering (integrity)?
  • Do we need to prove who created it (authenticity / signatures)?
  • Is verification public (anyone) or shared-secret (only trusted parties)?
  • Do we need replay protection (timestamps, nonces, sequence numbers)?

Quick mapping

  • Integrity only: hash
  • Integrity + shared-secret authentication: HMAC
  • Confidentiality (+ integrity): AEAD encryption
  • Authenticity with public verification: digital signature

Step 2 — Choose the primitive and “safe default” algorithms

Job Use Safe defaults (typical) Avoid
File/data fingerprint SHA-256 SHA-256, SHA-512 MD5, SHA-1
Webhook/API request authenticity (shared secret) HMAC HMAC-SHA-256 Plain hashes, “hash(secret + msg)” DIY constructions
Encrypting application data AEAD AES-GCM, ChaCha20-Poly1305 AES-ECB, “AES-CBC without MAC”, fixed IVs
Proving author/release authenticity Signatures Ed25519, RSA-PSS Raw RSA “textbook”, MD5withRSA
Password storage Password hash Argon2id, bcrypt, scrypt SHA-256/MD5 for passwords, reversible encryption for passwords

Step 3 — Prefer “encrypt-then-authenticate” by default (AEAD)

A common mistake is encrypting but not authenticating. Modern AEAD modes include an authentication tag so the receiver can detect tampering. If verification fails, you must treat the ciphertext as untrusted and abort.

AEAD also supports “Associated Data”

Associated Data (AAD) lets you bind metadata to the ciphertext (e.g., user ID, record type, version) without encrypting it. The ciphertext is only valid if AAD matches during decryption—useful for preventing mix-and-match attacks.

Step 4 — Implement the primitive with a vetted library (avoid DIY crypto)

In practice, implementation bugs are more common than algorithm failures. Use a standard library or a mature dependency, and stick to documented “recipes.” Below are three copy/paste patterns you can adapt.

Example 1: Hash a file for integrity (SHA-256)

Use this when you need a stable fingerprint for downloads, build artifacts, backups, or caching keys. This does not hide content.

import hashlib
from pathlib import Path

def sha256_file(path: str, chunk_size: int = 1024 * 1024) -> str:
    h = hashlib.sha256()
    with Path(path).open("rb") as f:
        for chunk in iter(lambda: f.read(chunk_size), b""):
            h.update(chunk)
    return h.hexdigest()

if __name__ == "__main__":
    # Example: print the SHA-256 fingerprint of a file
    print(sha256_file("release.tar.gz"))

Example 2: Encrypt/decrypt data safely (AES-256-GCM)

Use AEAD for application-level encryption (secrets, tokens, PII). Never reuse the same (key, nonce) pair. Store nonce + ciphertext + tag together. Keep keys in a KMS/secret manager in real systems.

import crypto from "node:crypto";

// 32 bytes = AES-256 key. In production: load from KMS/secret manager, not hardcoded.
const key = crypto.randomBytes(32);

export function encryptAesGcm(plaintextUtf8, aadUtf8 = "") {
  const nonce = crypto.randomBytes(12); // 96-bit nonce is standard for GCM
  const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);

  if (aadUtf8) cipher.setAAD(Buffer.from(aadUtf8, "utf8"));

  const ciphertext = Buffer.concat([
    cipher.update(Buffer.from(plaintextUtf8, "utf8")),
    cipher.final(),
  ]);

  const tag = cipher.getAuthTag(); // integrity/authentication tag

  // Store/transmit: nonce || ciphertext || tag
  return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
}

export function decryptAesGcm(payloadB64, aadUtf8 = "") {
  const payload = Buffer.from(payloadB64, "base64");
  const nonce = payload.subarray(0, 12);
  const tag = payload.subarray(payload.length - 16);
  const ciphertext = payload.subarray(12, payload.length - 16);

  const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
  if (aadUtf8) decipher.setAAD(Buffer.from(aadUtf8, "utf8"));
  decipher.setAuthTag(tag);

  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
  return plaintext.toString("utf8");
}

Example 3: Sign and verify data (Ed25519 with OpenSSL)

Use signatures when verifiers shouldn’t share a secret with the signer (public verification): release files, artifacts, configuration bundles, or messages published to many consumers. Protect the private key; rotate if compromised.

# Generate an Ed25519 keypair
openssl genpkey -algorithm ED25519 -out ed25519_private.pem
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

# Sign a file (creates a detached signature)
openssl pkeyutl -sign -inkey ed25519_private.pem -rawin -in artifact.bin -out artifact.sig

# Verify the signature
openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -rawin -in artifact.bin -sigfile artifact.sig
Operational reality: key management is the hard part
  • Generate keys with a strong RNG; never derive from user passwords.
  • Store keys in KMS/HSM where possible; restrict access by service identity.
  • Version keys and build rotation into your data format (key IDs).
  • Log verification failures and treat them as security signals.

Step 5 — Add “verification points” in your system

Crypto only helps if you verify at the right place. A few typical verification points:

Where hashes help

  • Validate downloads (compare to published checksum)
  • Detect file corruption in storage or transit
  • Cache keys / deduping (with collision-aware handling)

Where signatures/HMAC help

  • Verify webhooks before processing
  • Verify tokens/messages from untrusted networks
  • Verify build artifacts before deploy (supply-chain defense)

Step 6 — Design your “crypto envelope” format

Most bugs happen at boundaries: what do you store, and how do you parse it safely? A simple envelope is: version, algorithm, key id, and the payload (nonce/ciphertext/tag or signature). This makes rotation and backwards compatibility manageable.

Mini checklist: shipping a crypto format

  • Include a version field for future migrations
  • Store nonce/IV explicitly (never “recompute” it)
  • Store key identifier (kid) so you can rotate keys cleanly
  • Bind context with AAD (tenant, record type, schema version) where relevant
  • Fail closed on verification/auth tag failure

Common mistakes

These are the mistakes that show up in real apps. The fixes are usually straightforward once you know what property you actually need.

Mistake: Using a hash to “secure” a webhook

If you publish SHA-256(payload) as “verification,” anyone can recompute it and forge requests.

  • Fix: use HMAC with a shared secret, or use digital signatures with a public key.
  • Extra: include timestamp/nonce and enforce a freshness window to reduce replay.

Mistake: Encrypting without integrity

Encrypting with a non-authenticated mode can allow tampering that becomes a vulnerability.

  • Fix: use AEAD (AES-GCM / ChaCha20-Poly1305).
  • Fix: treat auth tag failure as a hard error (no partial plaintext).

Mistake: Hashing passwords with SHA-256

Fast hashes make offline cracking much easier; attackers can try billions of guesses.

  • Fix: use Argon2id, bcrypt, or scrypt with a unique salt per password.
  • Fix: tune cost parameters and rehash on login when upgrading settings.

Mistake: Reusing IV/nonce with the same key

Nonce reuse can catastrophically break confidentiality (and sometimes integrity) in modern schemes.

  • Fix: generate a fresh random nonce for each encryption (and store it).
  • Fix: use libraries that manage nonces safely where possible.

Mistake: Confusing base64 with encryption

If you can decode it without a key, it’s not secret.

  • Fix: base64 is for transport; use AEAD encryption for confidentiality.
  • Fix: keep “encoding” and “cryptography” separate in your docs and APIs.

Mistake: Picking outdated algorithms “because they work”

Old primitives (MD5/SHA-1) still compute, but they don’t meet modern security expectations.

  • Fix: use SHA-256+, HMAC-SHA-256, AEAD, Ed25519/RSA-PSS.
  • Fix: document crypto choices and add tests to prevent regressions.
“Crypto correctness” includes constant-time comparisons

When comparing MACs/signatures/tokens, avoid leaking timing information via naive string comparison. Many standard libraries provide constant-time compare functions—use them for verification results.

FAQ

Is hashing “more secure” than encryption?

No—hashing and encryption do different jobs. Hashing gives you a one-way fingerprint; encryption gives you confidentiality because it’s reversible with a key. If you need to hide data, use encryption (ideally AEAD).

What should I use to store passwords?

Use a password hashing function like Argon2id, bcrypt, or scrypt with a unique salt per password. Don’t use SHA-256/MD5 for password storage, and don’t encrypt passwords for later retrieval.

When do I use HMAC instead of digital signatures?

Use HMAC when both sides share a secret and you only need trusted parties to verify. Use digital signatures when verifiers should not share secrets with the signer (public verification), such as verifying releases or distributing signed messages to many consumers.

Does AES encryption automatically guarantee integrity?

Only if you use an authenticated mode (AEAD). AES is a block cipher; the mode matters. AES-GCM provides confidentiality and integrity; AES-CBC alone does not (it needs a separate MAC).

Is it safe to “encrypt then base64” a token?

Yes, as long as encryption is real encryption. Base64 is fine for transport once you already have ciphertext. Just remember: base64 adds no security, and you still need strong keys, nonces, and authentication tags.

How do I choose between SHA-256 and SHA-512?

Either is generally fine for integrity fingerprinting in modern systems. SHA-256 is the most common default and widely interoperable. The bigger difference is: don’t use obsolete hashes (MD5/SHA-1), and don’t use fast hashes for password storage.

What’s the simplest way to avoid crypto design mistakes?

Start from the property you need (confidentiality/integrity/authenticity), then pick a standard primitive and a documented library recipe. Avoid DIY constructions and build key rotation into your data formats.

Cheatsheet

Keep this as your “crypto pocket guide.” It’s designed to be scanned quickly during design reviews and implementations.

Choose the tool

  • Need secrecy? Encrypt (AEAD)
  • Need integrity only? Hash (SHA-256)
  • Need shared-secret authenticity? HMAC
  • Need public-verifiable authenticity? Digital signature
  • Need password storage? Argon2id/bcrypt/scrypt

Safe defaults (most apps)

  • Hash: SHA-256
  • HMAC: HMAC-SHA-256
  • Encryption: AES-256-GCM or ChaCha20-Poly1305
  • Signature: Ed25519 or RSA-PSS
  • Password hashing: Argon2id (preferred where available)

Rules that prevent 90% of mistakes

  • Use AEAD for encryption (don’t invent “encrypt + hash” combos)
  • Never reuse nonces for AEAD under the same key
  • Always verify MAC/signature/auth tag before trusting data
  • Don’t use fast hashes for passwords (use a slow password hash)
  • Encoding ≠ encryption (base64 is not secrecy)
  • Plan key rotation (key IDs + version fields)
Use case What to use One-liner reminder
Download checksum SHA-256 Hash compares expected vs actual bytes
Webhook validation HMAC-SHA-256 Must include a secret key
Encrypt PII in DB AES-GCM / ChaCha20-Poly1305 AEAD gives integrity too
Signed releases Ed25519 / RSA-PSS Public key verifies authenticity
Password storage Argon2id / bcrypt / scrypt Slow hash + unique salt
If you’re unsure, keep it boring

Boring crypto is good crypto: use standard algorithms, standard libraries, and standard formats. The “creative” solution is almost always the one that breaks later.

Wrap-up

The clean way to remember this post: hashing is for fingerprints, encryption is for secrecy, and signing is for proving authorship. Most production systems combine them—just don’t swap their roles.

Do this next (15 minutes)

  • Pick one place in your stack where crypto is used (tokens, webhooks, backups, PII)
  • Rewrite the requirement as properties (confidentiality/integrity/authenticity)
  • Confirm the primitive matches the property (hash vs HMAC vs AEAD vs signature)

Do this next (this week)

  • Add key rotation support (key IDs, versions)
  • Replace non-AEAD encryption with AEAD where feasible
  • Audit password storage (ensure Argon2id/bcrypt/scrypt + salts)
Crypto is a dependency, not a feature

Treat cryptography like you treat databases or auth providers: standard components, clear interfaces, tests, monitoring for failures, and operational playbooks (rotation, revocation, incident response).

If you want to deepen this, the related posts below cover threat modeling, DevSecOps controls, and how real apps get hacked— which is where crypto decisions become concrete.

Quiz

Quick self-check (demo). This quiz is auto-generated for cyber / security / cryptography.

1) You need to check if a downloaded file was corrupted in transit. What should you use?
2) You’re verifying incoming webhooks from a provider using a shared secret. What’s the right primitive?
3) Which statement is true about encryption in modern apps?
4) What’s the correct way to store user passwords?