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.

SyntaxString 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

FactoryCharacters 62 and 63Use for
getEncoder()+ and /JSON fields, Basic auth, data URIs
getUrlEncoder()- and _URLs, file names, cookies, JWTs, tokens
getMimeEncoder()+ and /, lines of 76 with CRLFemail 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 getUrlEncoder and decoding with getDecoder fails 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 with new 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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.empty

Rejecting 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.

Example · java
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 2d

MIME 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.

Example · java
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

  • Be the first to comment on this lesson.