Base64 Is Encoding, Not Encryption
Base64 appears in data URIs, JWTs, email attachments, API payloads and private-key files, and it is the single most misunderstood notation in everyday programming — mostly because people see U3dvcmQ= and assume something protective happened. Nothing was protected. Base64 is a alphabet-translation step with a closed-form length formula, and you can reverse it in your head once you know the rules.
What the algorithm does
Binary data is bytes: 8 bits each. Some transport layers were built for text and mangle bytes that are not printable ASCII. Base64 fixes that by re-cutting the same bit stream into 6-bit groups, because 2^6 = 64 and 64 printable characters were chosen to be safe almost everywhere. Three bytes (24 bits) therefore become exactly four 6-bit groups, and each group indexes a 64-character alphabet. That is the entire idea; the padding character = only records that the last group was short.
The standard alphabet is A–Z, a–z, 0–9, then + and /. Index 0 is A, index 26 is a, index 52 is 0, index 62 is +, index 63 is /.
A worked example, byte by byte
Take the three characters Man. Their bytes are 77, 97, 110. Laid end to end that is 24 bits, split into four 6-bit numbers:
| Byte group | 24-bit value | 6-bit indexes | Alphabet characters |
|---|---|---|---|
| 77, 97, 110 | 5071214 | 19, 22, 5, 46 | ‘T’ ‘W’ ‘F’ ‘u’ |
Concatenated, that is TWFu — which is exactly what Buffer.from(“Man”).toString(“base64”) returns. Nothing was hidden: the mapping is a lookup, and a lookup with a published alphabet has no key.
Why the output length is predictable
Because 3 bytes always become 4 characters, an n-byte input always produces ceil(n/3)×4 characters, with (3 − n mod 3) mod 3 padding signs. So you can tell how big the original file was from the length of the blob alone — another reason it is encoding rather than encryption:
| Input bytes | Base64 characters | Padding = | Size increase |
|---|---|---|---|
| 1 | 4 | 2 | 300% |
| 2 | 4 | 1 | 100% |
| 3 | 4 | 0 | 33.3% |
| 4 | 8 | 2 | 100% |
| 5 | 8 | 1 | 60% |
| 6 | 8 | 0 | 33.3% |
| 7 | 12 | 2 | 71.4% |
| 8 | 12 | 1 | 50% |
| 16 | 24 | 2 | 50% |
| 100 | 136 | 2 | 36% |
The asymptotic cost is 33.33% — four-thirds of the original. On a 100-byte payload the measured increase is 36% because padding lands at the end. This is why base64 is a poor choice for storing large binaries in a database column and a perfectly reasonable one inside JSON, where the alternative is worse.
The UTF-8 trap that bites everyone once
Base64 operates on bytes, so the first question is always “bytes of what?”. In JavaScript the classic pair btoa and atob is defined over Latin-1 code units, not UTF-8. Feed it anything outside that range and it refuses outright — on the runtime used for this page, btoa(“naïve café — 日本語”) throws DOMException: Invalid character.
The correct route is to convert to UTF-8 bytes first, then encode. Same string, three different answers depending on what you assumed:
| Interpretation | Result |
|---|---|
| JS string length (code units) | 16 |
| UTF-8 byte length | 26 |
| Base64 of the UTF-8 bytes | bmHDr3ZlIGNhZsOpIOKAlCDml6XmnKzoqp4= |
| Base64 of the same string read as Latin-1 | bmHvdmUgY2Fm6SAUIOUsng== |
| Do those two match? | No — this is the mojibake bug |
Round-tripping the UTF-8 form returns the original string exactly (verified), which is the property you want. The encoder on this site does the UTF-8 conversion with TextEncoder and TextDecoder rather than btoa, so non-ASCII input survives the trip.
URL-safe Base64, and why the padding disappears
Two characters in the standard alphabet are hostile inside URLs: + is read as a space in query strings and / looks like a path separator. Base64url swaps them for - and _, and most implementations also drop the = padding, since the length already determines how many bytes were missing. Three bytes chosen to force both symbols, 0xfb 0xff 0xbe, encode as:
| Variant | Output |
|---|---|
| Standard | +/++ |
| URL-safe | -_-- |
This is what you are looking at in a JWT: three base64url segments separated by dots, and — this surprises people the first time — the header and payload are simply JSON, so decoding a token you were given reveals its claims immediately. A JWT’s security comes from the signature on the third segment, never from the encoding of the first two.
Where you actually meet it
- Data URIs — images inlined into CSS or HTML. The ;base64, marker is the only clue.
- Email — MIME Content-Transfer-Encoding: base64 for attachments, which is exactly the “text-only transport” problem the format was designed for.
- HTTP Basic auth — the browser sends Authorization: Basic base64(user:password). Anyone on the path can undo it in one step, which is why Basic over plain HTTP is a bug report, not a design.
- Private keys and certificates — PEM is base64 of DER between the BEGIN/END lines.
- APIs and JSON — binary fields smuggled through a text format, including protobuf and gRPC-Web envelopes.
So when is it not reversible?
Never, without extra machinery. If you need one-way, that is a hash — and note that a hash is not secret either, it is only irreversible in practice for a good function and a non-guessable input; a common password hashed with SHA-256 and not salted is a few seconds of GPU work. If you need confidentiality, encrypt first and encode the ciphertext afterwards, in that order. Base64’s job is to survive a text-only pipe, and it does that job well; treating it as a security layer is the mistake.
Frequently asked questions
Can I tell base64 apart from random text?
Usually yes: the alphabet is restricted, length is a multiple of four in the standard form, and ASCII text encodes with a characteristic pattern because most English letters land in the low 7 bits. Base64 of compressed or encrypted data looks uniformly random apart from the alphabet.
Why does my decoded text have = signs in it?
You probably skipped padding before decoding. Base64url drops the = characters; a decoder that expects the standard form needs them re-added to reach a multiple of four. This site’s decoder normalises - and _ and re-pads automatically.
Is base64 a compression?
It is the opposite — a ${B.asymptoticInflationPct}% expansion. Compress first if size matters, then encode.
Why do two files with the same first bytes encode identically?
Because the transform is a sliding 3-byte window with no key, no salt and no chaining. That property is also why you can spot a PNG header in a base64 blob: iVBOR is the encoding of the PNG signature.
Related tools
Base64 Encoder & Decoder
Encode and decode with the standard and URL-safe alphabets, UTF-8 correct. Free browser-based tool on this site. Runs locally, nothing is uploaded.
Number Base Converter
See the same value in binary, octal, decimal, hexadecimal and arbitrary bases — the 6-bit grouping idea at a smaller scale.
SHA Hash Generator
The one-way operation people confuse Base64 with, side by side across four algorithms.
JWT Decoder
Split a token into its three base64url segments and read the header and payload claims.
Written and maintained by CHENYUWEN. First published 2026-10-30. If something here is wrong, tell us - corrections are dated on the page.