Decodificação Base64 em Java: um guia completo
Ela aparece num chamado de suporte, numa resposta de API, num secret do Kubernetes, ou enterrada no meio de uma URL: uma sequência longa de letras e dígitos com o ocasional +, /, - ou _, e talvez um ou dois = pendurados no final. Alguém diz que é Base64 e que contém algo que você precisa: uma senha, um payload JSON, um certificado, uma foto. Este guia é a receita Java para trazê-la de volta. Uma orientação rápida, porque a página inicial passeia pelo formato em profundidade: o Base64 reescreve cada três bytes de dados como quatro caracteres tirados de um alfabeto de 64 letras, e cola um ou dois pads = no final quando o último bloco fica curto. Decodificar é a direção de encolher daquela troca: quatro caracteres entram, três bytes saem, então o resultado sempre precisa de um quarto a menos de espaço que a entrada.
Aqui vai a manchete, e é uma boa. Desde 18 de março de 2014, todo JDK entrega uma caixa de ferramentas completa de Base64 na biblioteca padrão: java.util.Base64. Sem download, sem coordenada Maven, sem biblioteca nativa. Um import, sete métodos de fábrica, três alfabetos, e o mesmo comportamento do Java 8 até o Java 26 de hoje. Tudo neste artigo é construído sobre aquela única classe.
Um limite honesto antes de começarmos: esta é a parte do decodificador da história. Você vai aprender a escolher o decodificador certo para o alfabeto que estiver encontrando, a ler as mensagens de erro do JDK como um médico lê um exame, a transformar bytes em texto sem mojibake, a desembrulhar a blindagem PEM, a streamar payloads de vários gigabytes e a reconhecer as armadilhas de segurança que o formato deixa quieto no caminho. A outra direção, empacotar bytes em uma string, tem o próprio guia, e ele é linkado no final deste.
O que você já tem
Instalar Base64 no Java é a resposta de uma linha que você dá no quadro branco: "Está no JDK." A classe java.util.Base64 faz parte do módulo java.base desde o 1.8, e o javadoc dela ainda diz Since: 1.8 em 2026. A única coisa que você instala é um JDK, qualquer Java 8 ou mais novo de qualquer fornecedor (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) funciona, e numa máquina baseada em Debian isso é um único comando:
sudo apt install openjdk-17-jdk-headless
A API é uma fábrica: você nunca constrói um decodificador, pede um para a classe. Os sete métodos de fábrica entregam três personalidades em cada direção, e o lado do decodificador é este:
| Método de fábrica | Alfabeto | Personalidade | Quando recorrer a ele |
|---|---|---|---|
getDecoder() |
A-Z a-z 0-9 + / |
Estrito: rejeita qualquer caractere estrangeiro | Dados que você produz ou controla |
getUrlDecoder() |
A-Z a-z 0-9 - _ |
Estrito, alfabeto URL-safe | JWTs, tokens, IDs, qualquer coisa que nasça numa URL |
getMimeDecoder() |
A-Z a-z 0-9 + / |
Tolerante: pula todo caractere fora do alfabeto | E-mail, entrada genuinamente quebrada em linhas, corpos PEM |
getEncoder(), getUrlEncoder(), getMimeEncoder() |
como acima | Codificação, terreno do guia irmão | Sempre que você produz Base64 em vez de ler |
Três propriedades das instâncias retornadas valem a pena memorizar. Primeira, são thread-safe: o javadoc diz que as instâncias são "seguras para uso por múltiplas threads concorrentes", e o código-fonte mostra os métodos de fábrica retornando a mesma instância compartilhada em toda chamada, então Base64.getDecoder() == Base64.getDecoder() é verdadeiro. Crie um decodificador num campo estático e compartilhe no serviço inteiro, você nem está copiando nada. Segunda, são sem estado entre chamadas, então não há nada para resetar e nada para sincronizar ao redor. Terceira, passar null onde um array de bytes ou uma string é esperado não é um no-op gentil: é uma NullPointerException, exatamente como o javadoc da classe promete.
Você ainda vai encontrar bibliotecas mais velhas em codebases, então um mapa rápido do cenário. Apache Commons Codec (atualmente 1.22.1) carrega o próprio org.apache.commons.codec.binary.Base64 desde o 1.0, com uma API de Builder que expõe a política estrito-ou-tolerante, o comprimento de linha e o separador como controles; é a ferramenta certa apenas se você precisa suportar JVMs anteriores ao Java 8 ou quer os helpers de verificação de forma dele. O Guava entrega com.google.common.io.BaseEncoding, um veterano de capacidade similar, ainda popular em stacks de big data. Para qualquer coisa rodando num JVM moderno, java.util.Base64 é a escolha padrão: zero dependências, e benchmarks da comunidade continuam encontrando ele o mais rápido do grupo (mais sobre isso na seção de desempenho).
Decodificando sua primeira string
Noventa por cento da vida de decodificação cabe num punhado de linhas. Aqui está a cerimônia inteira, usando o menor exemplo que o próprio RFC usa para explicar o alfabeto:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstDecode {
public static void main(String[] args) {
byte[] bytes = Base64.getDecoder().decode("TWFu");
String text = new String(bytes, StandardCharsets.UTF_8);
System.out.println(text); // Man
}
}
Quatro frases sobre o que acabou de acontecer. Primeira, o ponto de entrada é uma instância, não a classe: decode() vive no objeto Base64.Decoder que você tirou da fábrica. Segunda, e esta é a decisão de design mais importante de toda a API, o resultado é um array de bytes, nunca uma String. O payload pode ser uma frase, um JPEG ou um hash, e nenhum deles deve ser tratado da mesma forma antes de você saber o que tem, então o JDK para nos bytes de propósito. Terceira, o salto de bytes para texto é um passo separado e deliberado, com um charset explícito, e é nesse passo que "café" vira mojibake se você for descuidado, a seção de charset abaixo é dedicada a isso. Quarta, a string vazia é um valor de primeira classe: Base64.getDecoder().decode("") te dá um array de comprimento zero, sem exceção, sem alarde.
Para dados de teste na sua cabeça, lembre-se de que TWFu é o próprio smoke test do padrão: se o seu código de decodificação transforma ele em Man, a máquina é honesta. A viagem de volta na outra direção são duas linhas da mesma API e ganham o tratamento completo no guia de codificação linkado no final.
A escalação dos decodificadores
O Java não te dá um decodificador, te dá três, e a diferença entre eles é uma decisão de política sobre qual alfabeto aceitar e quanta bagunça tolerar. Os três são instâncias da mesma classe aninhada Base64.Decoder. O javadoc da classe explicita a divisão em uma frase por personalidade. Para os decodificadores básico e URL-safe: o decodificador "rejeita dados que contenham caracteres fora do alfabeto base64". Para o decodificador MIME: "todos os separadores de linha e outros caracteres não encontrados na tabela do alfabeto base64 são ignorados na operação de decodificação". Essa segunda frase é a história inteira do MIME em uma linha, e ela tem dentes, porque "ignorar" significa tudo que não é um caractere do alfabeto, não apenas quebras de linha.
A regra de escolha é curta. Padrão: getDecoder(). Se o valor veio de uma URL, um token ou uma API que prometeu "URL-safe", mude para getUrlDecoder(). Só quando você genuinamente espera entrada com forma de MIME (quebras de linha a cada 76 caracteres, direto de um sistema de e-mail) é que você recorre ao getMimeDecoder(). Na dúvida, escolha estrito: o trabalho de um decodificador estrito é fazer as surpresas falharem, e é exatamente isso que você quer numa fronteira de confiança. Um decodificador tolerante, por outro lado, é uma lupa para corrupção: uma string com caracteres soltos decodifica para algo plausível e errado, sem nenhum erro.
Lendo as reclamações do decodificador
Os decodificadores estritos falham alto, e falham precisamente. Toda entrada ruim lança uma IllegalArgumentException cuja mensagem te conta exatamente o que deu errado, então na primeira vez que uma string de produção explode, esta tabela é o que você lê. As mensagens abaixo são o texto exato do JDK atual:
| Entrada (no getDecoder, salvo indicação) | O que há de errado | Mensagem exata |
|---|---|---|
"SGVs bG8s" |
um espaço se infiltrou | Illegal base64 character 20 |
"SGVs\nbG8s" |
uma quebra de linha se infiltrou | Illegal base64 character a |
"SGVs$bG8s" |
um cifrão não está no alfabeto | Illegal base64 character 24 |
"SGVsbG8-" |
um hífen URL-safe no decodificador padrão | Illegal base64 character 2d |
"ab+c" para o getUrlDecoder() |
um sinal de mais no decodificador URL-safe | Illegal base64 character 2b |
"S" |
um símbolo não forma um byte | Input byte[] should at least have 2 bytes for base64 bytes |
"SG=VsbG8s" |
padding no meio dos dados | Input byte array has wrong 4-byte ending unit |
"Zm8==" |
dois pads onde um é o correto | Input byte array has incorrect ending byte at 4 |
"Z=" |
um caractere seguido de um pad | Last unit does not have enough valid bits |
"SGVsbG8sIHdvcmxkIQ==xx" |
lixo depois dos pads | Input byte array has incorrect ending byte at 20 |
Aquele número hexadecimal na mensagem é o valor de byte do caractere ofensor, impresso com Integer.toString(byte, 16): 20 é um espaço, a é um line feed, d é um carriage return, 24 é um cifrão, 2d é o hífen URL-safe, 2b é o mais, 2f é a barra, 5f é o sublinhado. Dois truques para guardar no bolso. Primeiro, a mensagem pode ficar negativa: alimente o decodificador com uma string contendo é e ele reclama Illegal base64 character -17, porque o caractere é primeiro mapeado para o byte Latin-1 0xE9, que como byte Java com sinal é menos 23, e menos 23 em hexadecimal é menos 17. O seu log de erros, resumindo, está fazendo aritmética com sinal. Segundo, a posição: na família incorrect ending byte at N, N é o índice a partir de zero do primeiro byte que o decodificador não conseguiu fazer sentido, e isso é um presente quando você está fatiando um payload corrompido ao meio.
Uma troca de figurino para saber: quando a decodificação acontece pelo stream embrulhado (a variante wrap(InputStream), coberta abaixo), os mesmos problemas aparecem como uma IOException com um prefixo 0x no lugar: Illegal base64 character 0x20 (JDKs atuais; o decodificador de stream do JDK 8 imprime o valor de lookup, -1, em vez do byte). Mesmo problema, exceção diferente, grafia levemente diferente. E o decodificador MIME tolerante, claro, reclama de nenhuma disso: ele só pula. Esse é o preço do humor tolerante.
As regras de padding
Toda string Base64 do mundo faz uma promessa silenciosa sobre padding, e a promessa do Java é incomum de amigável. O javadoc do decodificador diz exatamente: o caractere de padding = "é aceito e interpretado como o fim dos dados de bytes codificados, mas não é obrigatório". Uma unidade final de dois ou três caracteres decodifica como se tivesse sido preenchida, e quando os pads estão presentes, eles devem estar presentes na quantidade exata certa. O comportamento do JDK atual em volta dos exemplos clássicos:
| Entrada | Resultado |
|---|---|
"" |
array de bytes vazio, sem erro |
"Zm8" |
"fo", padding simplesmente ausente |
"Zm8=" |
"fo", a grafia canônica |
"Zm8==" |
IllegalArgumentException: incorrect ending byte at 4 |
"Zm9v=" |
IllegalArgumentException: wrong 4-byte ending unit |
"Zg==" |
"f", um byte |
"Z=" |
IllegalArgumentException: last unit does not have enough valid bits |
"AA==" |
exatamente um byte, o byte NUL 0x00 |
"AAAA" |
três bytes NUL |
Leia essa tabela duas vezes. A string vazia decodifica para nada, enquanto AA== decodifica para um único byte NUL: no Base64, "nada" e "um zero" são criaturas diferentes, e ambas são entrada perfeitamente válida. E o padding, quando presente, deve ser exato: Zm8= está certo, Zm8== está errado, Zm9v= está errado, e um pad no meio da string está errado. Consequência prática para os seus próprios protocolos: escolha uma grafia (com padding ou sem) e imponha ela nas duas pontas, porque um valor que pode chegar em duas grafias é um valor que pode quebrar uma verificação de igualdade ingênua em algum lugar mais adiante.
base64url: o alfabeto feito para URLs
O Base64 padrão termina o alfabeto com + e /, e são exatamente os dois caracteres que não se comportam em URLs: um + numa query string já é um espaço antes do Java vê-lo, uma / é um separador de caminho, e um = pendurado quer percent-encoding para virar um monstro de três caracteres. A seção 5 do RFC 4648 desenha a correção: o alfabeto seguro para URLs e nomes de arquivo, onde + vira -, / vira _, e o padding = final é tipicamente descartado quando o comprimento é conhecido implicitamente. O RFC é insistente sobre o nome: esta codificação "não deve ser considerada a mesma que a codificação base64". Você vai encontrá-lo como base64url, e é ali que JSON Web Tokens, parâmetros de state do OAuth, IDs de sessão de API e IDs de vídeo de onze caracteres moram.
O payload base64url mais famoso da web é o JWT, e espiar dentro de um é trabalho de três linhas. As partes do token são convencialmente sem padding, e o decodificador URL está feliz com isso, porque padding é aceito mas não é obrigatório, lembre-se:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtPeek {
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+ ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
+ ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
String[] parts = token.split("\\.");
byte[] header = Base64.getUrlDecoder().decode(parts[0]);
byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
System.out.println(new String(header, StandardCharsets.UTF_8));
// {"alg":"HS256","typ":"JWT"}
System.out.println(new String(payload, StandardCharsets.UTF_8));
// {"sub":"1234567890","name":"John Doe","iat":1516239022}
}
}
Dois avisos honestos moram aqui. Primeiro, decodificar um JWT é espiar, não confiar: a terceira parte é uma assinatura, e as duas partes que você acabou de ler não são secretas e não são autenticadas. Confiar num payload antes de verificar a assinatura dele é o bug clássico de JWT, e a correção é entregar a verificação para uma biblioteca JOSE como JJWT (0.13.0) ou nimbus-jose-jwt (10.9.1) em vez de rolar a própria criptografia. Segundo, os erros identificam a direção: entregue uma string de alfabeto padrão para o getUrlDecoder() e você recebe Illegal base64 character 2b ou 2f, e o inverso lhe rende 2d ou 5f. Descombinação de alfabeto é a falha de decodificação Base64 mais comum do mundo, e a mensagem de erro aponta para ela num abrir e fechar de olhos. Se um token numa query string era suposto ser Base64 padrão, os + e / dele provavelmente foram massacrados pelo transporte antes de chegar até você, e o erro de decodificação está te contando sobre um bug a montante, não no seu decodificador.
De bytes para palavras
Toda chamada de decodificação neste artigo para nos bytes de propósito, porque Base64 é um formato de bytes, ponto final. A pergunta "que texto era esse?" é sua para responder, e a resposta padrão moderna é UTF-8. Há um detalhe de charset no lado da decodificação da API que surpreende as pessoas, então aqui vai. O overload decode(String) não interpreta a sua string como UTF-8. O javadoc diz exatamente: uma invocação "tem exatamente o mesmo efeito que invocar decode(src.getBytes(StandardCharsets.ISO_8859_1))". Isso não é um bug - é um truque: o alfabeto Base64 é ASCII puro, então mapear a string através do Latin-1 entrega ao decodificador exatamente os mesmos bytes com zero custo de conversão, e qualquer caractere não-ASCII na entrada simplesmente vira um símbolo inválido que o decodificador estrito rejeita (é de onde vêm os números hexadecimais negativos nas mensagens de erro).
O charset do payload é uma decisão completamente separada, a do passo new String(bytes, charset). Aqui está o caso clássico: "café" em UTF-8 são os cinco bytes 63 61 66 C3 A9, que codificam para Y2Fmw6k=:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetDecode {
public static void main(String[] args) {
byte[] packed = Base64.getDecoder().decode("Y2Fmw6k=");
System.out.println(new String(packed, StandardCharsets.UTF_8));
// café, o acento sobrevive
System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
// caf seguido de mojibake, os bytes UTF-8 mal lidos como Latin-1
}
}
Aquele segundo println é o modo de falha a reconhecer na hora: um payload UTF-8 lido através do Latin-1, produzindo uma string exatamente um caractere a mais longa e um byte fora. A cura é sempre combinar um charset com o produtor e passar ele explicitamente. E passar explicitamente no código, não só na cabeça: o construtor new String(bytes) sem argumentos usa o charset padrão da plataforma, que num servidor Windows pode ser Cp1252 e num Linux antigo pode ser o que a máquina entender. Desde o JDK 18 (JEP 400, "UTF-8 by Default") o padrão é UTF-8 em toda plataforma, então num JVM moderno a forma sem argumentos acontece de estar certa, mas o seu código ainda deve dizer isso, porque a próxima pessoa a lê-lo não deveria precisar saber qual é o padrão. E quando o payload nem é texto, o mesmo código só ganha um final diferente: bytes entram, bytes saem, até o último passo.
Quando o payload é um arquivo
O trabalho com arquivo mais comum é o inverso de alguma rotina de exportação: um arquivo de texto .b64 chega, e você precisa do arquivo original de volta. Com decodificação estrita, isso já está com cara de produção:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DecodeFile {
public static void main(String[] args) throws Exception {
byte[] packed = Files.readAllBytes(Paths.get("payload.bin.b64"));
byte[] raw = Base64.getDecoder().decode(packed);
Files.write(Paths.get("payload.bin"), raw);
}
}
Nada neste caminho se importa se o payload é um arquivo de texto, um arquivo ZIP ou um vídeo: byte[] é só bytes. A matemática do tamanho também joga a seu favor: a saída decodificada tem três quartos do comprimento da entrada codificada, então decodificar nunca piora a memória, e um arquivo codificado de várias centenas de megabytes é o menor dos dois. Um bom hábito é deixar os bytes se anunciarem antes de você confiar em qualquer rótulo. Os primeiros oito bytes de um PNG são sempre o número mágico 89 50 4E 47 0D 0A 1A 0A, o que significa que todo PNG codificado em Base64 que você vai encontrar começa com o mesmo prefixo, iVBORw0K: se um payload "afirma" ser uma imagem e não começa assim, algo já está errado.
Se você já tem o buffer de destino, o overload de dois arrays escreve direto nele e retorna exatamente quantos bytes chegaram, sem alocação intermediária:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DecodeInto {
public static void main(String[] args) {
byte[] src = "SGVsbG8sIHdvcmxkIQ==".getBytes(StandardCharsets.ISO_8859_1);
byte[] dst = new byte[16];
int written = Base64.getDecoder().decode(src, dst);
System.out.println(written); // 13
System.out.println(new String(dst, 0, written, StandardCharsets.UTF_8));
// Hello, world!
}
}
Um canto afiado nesse overload, documentado no javadoc: se o destino for pequeno demais, nenhum byte é escrito e você recebe IllegalArgumentException: Output byte array is too small for decoding all input bytes. Dimensione o buffer pela matemática simples, mais ou menos 3 * n / 4 menos o padding, e a exceção nunca mostra a cara. Existe também um overload de ByteBuffer que retorna um buffer fresco com o limite definido para o comprimento decodificado, útil quando o seu pipeline vive em NIO.
Da rede: headers, JSON e data URIs
Base64 encontra Java com mais frequência na borda da rede. Três formatos merecem um exemplo trabalhado cada.
Formato um: o header HTTP de autenticação Basic. O header de autenticação mais antigo da web ainda viaja de Base64. Segundo o RFC 7617, uma requisição Basic envia Authorization: Basic seguido da codificação Base64 de username:password, e o RFC é explícito de que isso é codificação, não proteção: qualquer pessoa com uma captura de pacotes lê as duas metades em uma tecla. O próprio exemplo do RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, decodifica para Aladdin:open sesame. Fazer o parse do header no lado do servidor são algumas linhas de trabalho:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuth {
public static String[] credentials(String header) {
if (header == null || !header.startsWith("Basic ")) {
return null;
}
byte[] packed = header.substring(6).getBytes(StandardCharsets.ISO_8859_1);
byte[] raw = Base64.getDecoder().decode(packed);
String userPass = new String(raw, StandardCharsets.UTF_8);
int colon = userPass.indexOf(':');
if (colon < 0) {
return null;
}
return new String[] {userPass.substring(0, colon), userPass.substring(colon + 1)};
}
}
Dois detalhes mantêm isso seguro. Fazer o split no primeiro dois-pontos importa, porque uma senha pode legalmente conter dois-pontos próprios. E a comparação da senha decodificada contra o seu valor armazenado deve ser em tempo constante: faça o hash dos dois valores com SHA-256 e compare os digests com MessageDigest.isEqual, nunca um equals simples que um atacante pode cronometrar até virar uma lista de usuários. Sirva isso somente sobre HTTPS; numa conexão simples, a camada Base64 é enfeite de vitrine.
Formato dois: binário dentro de JSON. Uma fatia grande das APIs modernas embute binário como texto Base64 dentro de JSON: endpoints de upload de arquivo, APIs de conteúdo, cofres de segredos e webhooks todos fazem isso, porque bytes crus quebrariam as regras de escaping da string JSON. O padrão é sempre o mesmo: o campo chega como uma string simples, e você decodifica ele na fronteira, não dentro dos seus objetos de domínio:
import java.util.Base64;
public class ApiField {
public static void main(String[] args) {
// O JSON parseado carregava: "content" : "iVBORw0KGgoAAA..."
String field = "iVBORw0KGgo=";
byte[] image = Base64.getUrlDecoder().decode(field);
// Algumas APIs falam Base64 padrão no lugar. Leia a especificação,
// depois escolha getDecoder() ou getUrlDecoder() de acordo.
System.out.println(image.length); // 8
}
}
A armadilha aqui não é decodificar, é ler a especificação. Algumas APIs querem Base64 padrão com padding, algumas querem base64url sem, e algumas poucas são tolerantes com ambos. Quando a especificação cala, o conserto mais barato é olhar um valor de exemplo do outro lado: um - ou _ em qualquer lugar do valor resolve o alfabeto, e = finais resolvem o padding.
Formato três: o data URI. Alguém cola uma imagem num formulário e o front end te entrega o data URI completo: data:image/png;base64,iVBORw0KGgo.... O RFC 2397 define o formato: data:, um media type opcional, uma flag ;base64 opcional, uma vírgula, e então os dados. Quando a flag está presente, o payload é Base64, quando ela está ausente, o payload é texto puro percent-encoded, mais raro mas legal. Se o media type for omitido, o padrão é text/plain;charset=US-ASCII. Fazer o split de um é direto:
import java.util.Base64;
public class DataUri {
public static void main(String[] args) {
String uri = "data:image/png;base64,iVBORw0KGgo=";
int comma = uri.indexOf(',');
String meta = uri.substring(5, comma);
String payload = uri.substring(comma + 1);
boolean isBase64 = meta.endsWith(";base64");
String mime = isBase64 ? meta.substring(0, meta.length() - 7) : meta;
byte[] raw = Base64.getDecoder().decode(payload);
System.out.println(mime + " -> " + raw.length + " bytes");
// image/png -> 8 bytes
}
}
Duas armadilhas moram neste formato. A flag ;base64 ausente é a primeira: um data URI legal sem a flag carrega um payload percent-encoded, e passar ele pelo Base64.getDecoder() lança exceção. A segunda é o media type afirmado: é uma dica do remetente, não um fato, então confira os bytes mágicos do que você decodificou antes de arquivar sob "png". E lembre-se do próprio conselho do RFC de que data URIs são para valores curtos, uma imagem de vários megabytes dentro de uma URL é um mau cheiro, não um padrão.
E-mail, MIME e blindagem PEM
Base64 nasceu para o e-mail, e Base64 com forma de e-mail ainda chega em programas Java o tempo todo. O padrão MIME (RFC 2045) fez do Base64 uma das codificações de transferência binária e adicionou duas regras da casa: linhas codificadas não podem passar de 76 caracteres, e decodificadores devem ignorar todo caractere fora do alfabeto, quebras de linha incluídas. Os decodificadores estritos rejeitam a primeira quebra de linha; o getMimeDecoder() foi feito exatamente para esta entrada:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class MimeDecode {
public static void main(String[] args) {
String wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
byte[] bytes = Base64.getMimeDecoder().decode(wrapped);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
// Hello, standard
}
}
Isso funciona, e você ainda deve saber da pegadinha, porque a pegadinha tem dentes. O decodificador tolerante não "ignora quebras de linha", ele ignora tudo que não está no alfabeto dele. Se uma string Base64 padrão for corrompida com caracteres soltos, o lixo some e o resto decodifica para algo plausível, então recorra ao getMimeDecoder() só quando você realmente espera entrada com forma de MIME.
O irmão do MIME no mundo é a blindagem PEM, o negócio de -----BEGIN CERTIFICATE----- que embrulha certificados e chaves. Aqui está a armadilha: as linhas da blindagem estão cheias de caracteres comuns do alfabeto. As letras de "BEGIN CERTIFICATE" são apenas letras Base64, então alimentar um bloco PEM inteiro, blindagem incluída, decodifica a blindagem como se fosse dado. Tire a blindagem você mesmo, e então entregue o corpo nu a um decodificador:
import java.util.Base64;
public class PemDecode {
public static void main(String[] args) {
String pem = "-----BEGIN CERTIFICATE-----\n"
+ "TUlJQm96Q0NBVWlnQXdJQkFnSUpBSXBhVDJUaVFvZU1BMEdDU3FHU0liM0RRRUE9\n"
+ "-----END CERTIFICATE-----\n";
String body = pem.replaceAll("(?m)^-----.*$", "").replaceAll("\\s", "");
byte[] der = Base64.getDecoder().decode(body);
System.out.println(der.length); // o corpo DER, blindagem excluída
}
}
PEM convencionalmente quebra em 64 caracteres por linha (MIME em 76), e uma vez que o espaço em branco some, o decodificador estrito e o decodificador MIME concordam no resultado. Use o estrito: uma surpresa ao menos tem a decência de lançar exceção. Para o caso sujo mas padrão, a receita clássica é tirar o espaço em branco conhecido e decodificar com a instância estrita, e deixar qualquer resto de lixo merecer uma IllegalArgumentException em vez de um certificado corrompido.
Valores em config, ambiente e banco de dados
Base64 é um recipiente de texto, e é por isso que aparece em lugares onde você não esperaria. Em bancos de dados, um blob binário (um arquivo, um ícone, uma estrutura serializada) pode morar numa coluna TEXT como Base64, sobrevivendo a toda ferramenta que assume texto; espere que o valor armazenado seja cerca de um terço maior que o original e dimensione a coluna conforme. Em arquivos de config e variáveis de ambiente, Base64 é o truque para contrabandear valores que quebrariam o formato de outra forma: um DSN com ponto-e-vírgulas, uma senha com aspas, um certificado multi-linha. Decodificar no boot é o trabalho inteiro:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigDecode {
public static void main(String[] args) {
String value = System.getenv("DB_DSN_B64");
if (value == null) {
return;
}
byte[] raw = Base64.getDecoder().decode(value);
String dsn = new String(raw, StandardCharsets.UTF_8);
// o dsn pode ser: pg:host=db;password=qu"ote
}
}
A mesma cautela se aplica duas vezes aqui. Primeira, isso é segurança de formato, não sigilo: no momento em que um desenvolvedor lê o arquivo de config, ele decodifica o valor em uma chamada, então nunca armazene um segredo como Base64 e chame de criptografado, a seção de segurança abaixo entra em detalhes. Segunda, valide no boot: um valor de env corrompido ou colado pela metade é uma IllegalArgumentException da chamada estrita, e uma verificação de duas linhas transforma um erro de runtime críptico numa mensagem de inicialização acionável. Uma nota específica de Java para a turma de banco de dados: mantenha o binário decodificado como byte[] (um parâmetro byte[] no seu código JDBC), e nunca faça round-trip de binário através de uma String, porque os construtores de string são para onde payloads binários vão morrer.
Streamando a bagagem grande
Para payloads que são grandes mas ainda cabem num buffer que você gerencia, as APIs de array servem. Para payloads que não deveriam caber em memória de jeito nenhum, o adaptador de stream é o lance: wrap(InputStream) retorna um input stream que decodifica enquanto você lê, então um arquivo codificado de vários gigabytes nunca precisa ficar num array de bytes:
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamDecode {
public static void main(String[] args) throws Exception {
InputStream packed = Base64.getDecoder().wrap(Files.newInputStream(Paths.get("bigfile.b64")));
OutputStream raw = Files.newOutputStream(Paths.get("bigfile.bin"));
byte[] buf = new byte[8192];
int n;
while ((n = packed.read(buf)) != -1) {
raw.write(buf, 0, n);
}
raw.close();
packed.close();
}
}
Dois detalhes que valem a pena conhecer. Os métodos de leitura do stream embrulhado lançam IOException quando encontram bytes que não podem ser decodificados, então um arquivo corrompido falha com uma exceção de stream em vez de uma IllegalArgumentException. E fechar o stream embrulhado fecha o stream subjacente, então o exemplo fecha packed por último, depois do loop de cópia, e em produção você colocaria os dois num bloco try-with-resources. (O buffer 8192 é só um buffer de leitura espaçoso, o stream embrulhado decodifica internamente, então o tamanho com que você lê é uma escolha de desempenho, não um requisito de protocolo.)
Agora um aviso de legado, porque este é o único bug de verdade na história toda e ele tem número de bug. Em todo JDK antes do 16 (o relatório de bug reproduz em 8, 10 e 11), ler um decodificador embrulhado com certos tamanhos de buffer acrescenta dois bytes zero soltos no fim dos dados decodificados: JDK 8222187, cuja reprodução clássica emparelha um buffer de leitura de sete bytes com uma entrada simples de oito bytes, e ele foi corrigido no JDK 16. Se você precisa streamar num JDK 8 legado, reconfira o comprimento decodificado depois da cópia, porque o bug dispara para combinações específicas de entrada e buffer e até um buffer de 4096 bytes já foi reportado no mundo, ou melhor, atualize o JDK, que corrigiria mais ou menos cem outras coisas de qualquer jeito.
Fita adesiva, não um cadeado
Agora a seção que separa os cuidadosos dos queimados. Base64 não é criptografia, e o próprio padrão diz isso duas vezes. Seção 12 do RFC 4648: a codificação Base "oculta visualmente informações que de outra forma seriam facilmente reconhecidas, como senhas, mas não fornece qualquer confidencialidade computacional", e ele continua notando que isso "já foi conhecido por causar incidentes de segurança" quando alguém cola uma troca de protocolo num chamado e acidentalmente revela a senha. O conselho do RFC para implementadores também merece um quadro: "um decodificador não deve quebrar em entrada inválida incluindo, por exemplo, caracteres NUL embutidos".
A armadilha mais sutil é a maleabilidade. Lembre-se de que cada símbolo carrega seis bits, e de que uma unidade final curta deixa bits sobrando que devem ser zero numa codificação bem formada. Um codificador descuidado ou hostil pode colocar lixo nesses bits sobrando, e o resultado ainda parece completamente válido: MQ== e MT== ambos decodificam para o único byte do dígito 1. O Java fica do lado tolerante disso: Base64.getDecoder().decode("MT==") não verifica os bits não-significativos e com alegria te entrega o mesmo byte. Por que se importar? Porque duas strings diferentes que decodificam para os mesmos dados quebram a suposição de "grafia única" em que verificações de hash, deduplicação e comparações de assinatura dependem quietamente, e um atacante que pode adulterar um valor codificado em trânsito pode trocar uma grafia pela outra. O paper de 2022 "Maleabilidade do Base64 na Prática" de Chatzigiannis e Chalkias (ACM ASIA CCS 2022) passeia por exatamente essas inconsistências através de implementações do mundo real. As próprias palavras do RFC sobre os bits sobrando: eles "podem ser abusados para vazar informações ou usados para burlar comparações de igualdade de strings ou para disparar problemas de implementação". A regra prática não é "nunca decodificar", é "conheça a sua fronteira": para dados entre os seus próprios sistemas, a generosidade do JDK é suficiente, mas para dados cruzando uma fronteira de confiança, imponha a forma canônica (comprimento correto, bits sobrando zerados, uma grafia de padding) antes de confiar em qualquer coisa que você decodificou.
Notas de desempenho
Aqui está a boa notícia em uma frase: num JVM moderno, o decodificador embutido é rápido o bastante para que Base64 quase nunca seja o seu gargalo, e ele é a referência contra a qual o resto do ecossistema faz o próprio benchmark. Um caso a ponto: em 2025 o projeto gRPC-java publicou um benchmark do seu tratamento de Base64 baseado em Guava contra o java.util.Base64 (issue 11857), e a implementação do JDK saiu mais ou menos 2,5 a 3,8 vezes mais rápida em codificação e 1,3 a 2,1 vezes mais rápida em decodificação no JDK 17 e 21, com as maiores diferenças no x86. Isso é uma dica forte sobre para onde foi o esforço de implementação do JDK, e é a mesma conclusão que você vai continuar encontrando em benchmarks de Base64: a versão da biblioteca padrão é a rápida agora, não a legado.
Duas notas práticas. Primeira, para arquivos enormes o perfil de memória, não a velocidade, é o que você está gerenciando, e é por isso que a seção de streaming existe: wrap(InputStream) mantém o conjunto de trabalho no seu buffer de leitura. Segunda, se você acabar num caminho quente que decodifica milhões de valores pequenos, compartilhe uma instância de decodificador (a fábrica já retorna a mesma compartilhada, como anotado no topo), pule o overload decode(String) quando você já tem bytes (ele copia a string através do Latin-1 primeiro), e deixe o overload decode(byte[], byte[]) escrever num array de destino pré-dimensionado para pular a dança de alocação.
Armadilhas com sotaque Java
As armadilhas reunidas num só lugar, todas específicas de Java:
- Decodificador errado para o alfabeto. Uma string base64url no
getDecoder()(ou o inverso) é o clássico crash deIllegal base64 character, geralmente com um2d,5f,2bou2fna mensagem. Case o decodificador com o protocolo, toda vez. - Espaço em branco final do mundo selvagem. Valores copiados de um terminal, uma variável de env ou um arquivo de config frequentemente chegam com uma quebra de linha, e o decodificador estrito transforma isso em
Illegal base64 character a.strip()a entrada, ou use o decodificador MIME só quando os dados são genuinamente quebrados em linhas. - A armadilha da blindagem. O
getMimeDecoder()não entende headers PEM, e as letras de BEGIN e CERTIFICATE decodificam como dados. Tire as linhas da blindagem você mesmo, sempre. - Tolerância MIME como atalho. Decodificar com o decodificador MIME só para "estar seguro" pula silenciosamente qualquer caractere não-alfabético solto, então um payload corrompido pode sair plausível e errado. Use para entrada MIME de verdade apenas.
- Charset entregue à sorte. O
new String(bytes)sem argumentos usa o padrão da plataforma. É UTF-8 no JDK 18+, mas o seu código deveria passarStandardCharsets.UTF_8explicitamente, ou aproveite o mojibake depois da próxima migração de servidor. - Stringificando binário.
new String(decodedPng)e de volta é destruição de dados: toda sequência de bytes que não é válida no seu charset vira o caractere de substituição, e a viagem de ida e volta é só de ida. Bytes entram, bytes saem, até o último passo. - Confiança nos bits sobrando.
MT==decodifica igual aMQ==, então um payload com lixo escondido nos bits não-significativos passa em toda verificação que o JDK roda. Se o protocolo importa, imponha a forma canônica. - Streams do JDK 8, 11 e 12. O decodificador embrulhado nessas versões pode acrescentar dois bytes zero soltos para certos tamanhos de buffer (JDK 8222187, corrigido no 16). No 16 e depois isso não é problema, nos mais antigos é.
- null não é vazio. Passar
nullparadecode()é umaNullPointerException, não um array vazio. Se uma variável pode ser null, coaleça ela antes da chamada. - Android é um zoológico diferente. No Android,
java.util.Base64só existe a partir do nível de API 26, abaixo disso a classe do framework éandroid.util.Base64com as próprias constantes de flag. Código que hard-codes uma ou outra sem checar quebra exatamente nos dispositivos que você nunca testou. - Esquecer que não é segurança. Base64 esconde uma senha de um relance e de mais ninguém. Se os dados são secretos, cifre primeiro e só então empacote se o canal exigir texto.
O longo caminho até o java.util.Base64
A história do formato é mais antiga que o Java. Nos anos 1980 a infraestrutura de e-mail da internet só conseguia carregar ASCII de 7 bits, e as pessoas que queriam mover binários inventaram dialetos locais: uuencode para UNIX (o alfabeto dele passa por códigos ASCII consecutivos, então codificar era uma adição de 32 sem tabela de lookup) e BinHex para máquinas Apple (que curou o alfabeto para descartar caracteres visualmente confundíveis como 7, O, g e o). Em 1987 o protocolo Privacy-Enhanced Mail (RFC 989) padronizou o esquema de 64 caracteres com linhas de 64 caracteres para carregar certificados, e o RFC 1421 em 1993 manteve o alfabeto e as regras de padding. Em 1996 o MIME (RFC 2045, atualizando o RFC 1521 de 1993) carregou o esquema já chamado de "base64" por causa do alfabeto de 64 caracteres, definiu o comprimento de linha de 76 caracteres que ainda quebra seus anexos de e-mail, e escreveu a regra do decodificador tolerante que o getMimeDecoder() implementa até hoje. Em 2003 o RFC 3548 tentou arrumar a família inteira e declarou que decodificadores deveriam rejeitar caracteres fora do alfabeto, e em 2006 o RFC 4648 virou o padrão que todo mundo cita, com as tabelas de alfabeto, a variante base64url na seção 5, e a seção de segurança que mantém a última das seções deste artigo honesta.
O próprio capítulo do Java é um pouco mais dramático. Por anos o único Base64 dentro do JDK era o par interno sun.misc.BASE64Encoder e sun.misc.BASE64Decoder, o tipo de API que compila hoje e some sem um aviso de deprecação, e se você precisava de Base64 num mundo de XML também existia o javax.xml.bind.DatatypeConverter do JAXB. Todo mundo mais usava Apache Commons Codec ou Guava. Então 18 de março de 2014: o Java 8 entregou o java.util.Base64, implementando RFC 4648 e RFC 2045 numa única classe com o padrão de método de fábrica que você vem usando o tempo todo. Três anos e meio depois, o Java 9 (21 de setembro de 2017) removeu o par sun.misc de vez, e o guia oficial de migração é direto sobre isso: "Notavelmente, sun.misc.BASE64Encoder e sun.misc.BASE64Decoder foram removidas. Em vez disso, use a classe java.util.Base64 suportada, que foi adicionada no JDK 8". Rode jdeps em código antigo que ainda referencia elas e a ferramenta marca a dependência como "JDK removed internal API". O Java 11 continuou removendo o módulo JAXB e seu DatatypeConverter junto (JEP 320). Desde o 1.8 a API pública não mudou um único método, e o javadoc ainda carrega a tag original Since: 1.8. O que se moveu é o motor por baixo: correções de bug (o bug de stream do JDK 8222187, corrigido no JDK 16) e trabalho de desempenho, e é por isso que benchmarks da comunidade continuam chegando à mesma conclusão. Doze anos, uma API, e ainda é o Base64 mais rápido que você não precisa pagar.
Curiosidades, edição Java
Porque um guia completo deveria terminar num sorriso, aqui vão alguns fatos específicos de Java que são simplesmente divertidos:
- O javadoc da Oracle para
decode(byte[] src, byte[] dst)promete que "alguns bytes podem ter sido escritos no array de bytes de saída antes que a IllegalargumentException seja lançada". Não IllegalArgumentException, IllegalargumentException, com um a minúsculo. O erro está no código-fonte real do JDK, e está lá desde 2014. Documentação tão comprometida com um erro é mais rara do que deveria ser. - Decodifique uma string contendo
ée a mensagem de erro éIllegal base64 character -17: um número hexadecimal negativo, porque o caractere vira o byte Latin-10xE9, que como byte Java com sinal é menos 23, e o JDK imprime ele em base 16. O seu log de erros, resumindo, está fazendo aritmética com sinal. Base64.getDecoder() == Base64.getDecoder()é verdadeiro. O código-fonte retorna uma instância estática compartilhada em toda chamada, então a API "pegue uma nova" é um figurino para um singleton, e a promessa de thread-safety é só uma descrição do que o JVM já está fazendo.- Alimente o decodificador URL com uma string de quatro sublinhados,
"____", e ele retorna três bytes de0xFFpuro. O sublinhado é valor de alfabeto 63, quatro deles fazem 24 bits, e 24 bits de uns são a tripla de bytesFF FF FF. Nada de ilegal nisso, e é a parte mais engraçada. AA==decodifica para um único byte NUL enquanto a string vazia decodifica para nada. No Base64, "nada" e "um zero" são criaturas diferentes, e ambas são entrada perfeitamente válida.- A stringzinha
TWFuque decodifica paraManvirou o smoke test favorito do ecossistema: aparece no RFC, na Wikipedia, em manuais de referência e na maioria dos tutoriais de Base64 do planeta, então todo decodificador escrito desde então vem pagando o mesmo pequeno tributo. - Todo PNG codificado em Base64 que você já decodificou começa com
iVBORw0K. É o número mágico do PNG disfarçado, e é um dos prefixos de oito caracteres mais reconhecíveis da internet. - A seção URL-safe do RFC 4648 é onde o nome "base64url" nasce: a especificação diz que a codificação "pode ser referida como base64url" e avisa que "não deve ser considerada a mesma que a codificação base64". A origem do alfabeto URL-safe é anotada em nota de rodapé como um post de 2001 numa mailing list de P2P-hackers, então o nome que você cola em toda URL tem linhagem de mailing list.
- IDs de vídeo do YouTube são base64url sem padding, a familiar string de onze caracteres que você pode colar em qualquer lugar de uma URL. O formato que foi desenhado para anexos de e-mail agora roda uma plataforma de vídeo, e
getUrlDecoder()é a parte do seu JDK que faz isso funcionar. - Decodifique a string
YmFzZTY0e você recebe a palavrabase64de volta, sem padding necessário, porque seis é múltiplo de três. Um formato se descrevendo é o equivalente técnico de um espelho que fala em Morse.
A outra direção
Essa é a parte do decodificador da história, e é onde mora a maior parte da dor, porque decodificar é onde você encontra os dados dos outros: as escolhas de padding deles, as quebras de linha deles, os charsets deles, os tokens deles, a blindagem deles. A outra direção, transformar bytes em uma string Base64 com os codificadores do java.util.Base64, é um animal mais calmo: ele nunca lança exceção em entrada inválida (não existe entrada inválida para codificar), tem uma conta de tamanho para pagar em vez de uma mensagem de erro para ler, e o próprio conjunto de armadilhas (o passo de charset, os controles do MIME, a decisão de padding para tokens) ganha um guia próprio. Codificação Base64 em Java, linkada desta página, cobre o codificador na mesma profundidade, e as duas se leem confortavelmente como par.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em Java: um guia completo