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…