Marcell CD

Encoding vs Encryption vs Hashing (for Everyday Developers)

These three terms are frequently confused, even among seasoned developers who should be well-versed in their distinctions. However, it is crucial to recognize that each of these terms addresses fundamentally different issues within the realm of software development. Misapplying any one of them can lead to significant security vulnerabilities in your application, potentially compromising its integrity and the safety of its users. Understanding the precise context and implications of each term is essential for maintaining robust security practices in your development process.

This post will help you understand key differences with practical Node.js examples. It clarifies concepts and enables immediate application, making it suitable for both experienced developers and newcomers to effectively integrate into their workflow and projects.


1. The Big Picture

Before diving into each one, here’s a quick orientation:

Concept One-line purpose Reversible? Requires a key? Primary use
Encoding Make data portable/transferable ✅ Yes ❌ No Data transport, serialisation
Encryption Keep data confidential ✅ Yes ✅ Yes Protecting sensitive data at rest or in transit
Hashing Fingerprint data (verify integrity) ❌ No ❌ No Passwords, checksums, data integrity

⚠️ The #1 mistake developers make: treating encoding as a form of security. It isn’t. Anyone can decode Base64 in two seconds.

Think of it this way:


2. Encoding — Make Data Travel Safely

What it is

Encoding converts data from one format to another so it can be safely stored or transmitted — then converted back. It has nothing to do with secrecy. The goal is compatibility, not confidentiality.

When you need it

Common encoding schemes

Scheme What it does Example use case
Base64 Encodes binary as ASCII text Embedding images in HTML/CSS, JWTs
URL encoding Escapes special characters in URLs Query strings: hello worldhello%20world
JSON Serialises objects to a string REST API request/response bodies
Hex Encodes bytes as hexadecimal characters Displaying hash digests, colour codes
UTF-8 Encodes Unicode characters as bytes Almost everything on the web

Base64 Encoding Example (Node.js)

const original = "Hello, Marcell! This is sensitive-looking text.";

// Encode to Base64
const encoded = Buffer.from(original).toString("base64");
console.log("Encoded:", encoded);
// SGVsbG8sIE1hcmNlbGwhIFRoaXMgaXMgc2Vuc2l0aXZlLWxvb2tpbmcgdGV4dC4=

// Decode back to original — trivially easy
const decoded = Buffer.from(encoded, "base64").toString("utf8");
console.log("Decoded:", decoded);
// Hello, Marcell! This is sensitive-looking text.

URL Encoding Example (Node.js)

const params = new URLSearchParams({
  name: "Marcell Ciszek",
  company: "Manta Marine",
  message: "Hello & welcome!",
});

console.log(params.toString());
// name=Marcell+Ciszek&company=Manta+Marine&message=Hello+%26+welcome%21

// Decode it back
console.log(decodeURIComponent("Hello+%26+welcome%21"));
// Hello & welcome!

Never do this:

// This is NOT security — it's just encoding
const "hidden" = Buffer.from(apiKey).toString("base64");

Every developer on the internet knows how to reverse Base64. It offers zero protection.


3. Encryption — Keep Data Secret

What it is

Encryption transforms data into an unreadable ciphertext using a key. Only someone with the correct key can reverse the process (decryption) and read the original data. Without the key, the data is meaningless.

The two main types

Type How it works Key structure Speed Best for
Symmetric Same key encrypts and decrypts Single shared key Fast Encrypting large data (files, databases)
Asymmetric Public key encrypts, private key decrypts Key pair (public + private) Slower Key exchange, digital signatures, TLS

Symmetric Encryption — AES-256-GCM (Node.js)

AES-256-GCM is the gold-standard symmetric algorithm. GCM (Galois/Counter Mode) provides both encryption and authentication in one step.

const crypto = require("crypto");

const ALGORITHM = "aes-256-gcm";

// In a real app, store this key securely (e.g. environment variable or secrets manager)
const KEY = crypto.randomBytes(32); // 256-bit key
const IV  = crypto.randomBytes(12); // 96-bit IV recommended for GCM

function encrypt(plaintext) {
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, IV);

  const encrypted = Buffer.concat([
    cipher.update(plaintext, "utf8"),
    cipher.final(),
  ]);

  // Auth tag verifies the data wasn't tampered with
  const authTag = cipher.getAuthTag();

  return {
    ciphertext: encrypted.toString("hex"),
    authTag: authTag.toString("hex"),
    iv: IV.toString("hex"),
  };
}

