DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Base64 Is Not Encryption (And Other Things Developers Get Wrong)

I once reviewed a codebase that stored user passwords as Base64-encoded strings in the database. The developer who wrote it told me the passwords were "encrypted." They weren't. Base64 is an encoding scheme, not an encryption algorithm. There's no key, no secret, no security. Anyone can decode a Base64 string instantly with a single function call. It's the equivalent of writing a secret message in pig Latin and calling it a cipher.

This misconception is dangerous and surprisingly common. Let me explain what Base64 actually is, why it exists, and when you should (and shouldn't) use it.

What Base64 actually does

Base64 converts binary data into a string of 64 printable ASCII characters. That's it. It's a way to represent arbitrary bytes using only letters (A-Z, a-z), digits (0-9), plus (+), and slash (/), with equals (=) for padding.

The algorithm works in groups of 3 bytes (24 bits). It splits those 24 bits into four 6-bit chunks, and each 6-bit chunk maps to one of the 64 characters in the Base64 alphabet.

Original bytes:  01001101 01100001 01101110  (Man)
Split into 6-bit: 010011 010110 000101 101110
Base64 indices:   19     22     5      46
Base64 chars:     T      W      F      u
Enter fullscreen mode Exit fullscreen mode

The string "Man" becomes "TWFu" in Base64.

When the input length isn't divisible by 3, padding is added:

  • 1 byte left over: encode it, add == (two padding characters)
  • 2 bytes left over: encode them, add = (one padding character)

This is why Base64 strings often end with one or two equals signs.

The overhead is exactly 33%. Every 3 bytes of input produce 4 bytes of output. A 1 MB file becomes approximately 1.33 MB when Base64 encoded. This is deterministic and unavoidable -- it's the mathematical cost of squeezing 256 possible byte values into 64 printable characters.

Why Base64 exists

Base64 was created to solve a specific problem: transmitting binary data through systems that only handle text.

Email (MIME). The original email protocol, SMTP, was designed for 7-bit ASCII text. Binary attachments (images, PDFs, executables) contain bytes outside the ASCII range. Base64 encodes these attachments into ASCII characters that can pass through any mail server without corruption. When you attach a file to an email, your email client Base64-encodes it and the recipient's client decodes it. This still happens today, even though modern servers handle 8-bit data fine.

URLs. URLs have a restricted character set. If you need to include binary data or complex strings in a URL parameter, Base64 encoding converts it to URL-safe characters. (Standard Base64 uses + and / which are not URL-safe, so there's a URL-safe variant that uses - and _ instead.)

Data URIs. The data: URI scheme lets you embed files directly in HTML or CSS as Base64 strings:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..." />
Enter fullscreen mode Exit fullscreen mode

This eliminates an HTTP request at the cost of a 33% larger payload and no caching. It's useful for tiny images (icons, 1px spacers) but counterproductive for anything larger than about 1-2 KB.

JSON. JSON has no binary type. If you need to include binary data in a JSON payload (an image thumbnail in an API response, for example), Base64 encoding it into a string is the standard approach.

Base64 in JavaScript

Encoding and decoding in the browser:

// String to Base64
const encoded = btoa('Hello, World!');
// "SGVsbG8sIFdvcmxkIQ=="

// Base64 to string
const decoded = atob('SGVsbG8sIFdvcmxkIQ==');
// "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

The btoa() and atob() functions are the built-in browser APIs. The names are unintuitive -- btoa means "binary to ASCII" (encode), and atob means "ASCII to binary" (decode).

The critical limitation: btoa() only handles characters in the Latin1 range (code points 0-255). If you try to encode a string with Unicode characters outside that range, it throws:

btoa('Hello');     // Works: "SGVsbG8="
btoa('Hello!');  // Throws: InvalidCharacterError
Enter fullscreen mode Exit fullscreen mode

The fix is to encode the string as UTF-8 first:

// Unicode-safe Base64 encoding
function encodeBase64(str) {
  const bytes = new TextEncoder().encode(str);
  const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
  return btoa(binString);
}

// Unicode-safe Base64 decoding
function decodeBase64(base64) {
  const binString = atob(base64);
  const bytes = Uint8Array.from(binString, c => c.codePointAt(0));
  return new TextDecoder().decode(bytes);
}
Enter fullscreen mode Exit fullscreen mode

In Node.js, use Buffer:

// Encode
const encoded = Buffer.from('Hello, World!').toString('base64');

// Decode
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');

// Encode binary data (file)
const fs = require('fs');
const fileBase64 = fs.readFileSync('image.png').toString('base64');
Enter fullscreen mode Exit fullscreen mode

Common mistakes

Using Base64 for "security." I'll say it again: Base64 is not encryption. It provides zero security. Every programming language has a built-in Base64 decoder. Any "hidden" data in a Base64 string is visible to anyone who spends two seconds decoding it. If you see credentials stored as Base64, that's a security vulnerability, not a security measure.

Base64-encoding large files for API transport. A 10 MB image becomes 13.3 MB in Base64, plus JSON overhead, plus the memory to parse it. For files larger than a few KB, use multipart form data or presigned upload URLs instead. Base64 in JSON is a convenience for small payloads, not an architecture for file transfer.

Double-encoding. Encoding an already-encoded string produces a valid Base64 string that's longer and decodes to the encoded version, not the original. This happens when middleware or libraries encode data that's already been encoded upstream. The symptom is usually a string that looks right but contains extra characters when decoded.

const original = 'Hello';
const encoded = btoa(original);      // "SGVsbG8="
const doubleEncoded = btoa(encoded); // "U0dWc2JHOD0="

atob(doubleEncoded); // "SGVsbG8=" -- not "Hello"
atob(atob(doubleEncoded)); // "Hello" -- needs two decodes
Enter fullscreen mode Exit fullscreen mode

Confusing Base64 with Base64URL. Standard Base64 uses + and /. Base64URL uses - and _. JWTs use Base64URL. If you decode a JWT with standard Base64, it might work for tokens that don't contain those characters, but it'll silently corrupt tokens that do. Always use the correct variant.

// Standard Base64 to Base64URL
function toBase64URL(base64) {
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Base64URL to Standard Base64
function fromBase64URL(base64url) {
  let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  while (base64.length % 4) base64 += '=';
  return base64;
}
Enter fullscreen mode Exit fullscreen mode

For quick encoding and decoding without writing code or opening a Node.js REPL, I keep a Base64 tool bookmarked at zovo.one/free-tools/base64-encoder-decoder. Paste text or upload a file, get the encoded/decoded output instantly. It handles both standard Base64 and Base64URL variants.

Base64 is a tool with a specific purpose: representing binary data as text. Use it for that purpose. Don't use it for security, don't use it for large files, and definitely don't store passwords with it.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)