Hashes and encoding
Base64 is encoding, not encryption
On this page 0%
I still find API keys sitting in repos as Base64 strings with a comment that says “encrypted.” They are not encrypted. They are dressed up. Base64 is a way to write bytes in ASCII. Anyone who can read the file can reverse it in one line. I have done it in a browser tab with the Base64 decoder more times than I want to count.
This is a cranky post about encoding versus encryption, UTF-8 footguns, JWT adjacency, and why “secret in Base64” keeps happening. I use Base64 all week. I do not trust it with a secret that matters. If I need a secret, I generate one with the password generator or a token tool, and I store it in a vault, not in a string that looks mildly obscure.
Encoding is a dialect
Bytes are not always legal in XML, JSON, URLs, or MIME. Base64 maps every 3 bytes to 4 characters from A-Z, a-z, 0-9, plus, and slash, with = padding. URL-safe Base64 swaps plus and slash and often drops padding. Hex maps each byte to two characters. UTF-8 maps Unicode code points to bytes. These are representations. They are reversible without a secret. That is the point. The receiver must recover the same bytes.
Encryption maps plaintext to ciphertext with a key. Without the key, you want that mapping to be useless. AES-GCM, for example, needs a key and a nonce. The output looks like random bytes. Those random bytes might then be Base64’d so they can live in JSON. People see the Base64 and think the Base64 was the protection. The protection was AES. The Base64 was the envelope.
Obfuscation is the third thing. XOR with a constant, ROT13, “encrypt” with a hardcoded passphrase in the client. It slows down a curious intern. It does not slow down a motivated one. I treat obfuscation as a product decision for anti-cheat or puzzle games, not for API keys.
UTF-8 before you encode
Base64 does not know what text is. You encode bytes. If you have a JavaScript string, you must pick an encoding. UTF-8 is the adult choice. btoa in browsers historically wanted “binary strings,” one byte per character, and it throws on emoji. That throw is a gift. The old workaround of unescape(encodeURIComponent(str)) is a UTF-8 conversion in a trench coat. Prefer TextEncoder then a Base64 encode of the Uint8Array.
If you skip this, you will disagree with Python b64encode(s.encode()) and with Go. I have watched a JWT library and a handmade Base64 helper fight over a non-ASCII name in a claim. The header and payload of a JWT are Base64url of UTF-8 JSON. Get the bytes wrong and the signature, which is actual crypto, verifies a different message than the one you think you printed.
Newlines in MIME Base64 (76-character lines) versus compact Base64 will break naive decoders. Strip whitespace before decode. Our decoder is meant for that paste-from-email moment. Our encoder emits compact unless you need wrapped.
JWT adjacency
A JWT is three Base64url parts: header, payload, signature. The first two are readable. I decode them when debugging. The signature is crypto (HMAC or a public-key scheme). If you see a token and you can read the payload, that is normal. Confidentiality was never the JWT promise. Do not put a password in a JWT claim. Do not put a PAN in a JWT claim. Signatures stop tampering by people who do not have the key. They do not hide the JSON from the bearer.
People “secure” localStorage by Base64-encoding the JWT. The XSS attacker types atob. If you are worried about XSS, you have an XSS problem. Encoding will not help. HttpOnly cookies and a tight CSP are a different conversation. I still paste tokens into the decoder to see claims. I do that on a machine I trust, because a token is a credential, even if the payload is public-ish.
Unsigned tokens (alg: none) are a historical wound. The Base64 parts still look like JWT. Verify the algorithm on the server. I am not building a JWT library here. I am telling you that readable payload is expected, not a breach by itself.
Secrets in Base64
Cloud providers emit credentials as Base64 because the credential contains bytes that are unhappy in YAML. Kubernetes secrets are Base64 in etcd manifests. That is encoding for transport in a text API. RBAC is the protection. If your Secret YAML is in a public gist, the Base64 did nothing. Decode, rotate, assume compromise.
I see password_b64 columns in databases. The application decodes and uses the password. An SQL dump is then a password dump with extra steps. Encrypt at rest with a real KMS if you needed confidentiality. Hash if you needed to verify a user password (and use a KDF, not a fast hex digest). Do not Base64.
Client-side “encryption” with a key shipped in the JS bundle is obfuscation. I will not pretend a blog generator is a substitute for TLS. HTTPS already encrypts the pipe. Base64 on the pipe is a bug (you wanted binary or you wanted compression, not a 33% size tax, unless a text protocol forced you).
When I use the tools
I encode a small binary so I can drop it in a JSON fixture. I decode a support ticket that contains a blob someone thought was encrypted. I check that a PEM’s inner DER matches. I do not encode a password and call it a day. For passwords, the password generator. For opaque tokens, a long random string. For identifiers, a UUID.
If you need to confirm a file was not garbled after a Base64 email trip, hash the bytes with SHA-256 before and after. Matching hashes mean matching bytes. Matching Base64 strings means matching the text representation, including padding and line breaks if you were sloppy.
Padding errors: decode libraries disagree on missing =. URL-safe versus standard alphabets disagree on - and _. If a decode fails, try the other alphabet before you assume corruption. If it succeeds and the bytes are junk, you probably decoded hex as Base64 or vice versa. Hex is 0-9a-f. It looks nothing like Base64 once you stare at it, except when the hex is short and you are tired.
A small decision rule
Need to put bytes in JSON or HTML? Base64. Need to put bytes in a URL path? Base64url. Need nobody to read the bytes? Encryption with a key you do not publish, or do not send the bytes. Need to prove who wrote the bytes? A signature. Need to store a user password? A KDF. Need to look clever in a config file? Resist.
I keep the encoder and decoder on Randomly because I use them as a workbench, not as a vault. They run locally. That is a courtesy so a key you pasted does not hit our server. It is not a reason to paste production keys. See privacy. Rotate if you pasted something real. I have rotated my own because I was careless in a demo. That is cheaper than pretending Base64 was a lock.
One more opinion on “we Base64 the PII in test fixtures so they are not PII.” Decode restores them. Generate synthetic PII instead. That is the whole point of PII-safe test data. Encoding is not anonymization. Hashing is a weak anonymization. Generation is the clean break.
PEM files are Base64 with headers. The -----BEGIN line is not encryption. The payload is DER bytes in Base64. A passphrase-encrypted PEM is encryption, and you will feel it when OpenSSL asks for a password. If it does not ask, it was not encrypted. I decode a PEM body when I am debugging a cert chain. I do not commit the PEM because I Base64’d it again in a comment.
Email attachments still wrap Base64. Copying from a quoted-printable mixed message will inject extra characters and line breaks. Decode will fail or emit junk. I paste into the decoder, strip headers, and keep only the alphabet plus padding. If the bytes should be a PNG, I check a SHA-256 of the output against the sender’s digest. The SHA-256 digest is the integrity check. Base64 is the truck.
People ask whether compressing then encrypting then Base64 is “too many steps.” Compress then encrypt is a real pattern (watch compression oracles on secrets). Encrypt then Base64 is how ciphertext rides in JSON. Base64 then encrypt is usually a mistake: you encrypted ASCII, not the original bytes, and you paid a size tax before the cipher. I see that mistake in homemade file formats. Bytes in, cipher out, then encode if a text channel requires it.
Frequently asked questions
Is Base64 encryption?
No. It is a reversible text encoding of bytes. No key.
Why do JWTs look scrambled but read as JSON after decode?
The scramble is Base64url. The JSON was never meant to be secret. The signature is the integrity part.
Why did btoa throw on my string?
A character needed more than one byte. Encode UTF-8 first, then Base64 the bytes.
Should Kubernetes secrets use encryption?
At rest, etcd can. In Git, a Secret manifest is still a secret in Git. Use sealed-secrets, SOPS, or an external vault. Base64 in the YAML is not that.