function decrypt({ ciphertext, authTag, iv }) {
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    KEY,
    Buffer.from(iv, "hex")
  );
  decipher.setAuthTag(Buffer.from(authTag, "hex"));

  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(ciphertext, "hex")),
    decipher.final(),
  ]);

  return decrypted.toString("utf8");
}

const encrypted = encrypt("Super secret message");
console.log("Encrypted:", encrypted);

const decrypted = decrypt(encrypted);
console.log("Decrypted:", decrypted); // Super secret message

Asymmetric Encryption — RSA (Node.js)

Asymmetric encryption is commonly used to securely exchange keys or encrypt small pieces of data. You share your public key freely — anyone can use it to encrypt data. Only you (with your private key) can decrypt it.

const crypto = require("crypto");

// Generate a key pair (do this once and store them securely)
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
  modulusLength: 2048,
  publicKeyEncoding:  { type: "spki",  format: "pem" },
  privateKeyEncoding: { type: "pkcs8", format: "pem" },
});

function encryptWithPublicKey(data) {
  return crypto.publicEncrypt(publicKey, Buffer.from(data)).toString("base64");
}

function decryptWithPrivateKey(encrypted) {
  return crypto
    .privateDecrypt(privateKey, Buffer.from(encrypted, "base64"))
    .toString("utf8");
}

const message = "Only the private key owner can read this";
const encrypted = encryptWithPublicKey(message);
console.log("Encrypted:", encrypted);

const decrypted = decryptWithPrivateKey(encrypted);
console.log("Decrypted:", decrypted); // Only the private key owner can read this

Key management rules

Rule Why it matters
Never hardcode keys in source code Source code is often shared or committed to git
Rotate keys periodically Limits damage if a key is ever compromised
Use a secrets manager (Vault, AWS KMS) Centralised, auditable key storage
Generate keys with sufficient length Short keys are vulnerable to brute-force
Separate keys per environment A dev key should never unlock production data

4. Hashing — Prove Something Without Revealing It

What it is

A hash function takes any input and produces a fixed-length output (the hash or digest). It is a one-way operation — you cannot reconstruct the original input from the hash. The same input always produces the same hash, and even a single character change produces a completely different hash (this is called the avalanche effect).

Properties of a good cryptographic hash function

Property What it means
Deterministic Same input always → same output
Fast to compute Efficient to run forward
Pre-image resistant Cannot reverse a hash back to its input
Collision resistant Near impossible to find two inputs with the same hash
Avalanche effect Tiny input change → completely different hash

SHA-256 Hashing Example (Node.js)

const crypto = require("crypto");

function sha256(input) {
  return crypto.createHash("sha256").update(input).digest("hex");
}

console.log(sha256("hello"));
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

console.log(sha256("hellо")); // Notice: different letter (Cyrillic 'о')
// 7e8edb3eb2f54f84de0a4a55b8c6a27a8c7a5d5fce22...
// Completely different hash — avalanche effect in action

console.log(sha256("hello")); // Same as first — always deterministic
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

HMAC — Hashing with a Secret Key (Node.js)

HMAC (Hash-based Message Authentication Code) combines hashing with a secret key. It’s used to verify that a message came from a trusted source and wasn’t tampered with — this is how JWTs are signed.

const crypto = require("crypto");

const SECRET_KEY = process.env.HMAC_SECRET || "super-secret-key";

function createHmac(data) {
  return crypto
    .createHmac("sha256", SECRET_KEY)
    .update(data)
    .digest("hex");
}

function verifyHmac(data, receivedHmac) {
  const expected = createHmac(data);
  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(receivedHmac)
  );
}

const payload = JSON.stringify({ userId: 42, role: "admin" });
const signature = createHmac(payload);
console.log("Signature:", signature);

// Later — verify the payload wasn't tampered with
console.log("Valid?", verifyHmac(payload, signature)); // true

const tamperedPayload = JSON.stringify({ userId: 42, role: "superadmin" });
console.log("Valid?", verifyHmac(tamperedPayload, signature)); // false
Algorithm Output size Speed Use for passwords? Status
MD5 128-bit Very fast ❌ No Broken — don’t use for security
SHA-1 160-bit Fast ❌ No Deprecated for security use
SHA-256 256-bit Fast ❌ No (too fast) ✅ Good for checksums, HMAC, integrity
SHA-3 256/512-bit Fast ❌ No ✅ Good for checksums, integrity
bcrypt 184-bit Intentionally slow ✅ Yes ✅ Industry standard for passwords
Argon2id Variable Intentionally slow ✅ Yes ✅ Current best practice for passwords

