Base64 Is Not Encryption: Alphabets, Padding, and Real Transport Failures
Base64 is older than HTTP. It first appears in 1989 as the heart of Privacy-Enhanced Mail (RFC 1113), a defunct attempt to add cryptography to email, and was canonized two years later in MIME. The reason it exists is much narrower than what people use it for today: SMTP was designed for 7-bit ASCII text, and anything else β binary, 8-bit characters, attachments β had to be smuggled through that 7-bit pipe somehow. Base64 was the smuggling technique that won.
Most modern uses of Base64 inherit that 1989 lineage. Embedding a PNG in a CSS file, a public key in a JWT header, a webhook payload in a JSON string β all of them are doing the same thing 1989's email was doing: forcing binary through a transport that only understands text.
What it actually is
Base64 takes 3 bytes (24 bits) and re-expresses them as 4 characters from a 64-symbol alphabet β AβZ, aβz, 0β9, plus two extras. Twenty-four bits split into four groups of six is exactly 64 possibilities per group, which is where the 64 in the name comes from. The output is 33% larger than the input, which is the price of using only printable ASCII to represent arbitrary bytes.
If your input length isn't a multiple of 3 bytes, the encoder pads the last group with one or two = characters so output length is always a multiple of 4. That's it. Nothing more clever is happening.
This is why people calling Base64 "encryption" are wrong in a way that should be embarrassing. Base64 is a syntactic transformation with a public, fixed alphabet. Anyone reading aGVsbG8= can decode it in their head with practice, and any decoder does it in microseconds. It hides nothing.
The two alphabets
Standard Base64 (RFC 4648 Β§4) uses + and / as the two non-alphanumeric symbols and = for padding. This is fine for email and most data formats, but +, /, and = all have special meaning in URLs β + is sometimes interpreted as a space, / is a path separator, = is reserved in query strings. Putting standard Base64 in a URL works only if you're disciplined about percent-encoding it first, and most people aren't.
The same RFC defines Base64url (Β§5), which swaps + β - and / β _, and lets you omit the padding. JWTs, WebAuthn, OAuth state tokens, and most modern APIs use Base64url for exactly this reason. The two alphabets are not interoperable. Decoding a Base64url string with a standard Base64 decoder produces garbage at best and a hard error at worst, depending on how strict the implementation is about its alphabet.
If you can't decode something that looks like Base64, the first thing to try is swapping alphabets.
Where it's misused
The pattern I see most often: someone wants to "send a JSON object" inside another JSON object, or inside a header, or inside a URL parameter. They Base64-encode the JSON to dodge escaping issues. This works, but it's almost always the wrong choice β the receiver now has to Base64-decode and JSON-parse separately, errors are buried one layer deeper, and the payload is 33% larger for no good reason. JSON inside JSON should be a JSON string with proper escaping; a header should be a structured field; a URL parameter should be percent-encoded.
The exception is when the data really is binary β public keys, certificates, image bytes, encrypted blobs. There Base64 is the correct tool. For genuinely textual data inside textual transports, it's overhead masquerading as a fix.
The other misuse is data URIs. Inlining a 200KB image as data:image/png;base64,... in HTML or CSS turns 200KB into 270KB and disables browser caching of that resource entirely. There are reasons to do it for very small icons where saving the HTTP request beats the bloat, but the threshold is around 1β4KB, not 200KB.
How decoders fail
Real-world Base64 strings contain a surprising amount of off-spec garbage:
- Whitespace. RFC 4648 says decoders MAY ignore line breaks and spaces. Most do, but some don't. If you've ever seen a Base64 string break decoding when copy-pasted from an email, this is why β Outlook line-wraps at 76 characters and inserts CRLF. PEM files do the same on purpose.
- Wrong alphabet. Standard vs. URL-safe, as above. The two are silently incompatible.
- Missing or extra padding. Strict decoders reject
aGVsbG8(no padding). Lenient ones accept it. Base64url often omits padding entirely, so a Base64url decoder that requires padding is broken. - Non-canonical encoding. When the input length isn't a multiple of 3, the trailing group has unused bits. RFC 4648 says encoders must zero-pad those bits; not all do. Two encoders can therefore produce two different valid Base64 strings for the same input bytes. Most decoders accept both forms; security-sensitive code (signed JWTs, deduplication keys) should reject non-canonical encodings to prevent malleability attacks.
If a Base64 string won't decode, run it through tr -d '[:space:]', swap -_ for +/, and add = padding to the next multiple of 4. That fixes the majority of real-world breakage.
What it doesn't do
- It doesn't encrypt. (Worth saying twice.)
- It doesn't compress. The output is always 33% larger.
- It doesn't fix character-encoding problems. If you have UTF-8 bytes and you Base64-encode them, you have a Base64 string of UTF-8 bytes; the consumer still has to UTF-8-decode after Base64-decoding.
- It doesn't make binary "safe to print." A Base64 string is safe to put in 7-bit ASCII transports and most text fields, but
+,/, and=will still trip up shell quoting, SQL identifiers, and URL parsers.
Practical rules
- For URLs, headers, filenames, and anything path-shaped: Base64url.
- For email, MIME, certificates, and PEM files: standard Base64.
- Don't Base64-wrap textual data that's already going through a textual transport. Solve the escaping problem at the right layer.
- Don't inline anything bigger than a few KB as a data URI.
- If a decode fails, try the other alphabet before assuming the data is corrupt.
- Never trust Base64 to hide anything. It's a transport convention, not a secret.
Primary references
Standards and official documentation used to check the technical details in this guide.
Try it in your browser
Encode and decode both alphabets locally. Useful when a string that should be Base64 won't decode and you need to swap +/_ before assuming it's corrupt. Nothing leaves your browser.
Open the Base64 toolRelated guides
Continue with practical guides from the same topic area.
What Actually Belongs in a Production Dockerfile for Node (and What Doesn't)
Most Node Dockerfiles in the wild copy node_modules into the image, run as root, and ship a 900 MB layer. A short guide to the few decisions that matter: base image, multi-stage builds, layer caching for dependencies, the NODE_ENV trap, and why your docker-compose should not mirror production.
Catching Missing Translation Keys and Interpolation Mismatches Before Users Do
A missing translation key renders the raw key path to users, and a mismatched interpolation parameter renders an empty string or a crash. Both are easy to miss in review because the developer's locale always has every key. A guide to comparing locale JSON files, finding missing keys, and catching parameter mismatches before they ship.
Mock Data That Actually Exercises Your UI (Not Just Fills It)
Most mock data is ten copies of the same row with a different id. It fills the page and tests nothing. A guide to generating mock data that exercises layout edge cases, long names, missing fields, empty states, and the date and number formats that break formatting code, with field inference so a sample JSON becomes a realistic dataset in one step.