Hex and Base64 Encode the Same Bytes Differently
Both hexadecimal and Base64 are binary-to-text encodings. They take arbitrary raw bytes and produce a printable string that can be safely stored in JSON, URLs, HTTP headers, or copied across terminals. The difference is only in the alphabet and density:
- Hex: 16-character alphabet (0-9, a-f). Each byte = 2 characters. 100% overhead. Trivial to read and compare mentally.
- Base64: 64-character alphabet (A-Z, a-z, 0-9, +, /, with = as padding). Each 3 bytes = 4 characters. ~33% overhead. Denser but harder to read.
Example: the 5 ASCII bytes H e l l o (hex 48 65 6c 6c 6f) Base64-encode to SGVsbG8=. Both representations describe the identical sequence of 5 bytes.
Why You End Up Converting Between Them
Most Base64 ↔ hex conversions happen for one of these reasons:
- Comparing a crypto hash. OpenSSL emits SHA-256 in hex by default; the Web Crypto API returns it as an ArrayBuffer that is usually re-encoded as Base64. You cannot
===them directly — one side must be converted. - Debugging a JWT signature. JWTs use URL-safe Base64; the HMAC comparison in your code might be hex. Convert the token signature segment to hex to visually confirm the match.
- Inspecting a PEM block.X.509 certs, PGP keys, and SSH keys ship as Base64 in PEM format. To get a human-readable byte dump (OpenSSL's
openssl x509 -noout -fingerprintfor example), convert the inner Base64 to hex. - Interop between APIs. AWS Signature V4 uses hex for the canonical request hash but Base64 for the HMAC. Mixing them up is a classic source of 403 errors.
- Hex-encoded binary in a config file. Many config systems disallow raw bytes (etcd, Kubernetes Secrets stringData, env vars). Hex and Base64 both work; converting lets you adopt whichever the surrounding config uses.
Convert in Every Common Language
# ─── Python ───
import base64
hex_str = base64.b64decode("SGVsbG8=").hex()
# "48656c6c6f"
# Reverse
b64 = base64.b64encode(bytes.fromhex("48656c6c6f")).decode()
# "SGVsbG8="
# ─── Node.js ───
const hex = Buffer.from("SGVsbG8=", "base64").toString("hex");
// "48656c6c6f"
const b64 = Buffer.from("48656c6c6f", "hex").toString("base64");
// "SGVsbG8="
# ─── Browser (no Buffer) ───
function base64ToHex(b64) {
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
}
function hexToBase64(hex) {
const bytes = new Uint8Array(hex.match(/.{2}/g).map(h => parseInt(h, 16)));
return btoa(String.fromCharCode(...bytes));
}
# ─── Go ───
import ("encoding/base64"; "encoding/hex")
raw, _ := base64.StdEncoding.DecodeString("SGVsbG8=")
hexStr := hex.EncodeToString(raw) // "48656c6c6f"
# ─── Bash / shell ───
echo "SGVsbG8=" | base64 -d | xxd -p
# 48656c6c6f
echo "48656c6c6f" | xxd -r -p | base64
# SGVsbG8=
# ─── PowerShell ───
[Convert]::ToHexString([Convert]::FromBase64String("SGVsbG8="))
# "48656C6C6F" (uppercase — lowercase with .ToLower())
# ─── Ruby ───
require 'base64'
Base64.strict_decode64("SGVsbG8=").unpack1("H*")
# "48656c6c6f"
# ─── PHP ───
bin2hex(base64_decode("SGVsbG8="));
// "48656c6c6f"Working with SHA Hashes
# A SHA-256 hash is 32 bytes.
# Hex representation = 64 chars.
# Base64 representation = 44 chars (one trailing =).
# ─── OpenSSL CLI emits hex by default ───
echo -n "Hello" | openssl dgst -sha256
# SHA2-256(stdin)= 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
# ─── Same hash, Base64 ───
echo -n "Hello" | openssl dgst -sha256 -binary | base64
# GF+Ns30nH+JfVhpvyTi+JkMG7DBO2lGAB9F2SCY4GWk=
# ─── Convert one to the other ───
# hex → bytes → Base64
echo "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969" | xxd -r -p | base64
# GF+Ns30nH+JfVhpvyTi+JkMG7DBO2lGAB9F2SCY4GWk=
# Base64 → bytes → hex
echo "GF+Ns30nH+JfVhpvyTi+JkMG7DBO2lGAB9F2SCY4GWk=" | base64 -d | xxd -p
# 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
# This is especially useful for comparing a hash from
# an API response (likely Base64) against a hash computed locally (likely hex).PEM Blocks Are Base64 — You Can Decode to Hex
# PEM certificate (strip header/footer, join lines, Base64-decode, hex-dump)
cat cert.pem \
| sed -n '/-----BEGIN /,/-----END /p' \
| sed '1d;$d' \
| tr -d '\n' \
| base64 -d \
| xxd -p
# Or more directly with OpenSSL
openssl x509 -in cert.pem -outform DER | xxd -p
# For a quick fingerprint (SHA-1 hash of the DER bytes in hex)
openssl x509 -in cert.pem -noout -fingerprint
# SHA1 Fingerprint=5A:0A:71:0D:F5:76:AC:9E:...Common Mistakes
- Missing
padStart(2, "0")in JavaScript.(5).toString(16)returns"5", not"05". Without padding, byte values 0-15 become ambiguous: hex"58"could be [0x58] or [0x5, 0x8]. Always pad to width 2. - Mixing uppercase and lowercase hex. Decoders accept both, but comparing strings fails. Normalize with
.toLowerCase()or.ToLower()before comparing. - Base64 input with whitespace / newlines. MIME-style Base64 wraps at 76 characters. Strip whitespace with
.replace(/\\s/g, '')or your language's equivalent before decoding, OR use a decoder that tolerates it (most do by default). - Hex input with odd length. Means a nibble is missing — the data is truncated or corrupted. Many decoders silently zero-pad or drop the last character; always validate
length % 2 === 0first. - Confusing URL-safe Base64 with standard Base64. URL-safe uses
-and_instead of+and/, and often strips padding (JWT style). Convert to standard Base64 first (tr -_/+/, re-add padding) before running through a hex converter.
Key Facts
- Hex alphabet:
- 0-9 a-f (lowercase convention) or 0-9 A-F (RFC 4648 Base16)
- Base64 alphabet:
- A-Z a-z 0-9 + / (standard) or A-Z a-z 0-9 - _ (URL-safe)
- Hex overhead:
- 100% (N bytes → 2N chars)
- Base64 overhead:
- ~33% (N bytes → ceil(N/3)*4 chars)
- SHA-256 as hex:
- 64 characters
- SHA-256 as Base64:
- 44 characters (one trailing =)
- Conversion rule:
- decode one → raw bytes → encode in other
Related Base64 Tools
- Base64 Encode Online — general-purpose encoder
- Base64 Decode Online — reverse direction
- URL-Safe Base64 — convert to the JWT variant before hex-decoding
- Base64 Encode in Python — Python patterns incl.
.hex() - Base64 Encode in JavaScript — browser + Node reference
- JWT Debugger — inspect JWTs that mix Base64 + hex HMACs