💡 Key insight: For passwords, you want a slow algorithm. Fast hashing lets attackers check billions of guesses per second. bcrypt and Argon2 are deliberately designed to be expensive to compute.


5. How Passwords Are Actually Stored

Why you should never store plaintext passwords

If your database is ever leaked (and breaches happen to everyone), plaintext passwords expose all your users instantly. Even using a fast hash like SHA-256 is dangerous because attackers use precomputed rainbow tables and can crack common passwords in milliseconds.

The right approach: slow hashing + salting

A salt is a random string added to the password before hashing. This ensures two users with the same password get completely different hashes, and defeats rainbow table attacks.

bcrypt Example (Node.js)

const bcrypt = require("bcrypt");

const SALT_ROUNDS = 12; // Higher = slower = more secure (10–14 is typical)

// --- Registration ---
async function registerUser(plainPassword) {
  // bcrypt automatically generates and embeds the salt
  const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);

  // Store `hash` in your database — NEVER the plain password
  console.log("Stored hash:", hash);
  return hash;
}

// --- Login ---
async function loginUser(plainPassword, storedHash) {
  const isMatch = await bcrypt.compare(plainPassword, storedHash);

  if (isMatch) {
    console.log("✅ Password correct — user authenticated");
  } else {
    console.log("❌ Wrong password");
  }

  return isMatch;
}

// Simulate registration and login
const storedHash = await registerUser("mySecurePassword123!");
await loginUser("mySecurePassword123!", storedHash); // ✅ correct
await loginUser("wrongPassword",        storedHash); // ❌ wrong

Argon2 Example (Node.js)

Argon2id is the current winner of the Password Hashing Competition and is recommended by OWASP as the first choice for new applications.

const argon2 = require("argon2");

// --- Registration ---
async function register(plainPassword) {
  const hash = await argon2.hash(plainPassword, {
    type: argon2.argon2id,    // Resistant to both GPU and side-channel attacks
    memoryCost: 64 * 1024,   // 64 MB of memory required
    timeCost: 3,              // Number of iterations
    parallelism: 1,
  });

  // Store hash in your database
  return hash;
}

// --- Login ---
async function login(plainPassword, storedHash) {
  const isValid = await argon2.verify(storedHash, plainPassword);
  return isValid;
}

const hash = await register("mySecurePassword123!");
console.log(await login("mySecurePassword123!", hash)); // true
console.log(await login("wrongPassword",        hash)); // false

bcrypt vs Argon2 — which should you use?

Factor bcrypt Argon2id
Maturity Very mature (1999) Newer (2015), OWASP #1 recommendation
Memory hardness ❌ No ✅ Yes (harder to parallelise on GPUs)
Configuration flexibility Limited (just cost factor) High (memory, time, parallelism)
Library support Excellent Good and growing
Best for Existing apps, widespread ecosystem support New applications

For new projects, prefer Argon2id. For existing projects already using bcrypt, it remains secure — no need to migrate unless you want to.


6. Common Mistakes (and Fixes)

Mistake Why it’s bad What to do instead
Using Base64 to “hide” API keys or passwords Trivially reversible in seconds Use environment variables or a secrets manager
Storing plaintext passwords A single DB leak exposes every user Hash with bcrypt or Argon2id
Using MD5 or SHA-1 for passwords Far too fast — billions of guesses/second on GPUs Use bcrypt, Argon2id, or scrypt
Using fast hashes (SHA-256) for passwords Still too fast for password storage Use bcrypt or Argon2id
Hardcoding encryption keys in source code Keys end up in git history, CI logs, etc. Load from env vars or secrets manager
Reusing the same IV/nonce across encryptions Breaks the security of GCM and CTR modes Always generate a fresh random IV per operation
Rolling your own crypto Subtle bugs invalidate all security guarantees Use established libraries (node:crypto, bcrypt, argon2)
Not using timingSafeEqual for comparing secrets Timing attacks can leak information bit by bit Always use crypto.timingSafeEqual() for secret comparison
Encrypting passwords instead of hashing them If your key leaks, all passwords are exposed Passwords should be hashed, never encrypted
Using the same key for everything One compromised key breaks everything Use separate keys per purpose and environment

