Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

Base64 isn't encryption — five places engineers still confuse them

4 min read #base64 #security #encoding #encryption

The first rule, written on every base64 tutorial since 2003: base64 is encoding, not encryption. Anyone with a browser console can decode it.

The first rule is right. And yet I keep finding production systems built on the assumption that base64 hides something. Five common varieties.

1. “Storing the API key in base64” in client-side code

const API_KEY = atob("c2tfbGl2ZV81MTIzNGFiY2Q="); // sk_live_51234abcd
fetch("https://api.example.com", { headers: { Authorization: `Bearer ${API_KEY}` } });

This was meant to “obfuscate” the key from someone scrolling the bundled JS. It does nothing. The decoded string ends up in network requests anyway, the source map ships the original, and atob is searchable.

Fix: client-side code can never hold a secret. Move the API call to a server you control, or use a key system designed for client exposure (scoped, short-lived, low-trust).

2. Kubernetes secrets

apiVersion: v1
kind: Secret
metadata:
  name: db-password
data:
  password: c3VwZXJzZWNyZXQxMjM=    # superscret123

K8s docs are explicit that this is base64, not encrypted. But the data: field plus the format (“Secret”) gives many teams the wrong impression. Anyone with read access to the namespace’s Secret resource can decode it. It’s not stored encrypted at rest by default — that requires enabling KMS encryption explicitly.

Fix: enable etcd encryption, restrict RBAC on secrets, and treat “can read secrets” as equivalent to “knows the value.” For higher sensitivity, use an external secret manager (Vault, AWS Secrets Manager) with the K8s integration.

(I wrote a whole separate post on this if you want the deeper version.)

3. JWT payload “encryption”

If you’re working with JWTs regularly, the JWT decoder at jwt.tooljo.com shows exactly what the base64url-encoded segments contain — header, payload, and signature bytes — so you can see the “this isn’t private” property with your own tokens.

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UifQ.signature

The middle segment is a base64url-encoded JSON payload. It is not encrypted. Anyone holding the token can decode the claims. The signature prevents tampering, not snooping.

War story: a SaaS put is_admin: true and the user’s home address in the JWT payload. The frontend used the address for a “location-based greeting.” A bug-bounty researcher noticed and reported it — to the SaaS’s credit, they fixed it within a day.

Fix: JWT claims should be public information. If you need to hide a value, use JWE (JSON Web Encryption) — but better, just don’t put secret data in tokens. Reference it by ID and look it up server-side.

4. Email tracking pixels with “encrypted” base64 IDs

https://email.example.com/track?id=dXNlcl8xMjM0NQ==

Base64-encoded user_12345. The marketer thought id=dXNlcl8xMjM0NQ== was less guessable than id=user_12345. It isn’t. The encoding is trivial, and the next user is user_12346 — so anyone who decodes one ID can enumerate the rest.

Fix: use opaque, random IDs (UUID v4, or a HMAC of the underlying ID). Don’t conflate “looks scrambled” with “actually unguessable.”

5. Storing PII as base64 in URLs

https://example.com/onboarding?u=eyJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIn0=

Base64-encoded {"email":"alice@example.com"}. Someone shared the link on Slack; the email got logged in Slack’s URL preview service, in the recipient’s CDN logs, and in the recipient’s browser history.

Fix: PII should not appear in URLs at all — they’re cached, logged, and shared. Use POST bodies for sensitive data, or one-time-use signed tokens that don’t reveal the underlying data.

When base64 is the right tool

The encoding-vs-encryption test

Two quick checks:

  1. Can someone with the encoded data and zero secrets recover the original? If yes (base64, hex, URL-encoding), it’s encoding.
  2. Does the operation require a key? If yes, it’s encryption (or a keyed hash like HMAC).

If the answer to #1 is yes, the data isn’t private. The data is transportable. That’s a different property and a different threat model.

The base64 tool is for the encoding case — making binary transportable. For the encryption case, you want libsodium, AGE, or your platform’s KMS, not a text encoder. And for the integrity case (detect tampering, but not encrypt), reach for a real hash function — hash.tooljo.com covers MD5/SHA-1/SHA-256/SHA-512 side-by-side. The decision tree for which one to use is at hash.tooljo.com/which-hash-function.