UseToolSuite UseToolSuite

Why Base64 is Not Encryption

Why Base64 encoding provides no security: how the algorithm works, the real Kubernetes/JWT misconceptions, and how to use AES-256-GCM and bcrypt instead.

Necmeddin Cunedioglu Necmeddin Cunedioglu 6 min read
Part of the Web Security: Encoding and Hashing Guide series

Practice what you learn

Base64 Encoder / Decoder

Try it free →

Why Base64 is Not Encryption

One of the most common security misconceptions in web development is treating Base64 encoding as encryption. It isn’t.

Base64 is a format conversion. Anyone with a Base64 string can decode it back to plain text instantly — no key, no password, no secret. Despite that, developers (junior and senior alike) regularly use Base64 to “hide” database passwords, API keys, and config values in source code and infrastructure files. That’s a vulnerability that automated bots exploit trivially.

This guide explains how Base64 works at the byte level, why it provides no security, the patterns where its misuse leads to breaches, and the right cryptographic alternatives (AES-256-GCM, bcrypt) for each case.

See it yourself: paste any text into our local Base64 Encoder/Decoder and watch it convert back and forth instantly — with no secret involved.

1. What Base64 Actually Does

Base64 converts binary data (an executable, an image, a string) into text using 64 ASCII characters (A-Z, a-z, 0-9, +, /). It’s a serialization format, not a security operation.

Input String:  Hello, World!
Base64 Output: SGVsbG8sIFdvcmxkIQ==

Anyone with a terminal can reverse this instantly — no key, no password, no salt. The algorithm is public, standardized by the IETF (RFC 4648), and built into the standard library of every language.

How the byte-level encoding works

Base64 operates on 3-byte groups, turning each group into 4 ASCII characters:

  1. It takes 3 bytes (24 bits) of input.
  2. It splits those 24 bits into 4 groups of 6 bits.
  3. It maps each 6-bit value (0–63) to a character in the Base64 alphabet.
  4. If the input isn’t a multiple of 3 bytes, it appends = as padding.
Input bytes:   H        e        l
Binary:        01001000 01100101 01101100

6-bit groups:  010010   000110   010101   101100
Integer index: 18       6        21       44
Output chars:  S        G        V        s

The process is deterministic and reversible. The same input always produces the same output, and the output always reverses to the input. There’s no randomness, no key, no salt.

2. Why Developers Confuse Base64 with Encryption

A. It looks unreadable

SGVsbG8sIFdvcmxkIQ== doesn’t look like “Hello, World!” to a human. That creates a false sense of security — the data looks obscured, but it’s fully readable to any script or parser.

B. Legacy API keys are often Base64-encoded

Many APIs encode credentials in Base64. HTTP Basic Authentication sends Authorization: Basic dXNlcjpwYXNz, which is just base64("user:pass").

This is for transport formatting (HTTP headers historically require ASCII and can’t handle spaces or colons), not security. The actual encryption comes from the HTTPS/TLS connection.

C. JWTs use Base64URL

JWT payloads are Base64URL-encoded, so they look like encrypted strings. But the payload is readable by anyone with a browser:

// A standard JWT — the payload is readable without any key
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoic3VwZXJ1c2VyIn0.xxx';

// Grab the middle payload, decode it, parse the JSON
const payload = JSON.parse(atob(token.split('.')[1]));
// Result → { user: "admin", role: "superuser" }

A JWT’s security comes from the signature (the third part), not the Base64 encoding. Anyone can read the payload; only the server with the secret key can verify the signature to confirm the payload wasn’t tampered with.

Decode any JWT instantly: use our local JWT Decoder to inspect token headers and payloads without sending the token to a server.

D. Kubernetes Secrets

Kubernetes Secrets store values in Base64, which leads many to believe the data is “encrypted” at rest:

# ❌ This is NOT encrypted.
apiVersion: v1
kind: Secret
metadata:
  name: prod-db-credentials
data:
  password: cGFzc3dvcmQxMjM=  # This is literally base64("password123")

The Kubernetes docs say it plainly: “Secrets are, by default, stored unencrypted in the API server’s underlying data store (etcd).” Base64 is used for binary-safe YAML encoding, not security.

3. What This Costs in the Real World

If you store sensitive data “protected” only by Base64 in frontend JavaScript or config files:

// ❌ This is NOT protected!
const secureToken = btoa("super_secret_admin_password_123");
// Result: c3VwZXJfc2VjcmV0X2FkbWluX3Bhc3N3b3JkXzEyMw==

// Any attacker can open the console and run:
atob("c3VwZXJfc2VjcmV0X2FkbWluX3Bhc3N3b3JkXzEyMw==");
// Result: "super_secret_admin_password_123"

An attacker with access to your database, server logs, or network traffic can decode Base64 instantly.

Common breach patterns

These are representative of how Base64 misuse leads to breaches:

ScenarioHow it fails
Stripe API keys stored as Base64 in client-side React code.Attackers scrape the JS bundle and decode the keys, then run fraudulent charges.
Session tokens stored as Base64 in mobile local storage.Malware extracts the tokens and takes over accounts.
Base64-encoded AWS credentials committed to a public GitHub repo.Bots scan the repo, decode the credentials, and breach the database within hours.

In each pattern, the assumption is that Base64 provides “security through obscurity.” It provides none.

4. Encoding vs Encryption vs Hashing

Understanding the difference between these three operations is what drives correct security decisions:

PropertyEncoding (Base64)Encryption (AES-256-GCM)Hashing (bcrypt / SHA-256)
PurposeBinary-to-text conversionData confidentialityOne-way integrity check
Reversible?✅ Yes — always✅ Yes — but only with the key❌ No
Requires a key?❌ No✅ Yes❌ No (but usually uses a salt)
Same input → same output?✅ Always❌ No (uses a random IV)✅ Always (unless salted)
Provides security?❌ None✅ Confidentiality✅ Integrity
Example outputSGVsbG8=a1b2c3d4e5f6... (random)$2b$12$7k... (fixed length)

Decision matrix

Need to HIDE data from unauthorized access but read it later?
  → Use ENCRYPTION (AES-256-GCM, ChaCha20-Poly1305)

Need to VERIFY a file hasn't been tampered with?
  → Use HASHING (SHA-256, SHA-3)

Need to STORE user passwords in a database?
  → Use PASSWORD HASHING (bcrypt, Argon2id, scrypt)

Just need to TRANSMIT a binary image inside an ASCII JSON payload?
  → Use ENCODING (Base64)

5. When to Use Base64 (Correctly)

Base64 is genuinely useful when used for its actual purpose — binary format conversion:

  • Data URIs in CSS — embedding small SVG icons in stylesheets to avoid an HTTP request: background-image: url(data:image/svg+xml;base64,...)
  • Email attachments — MIME encoding for transporting binary PDFs through ASCII email protocols (RFC 2045).
  • JSON API payloads — including binary file uploads inside a JSON string, since JSON only supports UTF-8 text.
  • Binary in text databases — storing a small binary when a column only accepts VARCHAR.

In all of these, Base64 is a transport mechanism. Actual security comes from elsewhere (HTTPS/TLS in transit, file-system permissions, database access controls).

6. Implementing Real Cryptography

Replacing Base64 “obfuscation” with AES-256-GCM (Node.js)

AES-GCM (Galois/Counter Mode) is the standard for symmetric encryption. It provides both confidentiality and integrity using an Initialization Vector (IV) and an auth tag.

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

// ❌ Base64 "encryption"
const fake_encrypted = Buffer.from('sensitive_api_key_123').toString('base64');

// ✅ AES-256-GCM encryption
function encryptSecurely(plaintextStr, secretKey32Byte) {
  // Generate a random 16-byte IV
  const iv = randomBytes(16);
  
  // Initialize the cipher
  const cipher = createCipheriv('aes-256-gcm', secretKey32Byte, iv);
  
  // Encrypt
  const encryptedBytes = Buffer.concat([cipher.update(plaintextStr, 'utf8'), cipher.final()]);
  
  // Get the auth tag to detect tampering
  const authTag = cipher.getAuthTag();
  
  // Concatenate IV + auth tag + ciphertext, then Base64 it for safe text storage
  return Buffer.concat([iv, authTag, encryptedBytes]).toString('base64');
}

// Decryption verifies the auth tag before returning the plaintext
function decryptSecurely(encryptedBase64Str, secretKey32Byte) {
  const data = Buffer.from(encryptedBase64Str, 'base64');
  
  // Extract the components by byte length
  const iv = data.subarray(0, 16);
  const authTag = data.subarray(16, 32);
  const encryptedPayload = data.subarray(32);
  
  const decipher = createDecipheriv('aes-256-gcm', secretKey32Byte, iv);
  decipher.setAuthTag(authTag);
  
  // If auth-tag validation fails, this throws
  return decipher.update(encryptedPayload, undefined, 'utf8') + decipher.final('utf8');
}

Password storage with bcrypt (Python)

import bcrypt
import base64

# ❌ Base64 "hashing" for a password
fake_hash = base64.b64encode(b"MySecurePassword123!")  # Reversible by attackers!

# ✅ bcrypt hashing
password_bytes = b"MySecurePassword123!"

# Generate a salt and hash with a work factor of 12
salt = bcrypt.gensalt(rounds=12)
secure_hash = bcrypt.hashpw(password_bytes, salt)  # One-way, irreversible

# Later, verify a login attempt
is_valid_login = bcrypt.checkpw(b"MySecurePassword123!", secure_hash)  # Returns True

Further Reading


Base64 is a great format converter and a terrible security protocol. Decode and inspect Base64 payloads locally with our Base64 Encoder / Decoder, and generate irreversible password hashes with our Bcrypt Generator.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
6 min read
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.