Base64 Encoding
Encode binary data as text with java.util.Base64, pick the right alphabet for headers, URLs and email, and remember that encoding is not encryption.
String s = Base64.getEncoder().encodeToString(bytes); // + / =
String u = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
byte[] b = Base64.getDecoder().decode(s); // throws if bad
byte[] v = Base64.getUrlDecoder().decode(u);Base64 turns arbitrary bytes into text made of 64 safe characters, so binary data can travel through places that only accept text: JSON fields, HTTP headers, URLs, environment variables and email. Every 3 bytes become 4 characters, so the output is about a third larger than the input.
It is not encryption. Anyone can decode it in one call. A Basic auth header or a JWT payload is readable by whoever sees it, which is why both depend on HTTPS and why neither should hold anything the client must not read.
Three alphabets
| Factory | Characters 62 and 63 | Use for |
|---|---|---|
getEncoder() | + and / | JSON fields, Basic auth, data URIs |
getUrlEncoder() | - and _ | URLs, file names, cookies, JWTs, tokens |
getMimeEncoder() | + and /, lines of 76 with CRLF | email bodies and attachments |
The basic alphabet's + and / mean something in URLs (+ often decodes to a space), so a token in a query string must use the URL alphabet. Padding with = rounds the output to a multiple of 4; withoutPadding() drops it, as JWTs do. The decoders accept input with or without padding, but each decoder only accepts its own alphabet.
String text = Base64.getEncoder().encodeToString(bytes);
byte[] back = Base64.getDecoder().decode(text);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Decoding untrusted input
decode throws IllegalArgumentException on a character outside the alphabet or a malformed ending. When the value comes from a request, catch it and return 400 rather than letting it surface as a 500. Check the length before decoding too: a 50 MB Base64 string in a JSON body becomes a 37 MB byte array.
Mistakes people make
- Mixing alphabets. Encoding with
getUrlEncoderand decoding withgetDecoderfails as soon as a-or_appears. About three in four 32-byte tokens contain one, but short test values often do not, so tests miss it. - Forgetting the charset. Encode text with
getBytes(StandardCharsets.UTF_8)and decode withnew String(bytes, StandardCharsets.UTF_8). - Base64 to hide secrets. A Base64 value in a config file or Kubernetes Secret manifest is still the plain secret to anyone who can read it.
- Large files inline. Images in JSON grow by a third and are held in memory twice. Upload them as binary and pass a URL instead.
Example
When to use it
- Send a small binary value, such as a thumbnail or a signature, inside a JSON response field.
- Build the Authorization header for a partner API that uses HTTP Basic authentication over HTTPS.
- Encode random token bytes with the URL-safe alphabet so they can go in links and cookies unescaped.
More examples
Building and parsing a Basic auth header
Splitting at the first colon matters because usernames cannot contain one but passwords can. The header is trivially decodable, so Basic auth is only acceptable over HTTPS.
record Credentials(String user, String password) {}
String basicHeader(String user, String password) {
String pair = user + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(pair.getBytes(StandardCharsets.UTF_8));
}
Optional<Credentials> parse(String header) {
if (header == null || !header.startsWith("Basic ")) return Optional.empty();
try {
String pair = new String(Base64.getDecoder().decode(header.substring(6)), StandardCharsets.UTF_8);
int colon = pair.indexOf(':'); // the password may contain colons
if (colon < 0) return Optional.empty();
return Optional.of(new Credentials(pair.substring(0, colon), pair.substring(colon + 1)));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
void main() {
String header = basicHeader("report-bot", "demo:pass-not-real");
IO.println("Authorization: " + header);
Credentials c = parse(header).orElseThrow();
IO.println("user=" + c.user() + " password=" + c.password());
IO.println("bad header: " + parse("Basic !!!"));
}
// Output:
// Authorization: Basic cmVwb3J0LWJvdDpkZW1vOnBhc3Mtbm90LXJlYWw=
// user=report-bot password=demo:pass-not-real
// bad header: Optional.emptyRejecting bad input with a 400
The message names the offending character as a hex code: 24 is '$' and 2d is '-'. Catching IllegalArgumentException turns a client mistake into a 400 instead of a server error.
String decodeField(String value) {
try {
byte[] bytes = Base64.getDecoder().decode(value);
return "200 decoded " + bytes.length + " bytes";
} catch (IllegalArgumentException e) {
return "400 " + e.getMessage();
}
}
void main() {
IO.println(decodeField("aGVsbG8=")); // hello
IO.println(decodeField("aGVsbG8")); // missing padding is accepted
IO.println(decodeField("aGVs$bG8=")); // '$' is not in any alphabet
IO.println(decodeField("-_-_EA==")); // URL alphabet given to the basic decoder
}
// Output:
// 200 decoded 5 bytes
// 200 decoded 5 bytes
// 400 Illegal base64 character 24
// 400 Illegal base64 character 2dMIME encoding for email attachments
The MIME encoder wraps at 76 characters with CRLF, as email requires, and only the MIME decoder skips those line breaks. The basic decoder rejects the carriage return, character 0d.
void main() {
byte[] attachment = new byte[120];
for (int i = 0; i < attachment.length; i++) {
attachment[i] = (byte) (i * 7);
}
String mime = Base64.getMimeEncoder().encodeToString(attachment);
List<String> lines = mime.lines().toList();
IO.println("lines: " + lines.size());
for (String line : lines) {
IO.println("length " + line.length());
}
IO.println("contains CRLF: " + mime.contains("\r\n"));
byte[] back = Base64.getMimeDecoder().decode(mime);
IO.println("MIME decoder round trip: " + Arrays.equals(attachment, back));
try {
Base64.getDecoder().decode(mime);
} catch (IllegalArgumentException e) {
IO.println("basic decoder: " + e.getMessage());
}
}
// Output:
// lines: 3
// length 76
// length 76
// length 8
// contains CRLF: true
// MIME decoder round trip: true
// basic decoder: Illegal base64 character d
Discussion