Base64 Encoder & Decoder

Convert text to Base64 and back, in standard or URL-safe form. UTF-8 is handled correctly — Korean, emoji and accents all survive the round trip, where a plain btoa() call would throw.

12 characters · 18 bytes as UTF-8

No padding needed — the input was an exact multiple of 3 bytes.

Base64 is encoding, not encryption

This is worth stating plainly because the mistake is so common. Base64 is a public transformation with no key: it re-spells arbitrary bytes using 64 characters that survive text-only channels intact. Anyone holding the output can recover the input in a single step, which is not a weakness but the entire point. It exists so binary data can pass through email bodies, JSON string fields, data URIs and HTTP headers without being mangled.

The practical consequence is that Base64 protects nothing. A credential wrapped in it is a credential in plain sight, and a value that must stay secret needs actual encryption or, better, needs not to be transmitted at all.

Standard versus URL-safe

Standard Base64 uses + and / for the last two of its 64 characters, and pads the output with =. All three have meaning inside a URL, so RFC 4648 §5 defines a variant that substitutes - and _ and drops the padding.

standard : w7vDv8O+
url-safe : w7vDv8O-
                  ^ the only difference, and it breaks a mismatched decoder

Mixing the two is a real source of bugs, because the failure is not always loud: depending on where the differing characters fall, a mismatched decoder may throw, or may quietly return different bytes. If you have ever seen a JWT fail to parse after being passed through a URL, this is usually why — the segments of a JWT are base64url, not standard Base64.

The UTF-8 trap in JavaScript

btoa() accepts only code points 0 through 255. Anything above that — any Korean, Japanese, Cyrillic or emoji character — throws an InvalidCharacterError rather than encoding:

btoa("hello")   // "aGVsbG8="
btoa("안녕")     // DOMException: InvalidCharacterError

// convert to UTF-8 bytes first
const bytes = new TextEncoder().encode("안녕하세요");
btoa(String.fromCharCode(...bytes))   // "7JWI64WV7ZWY7IS47JqU"

The function predates Unicode's dominance on the web and treats its argument as a byte string rather than as text. Every correct implementation converts to UTF-8 first, which is what the tool above does — and why pasting Hello, 안녕 🎉 works here.

Padding, and why base64url omits it

Base64 consumes three bytes at a time and emits four characters. When the input is not a multiple of three, the encoder marks the shortfall with =:

"a"    (1 byte)  -> YQ==     2 padding chars
"ab"   (2 bytes) -> YWI=     1 padding char
"abc"  (3 bytes) -> YWJj     none
"abcd" (4 bytes) -> YWJjZA== 2 padding chars

The padding is technically redundant — a decoder can work out the final group size from how many characters remain modulo four — which is why base64url drops it without losing information. If you are writing a decoder that accepts both forms, add the padding back before decoding rather than assuming it is present.

Frequently asked questions

Is Base64 encryption?

No, and treating it as such is the most consequential misunderstanding about it. Base64 is a public, reversible re-spelling of bytes as text: no key is involved, reversing it is the expected behaviour, and anyone can decode it instantly. Encryption is reversible only by whoever holds the key, and its entire purpose is confidentiality. Base64 provides exactly none. It exists so that binary data can travel through channels that only handle text safely — email bodies, JSON string fields, data URIs, HTTP headers. If you Base64 a password or an API key, you have obfuscated it against a casual glance and protected it against nothing at all. It also costs you size: four characters carry every three bytes, so encoded data runs about a third larger than the original, and more than that for short inputs once padding is added.

What is the difference between Base64 and base64url?

They encode identical bytes but use two different characters and treat padding differently. Standard Base64 uses + and / for values 62 and 63, both of which have meaning inside a URL, and pads the output with = so its length is a multiple of four. The URL-safe variant defined in RFC 4648 §5 substitutes - and _ instead, and normally omits the padding because = also needs escaping in a query string. The value w7vDv8O+ in standard form becomes w7vDv8O- in URL-safe form. This is not cosmetic: feeding one variant to a decoder expecting the other either fails outright or silently produces different bytes. The most common place to meet the URL-safe form is a JSON Web Token, whose three segments are base64url precisely so the token can sit in a URL or an HTTP header untouched.

Why does btoa() fail on non-English text?

Because btoa() only accepts characters in the range 0-255, and anything above that throws an InvalidCharacterError. We confirmed it: btoa("안녕") raises a DOMException in Node and in every browser. The function predates widespread Unicode on the web and treats its input as a byte string rather than text. The fix is to convert the string to UTF-8 bytes first, with new TextEncoder().encode(text), and pass those bytes through. This tool does that, which is why Korean text and emoji round-trip correctly here while a naive btoa() call would crash on the same input. In Node the equivalent is Buffer.from(text, "utf8").toString("base64"), which we verified produces the identical result. Note that the reverse function has its own failure mode: atob() throws on any character outside the Base64 alphabet, so a URL-safe string fed to it raises rather than decoding.

What do the equals signs at the end mean?

They are padding, and they tell you how many bytes the final group was short of three. Base64 works on three-byte groups, turning each into four characters. When the input length is not a multiple of three, the encoder fills the gap and marks it: one leftover byte produces two equals signs, two leftover bytes produce one, and an exact multiple of three produces none. Encoding "a" gives YQ==, "ab" gives YWI=, and "abc" gives YWJj. A decoder can infer the group size from the character count alone — a remainder of two characters means one byte, three means two — which is why base64url drops padding entirely without losing information. If you write a decoder that accepts both forms, add the padding back before calling atob(), because it requires a length that is a multiple of four.

Does anything I paste here get uploaded?

No. This page is a static file and the encoding runs in your browser through the standard btoa and atob functions plus TextEncoder. There is no API call behind the buttons, no server-side logging, and no analytics event carrying your text, so the page keeps working with the network disconnected once loaded. That makes it safe for a real token or a production payload rather than a redacted stand-in. The only network request the site makes at all is a cookie-less Cloudflare Web Analytics beacon that counts page views, and it carries the page URL rather than anything from the text boxes. Nothing is stored in your browser either — reloading the page clears whatever was there.

References

Base64 shows up most often inside JSON Web Tokens, whose three segments are base64url-encoded. More browser-only tools at withuse.io/tools.