7. Real-World Scenarios

Scenario Right tool Reasoning
Store a user’s password Hashing (bcrypt / Argon2id) Must not be reversible; slow hash defeats brute-force
Store a user’s credit card number Encryption (AES-256-GCM) Must be retrievable for charging; needs secrecy
Verify a file wasn’t corrupted in transit Hashing (SHA-256) Compare digest before and after transfer
Embed an image in an HTML email Encoding (Base64) Email protocols are text-based
Sign a JWT token HMAC-SHA256 Proves the token was issued by your server
Send an encrypted message to a user Asymmetric encryption (RSA / ECDH) Recipient’s public key encrypts, only their private key decrypts
Cache a sensitive config value Encryption (AES-256) Needs to be retrieved; encryption + key management
Build a URL with special characters URL encoding Safe transport over HTTP
Detect duplicate user-uploaded files Hashing (SHA-256) Same file always produces the same hash
Protect data at rest in a database column Encryption (AES-256-GCM) Data must be readable by the application

8. When to Use What (Quick Reference)

If you need to… Use
Make data safe to send over a network Encoding
Keep data secret from others Encryption
Verify data without storing it Hashing
Store a password bcrypt / Argon2id
Verify a file’s integrity SHA-256
Sign a token or message HMAC-SHA256
Send a secret to someone without a shared key Asymmetric encryption (RSA)
Encode binary data as text Base64
Pass parameters safely in a URL URL encoding

9. The Mental Model

Is the data meant to be read by anyone?
  → Yes → Encoding (Base64, URL, JSON)

Is the data secret, but you need to retrieve it later?
  → Yes → Encryption (AES-256-GCM for symmetric, RSA for asymmetric)

Is the data something you only need to verify, not retrieve?
  → Yes → Hashing (SHA-256 for integrity, bcrypt/Argon2 for passwords)
❌ Wrong ✅ Right
Buffer.from(password).toString("base64") await bcrypt.hash(password, 12)
sha256(password) stored in DB await argon2.hash(password) stored in DB
Hardcoded encryption key in config.js Key loaded from process.env.SECRET_KEY
MD5 for password hashing Argon2id with memory cost ≥ 64 MB
Same IV reused across encryptions Fresh crypto.randomBytes(12) per operation

If you take one thing away from this post: encoding is not security, hashing is not encryption, and passwords should never be decryptable. Get those three rules right and you’ll avoid the most common security mistakes in web development.

Conclusion

Encoding, encryption, and hashing are not interchangeable — they solve fundamentally different problems, and reaching for the wrong tool doesn’t just lead to broken code, it leads to broken security.

Encoding is about compatibility, not confidentiality. Use it whenever you need data to travel safely across a medium that wasn’t designed for raw bytes — an email body, a URL query string, or an HTML attribute. Base64 and URL encoding are workhorses of the web. Just remember: anyone can reverse them instantly. Never use encoding to hide something.

Encryption is about secrecy with recoverability. When you need to protect data but also need to read it again later — a stored credit card, an API token, a config value — encryption is the right choice. AES-256-GCM gives you confidentiality, integrity, and tamper detection in one primitive. The security lives entirely in the key, so key management is everything: use environment variables, rotate keys, and never hardcode them.

Hashing is about verification without storage. When you don’t need to recover the original value — you only need to confirm something matches — hashing is the tool. SHA-256 is fast and suitable for checksums and file integrity. For passwords, fast hashes are a liability; use bcrypt or Argon2id, which are intentionally slow and memory-hard, making brute-force attacks prohibitively expensive.

The real lesson isn’t just knowing what each tool does — it’s knowing what each one doesn’t do. Encoding doesn’t protect. Encryption can’t verify a password without exposing your key. A fast hash won’t protect your users if your database leaks. Each tool has a lane, and mixing them up is one of the most common sources of real-world security vulnerabilities.

If you take three rules into your next project:

  1. Encoding is not security — don’t hide secrets behind Base64.
  2. Passwords must not be decryptable — hash them with bcrypt or Argon2id, always.
  3. Encryption is only as strong as your key management — treat your keys like passwords.

Get those three right, and you’ll have sidestepped the majority of credential leaks, authentication bypasses, and data exposure bugs that make the news every year.