Base64 Is Not Encryption, and Three Other Things It Is Not
What Base64 actually does, why it makes files bigger, why btoa breaks on emoji, and when base64url is the one you need instead.
Table of contents
Every few months a breach report includes the phrase "the credentials were encoded". They were Base64-encoded. Anyone who found the file could read them in one click, because that is what Base64 is for: it makes bytes safe to transport, not private.
The confusion is understandable. The output looks scrambled, and scrambled looks secure. It is worth knowing exactly what the transformation does, because three other misconceptions follow from the same misunderstanding.
Key takeaways#
- It has no key. It is a public, reversible mapping, an encoding in the same sense ASCII is.
- Output is about 33% larger than input. That is inherent, not a tuning problem.
- The browser's
btoathrows on any character above U+00FF, which includes every emoji and most non-Latin text. - The base64url variant swaps
+and/for-and_. JWTs use it, and the two alphabets are not interchangeable. - It is the right tool for exactly one job: getting arbitrary bytes through a text-only channel.
What does Base64 actually do?#
It re-expresses binary data using 64 characters that survive any text channel: A–Z, a–z, 0–9, + and /, with = for padding.
The mechanism is simple arithmetic. Take three bytes, which is 24 bits, and re-slice them into four groups of six. Six bits gives 64 possible values, and each maps to one character in the alphabet. Three bytes in, four characters out, every time.
That ratio is where the size increase comes from. Four characters carrying three bytes of information is a 4/3 expansion, or roughly 33%, plus up to two padding characters when the input length is not a multiple of three. No implementation avoids this; it is what the encoding is.
The padding is worth a word, because it causes more trouble than its size suggests. Those trailing = characters exist so a decoder knows how many bits of the final group are real. Strip them and a strict decoder rejects the input; keep them somewhere that treats = as a separator, such as a query string or an environment file, and the value is silently cut short.
Why is Base64 not encryption?#
Because encryption requires a secret and Base64 has none. Decoding needs nothing but the algorithm, which is published, implemented in every language, and available in any browser console.
The practical test: if you can decode it without knowing anything you did not find alongside it, it is not protecting anything.
atob('c3VwZXItc2VjcmV0LWtleQ=='); // 'super-secret-key'This matters because "encoded" gets written into incident reports and code reviews as though it were mitigation. A token in a cookie, a config file or a query string is plaintext with extra steps.
Authorization: Basic headers are the canonical example. The username and password are joined with a colon and encoded, which is exactly why Basic auth is only acceptable over TLS. The transport provides the confidentiality; the encoding provides none. Strip the TLS and the credentials are readable by anyone on the path.
The same reasoning applies to a JWT. Its header and payload are readable by whoever holds the token, so a claim is not a place to put anything private. What makes a JWT trustworthy is the signature, and that is a separate mechanism doing separate work — the difference between decoding a JWT and verifying it.
Why does btoa break on emoji?#
Because btoa operates on bytes, and JavaScript strings are UTF-16.
Base64 is defined over bytes. To encode text you must first choose a text encoding, and btoa assumes each character is one byte, which is a Latin-1 assumption. Hand it anything above U+00FF and it throws InvalidCharacterError. That covers emoji, CJK, Cyrillic, Greek, and accented characters outside Latin-1.
The fix is to encode to UTF-8 bytes first:
// Throws on 'héllo 👋'
btoa(text);
// Works for anything
const bytes = new TextEncoder().encode(text);
const b64 = btoa(String.fromCharCode(...bytes));
// And back
const back = new TextDecoder().decode(
Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)),
);If a system is mangling non-English text through a Base64 round trip, this assumption is almost always the reason. Encode as UTF-8 on both sides and the trip is lossless.
When do I need base64url instead?#
Whenever the output goes into a URL, a filename, or anything that treats + and / as structure.
The standard alphabet uses + and /, both of which are meaningful in a URL: / is a path separator and + means a space in a query string. base64url, defined in RFC 4648, substitutes - and _ and usually drops the = padding.
| Standard | base64url | |
|---|---|---|
| Character 62 | + | - |
| Character 63 | / | _ |
| Padding | = required | usually omitted |
| Used by | email, data URLs | JWT, OAuth, URLs |
This is why pasting a JWT segment into a standard decoder sometimes fails or returns nonsense: the alphabets differ. Convert first, or use a decoder that accepts both.
When is it the right answer?#
When bytes have to cross a channel that only accepts text, and only then.
Legitimate uses: attaching a binary file to an email, embedding a small image as a data URL, putting a binary blob inside a JSON string field, carrying a signature in an HTTP header.
Poor uses: hiding a secret, "compressing" anything, storing a large image inline that could have been a cached request, or making data tamper-proof, since anyone can decode, edit and re-encode.
If you need confidentiality, use encryption. If you need integrity, use a signature or an HMAC. If you need a shorter string, use compression before encoding, not instead of it.
You can round-trip text and files with the Base64 encoder and decoder, both of which use UTF-8 throughout so the emoji problem above does not arise. For images specifically, Image to Base64 reports the encoded size, which is the number to check before deciding to inline anything.
The short version#
It is a byte-to-text mapping with no key, a fixed 33% overhead, and a URL-safe variant you need to know about. It solves transport, and only transport. Every other property people attribute to it, whether secrecy, integrity or compactness, belongs to a different tool. The same confusion turns up one layer higher, where HTML escaping gets mistaken for XSS protection.
Sources: RFC 4648: The Base16, Base32, and Base64 Data Encodings
Tags
- Base64
- Encoding
- Security
- JavaScript