Base64 Encode and Decode Without Leaking Tokens
Safely encode and decode sensitive authorization tokens, Basic Auth headers, and binary strings in local RAM without logging.
Base64 Encoder & Decoder
100% In-browser execution. Zero server uploads, instant results, free forever.
Base64 Encodes Bytes for Text Channels — It Does Not Protect Them
Base64 maps arbitrary binary data onto 64 printable ASCII characters, so that bytes can travel through channels that only reliably carry text: email bodies, JSON string fields, HTTP headers, data URIs, and source files.
The mechanism is mechanical and worth knowing. The encoder takes the input three bytes at a time — 24 bits — and re-splits those 24 bits into four 6-bit groups. Each 6-bit group indexes the alphabet A–Z, a–z, 0–9, +, /. When the input length is not a multiple of three, the final group is padded with = so the output length stays a multiple of four.
Three consequences follow directly from that arithmetic:
- Output is always about 33% larger than input — four characters for every three bytes, plus padding. A 3 MB file becomes roughly 4 MB of Base64.
- Length is always a multiple of four once padding is applied. A string whose length mod 4 is 1 cannot be valid Base64.
- It is completely reversible by anyone. There is no key.
That third point is the one that matters most, and it deserves stating without hedging: Base64 is an encoding, not encryption. A Base64 string in a log file, a config file or a screenshot is plaintext to anyone who pastes it into a decoder. Credentials stored "encoded for safety" are stored in the clear.
The Unicode Trap in btoa and atob
JavaScript's built-in btoa() fails on any string containing a character above U+00FF:
btoa('café');
// InvalidCharacterError: String contains an invalid character
The reason is historical. btoa was specified when JavaScript strings were treated as sequences of single bytes; it interprets each code unit as one byte and throws when a code unit exceeds 255. Emoji, accented Latin, Devanagari, Chinese and Cyrillic all break it.
The correct approach encodes text to UTF-8 bytes first, then Base64-encodes those bytes:
```js function encodeText(text) { const bytes = new TextEncoder().encode(text); // UTF-8 bytes let binary = ''; for (let i = 0; i < bytes.length; i += 0x8000) { binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); } return btoa(binary); }
function decodeText(encoded) { const bytes = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0)); return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } ```
Two details in that code are not decoration.
The 0x8000 chunking exists because String.fromCharCode(...bytes) spreads every byte as a separate function argument. On a large input this exceeds the JavaScript engine's argument limit and throws RangeError: Maximum call stack size exceeded — typically somewhere above 100 KB, which means the bug survives every small test and appears the first time a real file is used.
The `{ fatal: true }` flag on TextDecoder makes invalid UTF-8 throw instead of silently substituting U+FFFD replacement characters. Without it, corrupted input decodes to plausible-looking text containing �, and the corruption is discovered much later.
Base64URL: The Variant JWTs and URLs Use
Standard Base64 uses + and /, both of which have meaning in a URL, and =, which has meaning in a query string. Placing standard Base64 in a URL therefore requires percent-encoding and produces unreadable results.
Base64URL, defined in RFC 4648 §5, makes three substitutions:
+becomes-/becomes_- trailing
=padding is removed
This is the encoding used by JSON Web Tokens, by OAuth state and PKCE parameters, and by most modern URL-safe identifier schemes. Converting between the two is mechanical:
```js const toBase64Url = (b64) => b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromBase64Url = (value) => value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (value.length % 4)) % 4); ```
The padding restoration in the second function is the part people omit, and atob will reject the unpadded string. The expression (4 - (length % 4)) % 4 yields 0, 3, 2 or 1 and handles the case where no padding is needed without adding four equals signs.
A JWT is three Base64URL segments joined by dots: header, payload, signature. The payload is readable by anyone holding the token — decoding it requires no key, because the signature authenticates the contents rather than concealing them. Never place anything confidential in a JWT payload.
Data URIs: When Inlining Helps and When It Costs
A data URI embeds a Base64-encoded resource directly in markup or CSS:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." alt="">
This removes a network round trip, which is genuinely valuable for very small assets in the critical rendering path — an icon in above-the-fold CSS, for example.
The costs accumulate quickly and are frequently underestimated:
- 33% size penalty, applied to every byte, permanently.
- No independent caching. An inlined asset is re-downloaded with its containing document on every change to that document. A separate file is cached once and reused.
- Render blocking when inlined in CSS. A large data URI inside a stylesheet delays first paint, because the stylesheet must be fully parsed before rendering begins.
- No parallel download. The browser cannot fetch an inlined asset concurrently with anything else.
A defensible rule: inline below roughly 4 KB, and only for assets needed immediately. Above that, a separate cached request almost always wins. Inlining a 200 KB hero image as a data URI is a reliable way to make a page measurably slower while appearing to reduce request count.
SVG is the exception worth knowing: because SVG is text, it can be embedded with URL encoding instead of Base64, avoiding the 33% penalty entirely and often producing a smaller result than the original file.
Handling Tokens Safely While Encoding Them
The practical risk in Base64 work is not the algorithm. It is where the plaintext goes.
Developers encode and decode tokens constantly — inspecting a JWT payload, preparing an HTTP Basic credential, checking an API key embedded in a config. Doing that in a web tool that posts the value to a server means the credential has been transmitted to, and possibly logged by, a third party. Rotating that credential afterwards is the only safe remedy, and it is rarely done.
Three habits materially reduce exposure:
- Decode locally. A Base64 codec is pure arithmetic over bytes and needs no server. The OpenTools Base64 encoder runs in the page, so the value is not transmitted anywhere.
- Treat any credential pasted into any web page as compromised. Browser extensions, autofill managers and crash reporters all have access to page content. If the token protects something important, rotate it.
- Never commit Base64 credentials. Encoding does not obscure anything from a secret scanner, from a code reviewer, or from anyone with repository read access — and git history preserves them after deletion.
If you need the value to actually be protected rather than merely reformatted, the tool you want is encryption, not encoding — AES-GCM through the Web Crypto API, with a key you manage separately.
Frequently Asked Questions (FAQ)
Is Base64 a form of encryption?
No. Base64 is a reversible encoding that maps binary data onto 64 printable ASCII characters so it can travel through text-only channels. It uses no key and anyone can decode it instantly. A credential stored Base64-encoded is stored in plaintext for practical purposes, and if you need actual protection you need encryption such as AES-GCM through the Web Crypto API.
Why does btoa throw an InvalidCharacterError?
btoa treats each string code unit as a single byte and throws on any character above U+00FF, so accented Latin, emoji, Devanagari, Chinese and Cyrillic all fail. Convert the text to UTF-8 bytes with TextEncoder first, then Base64-encode those bytes. Decoding reverses it with TextDecoder using the fatal flag so invalid input throws rather than silently producing replacement characters.
Why does my Base64 encoder crash on large files?
The usual cause is String.fromCharCode(...bytes), which spreads every byte as a separate function argument and exceeds the JavaScript engine argument limit, throwing RangeError: Maximum call stack size exceeded. It typically appears above roughly 100 KB, so it survives small tests and fails on the first real file. Process the byte array in chunks of about 32,768 bytes instead.
What is the difference between Base64 and Base64URL?
Base64URL replaces the plus character with a hyphen, the forward slash with an underscore, and removes trailing equals padding, because those three characters have meaning inside URLs. It is defined in RFC 4648 section 5 and is the encoding used by JSON Web Tokens and by OAuth state and PKCE parameters. Converting back requires restoring the padding before calling atob.
How much larger does Base64 make a file?
About 33% larger. The encoder converts every three bytes of input into four output characters, plus up to two padding characters, so a 3 MB file becomes roughly 4 MB. This penalty applies to data URIs too, which is why inlining is only worthwhile below roughly 4 KB and for assets needed in the critical rendering path.
Is it safe to decode a JWT in an online tool?
Only if the tool decodes locally in your browser. A JWT payload is Base64URL and readable by anyone holding the token, so pasting one into a tool that sends it to a server exposes the token to a third party and its request logs. If you have already done so with a live token, rotate it. A Base64 codec is pure byte arithmetic and has no legitimate need for a server round trip.
Related Guides & Solutions
Generate RFC 4122 UUID v4s on Your Own Device
Convert Unix Epoch Timestamps to UTC and Local
JSON to Zod Schema, Generated in Your Browser
Ready to use Base64 Encoder & Decoder?
Execute this workflow privately on your device right now without creating an account or paying for cloud API credits.