iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Encoding vs Encryption

Encoding is not encryption. Base64, hex, URL-safe variants — when you need them, when you do not, and how they accidentally become security theatre.

Crypto — encoding (not encryption!)

EXAMPLE
// ===== The principle =====
// ENCRYPTION:  reversible only with a SECRET KEY  (confidentiality)
// HASHING:     one-way; same input -> same output (integrity / lookup)
// ENCODING:    reversible with NO secret           (format only)
//
// Base64 is encoding. Hex is encoding. URL encoding is encoding.
// They provide ZERO confidentiality.

// ===== Base64 =====
// 3 input bytes -> 4 output chars from [A-Za-z0-9+/], padded with =.
// Roughly +33% size overhead.

// Node:
import { Buffer } from 'node:buffer';
const b64 = Buffer.from('hello world').toString('base64');     // 'aGVsbG8gd29ybGQ='
const bytes = Buffer.from(b64, 'base64');                       // back to bytes
console.log(bytes.toString('utf8'));                            // 'hello world'

// Browser:
const enc = btoa('hello world');           // ASCII only; throws on multibyte
const dec = atob(enc);
// For UTF-8: TextEncoder + buffer + Base64 manually, or use a helper lib.

// ===== Base64 URL-safe =====
// Replaces +,/ with -,_ and strips padding. Safer in URLs / cookies / JWT.
function b64url(buf) {
  return Buffer.from(buf).toString('base64')
    .replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
}

// ===== Hex =====
// 1 byte -> 2 chars [0-9a-f].
const hex = Buffer.from('hi').toString('hex');     // '6869'
Buffer.from(hex, 'hex').toString('utf8');           // 'hi'

// ===== URL encoding =====
encodeURIComponent('a&b=c d');         // 'a%26b%3Dc%20d'
decodeURIComponent('a%26b%3Dc%20d');   // 'a&b=c d'

// ===== JSON, BSON, MessagePack, Protobuf =====
// Also 'encodings' (serialisation). Reversible without a key.

// ===== When encoding matters =====
// - Transport binary in text contexts (Base64 in JSON, hex in URLs)
// - URL-encode unsafe characters in query strings
// - JWT segments are Base64-URL-encoded (NOT encrypted by default!)
// - Webhook signatures are HMAC bytes hex/base64-encoded

// ===== The security mistake =====
// 'We Base64'd it so it is safe.'
// No. atob() / Buffer.from() reverses Base64 instantly.
// If you need confidentiality, ENCRYPT it (AES-GCM, AES-KW, NaCl secretbox).

// JWT is signed (or encrypted, in JWE), not 'encoded for safety'.
// Anyone can decode a JWT payload with no key. The SIGNATURE prevents tampering.

// ===== Constant-time comparison for encoded MACs =====
// Comparing signatures with == is a timing side-channel.
import { timingSafeEqual } from 'node:crypto';
const ok = timingSafeEqual(
  Buffer.from(provided, 'hex'),
  Buffer.from(expected, 'hex'),
);

// ===== Patterns to internalise =====
// - Encoding for FORMAT, encryption for CONFIDENTIALITY
// - Base64-URL in URLs / cookies / JWTs; standard Base64 in headers / JSON values
// - Hex for short binary representations + log readability
// - timingSafeEqual for any MAC / signature check

// ===== Pitfalls =====
// - 'It is Base64 so attackers cannot read it' (yes they can)
// - btoa with non-ASCII -> throws; encode to UTF-8 bytes first
// - Padding mistakes ('=' is sometimes stripped in URL contexts)
// - Mixing Buffer / Uint8Array / strings without explicit encodings

Why it matters

Encoding is format, not security. Base64 + hex + URL encoding move bytes through text-only channels; none of them buy confidentiality. When you genuinely need secrecy reach for AEAD; when you compare MACs use a constant-time function. Treat any "we Base64-encoded it" claim with deep suspicion.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Encoding (base64, hex) is reversible by anyone. NOT encryption.
// Encryption requires a key. If your "encrypted" output has no key, you have a bug.
Try it Yourself »

Discussion

Loading…