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:

ConceptOne-line purposeReversible?Requires a key?Primary use
EncodingMake data portable/transferable✅ Yes❌ NoData transport, serialisation
EncryptionKeep data confidential✅ Yes✅ YesProtecting sensitive data at rest or in transit
HashingFingerprint data (verify integrity)❌ No❌ NoPasswords, 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

SchemeWhat it doesExample use case
Base64Encodes binary as ASCII textEmbedding images in HTML/CSS, JWTs
URL encodingEscapes special characters in URLsQuery strings: hello worldhello%20world
JSONSerialises objects to a stringREST API request/response bodies
HexEncodes bytes as hexadecimal charactersDisplaying hash digests, colour codes
UTF-8Encodes Unicode characters as bytesAlmost 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

TypeHow it worksKey structureSpeedBest for
SymmetricSame key encrypts and decryptsSingle shared keyFastEncrypting large data (files, databases)
AsymmetricPublic key encrypts, private key decryptsKey pair (public + private)SlowerKey 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

RuleWhy it matters
Never hardcode keys in source codeSource code is often shared or committed to git
Rotate keys periodicallyLimits damage if a key is ever compromised
Use a secrets manager (Vault, AWS KMS)Centralised, auditable key storage
Generate keys with sufficient lengthShort keys are vulnerable to brute-force
Separate keys per environmentA 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

PropertyWhat it means
DeterministicSame input always → same output
Fast to computeEfficient to run forward
Pre-image resistantCannot reverse a hash back to its input
Collision resistantNear impossible to find two inputs with the same hash
Avalanche effectTiny 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
AlgorithmOutput sizeSpeedUse for passwords?Status
MD5128-bitVery fast❌ NoBroken — don’t use for security
SHA-1160-bitFast❌ NoDeprecated for security use
SHA-256256-bitFast❌ No (too fast)✅ Good for checksums, HMAC, integrity
SHA-3256/512-bitFast❌ No✅ Good for checksums, integrity
bcrypt184-bitIntentionally slow✅ Yes✅ Industry standard for passwords
Argon2idVariableIntentionally 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?

FactorbcryptArgon2id
MaturityVery mature (1999)Newer (2015), OWASP #1 recommendation
Memory hardness❌ No✅ Yes (harder to parallelise on GPUs)
Configuration flexibilityLimited (just cost factor)High (memory, time, parallelism)
Library supportExcellentGood and growing
Best forExisting apps, widespread ecosystem supportNew 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)

MistakeWhy it’s badWhat to do instead
Using Base64 to “hide” API keys or passwordsTrivially reversible in secondsUse environment variables or a secrets manager
Storing plaintext passwordsA single DB leak exposes every userHash with bcrypt or Argon2id
Using MD5 or SHA-1 for passwordsFar too fast — billions of guesses/second on GPUsUse bcrypt, Argon2id, or scrypt
Using fast hashes (SHA-256) for passwordsStill too fast for password storageUse bcrypt or Argon2id
Hardcoding encryption keys in source codeKeys end up in git history, CI logs, etc.Load from env vars or secrets manager
Reusing the same IV/nonce across encryptionsBreaks the security of GCM and CTR modesAlways generate a fresh random IV per operation
Rolling your own cryptoSubtle bugs invalidate all security guaranteesUse established libraries (node:crypto, bcrypt, argon2)
Not using timingSafeEqual for comparing secretsTiming attacks can leak information bit by bitAlways use crypto.timingSafeEqual() for secret comparison
Encrypting passwords instead of hashing themIf your key leaks, all passwords are exposedPasswords should be hashed, never encrypted
Using the same key for everythingOne compromised key breaks everythingUse separate keys per purpose and environment

7. Real-World Scenarios

ScenarioRight toolReasoning
Store a user’s passwordHashing (bcrypt / Argon2id)Must not be reversible; slow hash defeats brute-force
Store a user’s credit card numberEncryption (AES-256-GCM)Must be retrievable for charging; needs secrecy
Verify a file wasn’t corrupted in transitHashing (SHA-256)Compare digest before and after transfer
Embed an image in an HTML emailEncoding (Base64)Email protocols are text-based
Sign a JWT tokenHMAC-SHA256Proves the token was issued by your server
Send an encrypted message to a userAsymmetric encryption (RSA / ECDH)Recipient’s public key encrypts, only their private key decrypts
Cache a sensitive config valueEncryption (AES-256)Needs to be retrieved; encryption + key management
Build a URL with special charactersURL encodingSafe transport over HTTP
Detect duplicate user-uploaded filesHashing (SHA-256)Same file always produces the same hash
Protect data at rest in a database columnEncryption (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 networkEncoding
Keep data secret from othersEncryption
Verify data without storing itHashing
Store a passwordbcrypt / Argon2id
Verify a file’s integritySHA-256
Sign a token or messageHMAC-SHA256
Send a secret to someone without a shared keyAsymmetric encryption (RSA)
Encode binary data as textBase64
Pass parameters safely in a URLURL 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 DBawait argon2.hash(password) stored in DB
Hardcoded encryption key in config.jsKey loaded from process.env.SECRET_KEY
MD5 for password hashingArgon2id with memory cost ≥ 64 MB
Same IV reused across encryptionsFresh 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.