Base64 isn't encryption: what it's actually for, and 5 ways teams misuse it
Base64 encodes binary as ASCII. It does not hide, secure, or compress anything. Here is what it is for, and the misuses that bite.
Base64 isn't encryption: what it's actually for, and 5 ways teams misuse it
Once a quarter I'll see a PR that "encrypts" a password with Buffer.from(pw).toString('base64'). Once a year, that PR ships. This post is for the next reviewer.
What Base64 actually is
Base64 takes 3 bytes (24 bits) of binary and emits 4 ASCII characters (4 × 6 bits = 24 bits). The output uses 64 printable characters: A-Z, a-z, 0-9, +, /, plus = for padding. The size grows by exactly 4/3 plus padding — so 100 bytes in, 136 bytes out.
That's it. It's a deterministic, reversible mapping from arbitrary bytes to a subset of ASCII. No key, no secret, no compression, no integrity check. Anyone with the string can recover the original bytes in one shell call:
echo 'aGVsbG8gd29ybGQ=' | base64 -d
# hello world
You can verify in any direction at the Base64 Decode tool.
There's also URL-safe base64 (RFC 4648 §5), which swaps + for - and / for _ and usually drops padding. That's what JWTs use. Same idea, different alphabet.
What it's actually for
Three legitimate use cases:
1. Putting binary in a text-only channel
JSON has no bytes type. If you need to send a 4KB image thumbnail in a JSON document, you base64-encode it:
{
"avatar": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA..."
}
Same for SMTP body parts (MIME), XML payloads, env vars that store TLS certs. Anywhere the transport assumes ASCII-printable and you have raw bytes, base64 bridges the gap.
2. Data URIs
<img src="data:image/png;base64,iVBORw0KGgo..." />
Useful for inlining small icons, avoiding extra HTTP requests, or embedding fonts in CSS. The Image to Base64 tool emits the full data URI ready to paste. Don't inline anything over ~4KB — the size cost (33%) plus losing the browser's HTTP cache makes larger files worse.
3. Basic HTTP auth
Authorization: Basic dXNlcjpwYXNz
That's user:pass base64'd. The header is a transport convenience, not security — the actual security is the TLS connection wrapping it. Anyone sniffing plaintext HTTP can decode dXNlcjpwYXNz instantly.
The five misuses
1. "Encrypting" passwords or secrets
def encrypt(password):
return base64.b64encode(password.encode()).decode()
I have seen this in production. In a bank. The fix is to use a password hashing function — argon2 (preferred), bcrypt, or scrypt. If you need symmetric encryption, use cryptography.fernet (Python), libsodium, or AES-GCM with a real KMS. Base64 hides nothing.
2. Obfuscating API keys in client-side code
const apiKey = atob('c2tfbGl2ZV9hYmMxMjM='); // ¯\_(ツ)_/¯
Anyone with browser devtools is on byte 3. The fix is structural: don't ship server-side secrets in client code, full stop. Use a backend proxy, a short-lived signed token, or a public-key flow.
3. Padding-strict vs padding-loose mismatches
This one I've debugged in anger. Producer A emits aGVsbG8= (padded). Consumer B uses URL-safe alphabet and strict no-padding rules. The parser throws. Or worse, silently mis-decodes.
The fix: pick one. Internally, I default to URL-safe, no padding, because it survives URL paths, query strings, and JSON without re-escaping. The Base64 Encode tool lets you toggle both.
4. Storing binary in databases as base64
INSERT INTO uploads (data) VALUES ('iVBORw0KGgo...');
Postgres has bytea. MySQL has BLOB. SQLite has BLOB. Use them. Storing base64 in a TEXT column costs you 33% storage, makes queries slower, and forces decode work on every read. The only time this is OK is when the DB really can't hold bytes (rare in 2026) or when the storage is downstream of a system that already emitted base64 (e.g., a webhook payload).
5. Using base64 as a checksum
Two different inputs can't produce the same base64 output — it's a bijection. But base64 is also not a fingerprint; it's longer than the input. If you want to detect changes, use a hash. The Hash Generator does SHA-256 in the browser; that's the right tool.
A correctness note: encoding round-trips
Here's a snippet I keep in my toolbox to verify that an encoding round-trips correctly (useful when debugging "why does my decoded byte sequence have an extra ?"):
import base64
def roundtrip(s_bytes: bytes) -> bool:
encoded = base64.urlsafe_b64encode(s_bytes).rstrip(b'=')
pad = b'=' * (-len(encoded) % 4)
decoded = base64.urlsafe_b64decode(encoded + pad)
return decoded == s_bytes
assert roundtrip(b'\x00\x01\x02\xff')
assert roundtrip(b'hello world')
The rstrip then re-pad pattern is the cleanest way to handle URL-safe / no-padding interop between systems. If your input is text, encode to UTF-8 first, then base64 — don't trust string-level helpers in languages with implicit encodings.
TL;DR
| Use it for | Don't use it for |
|---|---|
| Embedding binary in JSON/XML/SMTP | Encrypting anything |
| Data URIs for tiny assets | Hiding API keys in client code |
| HTTP Basic auth (over TLS) | Storing blobs in TEXT columns |
| JWT segment encoding | Computing checksums |
If you find yourself reaching for base64 for a security property, stop. You want crypto, not encoding.
Try it
- Base64 Encode — text or binary, with URL-safe and padding toggles
- Base64 Decode — paste and inspect, never leaves your browser
- Image to Base64 — drag a PNG/JPG, get the data URI